Passed
Push — master ( 077199...c47ee2 )
by Jonas
04:20 queued 02:16
created

ConcatenatesRelationships   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 515
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 183
dl 0
loc 515
ccs 148
cts 148
cp 1
rs 8.48
c 0
b 0
f 0
wmc 49

17 Methods

Rating   Name   Duplication   Size   Complexity  
A normalizeVariadicRelations() 0 3 3
A hasOneOrManyDeepFromBelongsTo() 0 22 2
A hasOneDeepFromRelations() 0 21 1
A hasManyDeepFromRelationsWithConstraints() 0 5 1
A hasOneOrManyDeepFromHasOneOrMany() 0 22 2
A hasOneOrManyDeepFromMorphOneOrMany() 0 11 1
A hasManyDeepFromRelations() 0 22 1
A hasOneOrManyDeepFromHasManyThrough() 0 15 1
A hasOneDeepFromRelationsWithConstraints() 0 5 1
A hasOneOrManyDeepFromMorphToMany() 0 23 2
A hasOneOrManyDeepRelationMethod() 0 18 3
A addRemovedScopesToHasOneOrManyDeepRelationship() 0 28 6
A hasOneOrManyDeepFromBelongsToMany() 0 15 1
A addConstraintsToHasOneOrManyDeepRelationship() 0 22 2
A hasOneOrManyThroughParent() 0 23 6
C hasOneOrManyDeepFromRelations() 0 71 12
A customizeHasOneOrManyDeepRelationship() 0 22 4

How to fix   Complexity   

Complex Class

Complex classes like ConcatenatesRelationships 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 ConcatenatesRelationships, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Staudenmeir\EloquentHasManyDeep\Eloquent\Traits;
4
5
use Illuminate\Database\Eloquent\Relations\BelongsTo;
6
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
7
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
8
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
9
use Illuminate\Database\Eloquent\Relations\MorphOneOrMany;
10
use Illuminate\Database\Eloquent\Relations\MorphToMany;
11
use Illuminate\Database\Eloquent\Relations\Relation;
12
use Illuminate\Database\Eloquent\SoftDeletingScope;
13
use RuntimeException;
14
use Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey;
15
use Staudenmeir\EloquentHasManyDeep\HasManyDeep;
16
use Staudenmeir\EloquentHasManyDeep\HasOneDeep;
17
use Staudenmeir\EloquentHasManyDeepContracts\Interfaces\ConcatenableRelation;
18
19
trait ConcatenatesRelationships
20
{
21
    /**
22
     * Define a has-many-deep relationship from existing relationships.
23
     *
24
     * @param \Illuminate\Database\Eloquent\Relations\Relation|callable ...$relations
25
     * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep
26
     */
27 66
    public function hasManyDeepFromRelations(...$relations)
28
    {
29
        [
30
            $related,
31
            $through,
32
            $foreignKeys,
33
            $localKeys,
34
            $postGetCallbacks,
35
            $customThroughKeyCallback,
36
            $customEagerConstraintsCallback,
37
            $customEagerMatchingCallback
38
        ] =
39 66
            $this->hasOneOrManyDeepFromRelations($relations);
40
41 66
        $relation = $this->hasManyDeep($related, $through, $foreignKeys, $localKeys);
0 ignored issues
show
Bug introduced by
The method hasManyDeep() does not exist on Staudenmeir\EloquentHasM...ncatenatesRelationships. Did you maybe mean hasManyDeepFromRelationsWithConstraints()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

41
        /** @scrutinizer ignore-call */ 
42
        $relation = $this->hasManyDeep($related, $through, $foreignKeys, $localKeys);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
42
43 66
        return $this->customizeHasOneOrManyDeepRelationship(
44
            $relation,
45
            $postGetCallbacks,
46
            $customThroughKeyCallback,
47
            $customEagerConstraintsCallback,
48
            $customEagerMatchingCallback
49
        );
50
    }
51
52
    /**
53
     * Define a has-one-deep relationship from existing relationships.
54
     *
55
     * @param \Illuminate\Database\Eloquent\Relations\Relation|callable ...$relations
56
     * @return \Staudenmeir\EloquentHasManyDeep\HasOneDeep
57
     */
58 4
    public function hasOneDeepFromRelations(...$relations)
59
    {
60
        [
61
            $related,
62
            $through,
63
            $foreignKeys,
64
            $localKeys,
65
            $postGetCallbacks,
66
            $customThroughKeyCallback,
67
            $customEagerConstraintsCallback,
68
            $customEagerMatchingCallback
69 4
        ] = $this->hasOneOrManyDeepFromRelations($relations);
70
71 4
        $relation = $this->hasOneDeep($related, $through, $foreignKeys, $localKeys);
0 ignored issues
show
Bug introduced by
The method hasOneDeep() does not exist on Staudenmeir\EloquentHasM...ncatenatesRelationships. Did you maybe mean hasOneDeepFromRelationsWithConstraints()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

71
        /** @scrutinizer ignore-call */ 
72
        $relation = $this->hasOneDeep($related, $through, $foreignKeys, $localKeys);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
72
73 4
        return $this->customizeHasOneOrManyDeepRelationship(
74
            $relation,
75
            $postGetCallbacks,
76
            $customThroughKeyCallback,
77
            $customEagerConstraintsCallback,
78
            $customEagerMatchingCallback
79
        );
80
    }
81
82
    /**
83
     * Prepare a has-one-deep or has-many-deep relationship from existing relationships.
84
     *
85
     * @param \Illuminate\Database\Eloquent\Relations\Relation[]|callable[] $relations
86
     * @return array
87
     */
88 70
    protected function hasOneOrManyDeepFromRelations(array $relations)
89
    {
90 70
        $relations = $this->normalizeVariadicRelations($relations);
91
92 70
        foreach ($relations as $i => $relation) {
93 70
            if (is_callable($relation)) {
94 12
                $relations[$i] = $relation();
95
            }
96
        }
97
98 70
        $related = null;
99 70
        $through = [];
100 70
        $foreignKeys = [];
101 70
        $localKeys = [];
102 70
        $postGetCallbacks = [];
103 70
        $customThroughKeyCallback = null;
104 70
        $customEagerConstraintsCallback = null;
105 70
        $customEagerMatchingCallback = null;
106
107 70
        foreach ($relations as $i => $relation) {
108 70
            if ($relation instanceof ConcatenableRelation) {
109 34
                [$through, $foreignKeys, $localKeys] = $relation->appendToDeepRelationship(
110
                    $through,
111
                    $foreignKeys,
112
                    $localKeys,
113
                    $i
114
                );
115
116 34
                if (method_exists($relation, 'postGetCallback')) {
117 28
                    $postGetCallbacks[] = [$relation, 'postGetCallback'];
118
                }
119
120 34
                if ($i === 0) {
121 34
                    if (method_exists($relation, 'getThroughKeyForDeepRelationships')) {
122 28
                        $customThroughKeyCallback = [$relation, 'getThroughKeyForDeepRelationships'];
123
                    }
124
125 34
                    if (method_exists($relation, 'addEagerConstraintsToDeepRelationship')) {
126 28
                        $customEagerConstraintsCallback = [$relation, 'addEagerConstraintsToDeepRelationship'];
127
                    }
128
129 34
                    if (method_exists($relation, 'matchResultsForDeepRelationship')) {
130 34
                        $customEagerMatchingCallback = [$relation, 'matchResultsForDeepRelationship'];
131
                    }
132
                }
133
            } else {
134 64
                $method = $this->hasOneOrManyDeepRelationMethod($relation);
135
136 64
                [$through, $foreignKeys, $localKeys] = $this->$method($relation, $through, $foreignKeys, $localKeys);
137
            }
138
139 70
            if ($i === count($relations) - 1) {
140 70
                $related = get_class($relation->getRelated());
141
142 70
                if ((new $related())->getTable() !== $relation->getRelated()->getTable()) {
143 70
                    $related .= ' from ' . $relation->getRelated()->getTable();
144
                }
145
            } else {
146 64
                $through[] = $this->hasOneOrManyThroughParent($relation, $relations[$i + 1]);
147
            }
148
        }
149
150
        return [
151 70
            $related,
152
            $through,
153
            $foreignKeys,
154
            $localKeys,
155
            $postGetCallbacks,
156
            $customThroughKeyCallback,
157
            $customEagerConstraintsCallback,
158
            $customEagerMatchingCallback
159
        ];
160
    }
161
162
    /**
163
     * Prepare a has-one-deep or has-many-deep relationship from an existing belongs-to relationship.
164
     *
165
     * @param \Illuminate\Database\Eloquent\Relations\BelongsTo $relation
166
     * @param \Illuminate\Database\Eloquent\Model[] $through
167
     * @param array $foreignKeys
168
     * @param array $localKeys
169
     * @return array
170
     */
171 6
    protected function hasOneOrManyDeepFromBelongsTo(
172
        BelongsTo $relation,
173
        array $through,
174
        array $foreignKeys,
175
        array $localKeys
176
    ) {
177 6
        if (is_array($relation->getOwnerKeyName())) {
178
            // https://github.com/topclaudy/compoships
179 2
            $foreignKeys[] = new CompositeKey(
180 2
                ...(array)$relation->getOwnerKeyName()
181
            );
182
183 2
            $localKeys[] = new CompositeKey(
184 2
                ...(array)$relation->getForeignKeyName()
185
            );
186
        } else {
187 4
            $foreignKeys[] = $relation->getOwnerKeyName();
188
189 4
            $localKeys[] = $relation->getForeignKeyName();
190
        }
191
192 6
        return [$through, $foreignKeys, $localKeys];
193
    }
194
195
    /**
196
     * Prepare a has-one-deep or has-many-deep relationship from an existing belongs-to-many relationship.
197
     *
198
     * @param \Illuminate\Database\Eloquent\Relations\BelongsToMany $relation
199
     * @param \Illuminate\Database\Eloquent\Model[] $through
200
     * @param array $foreignKeys
201
     * @param array $localKeys
202
     * @return array
203
     */
204 2
    protected function hasOneOrManyDeepFromBelongsToMany(
205
        BelongsToMany $relation,
206
        array $through,
207
        array $foreignKeys,
208
        array $localKeys
209
    ) {
210 2
        $through[] = $relation->getTable();
211
212 2
        $foreignKeys[] = $relation->getForeignPivotKeyName();
213 2
        $foreignKeys[] = $relation->getRelatedKeyName();
214
215 2
        $localKeys[] = $relation->getParentKeyName();
216 2
        $localKeys[] = $relation->getRelatedPivotKeyName();
217
218 2
        return [$through, $foreignKeys, $localKeys];
219
    }
220
221
    /**
222
     * Prepare a has-one-deep or has-many-deep relationship from an existing has-one or has-many relationship.
223
     *
224
     * @param \Illuminate\Database\Eloquent\Relations\HasOneOrMany $relation
225
     * @param \Illuminate\Database\Eloquent\Model[] $through
226
     * @param array $foreignKeys
227
     * @param array $localKeys
228
     * @return array
229
     */
230 62
    protected function hasOneOrManyDeepFromHasOneOrMany(
231
        HasOneOrMany $relation,
232
        array $through,
233
        array $foreignKeys,
234
        array $localKeys
235
    ) {
236 62
        if (is_array($relation->getForeignKeyName())) {
237
            // https://github.com/topclaudy/compoships
238 2
            $foreignKeys[] = new CompositeKey(
239 2
                ...(array)$relation->getForeignKeyName()
240
            );
241
242 2
            $localKeys[] = new CompositeKey(
243 2
                ...(array)$relation->getLocalKeyName()
244
            );
245
        } else {
246 60
            $foreignKeys[] = $relation->getForeignKeyName();
247
248 60
            $localKeys[] = $relation->getLocalKeyName();
249
        }
250
251 62
        return [$through, $foreignKeys, $localKeys];
252
    }
253
254
    /**
255
     * Prepare a has-one-deep or has-many-deep relationship from an existing has-many-through relationship.
256
     *
257
     * @param \Illuminate\Database\Eloquent\Relations\HasManyThrough $relation
258
     * @param \Illuminate\Database\Eloquent\Model[] $through
259
     * @param array $foreignKeys
260
     * @param array $localKeys
261
     * @return array
262
     */
263 22
    protected function hasOneOrManyDeepFromHasManyThrough(
264
        HasManyThrough $relation,
265
        array $through,
266
        array $foreignKeys,
267
        array $localKeys
268
    ) {
269 22
        $through[] = get_class($relation->getParent());
270
271 22
        $foreignKeys[] = $relation->getFirstKeyName();
272 22
        $foreignKeys[] = $relation->getForeignKeyName();
273
274 22
        $localKeys[] = $relation->getLocalKeyName();
275 22
        $localKeys[] = $relation->getSecondLocalKeyName();
276
277 22
        return [$through, $foreignKeys, $localKeys];
278
    }
279
280
    /**
281
     * Prepare a has-one-deep or has-many-deep relationship from an existing morph-one or morph-many relationship.
282
     *
283
     * @param \Illuminate\Database\Eloquent\Relations\MorphOneOrMany $relation
284
     * @param \Illuminate\Database\Eloquent\Model[] $through
285
     * @param array $foreignKeys
286
     * @param array $localKeys
287
     * @return array
288
     */
289 2
    protected function hasOneOrManyDeepFromMorphOneOrMany(
290
        MorphOneOrMany $relation,
291
        array $through,
292
        array $foreignKeys,
293
        array $localKeys
294
    ) {
295 2
        $foreignKeys[] = [$relation->getQualifiedMorphType(), $relation->getForeignKeyName()];
296
297 2
        $localKeys[] = $relation->getLocalKeyName();
298
299 2
        return [$through, $foreignKeys, $localKeys];
300
    }
301
302
    /**
303
     * Prepare a has-one-deep or has-many-deep relationship from an existing morph-to-many relationship.
304
     *
305
     * @param \Illuminate\Database\Eloquent\Relations\MorphToMany $relation
306
     * @param \Illuminate\Database\Eloquent\Model[] $through
307
     * @param array $foreignKeys
308
     * @param array $localKeys
309
     * @return array
310
     */
311 4
    protected function hasOneOrManyDeepFromMorphToMany(
312
        MorphToMany $relation,
313
        array $through,
314
        array $foreignKeys,
315
        array $localKeys
316
    ) {
317 4
        $through[] = $relation->getTable();
318
319 4
        if ($relation->getInverse()) {
320 2
            $foreignKeys[] = $relation->getForeignPivotKeyName();
321 2
            $foreignKeys[] = $relation->getRelatedKeyName();
322
323 2
            $localKeys[] = $relation->getParentKeyName();
324 2
            $localKeys[] = [$relation->getMorphType(), $relation->getRelatedPivotKeyName()];
325
        } else {
326 2
            $foreignKeys[] = [$relation->getMorphType(), $relation->getForeignPivotKeyName()];
327 2
            $foreignKeys[] = $relation->getRelatedKeyName();
328
329 2
            $localKeys[] = $relation->getParentKeyName();
330 2
            $localKeys[] = $relation->getRelatedPivotKeyName();
331
        }
332
333 4
        return [$through, $foreignKeys, $localKeys];
334
    }
335
336
    /**
337
     * Get the relationship method name.
338
     *
339
     * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
340
     * @return string
341
     */
342 64
    protected function hasOneOrManyDeepRelationMethod(Relation $relation)
343
    {
344 64
        $classes = [
345
            BelongsTo::class,
346
            HasManyThrough::class,
347
            MorphOneOrMany::class,
348
            HasOneOrMany::class,
349
            MorphToMany::class,
350
            BelongsToMany::class,
351
        ];
352
353 64
        foreach ($classes as $class) {
354 64
            if ($relation instanceof $class) {
355 64
                return 'hasOneOrManyDeepFrom' . class_basename($class);
356
            }
357
        }
358
359
        throw new RuntimeException('This relationship is not supported.'); // @codeCoverageIgnore
360
    }
361
362
    /**
363
     * Prepare the through parent class from an existing relationship and its successor.
364
     *
365
     * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
366
     * @param \Illuminate\Database\Eloquent\Relations\Relation $successor
367
     * @return string
368
     */
369 64
    protected function hasOneOrManyThroughParent(Relation $relation, Relation $successor)
370
    {
371 64
        $through = get_class($relation->getRelated());
372
373 64
        if ($relation instanceof ConcatenableRelation && method_exists($relation, 'getTableForDeepRelationship')) {
374 28
            return $through . ' from ' . $relation->getTableForDeepRelationship();
375
        }
376
377 36
        if ((new $through())->getTable() !== $relation->getRelated()->getTable()) {
378 4
            $through .= ' from ' . $relation->getRelated()->getTable();
379
        }
380
381 36
        if (get_class($relation->getRelated()) === get_class($successor->getParent())) {
382 28
            $table = $successor->getParent()->getTable();
383
384 28
            $segments = explode(' as ', $table);
385
386 28
            if (isset($segments[1])) {
387 2
                $through .= ' as ' . $segments[1];
388
            }
389
        }
390
391 36
        return $through;
392
    }
393
394
    /**
395
     * Customize a has-one-deep or has-many-deep relationship.
396
     *
397
     * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $relation
398
     * @param callable[] $postGetCallbacks
399
     * @param callable|null $customThroughKeyCallback
400
     * @param callable|null $customEagerConstraintsCallback
401
     * @param callable|null $customEagerMatchingCallback
402
     * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep|\Staudenmeir\EloquentHasManyDeep\HasOneDeep
403
     */
404 70
    protected function customizeHasOneOrManyDeepRelationship(
405
        HasManyDeep $relation,
406
        array $postGetCallbacks,
407
        ?callable $customThroughKeyCallback,
408
        ?callable $customEagerConstraintsCallback,
409
        ?callable $customEagerMatchingCallback
410
    ): HasManyDeep|HasOneDeep {
411 70
        $relation->withPostGetCallbacks($postGetCallbacks);
412
413 70
        if ($customThroughKeyCallback) {
414 28
            $relation->withCustomThroughKeyCallback($customThroughKeyCallback);
415
        }
416
417 70
        if ($customEagerConstraintsCallback) {
418 28
            $relation->withCustomEagerConstraintsCallback($customEagerConstraintsCallback);
419
        }
420
421 70
        if ($customEagerMatchingCallback) {
422 28
            $relation->withCustomEagerMatchingCallback($customEagerMatchingCallback);
423
        }
424
425 70
        return $relation;
426
    }
427
428
    /**
429
     * Define a has-many-deep relationship with constraints from existing relationships.
430
     *
431
     * @param callable ...$relations
432
     * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep
433
     */
434 10
    public function hasManyDeepFromRelationsWithConstraints(...$relations): HasManyDeep
435
    {
436 10
        $hasManyDeep = $this->hasManyDeepFromRelations(...$relations);
437
438 10
        return $this->addConstraintsToHasOneOrManyDeepRelationship($hasManyDeep, $relations);
439
    }
440
441
    /**
442
     * Define a has-one-deep relationship with constraints from existing relationships.
443
     *
444
     * @param callable ...$relations
445
     * @return \Staudenmeir\EloquentHasManyDeep\HasOneDeep
446
     */
447 2
    public function hasOneDeepFromRelationsWithConstraints(...$relations): HasOneDeep
448
    {
449 2
        $hasOneDeep = $this->hasOneDeepFromRelations(...$relations);
450
451 2
        return $this->addConstraintsToHasOneOrManyDeepRelationship($hasOneDeep, $relations);
452
    }
453
454
    /**
455
     * Add the constraints from existing relationships to a has-one-deep or has-many-deep relationship.
456
     *
457
     * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $deepRelation
458
     * @param callable[] $relations
459
     * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep|\Staudenmeir\EloquentHasManyDeep\HasOneDeep
460
     */
461 12
    protected function addConstraintsToHasOneOrManyDeepRelationship(
462
        HasManyDeep $deepRelation,
463
        array $relations
464
    ): HasManyDeep|HasOneDeep {
465 12
        $relations = $this->normalizeVariadicRelations($relations);
466
467 12
        foreach ($relations as $i => $relation) {
468 12
            $relationWithoutConstraints = Relation::noConstraints(function () use ($relation) {
469 12
                return $relation();
470
            });
471
472 12
            $deepRelation->getQuery()->mergeWheres(
473 12
                $relationWithoutConstraints->getQuery()->getQuery()->wheres,
474 12
                $relationWithoutConstraints->getQuery()->getQuery()->getRawBindings()['where'] ?? []
475
            );
476
477 12
            $isLast = $i === count($relations) - 1;
478
479 12
            $this->addRemovedScopesToHasOneOrManyDeepRelationship($deepRelation, $relationWithoutConstraints, $isLast);
480
        }
481
482 12
        return $deepRelation;
483
    }
484
485
    /**
486
     * Add the removed scopes from an existing relationship to a has-one-deep or has-many-deep relationship.
487
     *
488
     * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $deepRelation
489
     * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
490
     * @param bool $isLastRelation
491
     * @return void
492
     */
493 12
    protected function addRemovedScopesToHasOneOrManyDeepRelationship(
494
        HasManyDeep $deepRelation,
495
        Relation $relation,
496
        bool $isLastRelation
497
    ): void {
498 12
        $removedScopes = $relation->getQuery()->removedScopes();
499
500 12
        foreach ($removedScopes as $scope) {
501 8
            if ($scope === SoftDeletingScope::class) {
502 4
                if ($isLastRelation) {
503 2
                    $deepRelation->withTrashed();
504
                } else {
505 2
                    $deletedAtColumn = $relation->getRelated()->getQualifiedDeletedAtColumn();
506
507 2
                    $deepRelation->withTrashed($deletedAtColumn);
508
                }
509
            }
510
511 8
            if ($scope === 'SoftDeletableHasManyThrough') {
512 2
                $deletedAtColumn = $relation->getParent()->getQualifiedDeletedAtColumn();
513
514 2
                $deepRelation->withTrashed($deletedAtColumn);
515
            }
516
517 8
            if (str_starts_with($scope, HasManyDeep::class . ':')) {
518 2
                $deletedAtColumn = explode(':', $scope)[1];
519
520 2
                $deepRelation->withTrashed($deletedAtColumn);
521
            }
522
        }
523
    }
524
525
    /**
526
     * Normalize the relations from a variadic parameter.
527
     *
528
     * @param array $relations
529
     * @return array
530
     */
531 70
    protected function normalizeVariadicRelations(array $relations): array
532
    {
533 70
        return is_array($relations[0]) && !is_callable($relations[0]) ? $relations[0] : $relations;
534
    }
535
}
536