Passed
Push — master ( 6cf215...65a336 )
by Jonas
12:16
created

ConcatenatesRelationships   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 515
Duplicated Lines 0 %

Test Coverage

Coverage 95.27%

Importance

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

17 Methods

Rating   Name   Duplication   Size   Complexity  
A normalizeVariadicRelations() 0 3 3
A hasOneOrManyThroughParent() 0 23 6
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
C hasOneOrManyDeepFromRelations() 0 71 12
A hasManyDeepFromRelations() 0 22 1
A customizeHasOneOrManyDeepRelationship() 0 22 4
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

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 19
    public function hasManyDeepFromRelations(...$relations)
28
    {
29
        [
30
            $related,
31
            $through,
32
            $foreignKeys,
33
            $localKeys,
34
            $postGetCallbacks,
35
            $customThroughKeyCallback,
36
            $customEagerConstraintsCallback,
37
            $customEagerMatchingCallback
38
        ] =
39 19
            $this->hasOneOrManyDeepFromRelations($relations);
40
41 19
        $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 19
        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 2
    public function hasOneDeepFromRelations(...$relations)
59
    {
60
        [
61
            $related,
62
            $through,
63
            $foreignKeys,
64
            $localKeys,
65
            $postGetCallbacks,
66
            $customThroughKeyCallback,
67
            $customEagerConstraintsCallback,
68
            $customEagerMatchingCallback
69 2
        ] = $this->hasOneOrManyDeepFromRelations($relations);
70
71 2
        $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 2
        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 21
    protected function hasOneOrManyDeepFromRelations(array $relations)
89
    {
90 21
        $relations = $this->normalizeVariadicRelations($relations);
91
92 21
        foreach ($relations as $i => $relation) {
93 21
            if (is_callable($relation)) {
94 6
                $relations[$i] = $relation();
95
            }
96
        }
97
98 21
        $related = null;
99 21
        $through = [];
100 21
        $foreignKeys = [];
101 21
        $localKeys = [];
102 21
        $postGetCallbacks = [];
103 21
        $customThroughKeyCallback = null;
104 21
        $customEagerConstraintsCallback = null;
105 21
        $customEagerMatchingCallback = null;
106
107 21
        foreach ($relations as $i => $relation) {
108 21
            if ($relation instanceof ConcatenableRelation) {
109 3
                [$through, $foreignKeys, $localKeys] = $relation->appendToDeepRelationship(
110
                    $through,
111
                    $foreignKeys,
112
                    $localKeys,
113
                    $i
114
                );
115
116 3
                if (method_exists($relation, 'postGetCallback')) {
117
                    $postGetCallbacks[] = [$relation, 'postGetCallback'];
118
                }
119
120 3
                if ($i === 0) {
121 3
                    if (method_exists($relation, 'getThroughKeyForDeepRelationships')) {
122
                        $customThroughKeyCallback = [$relation, 'getThroughKeyForDeepRelationships'];
123
                    }
124
125 3
                    if (method_exists($relation, 'addEagerConstraintsToDeepRelationship')) {
126
                        $customEagerConstraintsCallback = [$relation, 'addEagerConstraintsToDeepRelationship'];
127
                    }
128
129 3
                    if (method_exists($relation, 'matchResultsForDeepRelationship')) {
130 3
                        $customEagerMatchingCallback = [$relation, 'matchResultsForDeepRelationship'];
131
                    }
132
                }
133
            } else {
134 18
                $method = $this->hasOneOrManyDeepRelationMethod($relation);
135
136 18
                [$through, $foreignKeys, $localKeys] = $this->$method($relation, $through, $foreignKeys, $localKeys);
137
            }
138
139 21
            if ($i === count($relations) - 1) {
140 21
                $related = get_class($relation->getRelated());
141
142 21
                if ((new $related())->getTable() !== $relation->getRelated()->getTable()) {
143 21
                    $related .= ' from ' . $relation->getRelated()->getTable();
144
                }
145
            } else {
146 18
                $through[] = $this->hasOneOrManyThroughParent($relation, $relations[$i + 1]);
147
            }
148
        }
149
150
        return [
151 21
            $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 3
    protected function hasOneOrManyDeepFromBelongsTo(
172
        BelongsTo $relation,
173
        array $through,
174
        array $foreignKeys,
175
        array $localKeys
176
    ) {
177 3
        if (is_array($relation->getOwnerKeyName())) {
178
            // https://github.com/topclaudy/compoships
179 1
            $foreignKeys[] = new CompositeKey(
180 1
                ...(array)$relation->getOwnerKeyName()
181
            );
182
183 1
            $localKeys[] = new CompositeKey(
184 1
                ...(array)$relation->getForeignKeyName()
185
            );
186
        } else {
187 2
            $foreignKeys[] = $relation->getOwnerKeyName();
188
189 2
            $localKeys[] = $relation->getForeignKeyName();
190
        }
191
192 3
        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 1
    protected function hasOneOrManyDeepFromBelongsToMany(
205
        BelongsToMany $relation,
206
        array $through,
207
        array $foreignKeys,
208
        array $localKeys
209
    ) {
210 1
        $through[] = $relation->getTable();
211
212 1
        $foreignKeys[] = $relation->getForeignPivotKeyName();
213 1
        $foreignKeys[] = $relation->getRelatedKeyName();
214
215 1
        $localKeys[] = $relation->getParentKeyName();
216 1
        $localKeys[] = $relation->getRelatedPivotKeyName();
217
218 1
        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 17
    protected function hasOneOrManyDeepFromHasOneOrMany(
231
        HasOneOrMany $relation,
232
        array $through,
233
        array $foreignKeys,
234
        array $localKeys
235
    ) {
236 17
        if (is_array($relation->getForeignKeyName())) {
237
            // https://github.com/topclaudy/compoships
238 1
            $foreignKeys[] = new CompositeKey(
239 1
                ...(array)$relation->getForeignKeyName()
240
            );
241
242 1
            $localKeys[] = new CompositeKey(
243 1
                ...(array)$relation->getLocalKeyName()
244
            );
245
        } else {
246 16
            $foreignKeys[] = $relation->getForeignKeyName();
247
248 16
            $localKeys[] = $relation->getLocalKeyName();
249
        }
250
251 17
        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 11
    protected function hasOneOrManyDeepFromHasManyThrough(
264
        HasManyThrough $relation,
265
        array $through,
266
        array $foreignKeys,
267
        array $localKeys
268
    ) {
269 11
        $through[] = get_class($relation->getParent());
270
271 11
        $foreignKeys[] = $relation->getFirstKeyName();
272 11
        $foreignKeys[] = $relation->getForeignKeyName();
273
274 11
        $localKeys[] = $relation->getLocalKeyName();
275 11
        $localKeys[] = $relation->getSecondLocalKeyName();
276
277 11
        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 1
    protected function hasOneOrManyDeepFromMorphOneOrMany(
290
        MorphOneOrMany $relation,
291
        array $through,
292
        array $foreignKeys,
293
        array $localKeys
294
    ) {
295 1
        $foreignKeys[] = [$relation->getQualifiedMorphType(), $relation->getForeignKeyName()];
296
297 1
        $localKeys[] = $relation->getLocalKeyName();
298
299 1
        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 2
    protected function hasOneOrManyDeepFromMorphToMany(
312
        MorphToMany $relation,
313
        array $through,
314
        array $foreignKeys,
315
        array $localKeys
316
    ) {
317 2
        $through[] = $relation->getTable();
318
319 2
        if ($relation->getInverse()) {
320 1
            $foreignKeys[] = $relation->getForeignPivotKeyName();
321 1
            $foreignKeys[] = $relation->getRelatedKeyName();
322
323 1
            $localKeys[] = $relation->getParentKeyName();
324 1
            $localKeys[] = [$relation->getMorphType(), $relation->getRelatedPivotKeyName()];
325
        } else {
326 1
            $foreignKeys[] = [$relation->getMorphType(), $relation->getForeignPivotKeyName()];
327 1
            $foreignKeys[] = $relation->getRelatedKeyName();
328
329 1
            $localKeys[] = $relation->getParentKeyName();
330 1
            $localKeys[] = $relation->getRelatedPivotKeyName();
331
        }
332
333 2
        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 18
    protected function hasOneOrManyDeepRelationMethod(Relation $relation)
343
    {
344 18
        $classes = [
345
            BelongsTo::class,
346
            HasManyThrough::class,
347
            MorphOneOrMany::class,
348
            HasOneOrMany::class,
349
            MorphToMany::class,
350
            BelongsToMany::class,
351
        ];
352
353 18
        foreach ($classes as $class) {
354 18
            if ($relation instanceof $class) {
355 18
                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 18
    protected function hasOneOrManyThroughParent(Relation $relation, Relation $successor)
370
    {
371 18
        $through = get_class($relation->getRelated());
372
373 18
        if ($relation instanceof ConcatenableRelation && method_exists($relation, 'getTableForDeepRelationship')) {
374
            return $through . ' from ' . $relation->getTableForDeepRelationship();
375
        }
376
377 18
        if ((new $through())->getTable() !== $relation->getRelated()->getTable()) {
378 2
            $through .= ' from ' . $relation->getRelated()->getTable();
379
        }
380
381 18
        if (get_class($relation->getRelated()) === get_class($successor->getParent())) {
382 14
            $table = $successor->getParent()->getTable();
383
384 14
            $segments = explode(' as ', $table);
385
386 14
            if (isset($segments[1])) {
387 1
                $through .= ' as ' . $segments[1];
388
            }
389
        }
390
391 18
        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 21
    protected function customizeHasOneOrManyDeepRelationship(
405
        HasManyDeep $relation,
406
        array $postGetCallbacks,
407
        ?callable $customThroughKeyCallback,
408
        ?callable $customEagerConstraintsCallback,
409
        ?callable $customEagerMatchingCallback
410
    ): HasManyDeep|HasOneDeep {
411 21
        $relation->withPostGetCallbacks($postGetCallbacks);
412
413 21
        if ($customThroughKeyCallback) {
414
            $relation->withCustomThroughKeyCallback($customThroughKeyCallback);
415
        }
416
417 21
        if ($customEagerConstraintsCallback) {
418
            $relation->withCustomEagerConstraintsCallback($customEagerConstraintsCallback);
419
        }
420
421 21
        if ($customEagerMatchingCallback) {
422
            $relation->withCustomEagerMatchingCallback($customEagerMatchingCallback);
423
        }
424
425 21
        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 5
    public function hasManyDeepFromRelationsWithConstraints(...$relations): HasManyDeep
435
    {
436 5
        $hasManyDeep = $this->hasManyDeepFromRelations(...$relations);
437
438 5
        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 1
    public function hasOneDeepFromRelationsWithConstraints(...$relations): HasOneDeep
448
    {
449 1
        $hasOneDeep = $this->hasOneDeepFromRelations(...$relations);
450
451 1
        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 6
    protected function addConstraintsToHasOneOrManyDeepRelationship(
462
        HasManyDeep $deepRelation,
463
        array $relations
464
    ): HasManyDeep|HasOneDeep {
465 6
        $relations = $this->normalizeVariadicRelations($relations);
466
467 6
        foreach ($relations as $i => $relation) {
468 6
            $relationWithoutConstraints = Relation::noConstraints(function () use ($relation) {
469 6
                return $relation();
470
            });
471
472 6
            $deepRelation->getQuery()->mergeWheres(
473 6
                $relationWithoutConstraints->getQuery()->getQuery()->wheres,
474 6
                $relationWithoutConstraints->getQuery()->getQuery()->getRawBindings()['where'] ?? []
475
            );
476
477 6
            $isLast = $i === count($relations) - 1;
478
479 6
            $this->addRemovedScopesToHasOneOrManyDeepRelationship($deepRelation, $relationWithoutConstraints, $isLast);
480
        }
481
482 6
        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 6
    protected function addRemovedScopesToHasOneOrManyDeepRelationship(
494
        HasManyDeep $deepRelation,
495
        Relation $relation,
496
        bool $isLastRelation
497
    ): void {
498 6
        $removedScopes = $relation->getQuery()->removedScopes();
499
500 6
        foreach ($removedScopes as $scope) {
501 4
            if ($scope === SoftDeletingScope::class) {
502 2
                if ($isLastRelation) {
503 1
                    $deepRelation->withTrashed();
504
                } else {
505 1
                    $deletedAtColumn = $relation->getRelated()->getQualifiedDeletedAtColumn();
506
507 1
                    $deepRelation->withTrashed($deletedAtColumn);
508
                }
509
            }
510
511 4
            if ($scope === 'SoftDeletableHasManyThrough') {
512 1
                $deletedAtColumn = $relation->getParent()->getQualifiedDeletedAtColumn();
513
514 1
                $deepRelation->withTrashed($deletedAtColumn);
515
            }
516
517 4
            if (str_starts_with($scope, HasManyDeep::class . ':')) {
518 1
                $deletedAtColumn = explode(':', $scope)[1];
519
520 1
                $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 21
    protected function normalizeVariadicRelations(array $relations): array
532
    {
533 21
        return is_array($relations[0]) && !is_callable($relations[0]) ? $relations[0] : $relations;
534
    }
535
}
536