Passed
Push — master ( c2f1bc...d58289 )
by Jonas
01:57
created

hasOneOrManyDeepFromRelations()   A

Complexity

Conditions 6
Paths 12

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 18
nc 12
nop 1
dl 0
loc 32
ccs 18
cts 18
cp 1
crap 6
rs 9.0444
c 0
b 0
f 0
1
<?php
2
3
namespace Staudenmeir\EloquentHasManyDeep\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\HasManyDeep;
16
use Staudenmeir\EloquentHasManyDeep\HasOneDeep;
17
18
trait ConcatenatesRelationships
19
{
20
    /**
21
     * Define a has-many-deep relationship from existing relationships.
22
     *
23
     * @param \Illuminate\Database\Eloquent\Relations\Relation|callable ...$relations
24
     * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep
25
     */
26 17
    public function hasManyDeepFromRelations(...$relations)
27
    {
28 17
        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

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

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