Passed
Push — master ( 26ec70...92ce5e )
by Jonas
02:06 queued 13s
created

HasManyDeep   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 339
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 121
dl 0
loc 339
ccs 124
cts 124
cp 1
rs 8.64
c 5
b 0
f 0
wmc 47

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 4
A getForeignKeys() 0 3 1
A getFarParent() 0 3 1
A getLocalKeys() 0 3 1
A withTrashed() 0 17 4
A performJoin() 0 18 5
A match() 0 15 4
A getThroughParents() 0 3 1
A addEagerConstraints() 0 17 4
A shouldSelect() 0 22 4
A getRelationExistenceQueryForSelfRelation() 0 14 1
B getRelationExistenceQuery() 0 45 9
B addConstraints() 0 21 8

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 Closure;
6
use Exception;
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 Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\ExecutesQueries;
12
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\HasEagerLimit;
13
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\IsConcatenable;
14
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\JoinsThroughParents;
15
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\RetrievesIntermediateTables;
16
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\SupportsCompositeKeys;
17
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\IsCustomizable;
18
use Staudenmeir\EloquentHasManyDeepContracts\Interfaces\ConcatenableRelation;
19
20
/**
21
 * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
22
 * @extends \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel>
23
 */
24
class HasManyDeep extends HasManyThrough implements ConcatenableRelation
25
{
26
    use ExecutesQueries;
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 188
    public function __construct(Builder $query, Model $farParent, array $throughParents, array $foreignKeys, array $localKeys)
66
    {
67 188
        $this->throughParents = $throughParents;
68 188
        $this->foreignKeys = $foreignKeys;
69 188
        $this->localKeys = $localKeys;
70
71 188
        $firstKey = is_array($foreignKeys[0])
72 10
            ? $foreignKeys[0][1]
73 178
            : ($this->hasLeadingCompositeKey() ? $foreignKeys[0]->columns[0] : $foreignKeys[0]);
74
75 188
        $localKey = $this->hasLeadingCompositeKey() ? $localKeys[0]->columns[0] : $localKeys[0];
76
77 188
        parent::__construct($query, $farParent, $throughParents[0], $firstKey, $foreignKeys[1], $localKey, $localKeys[1]);
78
    }
79
80
    /**
81
     * Set the base constraints on the relation query.
82
     *
83
     * @return void
84
     */
85 188
    public function addConstraints()
86
    {
87 188
        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 37
            $this->performJoin();
89
        } else {
90 151
            parent::addConstraints();
91
        }
92
93 188
        if (static::$constraints) {
94 112
            if ($this->firstKey instanceof Closure) {
0 ignored issues
show
introduced by
$this->firstKey is never a sub-type of Closure.
Loading history...
95 11
                ($this->firstKey)($this->query);
96 101
            } elseif ($this->localKey instanceof Closure) {
0 ignored issues
show
introduced by
$this->localKey is never a sub-type of Closure.
Loading history...
97 2
                ($this->localKey)($this->query);
98 99
            } elseif (is_array($this->foreignKeys[0])) {
99 4
                $this->query->where(
100 4
                    $this->throughParent->qualifyColumn($this->foreignKeys[0][0]),
101 4
                    '=',
102 4
                    $this->farParent->getMorphClass()
103 4
                );
104 95
            } elseif ($this->hasLeadingCompositeKey()) {
105 8
                $this->addConstraintsWithCompositeKey();
106
            }
107
        }
108
    }
109
110
    /**
111
     * Set the join clauses on the query.
112
     *
113
     * @param \Illuminate\Database\Eloquent\Builder|null $query
114
     * @return void
115
     */
116 188
    protected function performJoin(Builder $query = null)
117
    {
118 188
        $query = $query ?: $this->query;
119
120 188
        $throughParents = array_reverse($this->throughParents);
121 188
        $foreignKeys = array_reverse($this->foreignKeys);
122 188
        $localKeys = array_reverse($this->localKeys);
123
124 188
        $segments = explode(' as ', $query->getQuery()->from);
125
126 188
        $alias = $segments[1] ?? null;
127
128 188
        foreach ($throughParents as $i => $throughParent) {
129 188
            $predecessor = $throughParents[$i - 1] ?? $this->related;
130
131 188
            $prefix = $i === 0 && $alias ? $alias.'.' : '';
132
133 188
            $this->joinThroughParent($query, $throughParent, $predecessor, $foreignKeys[$i], $localKeys[$i], $prefix);
134
        }
135
    }
136
137
    /**
138
     * Set the constraints for an eager load of the relation.
139
     *
140
     * @param array $models
141
     * @return void
142
     */
143 40
    public function addEagerConstraints(array $models)
144
    {
145 40
        if ($this->customEagerConstraintsCallback) {
146 12
            ($this->customEagerConstraintsCallback)($this->query, $models);
147 12
            return;
148
        }
149
150 28
        if ($this->hasLeadingCompositeKey()) {
151 4
            $this->addEagerConstraintsWithCompositeKey($models);
152
        } else {
153 24
            parent::addEagerConstraints($models);
154
155 24
            if (is_array($this->foreignKeys[0])) {
156 2
                $this->query->where(
157 2
                    $this->throughParent->qualifyColumn($this->foreignKeys[0][0]),
158 2
                    '=',
159 2
                    $this->farParent->getMorphClass()
160 2
                );
161
            }
162
        }
163
    }
164
165
    /**
166
     * Match the eagerly loaded results to their parents.
167
     *
168
     * @param array $models
169
     * @param \Illuminate\Database\Eloquent\Collection $results
170
     * @param string $relation
171
     * @return array
172
     */
173 36
    public function match(array $models, Collection $results, $relation)
174
    {
175 36
        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...
176 12
            foreach ($this->customEagerMatchingCallbacks as $callback) {
177 12
                $models = $callback($models, $results, $relation);
178
            }
179
180 12
            return $models;
181
        }
182
183 24
        if ($this->hasLeadingCompositeKey()) {
184 4
            return $this->matchWithCompositeKey($models, $results, $relation);
185
        }
186
187 20
        return parent::match($models, $results, $relation);
188
    }
189
190
    /**
191
     * Set the select clause for the relation query.
192
     *
193
     * @param array $columns
194
     * @return array
195
     */
196 151
    protected function shouldSelect(array $columns = ['*'])
197
    {
198 151
        if ($columns == ['*']) {
199 151
            $columns = [$this->related->getTable().'.*'];
200
        }
201
202 151
        $alias = 'laravel_through_key';
203
204 151
        if ($this->customThroughKeyCallback) {
205 23
            $columns[] = ($this->customThroughKeyCallback)($alias);
206
        } else {
207 128
            $columns[] = $this->getQualifiedFirstKeyName() . " as $alias";
208
        }
209
210 151
        if ($this->hasLeadingCompositeKey()) {
211 12
            $columns = array_merge(
212 12
                $columns,
213 12
                $this->shouldSelectWithCompositeKey()
214 12
            );
215
        }
216
217 151
        return array_merge($columns, $this->intermediateColumns());
218
    }
219
220
    /**
221
     * Add the constraints for a relationship query.
222
     *
223
     * @param \Illuminate\Database\Eloquent\Builder $query
224
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
225
     * @param array|mixed $columns
226
     * @return \Illuminate\Database\Eloquent\Builder
227
     */
228 36
    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
229
    {
230 36
        foreach ($this->throughParents as $throughParent) {
231 36
            if ($throughParent->getTable() === $parentQuery->getQuery()->from) {
232 9
                if (!in_array(HasTableAlias::class, class_uses_recursive($throughParent))) {
233 2
                    $traitClass = HasTableAlias::class;
234 2
                    $parentClass = get_class($throughParent);
235
236 2
                    throw new Exception(
237 2
                        <<<EOT
238 2
This query requires an additional trait. Please add the $traitClass trait to $parentClass.
239
See https://github.com/staudenmeir/eloquent-has-many-deep/issues/137 for details.
240 2
EOT
241 2
                    );
242
                }
243
244 7
                $table = $throughParent->getTable() . ' as ' . $this->getRelationCountHash();
245
246 7
                $throughParent->setTable($table);
247
248 7
                break;
249
            }
250
        }
251
252 34
        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...
253 12
            $this->performJoin($query);
254
255 12
            $closureKey = $this->firstKey instanceof Closure ? $this->firstKey : $this->localKey;
256
257 12
            $closureKey($query, $parentQuery);
258
259 12
            return $query->select($columns);
260
        }
261
262 22
        $query = parent::getRelationExistenceQuery($query, $parentQuery, $columns);
263
264 22
        if (is_array($this->foreignKeys[0])) {
265 4
            $column = $this->throughParent->qualifyColumn($this->foreignKeys[0][0]);
266
267 4
            $query->where($column, '=', $this->farParent->getMorphClass());
268 18
        } elseif ($this->hasLeadingCompositeKey()) {
269 2
            $this->getRelationExistenceQueryWithCompositeKey($query);
270
        }
271
272 22
        return $query;
273
    }
274
275
    /**
276
     * Add the constraints for a relationship query on the same table.
277
     *
278
     * @param \Illuminate\Database\Eloquent\Builder $query
279
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
280
     * @param array|mixed $columns
281
     * @return \Illuminate\Database\Eloquent\Builder
282
     */
283 4
    public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
284
    {
285 4
        $hash = $this->getRelationCountHash();
286
287 4
        $query->from($query->getModel()->getTable().' as '.$hash);
288
289 4
        $this->performJoin($query);
290
291 4
        $query->getModel()->setTable($hash);
292
293 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...
294 4
            $parentQuery->getQuery()->from.'.'.$this->localKey,
295 4
            '=',
296 4
            $this->getQualifiedFirstKeyName()
297 4
        );
298
    }
299
300
    /**
301
     * Restore soft-deleted models.
302
     *
303
     * @param array|string ...$columns
304
     * @return $this
305
     */
306 14
    public function withTrashed(...$columns)
307
    {
308 14
        if (empty($columns)) {
309 4
            $this->query->withTrashed();
310
311 4
            return $this;
312
        }
313
314 10
        if (is_array($columns[0])) {
315 2
            $columns = $columns[0];
316
        }
317
318 10
        foreach ($columns as $column) {
319 10
            $this->query->withoutGlobalScope(__CLASS__ . ":$column");
320
        }
321
322 10
        return $this;
323
    }
324
325
    /**
326
     * Get the far parent model instance.
327
     *
328
     * @return \Illuminate\Database\Eloquent\Model
329
     */
330 10
    public function getFarParent(): Model
331
    {
332 10
        return $this->farParent;
333
    }
334
335
    /**
336
     * Get the "through" parent model instances.
337
     *
338
     * @return \Illuminate\Database\Eloquent\Model[]
339
     */
340 10
    public function getThroughParents()
341
    {
342 10
        return $this->throughParents;
343
    }
344
345
    /**
346
     * Get the foreign keys on the relationship.
347
     *
348
     * @return array
349
     */
350 10
    public function getForeignKeys()
351
    {
352 10
        return $this->foreignKeys;
353
    }
354
355
    /**
356
     * Get the local keys on the relationship.
357
     *
358
     * @return array
359
     */
360 10
    public function getLocalKeys()
361
    {
362 10
        return $this->localKeys;
363
    }
364
}
365