Completed
Push — master ( d58822...10e381 )
by Jonas
15:56
created

HasManyDeep::match()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 3
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 2
rs 10
c 0
b 0
f 0
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 = [];
138
139 73
        if ($localKey instanceof CompositeKey) {
140 5
            foreach ($localKey->columns as $i => $column) {
141 5
                $joins[] = [$column, $foreignKey->columns[$i]];
142
            }
143
        } else {
144 68
            if (is_array($localKey)) {
145 5
                $query->where($throughParent->qualifyColumn($localKey[0]), '=', $predecessor->getMorphClass());
146
147 5
                $localKey = $localKey[1];
148
            }
149
150 68
            if (is_array($foreignKey)) {
151 5
                $query->where($predecessor->qualifyColumn($foreignKey[0]), '=', $throughParent->getMorphClass());
152
153 5
                $foreignKey = $foreignKey[1];
154
            }
155
156 68
            $joins[] = [$localKey, $foreignKey];
157
        }
158
159 73
        foreach ($joins as $i => [$first, $second]) {
160 73
            $joins[$i] = [
161 73
                $throughParent->qualifyColumn($first),
162 73
                $predecessor->qualifyColumn($prefix.$second),
163
            ];
164
        }
165
166 73
        $query->join(
167 73
            $throughParent->getTable(),
168 73
            function (JoinClause $join) use ($joins) {
169 73
                foreach ($joins as [$first, $second]) {
170 73
                    $join->on($first, '=', $second);
171
                }
172
            }
173
        );
174
175 73
        if ($this->throughParentInstanceSoftDeletes($throughParent)) {
176 41
            $column= $throughParent->getQualifiedDeletedAtColumn();
177
178 41
            $query->withGlobalScope(__CLASS__ . ":$column", function (Builder $query) use ($column) {
179 35
                $query->whereNull($column);
180
            });
181
        }
182
    }
183
184
    /**
185
     * Determine whether a "through" parent instance of the relation uses SoftDeletes.
186
     *
187
     * @param \Illuminate\Database\Eloquent\Model $instance
188
     * @return bool
189
     */
190 73
    public function throughParentInstanceSoftDeletes(Model $instance)
191
    {
192 73
        return in_array(SoftDeletes::class, class_uses_recursive($instance));
193
    }
194
195
    /**
196
     * Set the constraints for an eager load of the relation.
197
     *
198
     * @param array $models
199
     * @return void
200
     */
201 13
    public function addEagerConstraints(array $models)
202
    {
203 13
        if ($this->hasLeadingCompositeKey()) {
204 2
            $this->addEagerConstraintsWithCompositeKey($models);
205
        } else {
206 11
            parent::addEagerConstraints($models);
207
208 11
            if (is_array($this->foreignKeys[0])) {
209 1
                $this->query->where(
210 1
                    $this->throughParent->qualifyColumn($this->foreignKeys[0][0]),
211
                    '=',
212 1
                    $this->farParent->getMorphClass()
213
                );
214
            }
215
        }
216
    }
217
218
    /**
219
     * Match the eagerly loaded results to their parents.
220
     *
221
     * @param array $models
222
     * @param \Illuminate\Database\Eloquent\Collection $results
223
     * @param string $relation
224
     * @return array
225
     */
226 11
    public function match(array $models, Collection $results, $relation)
227
    {
228 11
        if ($this->hasLeadingCompositeKey()) {
229 2
            return $this->matchWithCompositeKey($models, $results, $relation);
230
        }
231
232 9
        return parent::match($models, $results, $relation);
233
    }
234
235
    /**
236
     * Execute the query as a "select" statement.
237
     *
238
     * @param array $columns
239
     * @return \Illuminate\Database\Eloquent\Collection
240
     */
241 55
    public function get($columns = ['*'])
242
    {
243 55
        $models = parent::get($columns);
244
245 55
        $this->hydrateIntermediateRelations($models->all());
246
247 55
        return $models;
248
    }
249
250
    /**
251
     * Get a paginator for the "select" statement.
252
     *
253
     * @param int $perPage
254
     * @param array $columns
255
     * @param string $pageName
256
     * @param int $page
257
     * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
258
     */
259 2
    public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
260
    {
261 2
        $columns = array_filter(
262 2
            $this->shouldSelect($columns),
263 2
            fn ($column) => !str_contains($column, ' as laravel_through_key')
264
        );
265
266 2
        $this->query->addSelect($columns);
267
268 2
        return tap($this->query->paginate($perPage, $columns, $pageName, $page), function (Paginator $paginator) {
269 2
            $this->hydrateIntermediateRelations($paginator->items());
270
        });
271
    }
272
273
    /**
274
     * Paginate the given query into a simple paginator.
275
     *
276
     * @param int $perPage
277
     * @param array $columns
278
     * @param string $pageName
279
     * @param int|null $page
280
     * @return \Illuminate\Contracts\Pagination\Paginator
281
     */
282 2
    public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
283
    {
284 2
        $columns = array_filter(
285 2
            $this->shouldSelect($columns),
286 2
            fn ($column) => !str_contains($column, ' as laravel_through_key')
287
        );
288
289 2
        $this->query->addSelect($columns);
290
291 2
        return tap($this->query->simplePaginate($perPage, $columns, $pageName, $page), function (Paginator $paginator) {
292 2
            $this->hydrateIntermediateRelations($paginator->items());
293
        });
294
    }
295
296
    /**
297
     * Paginate the given query into a cursor paginator.
298
     *
299
     * @param  int|null  $perPage
300
     * @param  array  $columns
301
     * @param  string  $cursorName
302
     * @param  string|null  $cursor
303
     * @return \Illuminate\Contracts\Pagination\CursorPaginator
304
     */
305 2
    public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
306
    {
307 2
        $columns = array_filter(
308 2
            $this->shouldSelect($columns),
309 2
            fn ($column) => !str_contains($column, ' as laravel_through_key')
310
        );
311
312 2
        $this->query->addSelect($columns);
313
314 2
        return tap($this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor), function (CursorPaginator $paginator) {
315 2
            $this->hydrateIntermediateRelations($paginator->items());
316
        });
317
    }
318
319
    /**
320
     * Set the select clause for the relation query.
321
     *
322
     * @param array $columns
323
     * @return array
324
     */
325 62
    protected function shouldSelect(array $columns = ['*'])
326
    {
327 62
        $columns = parent::shouldSelect($columns);
328
329 62
        if ($this->hasLeadingCompositeKey()) {
330 7
            $columns = array_merge(
331
                $columns,
332 7
                $this->shouldSelectWithCompositeKey()
333
            );
334
        }
335
336 62
        return array_merge($columns, $this->intermediateColumns());
337
    }
338
339
    /**
340
     * Chunk the results of the query.
341
     *
342
     * @param int $count
343
     * @param callable $callback
344
     * @return bool
345
     */
346 1
    public function chunk($count, callable $callback)
347
    {
348 1
        return $this->prepareQueryBuilder()->chunk($count, function (Collection $results) use ($callback) {
349 1
            $this->hydrateIntermediateRelations($results->all());
350
351 1
            return $callback($results);
352
        });
353
    }
354
355
    /**
356
     * Add the constraints for a relationship query.
357
     *
358
     * @param \Illuminate\Database\Eloquent\Builder $query
359
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
360
     * @param array|mixed $columns
361
     * @return \Illuminate\Database\Eloquent\Builder
362
     */
363 11
    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
364
    {
365 11
        foreach ($this->throughParents as $throughParent) {
366 11
            if ($throughParent->getTable() === $parentQuery->getQuery()->from) {
367 2
                if (!in_array(HasTableAlias::class, class_uses_recursive($throughParent))) {
368 1
                    $traitClass = HasTableAlias::class;
369 1
                    $parentClass = get_class($throughParent);
370
371 1
                    throw new Exception(
372
                        <<<EOT
373
This query requires an additional trait. Please add the $traitClass trait to $parentClass.
374
See https://github.com/staudenmeir/eloquent-has-many-deep/issues/137 for details.
375
EOT
376
                    );
377
                }
378
379 1
                $table = $throughParent->getTable() . ' as ' . $this->getRelationCountHash();
380
381 1
                $throughParent->setTable($table);
382
383 1
                break;
384
            }
385
        }
386
387 10
        $query = parent::getRelationExistenceQuery($query, $parentQuery, $columns);
388
389 10
        if (is_array($this->foreignKeys[0])) {
390 2
            $column = $this->throughParent->qualifyColumn($this->foreignKeys[0][0]);
391
392 2
            $query->where($column, '=', $this->farParent->getMorphClass());
393 8
        } elseif ($this->hasLeadingCompositeKey()) {
394 1
            $this->getRelationExistenceQueryWithCompositeKey($query);
395
        }
396
397 10
        return $query;
398
    }
399
400
    /**
401
     * Add the constraints for a relationship query on the same table.
402
     *
403
     * @param \Illuminate\Database\Eloquent\Builder $query
404
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
405
     * @param array|mixed $columns
406
     * @return \Illuminate\Database\Eloquent\Builder
407
     */
408 2
    public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
409
    {
410 2
        $hash = $this->getRelationCountHash();
411
412 2
        $query->from($query->getModel()->getTable().' as '.$hash);
413
414 2
        $this->performJoin($query);
415
416 2
        $query->getModel()->setTable($hash);
417
418 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...
419 2
            $parentQuery->getQuery()->from.'.'.$this->localKey,
420
            '=',
421 2
            $this->getQualifiedFirstKeyName()
422
        );
423
    }
424
425
    /**
426
     * Restore soft-deleted models.
427
     *
428
     * @param array|string ...$columns
429
     * @return $this
430
     */
431 7
    public function withTrashed(...$columns)
432
    {
433 7
        if (empty($columns)) {
434 2
            $this->query->withTrashed();
435
436 2
            return $this;
437
        }
438
439 5
        if (is_array($columns[0])) {
440 1
            $columns = $columns[0];
441
        }
442
443 5
        foreach ($columns as $column) {
444 5
            $this->query->withoutGlobalScope(__CLASS__ . ":$column");
445
        }
446
447 5
        return $this;
448
    }
449
450
    /**
451
     * Get the far parent model instance.
452
     *
453
     * @return \Illuminate\Database\Eloquent\Model
454
     */
455 5
    public function getFarParent(): Model
456
    {
457 5
        return $this->farParent;
458
    }
459
460
    /**
461
     * Get the "through" parent model instances.
462
     *
463
     * @return \Illuminate\Database\Eloquent\Model[]
464
     */
465 8
    public function getThroughParents()
466
    {
467 8
        return $this->throughParents;
468
    }
469
470
    /**
471
     * Get the foreign keys on the relationship.
472
     *
473
     * @return array
474
     */
475 8
    public function getForeignKeys()
476
    {
477 8
        return $this->foreignKeys;
478
    }
479
480
    /**
481
     * Get the local keys on the relationship.
482
     *
483
     * @return array
484
     */
485 8
    public function getLocalKeys()
486
    {
487 8
        return $this->localKeys;
488
    }
489
}
490