Passed
Push — master ( eef6ca...9b9fee )
by Jonas
03:50
created

IsOfDescendantsRelation::buildDictionary()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 34
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 6

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 18
c 2
b 1
f 0
dl 0
loc 34
ccs 20
cts 20
cp 1
rs 9.0444
cc 6
nc 9
nop 1
crap 6
1
<?php
2
3
namespace Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Traits;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Collection;
0 ignored issues
show
Bug introduced by
The type Illuminate\Database\Eloquent\Collection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use Illuminate\Database\Eloquent\Model;
8
use Illuminate\Support\Str;
9
use Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar;
10
11
trait IsOfDescendantsRelation
12
{
13
    use TracksIntermediateScopes;
14
15
    /**
16
     * Whether to include the parent model.
17
     *
18
     * @var bool
19
     */
20
    protected $andSelf;
21
22
    /**
23
     * The path list column alias.
24
     *
25
     * @var string
26
     */
27
    protected $pathListAlias = 'laravel_paths';
28
29
    /**
30
     * Set the base constraints on the relation query.
31
     *
32
     * @return void
33
     */
34 350
    public function addConstraints()
35
    {
36 350
        if (static::$constraints) {
37 217
            $constraint = function (Builder $query) {
38 217
                $this->addExpressionWhereConstraints($query);
39 217
            };
40
41 217
            $this->addExpression($constraint);
42
        }
43
    }
44
45
    /**
46
     * Set the where clause on the recursive expression query.
47
     *
48
     * @param \Illuminate\Database\Eloquent\Builder $query
49
     * @return void
50
     */
51
    abstract public function addExpressionWhereConstraints(Builder $query);
52
53
    /**
54
     * Set the constraints for an eager load of the relation.
55
     *
56
     * @param array $models
57
     * @return void
58
     */
59 85
    public function addEagerConstraints(array $models)
60
    {
61 85
        $constraint = function (Builder $query) use ($models) {
62 85
            $this->addEagerExpressionWhereConstraints($query, $models);
63 85
        };
64
65 85
        $grammar = $this->getExpressionGrammar();
66
67 85
        $pathSeparator = $this->parent->getPathSeparator();
68 85
        $listSeparator = $this->getPathListSeparator();
69
70 85
        $pathList = $grammar->selectPathList(
71 85
            $this->query->getQuery()->newQuery(),
72 85
            $this->parent->getExpressionName(),
73 85
            $this->parent->getPathName(),
74 85
            $pathSeparator,
75 85
            $listSeparator
76 85
        );
77
78 85
        $this->addExpression($constraint, null, null, true)
79 85
            ->select($this->query->getQuery()->from.'.*')
80 85
            ->selectSub($pathList, $this->pathListAlias);
81
    }
82
83
    /**
84
     * Set the where clause on the recursive expression query for an eager load of the relation.
85
     *
86
     * @param \Illuminate\Database\Eloquent\Builder $query
87
     * @param array $models
88
     * @return void
89
     */
90 85
    public function addEagerExpressionWhereConstraints(Builder $query, array $models)
91
    {
92 85
        $localKeyName = $this->getEagerLoadingLocalKeyName();
93
94 85
        $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

94
        /** @scrutinizer ignore-call */ 
95
        $whereIn = $this->whereInMethod($this->parent, $localKeyName);
Loading history...
95
96 85
        $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

96
        /** @scrutinizer ignore-call */ 
97
        $keys = $this->getKeys($models, $localKeyName);
Loading history...
97
98 85
        $query->$whereIn($localKeyName, $keys);
99
    }
100
101
    /**
102
     * Build model dictionary.
103
     *
104
     * @param \Illuminate\Database\Eloquent\Collection $results
105
     * @return array
106
     */
107 85
    protected function buildDictionary(Collection $results)
108
    {
109 85
        $dictionary = [];
110
111 85
        if ($results->isEmpty()) {
112 5
            return $dictionary;
113
        }
114
115 80
        $paths = explode(
116 80
            $this->getPathListSeparator(),
117 80
            $results[0]->{$this->pathListAlias}
118 80
        );
119
120 80
        $foreignKeyName = $this->getEagerLoadingForeignKeyName();
121 80
        $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...
122 80
        $pathSeparator = $this->parent->getPathSeparator();
123
124 80
        foreach ($results as $result) {
125 80
            foreach ($paths as $path) {
126 80
                if (!$this->pathBelongsToResult($result, $foreignKeyName, $accessor, $pathSeparator, $path)) {
127 80
                    continue;
128
                }
129
130 80
                $dictionary = $this->addResultToDictionary($dictionary, $result, $pathSeparator, $path);
131
            }
132
133 80
            unset($result->{$this->pathListAlias});
134
        }
135
136 80
        foreach ($dictionary as $key => $results) {
137 80
            $dictionary[$key] = $results->all();
138
        }
139
140 80
        return $dictionary;
141
    }
142
143
    /**
144
     * Determine whether a path belongs to a result.
145
     *
146
     * @param \Illuminate\Database\Eloquent\Model $result
147
     * @param string $foreignKeyName
148
     * @param string $accessor
149
     * @param string $pathSeparator
150
     * @param string $path
151
     * @return bool
152
     */
153 80
    protected function pathBelongsToResult(Model $result, $foreignKeyName, $accessor, $pathSeparator, $path)
154
    {
155 80
        $foreignKey = (string) ($accessor ? $result->$accessor : $result)->$foreignKeyName;
156
157 80
        $isDescendant = Str::endsWith($path, $pathSeparator.$foreignKey);
158
159 80
        if ($this->andSelf) {
160 40
            if ($isDescendant || $path === $foreignKey) {
161 40
                return true;
162
            }
163 40
        } elseif ($isDescendant) {
164 40
            return true;
165
        }
166
167 80
        return false;
168
    }
169
170
    /**
171
     * Add a result to the dictionary.
172
     *
173
     * @param array $dictionary
174
     * @param \Illuminate\Database\Eloquent\Model $result
175
     * @param string $pathSeparator
176
     * @param string $path
177
     * @return array
178
     */
179 80
    protected function addResultToDictionary(array $dictionary, Model $result, $pathSeparator, $path)
180
    {
181 80
        $keys = explode($pathSeparator, $path);
182
183 80
        if (!$this->andSelf) {
184 40
            array_pop($keys);
185
        }
186
187 80
        foreach ($keys as $key) {
188 80
            if (!isset($dictionary[$key])) {
189 80
                $dictionary[$key] = new Collection();
190
            }
191
192 80
            if (!$dictionary[$key]->contains($result)) {
193 80
                $dictionary[$key][] = $result;
194
            }
195
        }
196
197 80
        return $dictionary;
198
    }
199
200
    /**
201
     * Get the local key name for an eager load of the relation.
202
     *
203
     * @return string
204
     */
205
    abstract public function getEagerLoadingLocalKeyName();
206
207
    /**
208
     * Get the foreign key name for an eager load of the relation.
209
     *
210
     * @return string
211
     */
212
    abstract public function getEagerLoadingForeignKeyName();
213
214
    /**
215
     * Get the accessor for an eager load of the relation.
216
     *
217
     * @return string|null
218
     */
219 20
    public function getEagerLoadingAccessor()
220
    {
221 20
        return null;
222
    }
223
224
    /**
225
     * Add a recursive expression to the query.
226
     *
227
     * @param callable $constraint
228
     * @param \Illuminate\Database\Eloquent\Builder|null $query
229
     * @param string|null $alias
230
     * @param bool $selectPath
231
     * @return \Illuminate\Database\Eloquent\Builder
232
     */
233 350
    protected function addExpression(callable $constraint, Builder $query = null, $alias = null, $selectPath = false)
234
    {
235 350
        $name = $this->parent->getExpressionName();
236
237 350
        $query = $query ?: $this->query;
238
239 350
        $grammar = $this->getExpressionGrammar();
240
241 350
        $expression = $this->getInitialQuery($grammar, $constraint, $alias, $selectPath)
242 350
            ->unionAll(
243 350
                $this->getRecursiveQuery($grammar, $selectPath)
244 350
            );
245
246 350
        $query->withRecursiveExpression($name, $expression);
247
248 350
        $query->withGlobalScope(get_class(), function (Builder $query) use ($name) {
249 310
            $query->whereIn(
250 310
                $this->getExpressionForeignKeyName(),
251 310
                (new $this->parent())->setTable($name)
252 310
                    ->newQuery()
253 310
                    ->select($this->getExpressionLocalKeyName())
254 310
                    ->withGlobalScopes($this->intermediateScopes)
255 310
                    ->withoutGlobalScopes($this->removedIntermediateScopes)
256 310
            );
257 350
        });
258
259 350
        return $query;
260
    }
261
262
    /**
263
     * Get the initial query for a relationship expression.
264
     *
265
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
266
     * @param callable $constraint
267
     * @param string|null $alias
268
     * @param bool $selectPath
269
     * @return \Illuminate\Database\Eloquent\Builder $query
270
     */
271 350
    protected function getInitialQuery(ExpressionGrammar $grammar, callable $constraint, $alias, $selectPath)
272
    {
273 350
        $model = new $this->parent();
274
275 350
        $initialDepth = $this->andSelf ? 0 : 1;
276
277 350
        $depth = $grammar->wrap($model->getDepthName());
0 ignored issues
show
Bug introduced by
The method wrap() does not exist on Staudenmeir\LaravelAdjac...mmars\ExpressionGrammar. Since it exists in all sub-types, consider adding an abstract or default implementation to Staudenmeir\LaravelAdjac...mmars\ExpressionGrammar. ( Ignorable by Annotation )

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

277
        /** @scrutinizer ignore-call */ 
278
        $depth = $grammar->wrap($model->getDepthName());
Loading history...
278
279 350
        $query = $model->newModelQuery()
280 350
            ->select('*')
281 350
            ->selectRaw("$initialDepth as $depth");
282
283 350
        if ($alias) {
284 24
            $query->from($query->getQuery()->from, $alias);
285
        }
286
287 350
        $constraint($query);
288
289 350
        if ($selectPath) {
290 85
            $initialPath = $grammar->compileInitialPath(
291 85
                $this->getExpressionLocalKeyName(),
292 85
                $model->getPathName()
293 85
            );
294
295 85
            $query->selectRaw($initialPath);
296
        }
297
298 350
        return $query;
299
    }
300
301
    /**
302
     * Get the recursive query for a relationship expression.
303
     *
304
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
305
     * @param bool $selectPath
306
     * @return \Illuminate\Database\Eloquent\Builder $query
307
     */
308 350
    protected function getRecursiveQuery(ExpressionGrammar $grammar, $selectPath)
309
    {
310 350
        $model = new $this->parent();
311
312 350
        $name = $model->getExpressionName();
313
314 350
        $depth = $grammar->wrap($model->getDepthName());
315
316 350
        $recursiveDepth = "$depth + 1";
317
318 350
        $query = $model->newModelQuery();
319
320 350
        $query->select($query->getQuery()->from.'.*')
321 350
            ->selectRaw("$recursiveDepth as $depth")
322 350
            ->join(
323 350
                $name,
324 350
                $name.'.'.$model->getLocalKeyName(),
325 350
                '=',
326 350
                $query->qualifyColumn($model->getParentKeyName())
327 350
            );
328
329 350
        if ($selectPath) {
330 85
            $recursivePath = $grammar->compileRecursivePath(
331 85
                $model->qualifyColumn(
332 85
                    $this->getExpressionLocalKeyName()
333 85
                ),
334 85
                $model->getPathName()
335 85
            );
336
337 85
            $recursivePathBindings = $grammar->getRecursivePathBindings($model->getPathSeparator());
338
339 85
            $query->selectRaw($recursivePath, $recursivePathBindings);
340
        }
341
342 350
        return $query;
343
    }
344
345
    /**
346
     * Get the local key name for the recursion expression.
347
     *
348
     * @return string
349
     */
350
    abstract public function getExpressionLocalKeyName();
351
352
    /**
353
     * Get the foreign key name for the recursion expression.
354
     *
355
     * @return string
356
     */
357
    abstract public function getExpressionForeignKeyName();
358
359
    /**
360
     * Add the constraints for a relationship query.
361
     *
362
     * @param \Illuminate\Database\Eloquent\Builder $query
363
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
364
     * @param array|mixed $columns
365
     * @return \Illuminate\Database\Eloquent\Builder
366
     */
367 48
    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
368
    {
369 48
        $table = (new $this->parent())->getTable();
370
371 48
        if ($table === $parentQuery->getQuery()->from) {
372 24
            $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

372
            $table = $alias = $this->/** @scrutinizer ignore-call */ getRelationCountHash();
Loading history...
373
        } else {
374 24
            $alias = null;
375
        }
376
377 48
        $constraint = function (Builder $query) use ($table) {
378 48
            $this->addExistenceExpressionWhereConstraints($query, $table);
379 48
        };
380
381 48
        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

381
        return $this->addExpression($constraint, /** @scrutinizer ignore-type */ $query->select($columns), $alias);
Loading history...
382
    }
383
384
    /**
385
     * Set the where clause on the recursive expression query for an existence query.
386
     *
387
     * @param \Illuminate\Database\Eloquent\Builder $query
388
     * @param string $table
389
     * @return void
390
     */
391 48
    public function addExistenceExpressionWhereConstraints(Builder $query, $table)
392
    {
393 48
        $first = $this->andSelf
394 24
            ? $this->parent->getLocalKeyName()
395 24
            : $this->parent->getParentKeyName();
396
397 48
        $query->whereColumn(
398 48
            $table.'.'.$first,
399 48
            '=',
400 48
            $this->parent->getQualifiedLocalKeyName()
401 48
        );
402
    }
403
404
    /**
405
     * Get the expression grammar.
406
     *
407
     * @return \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar
408
     */
409 350
    protected function getExpressionGrammar()
410
    {
411 350
        return $this->parent->newQuery()->getExpressionGrammar();
412
    }
413
414
    /**
415
     * Get the path list separator.
416
     *
417
     * @return string
418
     */
419 85
    protected function getPathListSeparator()
420
    {
421 85
        return str_repeat(
422 85
            $this->parent->getPathSeparator(),
423 85
            2
424 85
        );
425
    }
426
}
427