Passed
Push — master ( 6cf215...65a336 )
by Jonas
12:16
created

HasManyDeep::addConstraints()   B

Complexity

Conditions 8
Paths 12

Size

Total Lines 21
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 8.512

Importance

Changes 0
Metric Value
cc 8
eloc 16
nc 12
nop 0
dl 0
loc 21
ccs 12
cts 15
cp 0.8
crap 8.512
rs 8.4444
c 0
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 74
    public function __construct(Builder $query, Model $farParent, array $throughParents, array $foreignKeys, array $localKeys)
66
    {
67 74
        $this->throughParents = $throughParents;
68 74
        $this->foreignKeys = $foreignKeys;
69 74
        $this->localKeys = $localKeys;
70
71 74
        $firstKey = is_array($foreignKeys[0])
72 5
            ? $foreignKeys[0][1]
73 69
            : ($this->hasLeadingCompositeKey() ? $foreignKeys[0]->columns[0] : $foreignKeys[0]);
74
75 74
        $localKey = $this->hasLeadingCompositeKey() ? $localKeys[0]->columns[0] : $localKeys[0];
76
77 74
        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 29
    public function getResults()
86
    {
87 29
        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
            return $this->get();
89
        }
90
91 29
        return parent::getResults();
92
    }
93
94
    /**
95
     * Set the base constraints on the relation query.
96
     *
97
     * @return void
98
     */
99 74
    public function addConstraints()
100
    {
101 74
        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
            $this->performJoin();
103
        } else {
104 74
            parent::addConstraints();
105
        }
106
107 74
        if (static::$constraints) {
108 50
            if ($this->firstKey instanceof Closure) {
0 ignored issues
show
introduced by
$this->firstKey is never a sub-type of Closure.
Loading history...
109
                ($this->firstKey)($this->query);
110 50
            } 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 50
            } elseif (is_array($this->foreignKeys[0])) {
113 2
                $this->query->where(
114 2
                    $this->throughParent->qualifyColumn($this->foreignKeys[0][0]),
115
                    '=',
116 2
                    $this->farParent->getMorphClass()
117
                );
118 48
            } elseif ($this->hasLeadingCompositeKey()) {
119 5
                $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 74
    protected function performJoin(Builder $query = null)
131
    {
132 74
        $query = $query ?: $this->query;
133
134 74
        $throughParents = array_reverse($this->throughParents);
135 74
        $foreignKeys = array_reverse($this->foreignKeys);
136 74
        $localKeys = array_reverse($this->localKeys);
137
138 74
        $segments = explode(' as ', $query->getQuery()->from);
139
140 74
        $alias = $segments[1] ?? null;
141
142 74
        foreach ($throughParents as $i => $throughParent) {
143 74
            $predecessor = $throughParents[$i - 1] ?? $this->related;
144
145 74
            $prefix = $i === 0 && $alias ? $alias.'.' : '';
146
147 74
            $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 13
    public function addEagerConstraints(array $models)
158
    {
159 13
        if ($this->customEagerConstraintsCallback) {
160
            ($this->customEagerConstraintsCallback)($this->query, $models);
161
            return;
162
        }
163
164 13
        if ($this->hasLeadingCompositeKey()) {
165 2
            $this->addEagerConstraintsWithCompositeKey($models);
166
        } else {
167 11
            parent::addEagerConstraints($models);
168
169 11
            if (is_array($this->foreignKeys[0])) {
170 1
                $this->query->where(
171 1
                    $this->throughParent->qualifyColumn($this->foreignKeys[0][0]),
172
                    '=',
173 1
                    $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 11
    public function match(array $models, Collection $results, $relation)
188
    {
189 11
        if ($this->customEagerMatchingCallbacks) {
190
            foreach ($this->customEagerMatchingCallbacks as $callback) {
191
                $models = $callback($models, $results, $relation);
192
            }
193
194
            return $models;
195
        }
196
197 11
        if ($this->hasLeadingCompositeKey()) {
198 2
            return $this->matchWithCompositeKey($models, $results, $relation);
199
        }
200
201 9
        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 56
    public function get($columns = ['*'])
211
    {
212 56
        $models = parent::get($columns);
213
214 56
        $this->hydrateIntermediateRelations($models->all());
215
216 56
        foreach ($this->postGetCallbacks as $postGetCallback) {
217
            $postGetCallback($models);
218
        }
219
220 56
        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 2
    public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
233
    {
234 2
        $columns = array_filter(
235 2
            $this->shouldSelect($columns),
236 2
            fn ($column) => !str_contains($column, 'laravel_through_key')
237
        );
238
239 2
        $this->query->addSelect($columns);
240
241 2
        return tap($this->query->paginate($perPage, $columns, $pageName, $page), function (Paginator $paginator) {
242 2
            $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 2
    public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
256
    {
257 2
        $columns = array_filter(
258 2
            $this->shouldSelect($columns),
259 2
            fn ($column) => !str_contains($column, 'laravel_through_key')
260
        );
261
262 2
        $this->query->addSelect($columns);
263
264 2
        return tap($this->query->simplePaginate($perPage, $columns, $pageName, $page), function (Paginator $paginator) {
265 2
            $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 2
    public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
279
    {
280 2
        $columns = array_filter(
281 2
            $this->shouldSelect($columns),
282 2
            fn ($column) => !str_contains($column, 'laravel_through_key')
283
        );
284
285 2
        $this->query->addSelect($columns);
286
287 2
        return tap($this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor), function (CursorPaginator $paginator) {
288 2
            $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 63
    protected function shouldSelect(array $columns = ['*'])
299
    {
300 63
        if ($columns == ['*']) {
301 63
            $columns = [$this->related->getTable().'.*'];
302
        }
303
304 63
        $alias = 'laravel_through_key';
305
306 63
        if ($this->customThroughKeyCallback) {
307
            $columns[] = ($this->customThroughKeyCallback)($alias);
308
        } else {
309 63
            $columns[] = $this->getQualifiedFirstKeyName() . " as $alias";
310
        }
311
312 63
        if ($this->hasLeadingCompositeKey()) {
313 7
            $columns = array_merge(
314
                $columns,
315 7
                $this->shouldSelectWithCompositeKey()
316
            );
317
        }
318
319 63
        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 1
    public function chunk($count, callable $callback)
330
    {
331 1
        return $this->prepareQueryBuilder()->chunk($count, function (Collection $results) use ($callback) {
332 1
            $this->hydrateIntermediateRelations($results->all());
333
334 1
            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 11
    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
347
    {
348 11
        foreach ($this->throughParents as $throughParent) {
349 11
            if ($throughParent->getTable() === $parentQuery->getQuery()->from) {
350 2
                if (!in_array(HasTableAlias::class, class_uses_recursive($throughParent))) {
351 1
                    $traitClass = HasTableAlias::class;
352 1
                    $parentClass = get_class($throughParent);
353
354 1
                    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 1
                $table = $throughParent->getTable() . ' as ' . $this->getRelationCountHash();
363
364 1
                $throughParent->setTable($table);
365
366 1
                break;
367
            }
368
        }
369
370 10
        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
            $this->performJoin($query);
372
373
            $closureKey = $this->firstKey instanceof Closure ? $this->firstKey : $this->localKey;
374
375
            $closureKey($query, $parentQuery);
376
377
            return $query->select($columns);
378
        }
379
380 10
        $query = parent::getRelationExistenceQuery($query, $parentQuery, $columns);
381
382 10
        if (is_array($this->foreignKeys[0])) {
383 2
            $column = $this->throughParent->qualifyColumn($this->foreignKeys[0][0]);
384
385 2
            $query->where($column, '=', $this->farParent->getMorphClass());
386 8
        } elseif ($this->hasLeadingCompositeKey()) {
387 1
            $this->getRelationExistenceQueryWithCompositeKey($query);
388
        }
389
390 10
        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 2
    public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
402
    {
403 2
        $hash = $this->getRelationCountHash();
404
405 2
        $query->from($query->getModel()->getTable().' as '.$hash);
406
407 2
        $this->performJoin($query);
408
409 2
        $query->getModel()->setTable($hash);
410
411 2
        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 2
            $parentQuery->getQuery()->from.'.'.$this->localKey,
413
            '=',
414 2
            $this->getQualifiedFirstKeyName()
415
        );
416
    }
417
418
    /**
419
     * Restore soft-deleted models.
420
     *
421
     * @param array|string ...$columns
422
     * @return $this
423
     */
424 7
    public function withTrashed(...$columns)
425
    {
426 7
        if (empty($columns)) {
427 2
            $this->query->withTrashed();
428
429 2
            return $this;
430
        }
431
432 5
        if (is_array($columns[0])) {
433 1
            $columns = $columns[0];
434
        }
435
436 5
        foreach ($columns as $column) {
437 5
            $this->query->withoutGlobalScope(__CLASS__ . ":$column");
438
        }
439
440 5
        return $this;
441
    }
442
443
    /**
444
     * Get the far parent model instance.
445
     *
446
     * @return \Illuminate\Database\Eloquent\Model
447
     */
448 5
    public function getFarParent(): Model
449
    {
450 5
        return $this->farParent;
451
    }
452
453
    /**
454
     * Get the "through" parent model instances.
455
     *
456
     * @return \Illuminate\Database\Eloquent\Model[]
457
     */
458 5
    public function getThroughParents()
459
    {
460 5
        return $this->throughParents;
461
    }
462
463
    /**
464
     * Get the foreign keys on the relationship.
465
     *
466
     * @return array
467
     */
468 5
    public function getForeignKeys()
469
    {
470 5
        return $this->foreignKeys;
471
    }
472
473
    /**
474
     * Get the local keys on the relationship.
475
     *
476
     * @return array
477
     */
478 5
    public function getLocalKeys()
479
    {
480 5
        return $this->localKeys;
481
    }
482
}
483