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

hasOneOrManyDeepRelationMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 3
nop 1
dl 0
loc 19
ccs 5
cts 5
cp 1
crap 3
rs 9.8666
c 0
b 0
f 0
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\Pivot;
12
use Illuminate\Database\Eloquent\Relations\Relation;
13
use Illuminate\Database\Eloquent\SoftDeletingScope;
14
use RuntimeException;
15
use Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey;
16
use Staudenmeir\EloquentHasManyDeep\HasManyDeep;
17
use Staudenmeir\EloquentHasManyDeep\HasOneDeep;
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 19
        return $this->hasManyDeep(...$this->hasOneOrManyDeepFromRelations($relations));
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

29
        return $this->/** @scrutinizer ignore-call */ hasManyDeep(...$this->hasOneOrManyDeepFromRelations($relations));

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...
30
    }
31
32
    /**
33
     * Define a has-one-deep relationship from existing relationships.
34
     *
35
     * @param \Illuminate\Database\Eloquent\Relations\Relation|callable ...$relations
36
     * @return \Staudenmeir\EloquentHasManyDeep\HasOneDeep
37
     */
38 2
    public function hasOneDeepFromRelations(...$relations)
39
    {
40 2
        return $this->hasOneDeep(...$this->hasOneOrManyDeepFromRelations($relations));
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

40
        return $this->/** @scrutinizer ignore-call */ hasOneDeep(...$this->hasOneOrManyDeepFromRelations($relations));

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...
41
    }
42
43
    /**
44
     * Prepare a has-one-deep or has-many-deep relationship from existing relationships.
45
     *
46
     * @param \Illuminate\Database\Eloquent\Relations\Relation[]|callable[] $relations
47
     * @return array
48
     */
49 21
    protected function hasOneOrManyDeepFromRelations(array $relations)
50
    {
51 21
        $relations = $this->normalizeVariadicRelations($relations);
52
53 21
        foreach ($relations as $i => $relation) {
54 21
            if (is_callable($relation)) {
55 6
                $relations[$i] = $relation();
56
            }
57
        }
58
59 21
        $related = null;
60 21
        $through = [];
61 21
        $foreignKeys = [];
62 21
        $localKeys = [];
63
64 21
        foreach ($relations as $i => $relation) {
65 21
            $method = $this->hasOneOrManyDeepRelationMethod($relation);
66
67 21
            [$through, $foreignKeys, $localKeys] = $this->$method($relation, $through, $foreignKeys, $localKeys);
68
69 21
            if ($i === count($relations) - 1) {
70 21
                $related = get_class($relation->getRelated());
71
72 21
                if ((new $related())->getTable() !== $relation->getRelated()->getTable()) {
73 21
                    $related .= ' from ' . $relation->getRelated()->getTable();
74
                }
75
            } else {
76 18
                $through[] = $this->hasOneOrManyThroughParent($relation, $relations[$i + 1]);
77
            }
78
        }
79
80 21
        return [$related, $through, $foreignKeys, $localKeys];
81
    }
82
83
    /**
84
     * Prepare a has-one-deep or has-many-deep relationship from an existing belongs-to relationship.
85
     *
86
     * @param \Illuminate\Database\Eloquent\Relations\BelongsTo $relation
87
     * @param \Illuminate\Database\Eloquent\Model[] $through
88
     * @param array $foreignKeys
89
     * @param array $localKeys
90
     * @return array
91
     */
92 3
    protected function hasOneOrManyDeepFromBelongsTo(BelongsTo $relation, array $through, array $foreignKeys, array $localKeys)
93
    {
94 3
        if (is_array($relation->getOwnerKeyName())) {
95
            // https://github.com/topclaudy/compoships
96 1
            $foreignKeys[] = new CompositeKey(
97 1
                ...(array) $relation->getOwnerKeyName()
98
            );
99
100 1
            $localKeys[] = new CompositeKey(
101 1
                ... (array) $relation->getForeignKeyName()
102
            );
103
        } else {
104 2
            $foreignKeys[] = $relation->getOwnerKeyName();
105
106 2
            $localKeys[] = $relation->getForeignKeyName();
107
        }
108
109 3
        return [$through, $foreignKeys, $localKeys];
110
    }
111
112
    /**
113
     * Prepare a has-one-deep or has-many-deep relationship from an existing belongs-to-many relationship.
114
     *
115
     * @param \Illuminate\Database\Eloquent\Relations\BelongsToMany $relation
116
     * @param \Illuminate\Database\Eloquent\Model[] $through
117
     * @param array $foreignKeys
118
     * @param array $localKeys
119
     * @return array
120
     */
121 1
    protected function hasOneOrManyDeepFromBelongsToMany(BelongsToMany $relation, array $through, array $foreignKeys, array $localKeys)
122
    {
123 1
        $through[] = $relation->getTable();
124
125 1
        $foreignKeys[] = $relation->getForeignPivotKeyName();
126 1
        $foreignKeys[] = $relation->getRelatedKeyName();
127
128 1
        $localKeys[] = $relation->getParentKeyName();
129 1
        $localKeys[] = $relation->getRelatedPivotKeyName();
130
131 1
        return [$through, $foreignKeys, $localKeys];
132
    }
133
134
    /**
135
     * Prepare a has-one-deep or has-many-deep relationship from an existing has-one or has-many relationship.
136
     *
137
     * @param \Illuminate\Database\Eloquent\Relations\HasOneOrMany $relation
138
     * @param \Illuminate\Database\Eloquent\Model[] $through
139
     * @param array $foreignKeys
140
     * @param array $localKeys
141
     * @return array
142
     */
143 17
    protected function hasOneOrManyDeepFromHasOneOrMany(HasOneOrMany $relation, array $through, array $foreignKeys, array $localKeys)
144
    {
145 17
        if (is_array($relation->getForeignKeyName())) {
146
            // https://github.com/topclaudy/compoships
147 1
            $foreignKeys[] = new CompositeKey(
148 1
                ...(array) $relation->getForeignKeyName()
149
            );
150
151 1
            $localKeys[] = new CompositeKey(
152 1
                ... (array) $relation->getLocalKeyName()
153
            );
154
        } else {
155 16
            $foreignKeys[] = $relation->getForeignKeyName();
156
157 16
            $localKeys[] = $relation->getLocalKeyName();
158
        }
159
160 17
        return [$through, $foreignKeys, $localKeys];
161
    }
162
163
    /**
164
     * Prepare a has-one-deep or has-many-deep relationship from an existing has-many-through relationship.
165
     *
166
     * @param \Illuminate\Database\Eloquent\Relations\HasManyThrough $relation
167
     * @param \Illuminate\Database\Eloquent\Model[] $through
168
     * @param array $foreignKeys
169
     * @param array $localKeys
170
     * @return array
171
     */
172 11
    protected function hasOneOrManyDeepFromHasManyThrough(HasManyThrough $relation, array $through, array $foreignKeys, array $localKeys)
173
    {
174 11
        $through[] = get_class($relation->getParent());
175
176 11
        $foreignKeys[] = $relation->getFirstKeyName();
177 11
        $foreignKeys[] = $relation->getForeignKeyName();
178
179 11
        $localKeys[] = $relation->getLocalKeyName();
180 11
        $localKeys[] = $relation->getSecondLocalKeyName();
181
182 11
        return [$through, $foreignKeys, $localKeys];
183
    }
184
185
    /**
186
     * Prepare a has-one-deep or has-many-deep relationship from an existing has-many-deep relationship.
187
     *
188
     * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $relation
189
     * @param \Illuminate\Database\Eloquent\Model[] $through
190
     * @param array $foreignKeys
191
     * @param array $localKeys
192
     * @return array
193
     */
194 3
    protected function hasOneOrManyDeepFromHasManyDeep(HasManyDeep $relation, array $through, array $foreignKeys, array $localKeys)
195
    {
196 3
        foreach ($relation->getThroughParents() as $throughParent) {
197 3
            $segments = explode(' as ', $throughParent->getTable());
198
199 3
            $class = get_class($throughParent);
200
201 3
            if (isset($segments[1])) {
202 1
                $class .= ' as '.$segments[1];
203 3
            } elseif ($throughParent instanceof Pivot) {
204 1
                $class = $throughParent->getTable();
205
            }
206
207 3
            $through[] = $class;
208
        }
209
210 3
        $foreignKeys = array_merge($foreignKeys, $relation->getForeignKeys());
211
212 3
        $localKeys = array_merge($localKeys, $relation->getLocalKeys());
213
214 3
        return [$through, $foreignKeys, $localKeys];
215
    }
216
217
    /**
218
     * Prepare a has-one-deep or has-many-deep relationship from an existing morph-one or morph-many relationship.
219
     *
220
     * @param \Illuminate\Database\Eloquent\Relations\MorphOneOrMany $relation
221
     * @param \Illuminate\Database\Eloquent\Model[] $through
222
     * @param array $foreignKeys
223
     * @param array $localKeys
224
     * @return array
225
     */
226 1
    protected function hasOneOrManyDeepFromMorphOneOrMany(MorphOneOrMany $relation, array $through, array $foreignKeys, array $localKeys)
227
    {
228 1
        $foreignKeys[] = [$relation->getQualifiedMorphType(), $relation->getForeignKeyName()];
229
230 1
        $localKeys[] = $relation->getLocalKeyName();
231
232 1
        return [$through, $foreignKeys, $localKeys];
233
    }
234
235
    /**
236
     * Prepare a has-one-deep or has-many-deep relationship from an existing morph-to-many relationship.
237
     *
238
     * @param \Illuminate\Database\Eloquent\Relations\MorphToMany $relation
239
     * @param \Illuminate\Database\Eloquent\Model[] $through
240
     * @param array $foreignKeys
241
     * @param array $localKeys
242
     * @return array
243
     */
244 2
    protected function hasOneOrManyDeepFromMorphToMany(MorphToMany $relation, array $through, array $foreignKeys, array $localKeys)
245
    {
246 2
        $through[] = $relation->getTable();
247
248 2
        if ($relation->getInverse()) {
249 1
            $foreignKeys[] = $relation->getForeignPivotKeyName();
250 1
            $foreignKeys[] = $relation->getRelatedKeyName();
251
252 1
            $localKeys[] = $relation->getParentKeyName();
253 1
            $localKeys[] = [$relation->getMorphType(), $relation->getRelatedPivotKeyName()];
254
        } else {
255 1
            $foreignKeys[] = [$relation->getMorphType(), $relation->getForeignPivotKeyName()];
256 1
            $foreignKeys[] = $relation->getRelatedKeyName();
257
258 1
            $localKeys[] = $relation->getParentKeyName();
259 1
            $localKeys[] = $relation->getRelatedPivotKeyName();
260
        }
261
262 2
        return [$through, $foreignKeys, $localKeys];
263
    }
264
265
    /**
266
     * Get the relationship method name.
267
     *
268
     * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
269
     * @return string
270
     */
271 21
    protected function hasOneOrManyDeepRelationMethod(Relation $relation)
272
    {
273 21
        $classes = [
274
            BelongsTo::class,
275
            HasManyDeep::class,
276
            HasManyThrough::class,
277
            MorphOneOrMany::class,
278
            HasOneOrMany::class,
279
            MorphToMany::class,
280
            BelongsToMany::class,
281
        ];
282
283 21
        foreach ($classes as $class) {
284 21
            if ($relation instanceof $class) {
285 21
                return 'hasOneOrManyDeepFrom'.class_basename($class);
286
            }
287
        }
288
289
        throw new RuntimeException('This relationship is not supported.'); // @codeCoverageIgnore
290
    }
291
292
    /**
293
     * Prepare the through parent class from an existing relationship and its successor.
294
     *
295
     * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
296
     * @param \Illuminate\Database\Eloquent\Relations\Relation $successor
297
     * @return string
298
     */
299 18
    protected function hasOneOrManyThroughParent(Relation $relation, Relation $successor)
300
    {
301 18
        $through = get_class($relation->getRelated());
302
303 18
        if ((new $through())->getTable() !== $relation->getRelated()->getTable()) {
304 2
            $through .= ' from ' . $relation->getRelated()->getTable();
305
        }
306
307 18
        if (get_class($relation->getRelated()) === get_class($successor->getParent())) {
308 14
            $table = $successor->getParent()->getTable();
309
310 14
            $segments = explode(' as ', $table);
311
312 14
            if (isset($segments[1])) {
313 1
                $through .= ' as '.$segments[1];
314
            }
315
        }
316
317 18
        return $through;
318
    }
319
320
    /**
321
     * Define a has-many-deep relationship with constraints from existing relationships.
322
     *
323
     * @param callable ...$relations
324
     * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep
325
     */
326 5
    public function hasManyDeepFromRelationsWithConstraints(...$relations): HasManyDeep
327
    {
328 5
        $hasManyDeep = $this->hasManyDeepFromRelations(...$relations);
329
330 5
        return $this->addConstraintsToHasOneOrManyDeepRelationship($hasManyDeep, $relations);
331
    }
332
333
    /**
334
     * Define a has-one-deep relationship with constraints from existing relationships.
335
     *
336
     * @param callable ...$relations
337
     * @return \Staudenmeir\EloquentHasManyDeep\HasOneDeep
338
     */
339 1
    public function hasOneDeepFromRelationsWithConstraints(...$relations): HasOneDeep
340
    {
341 1
        $hasOneDeep = $this->hasOneDeepFromRelations(...$relations);
342
343 1
        return $this->addConstraintsToHasOneOrManyDeepRelationship($hasOneDeep, $relations);
344
    }
345
346
    /**
347
     * Add the constraints from existing relationships to a has-one-deep or has-many-deep relationship.
348
     *
349
     * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $deepRelation
350
     * @param callable[] $relations
351
     * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep|\Staudenmeir\EloquentHasManyDeep\HasOneDeep
352
     */
353 6
    protected function addConstraintsToHasOneOrManyDeepRelationship(
354
        HasManyDeep $deepRelation,
355
        array $relations
356
    ): HasManyDeep|HasOneDeep {
357 6
        $relations = $this->normalizeVariadicRelations($relations);
358
359 6
        foreach ($relations as $i => $relation) {
360 6
            $relationWithoutConstraints = Relation::noConstraints(function () use ($relation) {
361 6
                return $relation();
362
            });
363
364 6
            $deepRelation->getQuery()->mergeWheres(
365 6
                $relationWithoutConstraints->getQuery()->getQuery()->wheres,
366 6
                $relationWithoutConstraints->getQuery()->getQuery()->getRawBindings()['where'] ?? []
367
            );
368
369 6
            $isLast = $i === count($relations) - 1;
370
371 6
            $this->addRemovedScopesToHasOneOrManyDeepRelationship($deepRelation, $relationWithoutConstraints, $isLast);
372
        }
373
374 6
        return $deepRelation;
375
    }
376
377
    /**
378
     * Add the removed scopes from an existing relationship to a has-one-deep or has-many-deep relationship.
379
     *
380
     * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $deepRelation
381
     * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
382
     * @param bool $isLastRelation
383
     * @return void
384
     */
385 6
    protected function addRemovedScopesToHasOneOrManyDeepRelationship(
386
        HasManyDeep $deepRelation,
387
        Relation $relation,
388
        bool $isLastRelation
389
    ): void {
390 6
        $removedScopes = $relation->getQuery()->removedScopes();
391
392 6
        foreach ($removedScopes as $scope) {
393 4
            if ($scope === SoftDeletingScope::class) {
394 2
                if ($isLastRelation) {
395 1
                    $deepRelation->withTrashed();
396
                } else {
397 1
                    $deletedAtColumn = $relation->getRelated()->getQualifiedDeletedAtColumn();
398
399 1
                    $deepRelation->withTrashed($deletedAtColumn);
400
                }
401
            }
402
403 4
            if ($scope === 'SoftDeletableHasManyThrough') {
404 1
                $deletedAtColumn = $relation->getParent()->getQualifiedDeletedAtColumn();
405
406 1
                $deepRelation->withTrashed($deletedAtColumn);
407
            }
408
409 4
            if (str_starts_with($scope, HasManyDeep::class . ':')) {
410 1
                $deletedAtColumn = explode(':', $scope)[1];
411
412 1
                $deepRelation->withTrashed($deletedAtColumn);
413
            }
414
        }
415
    }
416
417
    /**
418
     * Normalize the relations from a variadic parameter.
419
     *
420
     * @param array $relations
421
     * @return array
422
     */
423 21
    protected function normalizeVariadicRelations(array $relations): array
424
    {
425 21
        return is_array($relations[0]) && !is_callable($relations[0]) ? $relations[0] : $relations;
426
    }
427
}
428