Passed
Push — master ( 98eee9...aa1f26 )
by Jonas
09:56
created

IsOfDescendantsRelation   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 359
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 31
eloc 116
c 1
b 0
f 0
dl 0
loc 359
ccs 130
cts 130
cp 1
rs 9.92

12 Methods

Rating   Name   Duplication   Size   Complexity  
A getRelationExistenceQuery() 0 15 2
A getRecursiveQuery() 0 28 2
A getExpressionGrammar() 0 3 1
A addEagerConstraints() 0 22 1
A addExistenceExpressionWhereConstraints() 0 10 2
A addExpression() 0 27 2
A getInitialQuery() 0 21 3
A addEagerExpressionWhereConstraints() 0 9 1
A getPathListSeparator() 0 5 1
A addConstraints() 0 8 2
A getEagerLoadingAccessor() 0 3 1
C buildDictionary() 0 51 13
1
<?php
2
3
namespace Staudenmeir\LaravelAdjacencyList\Eloquent\Relations;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Collection;
7
use Illuminate\Support\Str;
8
use Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar;
9
10
trait IsOfDescendantsRelation
11
{
12
    use TracksIntermediateScopes;
13
14
    /**
15
     * Whether to include the parent model.
16
     *
17
     * @var bool
18
     */
19
    protected $andSelf;
20
21
    /**
22
     * The path list column alias.
23
     *
24
     * @var string
25
     */
26
    protected $pathListAlias = 'laravel_paths';
27
28
    /**
29
     * Set the base constraints on the relation query.
30
     *
31
     * @return void
32
     */
33 220
    public function addConstraints()
34
    {
35 220
        if (static::$constraints) {
36 136
            $constraint = function (Builder $query) {
37 136
                $this->addExpressionWhereConstraints($query);
38 136
            };
39
40 136
            $this->addExpression($constraint);
41
        }
42 220
    }
43
44
    /**
45
     * Set the where clause on the recursive expression query.
46
     *
47
     * @param \Illuminate\Database\Eloquent\Builder $query
48
     * @return void
49
     */
50
    abstract public function addExpressionWhereConstraints(Builder $query);
51
52
    /**
53
     * Set the constraints for an eager load of the relation.
54
     *
55
     * @param array $models
56
     * @return void
57
     */
58 48
    public function addEagerConstraints(array $models)
59
    {
60 48
        $constraint = function (Builder $query) use ($models) {
61 48
            $this->addEagerExpressionWhereConstraints($query, $models);
62 48
        };
63
64 48
        $grammar = $this->getExpressionGrammar();
65
66 48
        $pathSeparator = $this->parent->getPathSeparator();
67 48
        $listSeparator = $this->getPathListSeparator();
68
69 48
        $pathList = $grammar->selectPathList(
70 48
            $this->query->getQuery()->newQuery(),
71 48
            $this->parent->getExpressionName(),
72 48
            $this->parent->getPathName(),
73
            $pathSeparator,
74
            $listSeparator
75
        );
76
77 48
        $this->addExpression($constraint, null, null, true)
78 48
            ->select($this->query->getQuery()->from.'.*')
79 48
            ->selectSub($pathList, $this->pathListAlias);
80 48
    }
81
82
    /**
83
     * Set the where clause on the recursive expression query for an eager load of the relation.
84
     *
85
     * @param \Illuminate\Database\Eloquent\Builder $query
86
     * @param array $models
87
     * @return void
88
     */
89 48
    public function addEagerExpressionWhereConstraints(Builder $query, array $models)
90
    {
91 48
        $localKeyName = $this->getEagerLoadingLocalKeyName();
92
93 48
        $whereIn = $this->whereInMethod($this->parent, $localKeyName);
0 ignored issues
show
Bug introduced by
It seems like whereInMethod() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

93
        /** @scrutinizer ignore-call */ 
94
        $whereIn = $this->whereInMethod($this->parent, $localKeyName);
Loading history...
94
95 48
        $keys = $this->getKeys($models, $localKeyName);
0 ignored issues
show
Bug introduced by
It seems like getKeys() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

95
        /** @scrutinizer ignore-call */ 
96
        $keys = $this->getKeys($models, $localKeyName);
Loading history...
96
97 48
        $query->$whereIn($localKeyName, $keys);
98 48
    }
99
100
    /**
101
     * Build model dictionary.
102
     *
103
     * @param \Illuminate\Database\Eloquent\Collection $results
104
     * @return array
105
     */
106 48
    protected function buildDictionary(Collection $results)
107
    {
108 48
        $dictionary = [];
109
110 48
        $foreignKeyName = $this->getEagerLoadingForeignKeyName();
111 48
        $accessor = $this->getEagerLoadingAccessor();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $accessor is correct as $this->getEagerLoadingAccessor() targeting Staudenmeir\LaravelAdjac...tEagerLoadingAccessor() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
112
113 48
        $pathSeparator = $this->parent->getPathSeparator();
114 48
        $pathListSeparator = $this->getPathListSeparator();
115
116 48
        foreach ($results as $result) {
117 48
            $paths = explode($pathListSeparator, $result->{$this->pathListAlias});
118
119 48
            $foreignKey = (string) ($accessor ? $result->$accessor : $result)->$foreignKeyName;
120
121 48
            foreach ($paths as $path) {
122 48
                $isDescendant = Str::endsWith($path, $pathSeparator.$foreignKey);
123
124 48
                if ($this->andSelf) {
125 24
                    if (!$isDescendant && $path !== $foreignKey) {
126 24
                        continue;
127
                    }
128 24
                } elseif (!$isDescendant) {
129 24
                    continue;
130
                }
131
132 48
                $keys = explode($pathSeparator, $path);
133
134 48
                if (!$this->andSelf) {
135 24
                    array_pop($keys);
136
                }
137
138 48
                foreach ($keys as $key) {
139 48
                    if (!isset($dictionary[$key])) {
140 48
                        $dictionary[$key] = new Collection();
141
                    }
142
143 48
                    if (!$dictionary[$key]->contains($result)) {
144 48
                        $dictionary[$key][] = $result;
145
                    }
146
                }
147
            }
148
149 48
            unset($result->{$this->pathListAlias});
150
        }
151
152 48
        foreach ($dictionary as $key => $results) {
153 48
            $dictionary[$key] = $results->all();
154
        }
155
156 48
        return $dictionary;
157
    }
158
159
    /**
160
     * Get the local key name for an eager load of the relation.
161
     *
162
     * @return string
163
     */
164
    abstract public function getEagerLoadingLocalKeyName();
165
166
    /**
167
     * Get the foreign key name for an eager load of the relation.
168
     *
169
     * @return string
170
     */
171
    abstract public function getEagerLoadingForeignKeyName();
172
173
    /**
174
     * Get the accessor for an eager load of the relation.
175
     *
176
     * @return string|null
177
     */
178 16
    public function getEagerLoadingAccessor()
179
    {
180 16
        return null;
181
    }
182
183
    /**
184
     * Add a recursive expression to the query.
185
     *
186
     * @param callable $constraint
187
     * @param \Illuminate\Database\Eloquent\Builder|null $query
188
     * @param string|null $alias
189
     * @param bool $selectPath
190
     * @return \Illuminate\Database\Eloquent\Builder
191
     */
192 220
    protected function addExpression(callable $constraint, Builder $query = null, $alias = null, $selectPath = false)
193
    {
194 220
        $name = $this->parent->getExpressionName();
195
196 220
        $query = $query ?: $this->query;
197
198 220
        $grammar = $this->getExpressionGrammar();
199
200 220
        $expression = $this->getInitialQuery($grammar, $constraint, $alias, $selectPath)
201 220
            ->unionAll(
202 220
                $this->getRecursiveQuery($grammar, $selectPath)
203
            );
204
205 220
        $query->withRecursiveExpression($name, $expression);
206
207 220
        $query->withGlobalScope(get_class(), function (Builder $query) use ($name) {
208 196
            $query->whereIn(
209 196
                $this->getExpressionForeignKeyName(),
210 196
                (new $this->parent())->setTable($name)
211 196
                    ->newQuery()
212 196
                    ->select($this->getExpressionLocalKeyName())
213 196
                    ->withGlobalScopes($this->intermediateScopes)
214 196
                    ->withoutGlobalScopes($this->removedIntermediateScopes)
215
            );
216 220
        });
217
218 220
        return $query;
219
    }
220
221
    /**
222
     * Get the initial query for a relationship expression.
223
     *
224
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
225
     * @param callable $constraint
226
     * @param string|null $alias
227
     * @param bool $selectPath
228
     * @return \Illuminate\Database\Eloquent\Builder $query
229
     */
230 220
    protected function getInitialQuery(ExpressionGrammar $grammar, callable $constraint, $alias, $selectPath)
231
    {
232 220
        $model = new $this->parent();
233 220
        $query = $model->newModelQuery();
234
235 220
        if ($alias) {
236 18
            $query->from($query->getQuery()->from, $alias);
237
        }
238
239 220
        $constraint($query);
240
241 220
        if ($selectPath) {
242 48
            $initialPath = $grammar->compileInitialPath(
243 48
                $this->getExpressionLocalKeyName(),
244 48
                $model->getPathName()
245
            );
246
247 48
            $query->select('*')->selectRaw($initialPath);
248
        }
249
250 220
        return $query;
251
    }
252
253
    /**
254
     * Get the recursive query for a relationship expression.
255
     *
256
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
257
     * @param bool $selectPath
258
     * @return \Illuminate\Database\Eloquent\Builder $query
259
     */
260 220
    protected function getRecursiveQuery(ExpressionGrammar $grammar, $selectPath)
261
    {
262 220
        $model = new $this->parent();
263 220
        $name = $model->getExpressionName();
264 220
        $query = $model->newModelQuery();
265
266 220
        $query->select($query->getQuery()->from.'.*')
267 220
            ->join(
268 220
                $name,
269 220
                $name.'.'.$model->getLocalKeyName(),
270 220
                '=',
271 220
                $query->qualifyColumn($model->getParentKeyName())
272
            );
273
274 220
        if ($selectPath) {
275 48
            $recursivePath = $grammar->compileRecursivePath(
276 48
                $model->qualifyColumn(
277 48
                    $this->getExpressionLocalKeyName()
278
                ),
279 48
                $model->getPathName()
280
            );
281
282 48
            $recursivePathBindings = $grammar->getRecursivePathBindings($model->getPathSeparator());
283
284 48
            $query->selectRaw($recursivePath, $recursivePathBindings);
285
        }
286
287 220
        return $query;
288
    }
289
290
    /**
291
     * Get the local key name for the recursion expression.
292
     *
293
     * @return string
294
     */
295
    abstract public function getExpressionLocalKeyName();
296
297
    /**
298
     * Get the foreign key name for the recursion expression.
299
     *
300
     * @return string
301
     */
302
    abstract public function getExpressionForeignKeyName();
303
304
    /**
305
     * Add the constraints for a relationship query.
306
     *
307
     * @param \Illuminate\Database\Eloquent\Builder $query
308
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
309
     * @param array|mixed $columns
310
     * @return \Illuminate\Database\Eloquent\Builder
311
     */
312 36
    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
313
    {
314 36
        $table = (new $this->parent())->getTable();
315
316 36
        if ($table === $parentQuery->getQuery()->from) {
317 18
            $table = $alias = $this->getRelationCountHash();
0 ignored issues
show
Bug introduced by
It seems like getRelationCountHash() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

317
            $table = $alias = $this->/** @scrutinizer ignore-call */ getRelationCountHash();
Loading history...
318
        } else {
319 18
            $alias = null;
320
        }
321
322 36
        $constraint = function (Builder $query) use ($table) {
323 36
            $this->addExistenceExpressionWhereConstraints($query, $table);
324 36
        };
325
326 36
        return $this->addExpression($constraint, $query->select($columns), $alias);
0 ignored issues
show
Bug introduced by
It seems like $query->select($columns) can also be of type Illuminate\Database\Query\Builder; however, parameter $query of Staudenmeir\LaravelAdjac...lation::addExpression() does only seem to accept Illuminate\Database\Eloquent\Builder|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

326
        return $this->addExpression($constraint, /** @scrutinizer ignore-type */ $query->select($columns), $alias);
Loading history...
327
    }
328
329
    /**
330
     * Set the where clause on the recursive expression query for an existence query.
331
     *
332
     * @param \Illuminate\Database\Eloquent\Builder $query
333
     * @param string $table
334
     * @return void
335
     */
336 36
    public function addExistenceExpressionWhereConstraints(Builder $query, $table)
337
    {
338 36
        $first = $this->andSelf
339 18
            ? $this->parent->getLocalKeyName()
340 18
            : $this->parent->getParentKeyName();
341
342 36
        $query->whereColumn(
343 36
            $table.'.'.$first,
344 36
            '=',
345 36
            $this->parent->getQualifiedLocalKeyName()
346
        );
347 36
    }
348
349
    /**
350
     * Get the expression grammar.
351
     *
352
     * @return \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar
353
     */
354 220
    protected function getExpressionGrammar()
355
    {
356 220
        return $this->parent->newQuery()->getExpressionGrammar();
357
    }
358
359
    /**
360
     * Get the path list separator.
361
     *
362
     * @return string
363
     */
364 48
    protected function getPathListSeparator()
365
    {
366 48
        return str_repeat(
367 48
            $this->parent->getPathSeparator(),
368 48
            2
369
        );
370
    }
371
}
372