IsOfDescendantsRelation::addExpression()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 16
c 0
b 0
f 0
dl 0
loc 27
ccs 20
cts 20
cp 1
rs 9.7333
cc 2
nc 1
nop 4
crap 2
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 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($this), 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. It seems like you code against a sub-type of said class. However, the method does not exist in Staudenmeir\LaravelAdjac...rammars\FirebirdGrammar. Are you sure you never get one of those? ( 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
281 350
        $table = $alias ?: $query->getQuery()->from;
282
283 350
        $query->select("$table.*")
284 24
            ->selectRaw("$initialDepth as $depth");
285
286
        if ($alias) {
287 350
            $query->from($query->getQuery()->from, $alias);
288
        }
289 350
290 85
        $constraint($query);
291 85
292 85
        if ($selectPath) {
293 85
            $initialPath = $grammar->compileInitialPath(
294
                $this->getExpressionLocalKeyName(),
295 85
                $model->getPathName()
296
            );
297
298 350
            $query->selectRaw($initialPath);
299
        }
300
301
        return $query;
302
    }
303
304
    /**
305
     * Get the recursive query for a relationship expression.
306
     *
307
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
308 350
     * @param bool $selectPath
309
     * @return \Illuminate\Database\Eloquent\Builder $query
310 350
     */
311
    protected function getRecursiveQuery(ExpressionGrammar $grammar, $selectPath)
312 350
    {
313
        $model = new $this->parent();
314 350
315
        $name = $model->getExpressionName();
316 350
317
        $depth = $grammar->wrap($model->getDepthName());
318 350
319
        $recursiveDepth = "$depth + 1";
320 350
321 350
        $query = $model->newModelQuery();
322 350
323 350
        $query->select($query->getQuery()->from.'.*')
324 350
            ->selectRaw("$recursiveDepth as $depth")
325 350
            ->join(
326 350
                $name,
327 350
                $name.'.'.$model->getLocalKeyName(),
328
                '=',
329 350
                $query->qualifyColumn($model->getParentKeyName())
330 85
            );
331 85
332 85
        if ($selectPath) {
333 85
            $recursivePath = $grammar->compileRecursivePath(
334 85
                $model->qualifyColumn(
335 85
                    $this->getExpressionLocalKeyName()
336
                ),
337 85
                $model->getPathName()
338
            );
339 85
340
            $recursivePathBindings = $grammar->getRecursivePathBindings($model->getPathSeparator());
341
342 350
            $query->selectRaw($recursivePath, $recursivePathBindings);
343
        }
344
345
        return $query;
346
    }
347
348
    /**
349
     * Get the local key name for the recursion expression.
350
     *
351
     * @return string
352
     */
353
    abstract public function getExpressionLocalKeyName();
354
355
    /**
356
     * Get the foreign key name for the recursion expression.
357
     *
358
     * @return string
359
     */
360
    abstract public function getExpressionForeignKeyName();
361
362
    /**
363
     * Add the constraints for a relationship query.
364
     *
365
     * @param \Illuminate\Database\Eloquent\Builder $query
366
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
367 48
     * @param array|mixed $columns
368
     * @return \Illuminate\Database\Eloquent\Builder
369 48
     */
370
    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
371 48
    {
372 24
        $table = (new $this->parent())->getTable();
373
374 24
        if ($table === $parentQuery->getQuery()->from) {
375
            $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

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

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