Passed
Push — master ( 10e381...0f718c )
by Jonas
01:58
created

HasManyDeep::getThroughParentsJoin()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 13
c 0
b 0
f 0
nc 6
nop 5
dl 0
loc 25
ccs 13
cts 13
cp 1
crap 5
rs 9.5222
1
<?php
2
3
namespace Staudenmeir\EloquentHasManyDeep;
4
5
use Exception;
6
use Illuminate\Contracts\Pagination\Paginator;
7
use Illuminate\Database\Eloquent\Builder;
8
use Illuminate\Database\Eloquent\Collection;
9
use Illuminate\Database\Eloquent\Model;
10
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
11
use Illuminate\Database\Eloquent\SoftDeletes;
12
use Illuminate\Database\Query\JoinClause;
13
use Illuminate\Pagination\CursorPaginator;
14
use Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey;
15
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\HasEagerLimit;
16
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\RetrievesIntermediateTables;
17
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\SupportsCompositeKeys;
18
19
/**
20
 * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
21
 * @extends \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel>
22
 */
23
class HasManyDeep extends HasManyThrough
24
{
25
    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...
26
    use RetrievesIntermediateTables;
27
    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...
28
29
    /**
30
     * The "through" parent model instances.
31
     *
32
     * @var \Illuminate\Database\Eloquent\Model[]
33
     */
34
    protected $throughParents;
35
36
    /**
37
     * The foreign keys on the relationship.
38
     *
39
     * @var array
40
     */
41
    protected $foreignKeys;
42
43
    /**
44
     * The local keys on the relationship.
45
     *
46
     * @var array
47
     */
48
    protected $localKeys;
49
50
    /**
51
     * Create a new has many deep relationship instance.
52
     *
53
     * @param \Illuminate\Database\Eloquent\Builder $query
54
     * @param \Illuminate\Database\Eloquent\Model $farParent
55
     * @param \Illuminate\Database\Eloquent\Model[] $throughParents
56
     * @param array $foreignKeys
57
     * @param array $localKeys
58
     * @return void
59
     */
60 73
    public function __construct(Builder $query, Model $farParent, array $throughParents, array $foreignKeys, array $localKeys)
61
    {
62 73
        $this->throughParents = $throughParents;
63 73
        $this->foreignKeys = $foreignKeys;
64 73
        $this->localKeys = $localKeys;
65
66 73
        $firstKey = is_array($foreignKeys[0])
67 5
            ? $foreignKeys[0][1]
68 68
            : ($this->hasLeadingCompositeKey() ? $foreignKeys[0]->columns[0] : $foreignKeys[0]);
69
70 73
        $localKey = $this->hasLeadingCompositeKey() ? $localKeys[0]->columns[0] : $localKeys[0];
71
72 73
        parent::__construct($query, $farParent, $throughParents[0], $firstKey, $foreignKeys[1], $localKey, $localKeys[1]);
73
    }
74
75
    /**
76
     * Set the base constraints on the relation query.
77
     *
78
     * @return void
79
     */
80 73
    public function addConstraints()
81
    {
82 73
        parent::addConstraints();
83
84 73
        if (static::$constraints) {
85 49
            if (is_array($this->foreignKeys[0])) {
86 2
                $this->query->where(
87 2
                    $this->throughParent->qualifyColumn($this->foreignKeys[0][0]),
88
                    '=',
89 2
                    $this->farParent->getMorphClass()
90
                );
91 47
            } elseif ($this->hasLeadingCompositeKey()) {
92 5
                $this->addConstraintsWithCompositeKey();
93
            }
94
        }
95
    }
96
97
    /**
98
     * Set the join clauses on the query.
99
     *
100
     * @param \Illuminate\Database\Eloquent\Builder|null $query
101
     * @return void
102
     */
103 73
    protected function performJoin(Builder $query = null)
104
    {
105 73
        $query = $query ?: $this->query;
106
107 73
        $throughParents = array_reverse($this->throughParents);
108 73
        $foreignKeys = array_reverse($this->foreignKeys);
109 73
        $localKeys = array_reverse($this->localKeys);
110
111 73
        $segments = explode(' as ', $query->getQuery()->from);
112
113 73
        $alias = $segments[1] ?? null;
114
115 73
        foreach ($throughParents as $i => $throughParent) {
116 73
            $predecessor = $throughParents[$i - 1] ?? $this->related;
117
118 73
            $prefix = $i === 0 && $alias ? $alias.'.' : '';
119
120 73
            $this->joinThroughParent($query, $throughParent, $predecessor, $foreignKeys[$i], $localKeys[$i], $prefix);
121
        }
122
    }
123
124
    /**
125
     * Join a through parent table.
126
     *
127
     * @param \Illuminate\Database\Eloquent\Builder $query
128
     * @param \Illuminate\Database\Eloquent\Model $throughParent
129
     * @param \Illuminate\Database\Eloquent\Model $predecessor
130
     * @param \Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey|array|string $foreignKey
131
     * @param \Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey|array|string $localKey
132
     * @param string $prefix
133
     * @return void
134
     */
135 73
    protected function joinThroughParent(Builder $query, Model $throughParent, Model $predecessor, $foreignKey, $localKey, $prefix)
136
    {
137 73
        $joins = $this->getThroughParentsJoin($query, $throughParent, $predecessor, $foreignKey, $localKey);
138
139 73
        foreach ($joins as $i => [$first, $second]) {
140 73
            $joins[$i] = [
141 73
                $throughParent->qualifyColumn($first),
142 73
                $predecessor->qualifyColumn($prefix.$second),
143
            ];
144
        }
145
146 73
        $query->join(
147 73
            $throughParent->getTable(),
148 73
            function (JoinClause $join) use ($joins) {
149 73
                foreach ($joins as [$first, $second]) {
150 73
                    $join->on($first, '=', $second);
151
                }
152
            }
153
        );
154
155 73
        if ($this->throughParentInstanceSoftDeletes($throughParent)) {
156 41
            $column= $throughParent->getQualifiedDeletedAtColumn();
157
158 41
            $query->withGlobalScope(__CLASS__ . ":$column", function (Builder $query) use ($column) {
159 35
                $query->whereNull($column);
160
            });
161
        }
162
    }
163
164
    /**
165
     * Get the joins for a through parent table.
166
     *
167
     * @param \Illuminate\Database\Eloquent\Builder $query
168
     * @param \Illuminate\Database\Eloquent\Model $throughParent
169
     * @param \Illuminate\Database\Eloquent\Model $predecessor
170
     * @param \Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey|array|string $foreignKey
171
     * @param \Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey|array|string $localKey
172
     * @return array
173
     */
174 73
    protected function getThroughParentsJoin(Builder $query, Model $throughParent, Model $predecessor, $foreignKey, $localKey): array
175
    {
176 73
        $joins = [];
177
178 73
        if ($localKey instanceof CompositeKey) {
179 5
            foreach ($localKey->columns as $i => $column) {
180 5
                $joins[] = [$column, $foreignKey->columns[$i]];
181
            }
182
        } else {
183 68
            if (is_array($localKey)) {
184 5
                $query->where($throughParent->qualifyColumn($localKey[0]), '=', $predecessor->getMorphClass());
185
186 5
                $localKey = $localKey[1];
187
            }
188
189 68
            if (is_array($foreignKey)) {
190 5
                $query->where($predecessor->qualifyColumn($foreignKey[0]), '=', $throughParent->getMorphClass());
191
192 5
                $foreignKey = $foreignKey[1];
193
            }
194
195 68
            $joins[] = [$localKey, $foreignKey];
196
        }
197
198 73
        return $joins;
199
    }
200
201
    /**
202
     * Determine whether a "through" parent instance of the relation uses SoftDeletes.
203
     *
204
     * @param \Illuminate\Database\Eloquent\Model $instance
205
     * @return bool
206
     */
207 73
    public function throughParentInstanceSoftDeletes(Model $instance)
208
    {
209 73
        return in_array(SoftDeletes::class, class_uses_recursive($instance));
210
    }
211
212
    /**
213
     * Set the constraints for an eager load of the relation.
214
     *
215
     * @param array $models
216
     * @return void
217
     */
218 13
    public function addEagerConstraints(array $models)
219
    {
220 13
        if ($this->hasLeadingCompositeKey()) {
221 2
            $this->addEagerConstraintsWithCompositeKey($models);
222
        } else {
223 11
            parent::addEagerConstraints($models);
224
225 11
            if (is_array($this->foreignKeys[0])) {
226 1
                $this->query->where(
227 1
                    $this->throughParent->qualifyColumn($this->foreignKeys[0][0]),
228
                    '=',
229 1
                    $this->farParent->getMorphClass()
230
                );
231
            }
232
        }
233
    }
234
235
    /**
236
     * Match the eagerly loaded results to their parents.
237
     *
238
     * @param array $models
239
     * @param \Illuminate\Database\Eloquent\Collection $results
240
     * @param string $relation
241
     * @return array
242
     */
243 11
    public function match(array $models, Collection $results, $relation)
244
    {
245 11
        if ($this->hasLeadingCompositeKey()) {
246 2
            return $this->matchWithCompositeKey($models, $results, $relation);
247
        }
248
249 9
        return parent::match($models, $results, $relation);
250
    }
251
252
    /**
253
     * Execute the query as a "select" statement.
254
     *
255
     * @param array $columns
256
     * @return \Illuminate\Database\Eloquent\Collection
257
     */
258 55
    public function get($columns = ['*'])
259
    {
260 55
        $models = parent::get($columns);
261
262 55
        $this->hydrateIntermediateRelations($models->all());
263
264 55
        return $models;
265
    }
266
267
    /**
268
     * Get a paginator for the "select" statement.
269
     *
270
     * @param int $perPage
271
     * @param array $columns
272
     * @param string $pageName
273
     * @param int $page
274
     * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
275
     */
276 2
    public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
277
    {
278 2
        $columns = array_filter(
279 2
            $this->shouldSelect($columns),
280 2
            fn ($column) => !str_contains($column, ' as laravel_through_key')
281
        );
282
283 2
        $this->query->addSelect($columns);
284
285 2
        return tap($this->query->paginate($perPage, $columns, $pageName, $page), function (Paginator $paginator) {
286 2
            $this->hydrateIntermediateRelations($paginator->items());
287
        });
288
    }
289
290
    /**
291
     * Paginate the given query into a simple paginator.
292
     *
293
     * @param int $perPage
294
     * @param array $columns
295
     * @param string $pageName
296
     * @param int|null $page
297
     * @return \Illuminate\Contracts\Pagination\Paginator
298
     */
299 2
    public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
300
    {
301 2
        $columns = array_filter(
302 2
            $this->shouldSelect($columns),
303 2
            fn ($column) => !str_contains($column, ' as laravel_through_key')
304
        );
305
306 2
        $this->query->addSelect($columns);
307
308 2
        return tap($this->query->simplePaginate($perPage, $columns, $pageName, $page), function (Paginator $paginator) {
309 2
            $this->hydrateIntermediateRelations($paginator->items());
310
        });
311
    }
312
313
    /**
314
     * Paginate the given query into a cursor paginator.
315
     *
316
     * @param  int|null  $perPage
317
     * @param  array  $columns
318
     * @param  string  $cursorName
319
     * @param  string|null  $cursor
320
     * @return \Illuminate\Contracts\Pagination\CursorPaginator
321
     */
322 2
    public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
323
    {
324 2
        $columns = array_filter(
325 2
            $this->shouldSelect($columns),
326 2
            fn ($column) => !str_contains($column, ' as laravel_through_key')
327
        );
328
329 2
        $this->query->addSelect($columns);
330
331 2
        return tap($this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor), function (CursorPaginator $paginator) {
332 2
            $this->hydrateIntermediateRelations($paginator->items());
333
        });
334
    }
335
336
    /**
337
     * Set the select clause for the relation query.
338
     *
339
     * @param array $columns
340
     * @return array
341
     */
342 62
    protected function shouldSelect(array $columns = ['*'])
343
    {
344 62
        $columns = parent::shouldSelect($columns);
345
346 62
        if ($this->hasLeadingCompositeKey()) {
347 7
            $columns = array_merge(
348
                $columns,
349 7
                $this->shouldSelectWithCompositeKey()
350
            );
351
        }
352
353 62
        return array_merge($columns, $this->intermediateColumns());
354
    }
355
356
    /**
357
     * Chunk the results of the query.
358
     *
359
     * @param int $count
360
     * @param callable $callback
361
     * @return bool
362
     */
363 1
    public function chunk($count, callable $callback)
364
    {
365 1
        return $this->prepareQueryBuilder()->chunk($count, function (Collection $results) use ($callback) {
366 1
            $this->hydrateIntermediateRelations($results->all());
367
368 1
            return $callback($results);
369
        });
370
    }
371
372
    /**
373
     * Add the constraints for a relationship query.
374
     *
375
     * @param \Illuminate\Database\Eloquent\Builder $query
376
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
377
     * @param array|mixed $columns
378
     * @return \Illuminate\Database\Eloquent\Builder
379
     */
380 11
    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
381
    {
382 11
        foreach ($this->throughParents as $throughParent) {
383 11
            if ($throughParent->getTable() === $parentQuery->getQuery()->from) {
384 2
                if (!in_array(HasTableAlias::class, class_uses_recursive($throughParent))) {
385 1
                    $traitClass = HasTableAlias::class;
386 1
                    $parentClass = get_class($throughParent);
387
388 1
                    throw new Exception(
389
                        <<<EOT
390
This query requires an additional trait. Please add the $traitClass trait to $parentClass.
391
See https://github.com/staudenmeir/eloquent-has-many-deep/issues/137 for details.
392
EOT
393
                    );
394
                }
395
396 1
                $table = $throughParent->getTable() . ' as ' . $this->getRelationCountHash();
397
398 1
                $throughParent->setTable($table);
399
400 1
                break;
401
            }
402
        }
403
404 10
        $query = parent::getRelationExistenceQuery($query, $parentQuery, $columns);
405
406 10
        if (is_array($this->foreignKeys[0])) {
407 2
            $column = $this->throughParent->qualifyColumn($this->foreignKeys[0][0]);
408
409 2
            $query->where($column, '=', $this->farParent->getMorphClass());
410 8
        } elseif ($this->hasLeadingCompositeKey()) {
411 1
            $this->getRelationExistenceQueryWithCompositeKey($query);
412
        }
413
414 10
        return $query;
415
    }
416
417
    /**
418
     * Add the constraints for a relationship query on the same table.
419
     *
420
     * @param \Illuminate\Database\Eloquent\Builder $query
421
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
422
     * @param array|mixed $columns
423
     * @return \Illuminate\Database\Eloquent\Builder
424
     */
425 2
    public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
426
    {
427 2
        $hash = $this->getRelationCountHash();
428
429 2
        $query->from($query->getModel()->getTable().' as '.$hash);
430
431 2
        $this->performJoin($query);
432
433 2
        $query->getModel()->setTable($hash);
434
435 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...
436 2
            $parentQuery->getQuery()->from.'.'.$this->localKey,
437
            '=',
438 2
            $this->getQualifiedFirstKeyName()
439
        );
440
    }
441
442
    /**
443
     * Restore soft-deleted models.
444
     *
445
     * @param array|string ...$columns
446
     * @return $this
447
     */
448 7
    public function withTrashed(...$columns)
449
    {
450 7
        if (empty($columns)) {
451 2
            $this->query->withTrashed();
452
453 2
            return $this;
454
        }
455
456 5
        if (is_array($columns[0])) {
457 1
            $columns = $columns[0];
458
        }
459
460 5
        foreach ($columns as $column) {
461 5
            $this->query->withoutGlobalScope(__CLASS__ . ":$column");
462
        }
463
464 5
        return $this;
465
    }
466
467
    /**
468
     * Get the far parent model instance.
469
     *
470
     * @return \Illuminate\Database\Eloquent\Model
471
     */
472 5
    public function getFarParent(): Model
473
    {
474 5
        return $this->farParent;
475
    }
476
477
    /**
478
     * Get the "through" parent model instances.
479
     *
480
     * @return \Illuminate\Database\Eloquent\Model[]
481
     */
482 8
    public function getThroughParents()
483
    {
484 8
        return $this->throughParents;
485
    }
486
487
    /**
488
     * Get the foreign keys on the relationship.
489
     *
490
     * @return array
491
     */
492 8
    public function getForeignKeys()
493
    {
494 8
        return $this->foreignKeys;
495
    }
496
497
    /**
498
     * Get the local keys on the relationship.
499
     *
500
     * @return array
501
     */
502 8
    public function getLocalKeys()
503
    {
504 8
        return $this->localKeys;
505
    }
506
}
507