1 | <?php |
||
2 | |||
3 | namespace Blasttech\EloquentRelatedPlus; |
||
4 | |||
5 | use DB; |
||
6 | use Illuminate\Database\Eloquent\Builder; |
||
7 | use Illuminate\Database\Eloquent\Relations\BelongsTo; |
||
8 | use Illuminate\Database\Eloquent\Relations\HasMany; |
||
9 | use Illuminate\Database\Eloquent\Relations\HasOneOrMany; |
||
10 | use Illuminate\Database\Query\Expression; |
||
11 | use Illuminate\Database\Query\JoinClause; |
||
12 | |||
13 | /** |
||
14 | * Class RelationPlus |
||
15 | * |
||
16 | * @package Blasttech\EloquentRelatedPlus\Relations |
||
17 | */ |
||
18 | class RelationPlus |
||
19 | { |
||
20 | use HelpersTrait; |
||
21 | |||
22 | /** |
||
23 | * @var string $tableName |
||
24 | */ |
||
25 | public $tableName; |
||
26 | |||
27 | /** |
||
28 | * @var string $tableAlias |
||
29 | */ |
||
30 | public $tableAlias; |
||
31 | |||
32 | /** |
||
33 | * Initialise $relation, $tableName and $tableAlias |
||
34 | * If using a 'table' AS 'tableAlias' in a from statement, otherwise alias will be the table name |
||
35 | * |
||
36 | * @var BelongsTo|HasOneOrMany $relation |
||
37 | */ |
||
38 | private $relation; |
||
39 | |||
40 | public function __construct($relation) |
||
41 | { |
||
42 | $this->setRelation($relation); |
||
43 | $this->tableName = $this->relation->getRelated()->getTable(); |
||
44 | $from = explode(' ', $this->relation->getQuery()->getQuery()->from); |
||
45 | $this->tableAlias = array_pop($from); |
||
46 | } |
||
47 | |||
48 | /** |
||
49 | * @return BelongsTo|HasOneOrMany |
||
50 | */ |
||
51 | public function getRelation() |
||
52 | { |
||
53 | return $this->relation; |
||
54 | } |
||
55 | |||
56 | /** |
||
57 | * @param BelongsTo|HasOneOrMany $relation |
||
58 | */ |
||
59 | public function setRelation($relation) |
||
60 | { |
||
61 | $this->relation = $relation; |
||
62 | } |
||
63 | |||
64 | /** |
||
65 | * Check relation type and get join |
||
66 | * |
||
67 | * @param JoinClause $join |
||
68 | * @param string $operator |
||
69 | * @param string|null $direction |
||
70 | * @return Builder|JoinClause |
||
71 | */ |
||
72 | public function getRelationJoin($join, $operator, $direction = null) |
||
73 | { |
||
74 | // If a HasOne relation and ordered - ie join to the latest/earliest |
||
75 | if (class_basename($this->relation) === 'HasOne') { |
||
76 | $this->relation = $this->removeGlobalScopes($this->relation->getRelated(), $this->relation, 'order'); |
||
77 | |||
78 | if (!empty($this->getOrders())) { |
||
79 | return $this->hasOneJoin($join); |
||
80 | } |
||
81 | } |
||
82 | |||
83 | return $this->hasManyJoin($join, $operator, $direction); |
||
84 | } |
||
85 | |||
86 | /** |
||
87 | * Get the orders for the relation |
||
88 | * |
||
89 | * @return array |
||
90 | */ |
||
91 | private function getOrders() |
||
92 | { |
||
93 | return $this->relation->toBase()->orders; |
||
94 | } |
||
95 | |||
96 | /** |
||
97 | * Join a HasOne relation which is ordered |
||
98 | * |
||
99 | * @param JoinClause $join |
||
100 | * @return JoinClause |
||
101 | */ |
||
102 | private function hasOneJoin($join) |
||
103 | { |
||
104 | // Get first relation order (should only be one) |
||
105 | $order = $this->getOrders()[0]; |
||
106 | |||
107 | return $join->on($order['column'], $this->hasOneJoinSql($order)); |
||
108 | } |
||
109 | |||
110 | /** |
||
111 | * Get join sql for a HasOne relation |
||
112 | * |
||
113 | * @param array $order |
||
114 | * @return Expression |
||
115 | */ |
||
116 | private function hasOneJoinSql($order) |
||
117 | { |
||
118 | // Build subquery for getting first/last record in related table |
||
119 | $subQuery = $this |
||
120 | ->joinOne( |
||
121 | $this->relation->getRelated()->newQuery(), |
||
122 | $order['column'], |
||
123 | $order['direction'] |
||
124 | ) |
||
125 | ->setBindings($this->relation->getBindings()); |
||
126 | |||
127 | return DB::raw('(' . $this->toSqlWithBindings($subQuery) . ')'); |
||
128 | } |
||
129 | |||
130 | /** |
||
131 | * Adds a where for a relation's join columns and and min/max for a given column |
||
132 | * |
||
133 | * @param Builder $query |
||
134 | * @param string $column |
||
135 | * @param string $direction |
||
136 | * @return Builder |
||
137 | */ |
||
138 | private function joinOne($query, $column, $direction) |
||
139 | { |
||
140 | // Get join fields |
||
141 | $joinColumns = $this->getJoinColumns(); |
||
142 | |||
143 | return $this->selectMinMax( |
||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() |
|||
144 | $query->whereColumn($joinColumns->first, '=', $joinColumns->second), |
||
145 | $column, |
||
146 | $direction |
||
147 | ); |
||
148 | } |
||
149 | |||
150 | /** |
||
151 | * Get the join columns for a relation |
||
152 | * |
||
153 | * @return \stdClass |
||
154 | */ |
||
155 | private function getJoinColumns() |
||
156 | { |
||
157 | // Get keys with table names |
||
158 | if ($this->relation instanceof BelongsTo) { |
||
159 | return $this->getBelongsToColumns(); |
||
160 | } |
||
161 | |||
162 | return $this->getHasOneOrManyColumns(); |
||
163 | } |
||
164 | |||
165 | /** |
||
166 | * Get the join columns for a BelongsTo relation |
||
167 | * |
||
168 | * @return object |
||
169 | */ |
||
170 | private function getBelongsToColumns() |
||
171 | { |
||
172 | // Use relation ownerKey if it contains table name, otherwise use getQualifiedOwnerKeyName |
||
173 | $first = $this->relation->getOwnerKeyName(); |
||
174 | if (!strpos($first, '.')) { |
||
175 | $first = $this->relation->getQualifiedOwnerKeyName(); |
||
176 | } |
||
177 | |||
178 | // Use relation foreignKey if it contains table name, otherwise use getQualifiedForeignKey |
||
179 | $second = $this->relation->getForeignKeyName(); |
||
180 | if (!strpos($second, '.')) { |
||
181 | $second = $this->relation->getQualifiedForeignKeyName(); |
||
182 | } |
||
183 | |||
184 | return (object)['first' => $first, 'second' => $second]; |
||
185 | } |
||
186 | |||
187 | /** |
||
188 | * Get the join columns for a HasOneOrMany relation |
||
189 | * |
||
190 | * @return object |
||
191 | */ |
||
192 | private function getHasOneOrManyColumns() |
||
193 | { |
||
194 | $first = $this->relation->getQualifiedParentKeyName(); |
||
195 | $second = $this->relation->getQualifiedForeignKeyName(); |
||
196 | |||
197 | return (object)['first' => $first, 'second' => $second]; |
||
198 | } |
||
199 | |||
200 | /** |
||
201 | * Adds a select for a min or max on the given column, depending on direction given |
||
202 | * |
||
203 | * @param Builder|\Illuminate\Database\Query\Builder $query |
||
204 | * @param string $column |
||
205 | * @param string $direction |
||
206 | * @return Builder|\Illuminate\Database\Query\Builder |
||
207 | */ |
||
208 | private function selectMinMax($query, $column, $direction) |
||
209 | { |
||
210 | $sqlDirection = ($direction == 'asc' ? 'MIN' : 'MAX'); |
||
211 | |||
212 | return $query->select(DB::raw($sqlDirection . '(' . $this->addBackticks($column) . ')')); |
||
213 | } |
||
214 | |||
215 | /** |
||
216 | * Add backticks to a table/column |
||
217 | * |
||
218 | * @param string $column |
||
219 | * @return string |
||
220 | */ |
||
221 | private function addBackticks($column) |
||
222 | { |
||
223 | return preg_match('/^[0-9a-zA-Z\.]*$/', $column) ? |
||
224 | '`' . str_replace(['`', '.'], ['', '`.`'], $column) . '`' : $column; |
||
225 | } |
||
226 | |||
227 | /** |
||
228 | * Join a HasMany Relation |
||
229 | * |
||
230 | * @param JoinClause $join |
||
231 | * @param string $operator |
||
232 | * @param string $direction |
||
233 | * @return Builder|JoinClause |
||
234 | */ |
||
235 | private function hasManyJoin($join, $operator, $direction) |
||
236 | { |
||
237 | // Get relation join columns |
||
238 | $joinColumns = $this->replaceColumnTables($this->getJoinColumns()); |
||
239 | |||
240 | $join->on($joinColumns->first, $operator, $joinColumns->second); |
||
241 | |||
242 | // Add any where clauses from the relationship |
||
243 | $join = $this->addRelatedWhereConstraints($join); // $table->alias |
||
244 | |||
245 | if (!is_null($direction) && get_class($this->relation) === HasMany::class) { |
||
0 ignored issues
–
show
|
|||
246 | $join = $this->hasManyJoinWhere($join, $joinColumns->first, $direction); // $table->alias, |
||
247 | } |
||
248 | |||
249 | return $join; |
||
250 | } |
||
251 | |||
252 | /** |
||
253 | * Replace column table names with aliases |
||
254 | * |
||
255 | * @param \stdClass $joinColumns |
||
256 | * @return \stdClass |
||
257 | */ |
||
258 | private function replaceColumnTables($joinColumns) |
||
259 | { |
||
260 | if ($this->tableName !== $this->tableAlias) { |
||
261 | $joinColumns->first = str_replace($this->tableName, $this->tableAlias, $joinColumns->first); |
||
262 | $joinColumns->second = str_replace($this->tableName, $this->tableAlias, $joinColumns->second); |
||
263 | } |
||
264 | |||
265 | return $joinColumns; |
||
266 | } |
||
267 | |||
268 | /** |
||
269 | * Add wheres if they exist for a relation |
||
270 | * |
||
271 | * @param Builder|JoinClause $builder |
||
272 | * @return Builder|JoinClause $builder |
||
273 | */ |
||
274 | private function addRelatedWhereConstraints($builder) |
||
275 | { |
||
276 | // Get where clauses from the relationship |
||
277 | $wheres = collect($this->relation->toBase()->wheres) |
||
278 | ->whereIn('type', ['Basic', 'Nested']) |
||
279 | ->map(function ($where) { |
||
280 | return collect($where['type'] == 'Basic' ? [$where] : $where['query']->wheres) |
||
281 | ->map(function ($where) { |
||
282 | // Add table name to column if it is absent |
||
283 | return [ |
||
284 | $this->columnWithTableName($where['column']), |
||
285 | $where['operator'], |
||
286 | $where['value'] |
||
287 | ]; |
||
288 | }); |
||
289 | }) |
||
290 | ->flatten(1) |
||
291 | ->toArray(); |
||
292 | |||
293 | if (!empty($wheres)) { |
||
294 | $builder->where($wheres); |
||
295 | } |
||
296 | |||
297 | return $builder; |
||
298 | } |
||
299 | |||
300 | /** |
||
301 | * Add table name to column name if table name not already included in column name |
||
302 | * |
||
303 | * @param string $column |
||
304 | * @return string |
||
305 | */ |
||
306 | private function columnWithTableName($column) |
||
307 | { |
||
308 | return (preg_match('/(' . $this->tableAlias . '\.|`' . $this->tableAlias . '`)/i', $column) > 0 |
||
309 | ? '' : $this->tableAlias . '.') . $column; |
||
310 | } |
||
311 | |||
312 | /** |
||
313 | * If the relation is one-to-many, just get the first related record |
||
314 | * |
||
315 | * @param JoinClause $joinClause |
||
316 | * @param string $column |
||
317 | * @param string $direction |
||
318 | * |
||
319 | * @return JoinClause |
||
320 | */ |
||
321 | private function hasManyJoinWhere(JoinClause $joinClause, $column, $direction) |
||
322 | { |
||
323 | return $joinClause->where( |
||
324 | $column, |
||
325 | function ($subQuery) use ($column, $direction) { |
||
326 | $subQuery = $this->joinOne( |
||
327 | $subQuery->from($this->tableAlias), |
||
328 | $column, |
||
329 | $direction |
||
330 | ); |
||
331 | |||
332 | // Add any where statements with the relationship |
||
333 | $subQuery = $this->addRelatedWhereConstraints($subQuery); // $this->tableAlias |
||
334 | |||
335 | // Add any order statements with the relationship |
||
336 | return $this->addOrder($subQuery); // $this->tableAlias |
||
337 | } |
||
338 | ); |
||
339 | } |
||
340 | |||
341 | /** |
||
342 | * Add orderBy if orders exist for a relation |
||
343 | * |
||
344 | * @param Builder|JoinClause $builder |
||
345 | * @return Builder|JoinClause $builder |
||
346 | */ |
||
347 | private function addOrder($builder) |
||
348 | { |
||
349 | if (!empty($this->getOrders())) { |
||
350 | // Get where clauses from the relationship |
||
351 | foreach ($this->getOrders() as $order) { |
||
352 | $builder->orderBy($this->columnWithTableName($order['column']), $order['direction']); |
||
353 | } |
||
354 | } |
||
355 | |||
356 | return $builder; |
||
357 | } |
||
358 | |||
359 | /** |
||
360 | * Get table name with alias if different to table name |
||
361 | * |
||
362 | * @return string |
||
363 | */ |
||
364 | public function getTableWithAlias() |
||
365 | { |
||
366 | if ($this->tableAlias !== '' && $this->tableName !== $this->tableAlias) { |
||
367 | return $this->tableName . ' AS ' . $this->tableAlias; |
||
368 | } |
||
369 | |||
370 | // it means the connection DB name is not on the table alias yet |
||
371 | if (strpos('.', $this->tableName) === null) { |
||
372 | return $this->relation->getConnection()->getDatabaseName() . '.' . $this->tableName; |
||
373 | } |
||
374 | |||
375 | return $this->tableName; |
||
376 | } |
||
377 | } |
||
378 |