Passed
Push — master ( 926d18...c2f1bc )
by Jonas
01:50
created

addRemovedScopesToHasOneOrManyDeepRelationship()   A

Complexity

Conditions 6
Paths 13

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6

Importance

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