Passed
Push — master ( 3fade5...c39224 )
by Jonas
11:04
created

IsOfDescendantsRelation::addConstraints()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 10
cc 2
nc 2
nop 0
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 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->pathMatches($result, $foreignKeyName, $accessor, $pathSeparator, $path)) {
123 80
                    continue;
124
                }
125
126 80
                $keys = explode($pathSeparator, $path);
127
128 80
                if (!$this->andSelf) {
129 40
                    array_pop($keys);
130
                }
131
132 80
                foreach ($keys as $key) {
133 80
                    if (!isset($dictionary[$key])) {
134 80
                        $dictionary[$key] = new Collection();
135
                    }
136
137 80
                    if (!$dictionary[$key]->contains($result)) {
138 80
                        $dictionary[$key][] = $result;
139
                    }
140
                }
141
            }
142
143 80
            unset($result->{$this->pathListAlias});
144
        }
145
146 80
        foreach ($dictionary as $key => $results) {
147 80
            $dictionary[$key] = $results->all();
148
        }
149
150 80
        return $dictionary;
151
    }
152
153
    /**
154
     * Determine whether a path belongs to a result.
155
     *
156
     * @param \Illuminate\Database\Eloquent\Model $result
157
     * @param string $foreignKeyName
158
     * @param string $accessor
159
     * @param string $pathSeparator
160
     * @param string $path
161
     * @return bool
162
     */
163 80
    protected function pathMatches(Model $result, $foreignKeyName, $accessor, $pathSeparator, $path)
164
    {
165 80
        $foreignKey = (string) ($accessor ? $result->$accessor : $result)->$foreignKeyName;
166
167 80
        $isDescendant = Str::endsWith($path, $pathSeparator.$foreignKey);
168
169 80
        if ($this->andSelf) {
170 40
            if ($isDescendant || $path === $foreignKey) {
171 40
                return true;
172
            }
173 40
        } elseif ($isDescendant) {
174 40
            return true;
175
        }
176
177 80
        return false;
178
    }
179
180
    /**
181
     * Get the local key name for an eager load of the relation.
182
     *
183
     * @return string
184
     */
185
    abstract public function getEagerLoadingLocalKeyName();
186
187
    /**
188
     * Get the foreign key name for an eager load of the relation.
189
     *
190
     * @return string
191
     */
192
    abstract public function getEagerLoadingForeignKeyName();
193
194
    /**
195
     * Get the accessor for an eager load of the relation.
196
     *
197
     * @return string|null
198
     */
199 20
    public function getEagerLoadingAccessor()
200
    {
201 20
        return null;
202
    }
203
204
    /**
205
     * Add a recursive expression to the query.
206
     *
207
     * @param callable $constraint
208
     * @param \Illuminate\Database\Eloquent\Builder|null $query
209
     * @param string|null $alias
210
     * @param bool $selectPath
211
     * @return \Illuminate\Database\Eloquent\Builder
212
     */
213 345
    protected function addExpression(callable $constraint, Builder $query = null, $alias = null, $selectPath = false)
214
    {
215 345
        $name = $this->parent->getExpressionName();
216
217 345
        $query = $query ?: $this->query;
218
219 345
        $grammar = $this->getExpressionGrammar();
220
221 345
        $expression = $this->getInitialQuery($grammar, $constraint, $alias, $selectPath)
222 345
            ->unionAll(
223 345
                $this->getRecursiveQuery($grammar, $selectPath)
224
            );
225
226 345
        $query->withRecursiveExpression($name, $expression);
227
228 345
        $query->withGlobalScope(get_class(), function (Builder $query) use ($name) {
229 305
            $query->whereIn(
230 305
                $this->getExpressionForeignKeyName(),
231 305
                (new $this->parent())->setTable($name)
232 305
                    ->newQuery()
233 305
                    ->select($this->getExpressionLocalKeyName())
234 305
                    ->withGlobalScopes($this->intermediateScopes)
235 305
                    ->withoutGlobalScopes($this->removedIntermediateScopes)
236
            );
237
        });
238
239 345
        return $query;
240
    }
241
242
    /**
243
     * Get the initial query for a relationship expression.
244
     *
245
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
246
     * @param callable $constraint
247
     * @param string|null $alias
248
     * @param bool $selectPath
249
     * @return \Illuminate\Database\Eloquent\Builder $query
250
     */
251 345
    protected function getInitialQuery(ExpressionGrammar $grammar, callable $constraint, $alias, $selectPath)
252
    {
253 345
        $model = new $this->parent();
254
255 345
        $initialDepth = $this->andSelf ? 0 : 1;
256
257 345
        $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

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

352
            $table = $alias = $this->/** @scrutinizer ignore-call */ getRelationCountHash();
Loading history...
353
        } else {
354 24
            $alias = null;
355
        }
356
357 48
        $constraint = function (Builder $query) use ($table) {
358 48
            $this->addExistenceExpressionWhereConstraints($query, $table);
359
        };
360
361 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

361
        return $this->addExpression($constraint, /** @scrutinizer ignore-type */ $query->select($columns), $alias);
Loading history...
362
    }
363
364
    /**
365
     * Set the where clause on the recursive expression query for an existence query.
366
     *
367
     * @param \Illuminate\Database\Eloquent\Builder $query
368
     * @param string $table
369
     * @return void
370
     */
371 48
    public function addExistenceExpressionWhereConstraints(Builder $query, $table)
372
    {
373 48
        $first = $this->andSelf
374 24
            ? $this->parent->getLocalKeyName()
375 24
            : $this->parent->getParentKeyName();
376
377 48
        $query->whereColumn(
378 48
            $table.'.'.$first,
379
            '=',
380 48
            $this->parent->getQualifiedLocalKeyName()
381
        );
382
    }
383
384
    /**
385
     * Get the expression grammar.
386
     *
387
     * @return \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar
388
     */
389 345
    protected function getExpressionGrammar()
390
    {
391 345
        return $this->parent->newQuery()->getExpressionGrammar();
392
    }
393
394
    /**
395
     * Get the path list separator.
396
     *
397
     * @return string
398
     */
399 80
    protected function getPathListSeparator()
400
    {
401 80
        return str_repeat(
402 80
            $this->parent->getPathSeparator(),
403
            2
404
        );
405
    }
406
}
407