Passed
Push — master ( 0f718c...f63467 )
by Jonas
01:55
created

HasManyDeep   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 395
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 117
c 5
b 0
f 0
dl 0
loc 395
ccs 118
cts 118
cp 1
rs 9.2
wmc 40

18 Methods

Rating   Name   Duplication   Size   Complexity  
A cursorPaginate() 0 11 1
A addEagerConstraints() 0 12 3
A shouldSelect() 0 12 2
A getForeignKeys() 0 3 1
A simplePaginate() 0 11 1
A getFarParent() 0 3 1
A chunk() 0 6 1
A getLocalKeys() 0 3 1
A withTrashed() 0 17 4
A performJoin() 0 18 5
A get() 0 7 1
A match() 0 7 2
A getRelationExistenceQueryForSelfRelation() 0 14 1
A __construct() 0 13 4
A getThroughParents() 0 3 1
A addConstraints() 0 13 4
A paginate() 0 11 1
A getRelationExistenceQuery() 0 35 6

How to fix   Complexity   

Complex Class

Complex classes like HasManyDeep often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HasManyDeep, and based on these observations, apply Extract Interface, too.

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