Passed
Push — master ( 077199...c47ee2 )
by Jonas
04:20 queued 02:16
created

HasManyDeep::getRelationExistenceQuery()   B

Complexity

Conditions 9
Paths 16

Size

Total Lines 45
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 9

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 24
nc 16
nop 3
dl 0
loc 45
ccs 22
cts 22
cp 1
crap 9
rs 8.0555
c 1
b 0
f 0
1
<?php
2
3
namespace Staudenmeir\EloquentHasManyDeep;
4
5
use Closure;
6
use Exception;
7
use Illuminate\Contracts\Pagination\Paginator;
8
use Illuminate\Database\Eloquent\Builder;
9
use Illuminate\Database\Eloquent\Collection;
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
12
use Illuminate\Pagination\CursorPaginator;
13
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\HasEagerLimit;
14
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\IsConcatenable;
15
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\JoinsThroughParents;
16
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\RetrievesIntermediateTables;
17
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\SupportsCompositeKeys;
18
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\IsCustomizable;
19
use Staudenmeir\EloquentHasManyDeepContracts\Interfaces\ConcatenableRelation;
20
21
/**
22
 * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
23
 * @extends \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel>
24
 */
25
class HasManyDeep extends HasManyThrough implements ConcatenableRelation
26
{
27
    use HasEagerLimit;
0 ignored issues
show
Bug introduced by
The trait Staudenmeir\EloquentHasM...ns\Traits\HasEagerLimit requires the property $exists which is not provided by Staudenmeir\EloquentHasManyDeep\HasManyDeep.
Loading history...
28
    use IsConcatenable;
29
    use IsCustomizable;
30
    use JoinsThroughParents;
0 ignored issues
show
Bug introduced by
The trait Staudenmeir\EloquentHasM...its\JoinsThroughParents requires the property $columns which is not provided by Staudenmeir\EloquentHasManyDeep\HasManyDeep.
Loading history...
31
    use RetrievesIntermediateTables;
32
    use SupportsCompositeKeys;
0 ignored issues
show
Bug introduced by
The trait Staudenmeir\EloquentHasM...s\SupportsCompositeKeys requires the property $columns which is not provided by Staudenmeir\EloquentHasManyDeep\HasManyDeep.
Loading history...
33
34
    /**
35
     * The "through" parent model instances.
36
     *
37
     * @var \Illuminate\Database\Eloquent\Model[]
38
     */
39
    protected $throughParents;
40
41
    /**
42
     * The foreign keys on the relationship.
43
     *
44
     * @var array
45
     */
46
    protected $foreignKeys;
47
48
    /**
49
     * The local keys on the relationship.
50
     *
51
     * @var array
52
     */
53
    protected $localKeys;
54
55
    /**
56
     * Create a new has many deep relationship instance.
57
     *
58
     * @param \Illuminate\Database\Eloquent\Builder $query
59
     * @param \Illuminate\Database\Eloquent\Model $farParent
60
     * @param \Illuminate\Database\Eloquent\Model[] $throughParents
61
     * @param array $foreignKeys
62
     * @param array $localKeys
63
     * @return void
64
     */
65 176
    public function __construct(Builder $query, Model $farParent, array $throughParents, array $foreignKeys, array $localKeys)
66
    {
67 176
        $this->throughParents = $throughParents;
68 176
        $this->foreignKeys = $foreignKeys;
69 176
        $this->localKeys = $localKeys;
70
71 176
        $firstKey = is_array($foreignKeys[0])
72 10
            ? $foreignKeys[0][1]
73 166
            : ($this->hasLeadingCompositeKey() ? $foreignKeys[0]->columns[0] : $foreignKeys[0]);
74
75 176
        $localKey = $this->hasLeadingCompositeKey() ? $localKeys[0]->columns[0] : $localKeys[0];
76
77 176
        parent::__construct($query, $farParent, $throughParents[0], $firstKey, $foreignKeys[1], $localKey, $localKeys[1]);
78
    }
79
80
    /**
81
     * Get the results of the relationship.
82
     *
83
     * @return mixed
84
     */
85 60
    public function getResults()
86
    {
87 60
        if ($this->firstKey instanceof Closure || $this->localKey instanceof Closure) {
0 ignored issues
show
introduced by
$this->localKey is never a sub-type of Closure.
Loading history...
88 2
            return $this->get();
89
        }
90
91 58
        return parent::getResults();
92
    }
93
94
    /**
95
     * Set the base constraints on the relation query.
96
     *
97
     * @return void
98
     */
99 176
    public function addConstraints()
100
    {
101 176
        if ($this->firstKey instanceof Closure || $this->localKey instanceof Closure) {
0 ignored issues
show
introduced by
$this->localKey is never a sub-type of Closure.
Loading history...
102 28
            $this->performJoin();
103
        } else {
104 148
            parent::addConstraints();
105
        }
106
107 176
        if (static::$constraints) {
108 108
            if ($this->firstKey instanceof Closure) {
0 ignored issues
show
introduced by
$this->firstKey is never a sub-type of Closure.
Loading history...
109 8
                ($this->firstKey)($this->query);
110 100
            } elseif ($this->localKey instanceof Closure) {
0 ignored issues
show
introduced by
$this->localKey is never a sub-type of Closure.
Loading history...
111
                ($this->localKey)($this->query);
112 100
            } elseif (is_array($this->foreignKeys[0])) {
113 4
                $this->query->where(
114 4
                    $this->throughParent->qualifyColumn($this->foreignKeys[0][0]),
115
                    '=',
116 4
                    $this->farParent->getMorphClass()
117
                );
118 96
            } elseif ($this->hasLeadingCompositeKey()) {
119 10
                $this->addConstraintsWithCompositeKey();
120
            }
121
        }
122
    }
123
124
    /**
125
     * Set the join clauses on the query.
126
     *
127
     * @param \Illuminate\Database\Eloquent\Builder|null $query
128
     * @return void
129
     */
130 176
    protected function performJoin(Builder $query = null)
131
    {
132 176
        $query = $query ?: $this->query;
133
134 176
        $throughParents = array_reverse($this->throughParents);
135 176
        $foreignKeys = array_reverse($this->foreignKeys);
136 176
        $localKeys = array_reverse($this->localKeys);
137
138 176
        $segments = explode(' as ', $query->getQuery()->from);
139
140 176
        $alias = $segments[1] ?? null;
141
142 176
        foreach ($throughParents as $i => $throughParent) {
143 176
            $predecessor = $throughParents[$i - 1] ?? $this->related;
144
145 176
            $prefix = $i === 0 && $alias ? $alias.'.' : '';
146
147 176
            $this->joinThroughParent($query, $throughParent, $predecessor, $foreignKeys[$i], $localKeys[$i], $prefix);
148
        }
149
    }
150
151
    /**
152
     * Set the constraints for an eager load of the relation.
153
     *
154
     * @param array $models
155
     * @return void
156
     */
157 36
    public function addEagerConstraints(array $models)
158
    {
159 36
        if ($this->customEagerConstraintsCallback) {
160 10
            ($this->customEagerConstraintsCallback)($this->query, $models);
161 10
            return;
162
        }
163
164 26
        if ($this->hasLeadingCompositeKey()) {
165 4
            $this->addEagerConstraintsWithCompositeKey($models);
166
        } else {
167 22
            parent::addEagerConstraints($models);
168
169 22
            if (is_array($this->foreignKeys[0])) {
170 2
                $this->query->where(
171 2
                    $this->throughParent->qualifyColumn($this->foreignKeys[0][0]),
172
                    '=',
173 2
                    $this->farParent->getMorphClass()
174
                );
175
            }
176
        }
177
    }
178
179
    /**
180
     * Match the eagerly loaded results to their parents.
181
     *
182
     * @param array $models
183
     * @param \Illuminate\Database\Eloquent\Collection $results
184
     * @param string $relation
185
     * @return array
186
     */
187 32
    public function match(array $models, Collection $results, $relation)
188
    {
189 32
        if ($this->customEagerMatchingCallbacks) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customEagerMatchingCallbacks of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
190 10
            foreach ($this->customEagerMatchingCallbacks as $callback) {
191 10
                $models = $callback($models, $results, $relation);
192
            }
193
194 10
            return $models;
195
        }
196
197 22
        if ($this->hasLeadingCompositeKey()) {
198 4
            return $this->matchWithCompositeKey($models, $results, $relation);
199
        }
200
201 18
        return parent::match($models, $results, $relation);
202
    }
203
204
    /**
205
     * Execute the query as a "select" statement.
206
     *
207
     * @param array $columns
208
     * @return \Illuminate\Database\Eloquent\Collection
209
     */
210 130
    public function get($columns = ['*'])
211
    {
212 130
        $models = parent::get($columns);
213
214 130
        $this->hydrateIntermediateRelations($models->all());
215
216 130
        foreach ($this->postGetCallbacks as $postGetCallback) {
217 18
            $postGetCallback($models);
218
        }
219
220 130
        return $models;
221
    }
222
223
    /**
224
     * Get a paginator for the "select" statement.
225
     *
226
     * @param int $perPage
227
     * @param array $columns
228
     * @param string $pageName
229
     * @param int $page
230
     * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
231
     */
232 4
    public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
233
    {
234 4
        $columns = array_filter(
235 4
            $this->shouldSelect($columns),
236 4
            fn ($column) => !str_contains($column, 'laravel_through_key')
237
        );
238
239 4
        $this->query->addSelect($columns);
240
241 4
        return tap($this->query->paginate($perPage, $columns, $pageName, $page), function (Paginator $paginator) {
242 4
            $this->hydrateIntermediateRelations($paginator->items());
243
        });
244
    }
245
246
    /**
247
     * Paginate the given query into a simple paginator.
248
     *
249
     * @param int $perPage
250
     * @param array $columns
251
     * @param string $pageName
252
     * @param int|null $page
253
     * @return \Illuminate\Contracts\Pagination\Paginator
254
     */
255 4
    public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
256
    {
257 4
        $columns = array_filter(
258 4
            $this->shouldSelect($columns),
259 4
            fn ($column) => !str_contains($column, 'laravel_through_key')
260
        );
261
262 4
        $this->query->addSelect($columns);
263
264 4
        return tap($this->query->simplePaginate($perPage, $columns, $pageName, $page), function (Paginator $paginator) {
265 4
            $this->hydrateIntermediateRelations($paginator->items());
266
        });
267
    }
268
269
    /**
270
     * Paginate the given query into a cursor paginator.
271
     *
272
     * @param  int|null  $perPage
273
     * @param  array  $columns
274
     * @param  string  $cursorName
275
     * @param  string|null  $cursor
276
     * @return \Illuminate\Contracts\Pagination\CursorPaginator
277
     */
278 4
    public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
279
    {
280 4
        $columns = array_filter(
281 4
            $this->shouldSelect($columns),
282 4
            fn ($column) => !str_contains($column, 'laravel_through_key')
283
        );
284
285 4
        $this->query->addSelect($columns);
286
287 4
        return tap($this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor), function (CursorPaginator $paginator) {
288 4
            $this->hydrateIntermediateRelations($paginator->items());
289
        });
290
    }
291
292
    /**
293
     * Set the select clause for the relation query.
294
     *
295
     * @param array $columns
296
     * @return array
297
     */
298 144
    protected function shouldSelect(array $columns = ['*'])
299
    {
300 144
        if ($columns == ['*']) {
301 144
            $columns = [$this->related->getTable().'.*'];
302
        }
303
304 144
        $alias = 'laravel_through_key';
305
306 144
        if ($this->customThroughKeyCallback) {
307 18
            $columns[] = ($this->customThroughKeyCallback)($alias);
308
        } else {
309 126
            $columns[] = $this->getQualifiedFirstKeyName() . " as $alias";
310
        }
311
312 144
        if ($this->hasLeadingCompositeKey()) {
313 14
            $columns = array_merge(
314
                $columns,
315 14
                $this->shouldSelectWithCompositeKey()
316
            );
317
        }
318
319 144
        return array_merge($columns, $this->intermediateColumns());
320
    }
321
322
    /**
323
     * Chunk the results of the query.
324
     *
325
     * @param int $count
326
     * @param callable $callback
327
     * @return bool
328
     */
329 2
    public function chunk($count, callable $callback)
330
    {
331 2
        return $this->prepareQueryBuilder()->chunk($count, function (Collection $results) use ($callback) {
332 2
            $this->hydrateIntermediateRelations($results->all());
333
334 2
            return $callback($results);
335
        });
336
    }
337
338
    /**
339
     * Add the constraints for a relationship query.
340
     *
341
     * @param \Illuminate\Database\Eloquent\Builder $query
342
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
343
     * @param array|mixed $columns
344
     * @return \Illuminate\Database\Eloquent\Builder
345
     */
346 32
    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
347
    {
348 32
        foreach ($this->throughParents as $throughParent) {
349 32
            if ($throughParent->getTable() === $parentQuery->getQuery()->from) {
350 9
                if (!in_array(HasTableAlias::class, class_uses_recursive($throughParent))) {
351 2
                    $traitClass = HasTableAlias::class;
352 2
                    $parentClass = get_class($throughParent);
353
354 2
                    throw new Exception(
355
                        <<<EOT
356
This query requires an additional trait. Please add the $traitClass trait to $parentClass.
357
See https://github.com/staudenmeir/eloquent-has-many-deep/issues/137 for details.
358
EOT
359
                    );
360
                }
361
362 7
                $table = $throughParent->getTable() . ' as ' . $this->getRelationCountHash();
363
364 7
                $throughParent->setTable($table);
365
366 7
                break;
367
            }
368
        }
369
370 30
        if ($this->firstKey instanceof Closure || $this->localKey instanceof Closure) {
0 ignored issues
show
introduced by
$this->localKey is never a sub-type of Closure.
Loading history...
371 10
            $this->performJoin($query);
372
373 10
            $closureKey = $this->firstKey instanceof Closure ? $this->firstKey : $this->localKey;
374
375 10
            $closureKey($query, $parentQuery);
376
377 10
            return $query->select($columns);
378
        }
379
380 20
        $query = parent::getRelationExistenceQuery($query, $parentQuery, $columns);
381
382 20
        if (is_array($this->foreignKeys[0])) {
383 4
            $column = $this->throughParent->qualifyColumn($this->foreignKeys[0][0]);
384
385 4
            $query->where($column, '=', $this->farParent->getMorphClass());
386 16
        } elseif ($this->hasLeadingCompositeKey()) {
387 2
            $this->getRelationExistenceQueryWithCompositeKey($query);
388
        }
389
390 20
        return $query;
391
    }
392
393
    /**
394
     * Add the constraints for a relationship query on the same table.
395
     *
396
     * @param \Illuminate\Database\Eloquent\Builder $query
397
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
398
     * @param array|mixed $columns
399
     * @return \Illuminate\Database\Eloquent\Builder
400
     */
401 4
    public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
402
    {
403 4
        $hash = $this->getRelationCountHash();
404
405 4
        $query->from($query->getModel()->getTable().' as '.$hash);
406
407 4
        $this->performJoin($query);
408
409 4
        $query->getModel()->setTable($hash);
410
411 4
        return $query->select($columns)->whereColumn(
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->select($c...ualifiedFirstKeyName()) also could return the type Illuminate\Database\Query\Builder which is incompatible with the documented return type Illuminate\Database\Eloquent\Builder.
Loading history...
412 4
            $parentQuery->getQuery()->from.'.'.$this->localKey,
413
            '=',
414 4
            $this->getQualifiedFirstKeyName()
415
        );
416
    }
417
418
    /**
419
     * Restore soft-deleted models.
420
     *
421
     * @param array|string ...$columns
422
     * @return $this
423
     */
424 14
    public function withTrashed(...$columns)
425
    {
426 14
        if (empty($columns)) {
427 4
            $this->query->withTrashed();
428
429 4
            return $this;
430
        }
431
432 10
        if (is_array($columns[0])) {
433 2
            $columns = $columns[0];
434
        }
435
436 10
        foreach ($columns as $column) {
437 10
            $this->query->withoutGlobalScope(__CLASS__ . ":$column");
438
        }
439
440 10
        return $this;
441
    }
442
443
    /**
444
     * Get the far parent model instance.
445
     *
446
     * @return \Illuminate\Database\Eloquent\Model
447
     */
448 10
    public function getFarParent(): Model
449
    {
450 10
        return $this->farParent;
451
    }
452
453
    /**
454
     * Get the "through" parent model instances.
455
     *
456
     * @return \Illuminate\Database\Eloquent\Model[]
457
     */
458 10
    public function getThroughParents()
459
    {
460 10
        return $this->throughParents;
461
    }
462
463
    /**
464
     * Get the foreign keys on the relationship.
465
     *
466
     * @return array
467
     */
468 10
    public function getForeignKeys()
469
    {
470 10
        return $this->foreignKeys;
471
    }
472
473
    /**
474
     * Get the local keys on the relationship.
475
     *
476
     * @return array
477
     */
478 10
    public function getLocalKeys()
479
    {
480 10
        return $this->localKeys;
481
    }
482
}
483