Passed
Push — master ( c197a0...d72f0b )
by Jonas
11:51 queued 20s
created

IsOfDescendantsRelation   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 409
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 35
eloc 129
c 1
b 0
f 0
dl 0
loc 409
ccs 130
cts 130
cp 1
rs 9.6

14 Methods

Rating   Name   Duplication   Size   Complexity  
A addConstraints() 0 8 2
A addEagerConstraints() 0 22 1
A addEagerExpressionWhereConstraints() 0 9 1
A getPathListSeparator() 0 5 1
A pathBelongsToResult() 0 15 6
A getInitialQuery() 0 28 4
A addExpression() 0 27 2
A getRecursiveQuery() 0 35 2
A buildDictionary() 0 30 5
A addExistenceExpressionWhereConstraints() 0 10 2
A getEagerLoadingAccessor() 0 3 1
A getExpressionGrammar() 0 3 1
A getRelationExistenceQuery() 0 15 2
A addResultToDictionary() 0 19 5
1
<?php
2
3
namespace Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Traits;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Collection;
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 345
    public function addConstraints()
35
    {
36 345
        if (static::$constraints) {
37 217
            $constraint = function (Builder $query) {
38 217
                $this->addExpressionWhereConstraints($query);
39
            };
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 80
    public function addEagerConstraints(array $models)
60
    {
61 80
        $constraint = function (Builder $query) use ($models) {
62 80
            $this->addEagerExpressionWhereConstraints($query, $models);
63
        };
64
65 80
        $grammar = $this->getExpressionGrammar();
66
67 80
        $pathSeparator = $this->parent->getPathSeparator();
68 80
        $listSeparator = $this->getPathListSeparator();
69
70 80
        $pathList = $grammar->selectPathList(
71 80
            $this->query->getQuery()->newQuery(),
72 80
            $this->parent->getExpressionName(),
73 80
            $this->parent->getPathName(),
74
            $pathSeparator,
75
            $listSeparator
76
        );
77
78 80
        $this->addExpression($constraint, null, null, true)
79 80
            ->select($this->query->getQuery()->from.'.*')
80 80
            ->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 80
    public function addEagerExpressionWhereConstraints(Builder $query, array $models)
91
    {
92 80
        $localKeyName = $this->getEagerLoadingLocalKeyName();
93
94 80
        $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 80
        $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 80
        $query->$whereIn($localKeyName, $keys);
99
    }
100
101
    /**
102
     * Build model dictionary.
103
     *
104
     * @param \Illuminate\Database\Eloquent\Collection $results
105
     * @return array
106
     */
107 80
    protected function buildDictionary(Collection $results)
108
    {
109 80
        $dictionary = [];
110
111 80
        $paths = explode(
112 80
            $this->getPathListSeparator(),
113 80
            $results[0]->{$this->pathListAlias}
114
        );
115
116 80
        $foreignKeyName = $this->getEagerLoadingForeignKeyName();
117 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...
118 80
        $pathSeparator = $this->parent->getPathSeparator();
119
120 80
        foreach ($results as $result) {
121 80
            foreach ($paths as $path) {
122 80
                if (!$this->pathBelongsToResult($result, $foreignKeyName, $accessor, $pathSeparator, $path)) {
123 80
                    continue;
124
                }
125
126 80
                $dictionary = $this->addResultToDictionary($dictionary, $result, $pathSeparator, $path);
127
            }
128 80
129 40
            unset($result->{$this->pathListAlias});
130
        }
131
132 80
        foreach ($dictionary as $key => $results) {
133 80
            $dictionary[$key] = $results->all();
134 80
        }
135
136
        return $dictionary;
137 80
    }
138 80
139
    /**
140
     * Determine whether a path belongs to a result.
141
     *
142
     * @param \Illuminate\Database\Eloquent\Model $result
143 80
     * @param string $foreignKeyName
144
     * @param string $accessor
145
     * @param string $pathSeparator
146 80
     * @param string $path
147 80
     * @return bool
148
     */
149
    protected function pathBelongsToResult(Model $result, $foreignKeyName, $accessor, $pathSeparator, $path)
150 80
    {
151
        $foreignKey = (string) ($accessor ? $result->$accessor : $result)->$foreignKeyName;
152
153
        $isDescendant = Str::endsWith($path, $pathSeparator.$foreignKey);
154
155
        if ($this->andSelf) {
156
            if ($isDescendant || $path === $foreignKey) {
157
                return true;
158
            }
159
        } elseif ($isDescendant) {
160
            return true;
161
        }
162
163 80
        return false;
164
    }
165 80
166
    /**
167 80
     * Add a result to the dictionary.
168
     *
169 80
     * @param array $dictionary
170 40
     * @param \Illuminate\Database\Eloquent\Model $result
171 40
     * @param string $pathSeparator
172
     * @param string $path
173 40
     * @return array
174 40
     */
175
    protected function addResultToDictionary(array $dictionary, Model $result, $pathSeparator, $path)
176
    {
177 80
        $keys = explode($pathSeparator, $path);
178
179
        if (!$this->andSelf) {
180
            array_pop($keys);
181
        }
182
183
        foreach ($keys as $key) {
184
            if (!isset($dictionary[$key])) {
185
                $dictionary[$key] = new Collection();
186
            }
187
188
            if (!$dictionary[$key]->contains($result)) {
189
                $dictionary[$key][] = $result;
190
            }
191
        }
192
193
        return $dictionary;
194
    }
195
196
    /**
197
     * Get the local key name for an eager load of the relation.
198
     *
199 20
     * @return string
200
     */
201 20
    abstract public function getEagerLoadingLocalKeyName();
202
203
    /**
204
     * Get the foreign key name for an eager load of the relation.
205
     *
206
     * @return string
207
     */
208
    abstract public function getEagerLoadingForeignKeyName();
209
210
    /**
211
     * Get the accessor for an eager load of the relation.
212
     *
213 345
     * @return string|null
214
     */
215 345
    public function getEagerLoadingAccessor()
216
    {
217 345
        return null;
218
    }
219 345
220
    /**
221 345
     * Add a recursive expression to the query.
222 345
     *
223 345
     * @param callable $constraint
224
     * @param \Illuminate\Database\Eloquent\Builder|null $query
225
     * @param string|null $alias
226 345
     * @param bool $selectPath
227
     * @return \Illuminate\Database\Eloquent\Builder
228 345
     */
229 305
    protected function addExpression(callable $constraint, Builder $query = null, $alias = null, $selectPath = false)
230 305
    {
231 305
        $name = $this->parent->getExpressionName();
232 305
233 305
        $query = $query ?: $this->query;
234 305
235 305
        $grammar = $this->getExpressionGrammar();
236
237
        $expression = $this->getInitialQuery($grammar, $constraint, $alias, $selectPath)
238
            ->unionAll(
239 345
                $this->getRecursiveQuery($grammar, $selectPath)
240
            );
241
242
        $query->withRecursiveExpression($name, $expression);
243
244
        $query->withGlobalScope(get_class(), function (Builder $query) use ($name) {
245
            $query->whereIn(
246
                $this->getExpressionForeignKeyName(),
247
                (new $this->parent())->setTable($name)
248
                    ->newQuery()
249
                    ->select($this->getExpressionLocalKeyName())
250
                    ->withGlobalScopes($this->intermediateScopes)
251 345
                    ->withoutGlobalScopes($this->removedIntermediateScopes)
252
            );
253 345
        });
254
255 345
        return $query;
256
    }
257 345
258
    /**
259 345
     * Get the initial query for a relationship expression.
260 345
     *
261 345
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
262
     * @param callable $constraint
263 345
     * @param string|null $alias
264 24
     * @param bool $selectPath
265
     * @return \Illuminate\Database\Eloquent\Builder $query
266
     */
267 345
    protected function getInitialQuery(ExpressionGrammar $grammar, callable $constraint, $alias, $selectPath)
268
    {
269 345
        $model = new $this->parent();
270 80
271 80
        $initialDepth = $this->andSelf ? 0 : 1;
272 80
273
        $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

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

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

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