ConcatenatesRelationships   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 336
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 38
eloc 134
c 1
b 0
f 0
dl 0
loc 336
rs 9.36

10 Methods

Rating   Name   Duplication   Size   Complexity  
A normalizeVariadicRelations() 0 3 3
A hasOneDeepFromRelations() 0 24 1
A hasOneOrManyThroughParent() 0 23 6
A hasManyDeepFromRelationsWithConstraints() 0 7 1
A hasManyDeepFromRelations() 0 25 1
A customizeHasOneOrManyDeepRelationship() 0 22 4
F hasOneOrManyDeepFromRelations() 0 76 13
A hasOneDeepFromRelationsWithConstraints() 0 7 1
A addRemovedScopesToHasOneOrManyDeepRelationship() 0 34 6
A addConstraintsToHasOneOrManyDeepRelationship() 0 22 2
1
<?php
2
3
namespace Staudenmeir\EloquentHasManyDeep\Eloquent\Traits;
4
5
use Illuminate\Database\Eloquent\Relations\Relation;
6
use Illuminate\Database\Eloquent\SoftDeletingScope;
7
use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\ThirdParty\LaravelHasManyMerged\HasManyMerged;
8
use Staudenmeir\EloquentHasManyDeep\HasManyDeep;
9
use Staudenmeir\EloquentHasManyDeep\HasOneDeep;
10
use Staudenmeir\EloquentHasManyDeepContracts\Interfaces\ConcatenableRelation;
11
12
trait ConcatenatesRelationships
13
{
14
    use ConcatenatesNativeRelationships;
15
16
    /**
17
     * Define a has-many-deep relationship from existing relationships.
18
     *
19
     * @param \Illuminate\Database\Eloquent\Relations\Relation|callable ...$relations
20
     * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep
21
     */
22
    public function hasManyDeepFromRelations(...$relations)
23
    {
24
        [
25
            $related,
26
            $through,
27
            $foreignKeys,
28
            $localKeys,
29
            $postGetCallbacks,
30
            $customThroughKeyCallback,
31
            $customEagerConstraintsCallback,
32
            $customEagerMatchingCallback
33
        ] =
34
            $this->hasOneOrManyDeepFromRelations($relations);
35
36
        $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

36
        /** @scrutinizer ignore-call */ 
37
        $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...
37
38
        $this->customizeHasOneOrManyDeepRelationship(
39
            $relation,
40
            $postGetCallbacks,
41
            $customThroughKeyCallback,
42
            $customEagerConstraintsCallback,
43
            $customEagerMatchingCallback
44
        );
45
46
        return $relation;
47
    }
48
49
    /**
50
     * Define a has-one-deep relationship from existing relationships.
51
     *
52
     * @param \Illuminate\Database\Eloquent\Relations\Relation|callable ...$relations
53
     * @return \Staudenmeir\EloquentHasManyDeep\HasOneDeep
54
     */
55
    public function hasOneDeepFromRelations(...$relations)
56
    {
57
        [
58
            $related,
59
            $through,
60
            $foreignKeys,
61
            $localKeys,
62
            $postGetCallbacks,
63
            $customThroughKeyCallback,
64
            $customEagerConstraintsCallback,
65
            $customEagerMatchingCallback
66
        ] = $this->hasOneOrManyDeepFromRelations($relations);
67
68
        $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

68
        /** @scrutinizer ignore-call */ 
69
        $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...
69
70
        $this->customizeHasOneOrManyDeepRelationship(
71
            $relation,
72
            $postGetCallbacks,
73
            $customThroughKeyCallback,
74
            $customEagerConstraintsCallback,
75
            $customEagerMatchingCallback
76
        );
77
78
        return $relation;
79
    }
80
81
    /**
82
     * Prepare a has-one-deep or has-many-deep relationship from existing relationships.
83
     *
84
     * @param \Illuminate\Database\Eloquent\Relations\Relation[]|callable[] $relations
85
     * @return array
86
     */
87
    protected function hasOneOrManyDeepFromRelations(array $relations)
88
    {
89
        $relations = $this->normalizeVariadicRelations($relations);
90
91
        foreach ($relations as $i => $relation) {
92
            if (is_callable($relation)) {
93
                $relations[$i] = $relation();
94
            }
95
        }
96
97
        $related = null;
98
        $through = [];
99
        $foreignKeys = [];
100
        $localKeys = [];
101
        $postGetCallbacks = [];
102
        $customThroughKeyCallback = null;
103
        $customEagerConstraintsCallback = null;
104
        $customEagerMatchingCallback = null;
105
106
        foreach ($relations as $i => $relation) {
107
            // https://github.com/korridor/laravel-has-many-merged
108
            if (is_a($relation, 'Korridor\LaravelHasManyMerged\HasManyMerged', true)) {
109
                $relation = HasManyMerged::fromBaseRelation($relation);
110
            }
111
112
            if ($relation instanceof ConcatenableRelation) {
113
                [$through, $foreignKeys, $localKeys] = $relation->appendToDeepRelationship(
114
                    $through,
115
                    $foreignKeys,
116
                    $localKeys,
117
                    $i
118
                );
119
120
                if (method_exists($relation, 'postGetCallback')) {
121
                    $postGetCallbacks[] = [$relation, 'postGetCallback'];
122
                }
123
124
                if ($i === 0) {
125
                    if (method_exists($relation, 'getThroughKeyForDeepRelationships')) {
126
                        $customThroughKeyCallback = [$relation, 'getThroughKeyForDeepRelationships'];
127
                    }
128
129
                    if (method_exists($relation, 'addEagerConstraintsToDeepRelationship')) {
130
                        $customEagerConstraintsCallback = [$relation, 'addEagerConstraintsToDeepRelationship'];
131
                    }
132
133
                    if (method_exists($relation, 'matchResultsForDeepRelationship')) {
134
                        $customEagerMatchingCallback = [$relation, 'matchResultsForDeepRelationship'];
135
                    }
136
                }
137
            } else {
138
                $method = $this->hasOneOrManyDeepRelationMethod($relation);
139
140
                [$through, $foreignKeys, $localKeys] = $this->$method($relation, $through, $foreignKeys, $localKeys);
141
            }
142
143
            if ($i === count($relations) - 1) {
144
                $related = get_class($relation->getRelated());
145
146
                if ((new $related())->getTable() !== $relation->getRelated()->getTable()) {
147
                    $related .= ' from ' . $relation->getRelated()->getTable();
148
                }
149
            } else {
150
                $through[] = $this->hasOneOrManyThroughParent($relation, $relations[$i + 1]);
151
            }
152
        }
153
154
        return [
155
            $related,
156
            $through,
157
            $foreignKeys,
158
            $localKeys,
159
            $postGetCallbacks,
160
            $customThroughKeyCallback,
161
            $customEagerConstraintsCallback,
162
            $customEagerMatchingCallback
163
        ];
164
    }
165
166
    /**
167
     * Prepare the through parent class from an existing relationship and its successor.
168
     *
169
     * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
170
     * @param \Illuminate\Database\Eloquent\Relations\Relation $successor
171
     * @return string
172
     */
173
    protected function hasOneOrManyThroughParent(Relation $relation, Relation $successor)
174
    {
175
        $through = get_class($relation->getRelated());
176
177
        if ($relation instanceof ConcatenableRelation && method_exists($relation, 'getTableForDeepRelationship')) {
178
            return $through . ' from ' . $relation->getTableForDeepRelationship();
179
        }
180
181
        if ((new $through())->getTable() !== $relation->getRelated()->getTable()) {
182
            $through .= ' from ' . $relation->getRelated()->getTable();
183
        }
184
185
        if (get_class($relation->getRelated()) === get_class($successor->getParent())) {
186
            $table = $successor->getParent()->getTable();
187
188
            $segments = explode(' as ', $table);
189
190
            if (isset($segments[1])) {
191
                $through .= ' as ' . $segments[1];
192
            }
193
        }
194
195
        return $through;
196
    }
197
198
    /**
199
     * Customize a has-one-deep or has-many-deep relationship.
200
     *
201
     * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $relation
202
     * @param callable[] $postGetCallbacks
203
     * @param callable|null $customThroughKeyCallback
204
     * @param callable|null $customEagerConstraintsCallback
205
     * @param callable|null $customEagerMatchingCallback
206
     * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep|\Staudenmeir\EloquentHasManyDeep\HasOneDeep
207
     */
208
    protected function customizeHasOneOrManyDeepRelationship(
209
        HasManyDeep $relation,
210
        array $postGetCallbacks,
211
        ?callable $customThroughKeyCallback,
212
        ?callable $customEagerConstraintsCallback,
213
        ?callable $customEagerMatchingCallback
214
    ): HasManyDeep|HasOneDeep {
215
        $relation->withPostGetCallbacks($postGetCallbacks);
216
217
        if ($customThroughKeyCallback) {
218
            $relation->withCustomThroughKeyCallback($customThroughKeyCallback);
219
        }
220
221
        if ($customEagerConstraintsCallback) {
222
            $relation->withCustomEagerConstraintsCallback($customEagerConstraintsCallback);
223
        }
224
225
        if ($customEagerMatchingCallback) {
226
            $relation->withCustomEagerMatchingCallback($customEagerMatchingCallback);
227
        }
228
229
        return $relation;
230
    }
231
232
    /**
233
     * Define a has-many-deep relationship with constraints from existing relationships.
234
     *
235
     * @param callable ...$relations
236
     * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep
237
     */
238
    public function hasManyDeepFromRelationsWithConstraints(...$relations): HasManyDeep
239
    {
240
        $hasManyDeep = $this->hasManyDeepFromRelations(...$relations);
241
242
        $this->addConstraintsToHasOneOrManyDeepRelationship($hasManyDeep, $relations);
243
244
        return $hasManyDeep;
245
    }
246
247
    /**
248
     * Define a has-one-deep relationship with constraints from existing relationships.
249
     *
250
     * @param callable ...$relations
251
     * @return \Staudenmeir\EloquentHasManyDeep\HasOneDeep
252
     */
253
    public function hasOneDeepFromRelationsWithConstraints(...$relations): HasOneDeep
254
    {
255
        $hasOneDeep = $this->hasOneDeepFromRelations(...$relations);
256
257
        $this->addConstraintsToHasOneOrManyDeepRelationship($hasOneDeep, $relations);
258
259
        return $hasOneDeep;
260
    }
261
262
    /**
263
     * Add the constraints from existing relationships to a has-one-deep or has-many-deep relationship.
264
     *
265
     * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $deepRelation
266
     * @param callable[] $relations
267
     * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep|\Staudenmeir\EloquentHasManyDeep\HasOneDeep
268
     */
269
    protected function addConstraintsToHasOneOrManyDeepRelationship(
270
        HasManyDeep $deepRelation,
271
        array $relations
272
    ): HasManyDeep|HasOneDeep {
273
        $relations = $this->normalizeVariadicRelations($relations);
274
275
        foreach ($relations as $i => $relation) {
276
            $relationWithoutConstraints = Relation::noConstraints(function () use ($relation) {
277
                return $relation();
278
            });
279
280
            $deepRelation->getQuery()->mergeWheres(
281
                $relationWithoutConstraints->getQuery()->getQuery()->wheres,
282
                $relationWithoutConstraints->getQuery()->getQuery()->getRawBindings()['where'] ?? []
283
            );
284
285
            $isLast = $i === count($relations) - 1;
286
287
            $this->addRemovedScopesToHasOneOrManyDeepRelationship($deepRelation, $relationWithoutConstraints, $isLast);
288
        }
289
290
        return $deepRelation;
291
    }
292
293
    /**
294
     * Add the removed scopes from an existing relationship to a has-one-deep or has-many-deep relationship.
295
     *
296
     * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $deepRelation
297
     * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
298
     * @param bool $isLastRelation
299
     * @return void
300
     */
301
    protected function addRemovedScopesToHasOneOrManyDeepRelationship(
302
        HasManyDeep $deepRelation,
303
        Relation $relation,
304
        bool $isLastRelation
305
    ): void {
306
        $removedScopes = $relation->getQuery()->removedScopes();
307
308
        foreach ($removedScopes as $scope) {
309
            if ($scope === SoftDeletingScope::class) {
310
                if ($isLastRelation) {
311
                    /** @phpstan-ignore method.notFound */
312
                    $deepRelation->withTrashed();
313
                } else {
314
                    /** @phpstan-ignore method.notFound */
315
                    $deletedAtColumn = $relation->getRelated()->getQualifiedDeletedAtColumn();
316
317
                    /** @phpstan-ignore method.notFound */
318
                    $deepRelation->withTrashed($deletedAtColumn);
319
                }
320
            }
321
322
            if ($scope === 'SoftDeletableHasManyThrough') {
323
                /** @phpstan-ignore method.notFound */
324
                $deletedAtColumn = $relation->getParent()->getQualifiedDeletedAtColumn();
325
326
                /** @phpstan-ignore method.notFound */
327
                $deepRelation->withTrashed($deletedAtColumn);
328
            }
329
330
            if (str_starts_with($scope, HasManyDeep::class . ':')) {
331
                $deletedAtColumn = explode(':', $scope)[1];
332
333
                /** @phpstan-ignore method.notFound */
334
                $deepRelation->withTrashed($deletedAtColumn);
335
            }
336
        }
337
    }
338
339
    /**
340
     * Normalize the relations from a variadic parameter.
341
     *
342
     * @param array $relations
343
     * @return array
344
     */
345
    protected function normalizeVariadicRelations(array $relations): array
346
    {
347
        return is_array($relations[0]) && !is_callable($relations[0]) ? $relations[0] : $relations;
348
    }
349
}
350