Passed
Pull Request — master (#209)
by
unknown
15:12
created

scopeDoesntHaveChildren()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 7
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Staudenmeir\LaravelAdjacencyList\Eloquent\Traits;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Database\Query\JoinClause;
8
use Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar;
9
10
trait HasRecursiveRelationshipScopes
11
{
12
    /**
13
     * Add a recursive expression for the whole tree to the query.
14
     *
15
     * @param \Illuminate\Database\Eloquent\Builder $query
16
     * @param int|null $maxDepth
17
     * @return \Illuminate\Database\Eloquent\Builder
18
     */
19 53
    public function scopeTree(Builder $query, $maxDepth = null)
20
    {
21 53
        $constraint = function (Builder $query) {
22 53
            $query->isRoot();
23 53
        };
24
25 53
        return $query->treeOf($constraint, $maxDepth);
26
    }
27
28
    /**
29
     * Add a recursive expression for a custom tree to the query.
30
     *
31
     * @param \Illuminate\Database\Eloquent\Builder $query
32
     * @param callable|\Illuminate\Database|Eloquent\Model $constraint
0 ignored issues
show
Bug introduced by
The type Staudenmeir\LaravelAdjac...t\Traits\Eloquent\Model 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...
33
     * @param int|null $maxDepth
34
     * @return \Illuminate\Database\Eloquent\Builder
35
     */
36 73
    public function scopeTreeOf(Builder $query, callable|Model $constraint, $maxDepth = null)
37
    {
38 73
        if ($constraint instanceof Model) {
39 10
            $constraint = function ($query) use ($constraint) {
40 10
                $query->whereKey($constraint->getKey());
41 10
            };
42
        }
43
44 73
        return $query->withRelationshipExpression('desc', $constraint, 0, null, $maxDepth);
45
    }
46
47
    /**
48
     * Limit the query to models with children.
49
     *
50
     * @param \Illuminate\Database\Eloquent\Builder $query
51
     * @return \Illuminate\Database\Eloquent\Builder
52
     */
53 5
    public function scopeHasChildren(Builder $query)
54
    {
55 5
        $keys = (new static())->newQuery()
0 ignored issues
show
Bug introduced by
It seems like newQuery() 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

55
        $keys = (new static())->/** @scrutinizer ignore-call */ newQuery()
Loading history...
56 5
            ->select($this->getParentKeyName())
0 ignored issues
show
Bug introduced by
It seems like getParentKeyName() 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

56
            ->select($this->/** @scrutinizer ignore-call */ getParentKeyName())
Loading history...
57 5
            ->hasParent();
58
59 5
        return $query->whereIn($this->getLocalKeyName(), $keys);
0 ignored issues
show
Bug introduced by
It seems like getLocalKeyName() 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

59
        return $query->whereIn($this->/** @scrutinizer ignore-call */ getLocalKeyName(), $keys);
Loading history...
Bug Best Practice introduced by
The expression return $query->whereIn($...tLocalKeyName(), $keys) also could return the type Illuminate\Database\Query\Builder which is incompatible with the documented return type Illuminate\Database\Eloquent\Builder.
Loading history...
60
    }
61
62
    /**
63
     * Limit the query to models without children.
64
     *
65
     * @param \Illuminate\Database\Eloquent\Builder $query
66
     * @return \Illuminate\Database\Eloquent\Builder
67
     */
68 15
    public function scopeDoesntHaveChildren(Builder $query)
69
    {
70 15
        $keys = (new static())->newQuery()
71
            ->select($this->getParentKeyName())
72
            ->hasParent();
73
74
        return $query->whereNotIn($this->getLocalKeyName(), $keys);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->whereNotI...tLocalKeyName(), $keys) also could return the type Illuminate\Database\Query\Builder which is incompatible with the documented return type Illuminate\Database\Eloquent\Builder.
Loading history...
75
    }
76
77
    /**
78
     * Limit the query to models with a parent.
79 5
     *
80
     * @param \Illuminate\Database\Eloquent\Builder $query
81 5
     * @return \Illuminate\Database\Eloquent\Builder
82 5
     */
83 5
    public function scopeHasParent(Builder $query)
84
    {
85 5
        return $query->whereNotNull($this->getParentKeyName());
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->whereNotN...is->getParentKeyName()) also could return the type Illuminate\Database\Query\Builder which is incompatible with the documented return type Illuminate\Database\Eloquent\Builder.
Loading history...
86
    }
87
88
    /**
89
     * Limit the query to leaf models.
90
     *
91
     * @param \Illuminate\Database\Eloquent\Builder $query
92
     * @return \Illuminate\Database\Eloquent\Builder
93
     */
94 83
    public function scopeIsLeaf(Builder $query)
95
    {
96 83
        $keys = (new static())->newQuery()
97
            ->select($this->getParentKeyName())
98
            ->hasParent();
99
100
        return $query->whereNotIn($this->getLocalKeyName(), $keys);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->whereNotI...tLocalKeyName(), $keys) also could return the type Illuminate\Database\Query\Builder which is incompatible with the documented return type Illuminate\Database\Eloquent\Builder.
Loading history...
101
    }
102
103
    /**
104
     * Limit the query to root models.
105
     *
106
     * @param \Illuminate\Database\Eloquent\Builder $query
107 50
     * @return \Illuminate\Database\Eloquent\Builder
108
     */
109 50
    public function scopeIsRoot(Builder $query)
110
    {
111 50
        return $query->whereNull($this->getParentKeyName());
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->whereNull...is->getParentKeyName()) also could return the type Illuminate\Database\Query\Builder which is incompatible with the documented return type Illuminate\Database\Eloquent\Builder.
Loading history...
112
    }
113
114
    /**
115
     * Limit the query by depth.
116
     *
117
     * @param \Illuminate\Database\Eloquent\Builder $query
118
     * @param mixed $operator
119
     * @param mixed|null $value
120 20
     * @return \Illuminate\Database\Eloquent\Builder
121
     */
122 20
    public function scopeWhereDepth(Builder $query, $operator, $value = null)
123
    {
124
        $arguments = array_slice(func_get_args(), 1);
125
126
        return $query->where($this->getDepthName(), ...$arguments);
0 ignored issues
show
Bug introduced by
It seems like getDepthName() 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

126
        return $query->where($this->/** @scrutinizer ignore-call */ getDepthName(), ...$arguments);
Loading history...
127
    }
128
129
    /**
130
     * Order the query breadth-first.
131 18
     *
132
     * @param \Illuminate\Database\Eloquent\Builder $query
133 18
     * @return \Illuminate\Database\Eloquent\Builder
134
     */
135 18
    public function scopeBreadthFirst(Builder $query)
136
    {
137
        return $query->orderBy($this->getDepthName());
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->orderBy($this->getDepthName()) also could return the type Illuminate\Database\Query\Builder which is incompatible with the documented return type Illuminate\Database\Eloquent\Builder.
Loading history...
138
    }
139
140
    /**
141
     * Order the query depth-first.
142
     *
143
     * @param \Illuminate\Database\Eloquent\Builder $query
144
     * @return \Illuminate\Database\Eloquent\Builder
145
     */
146
    public function scopeDepthFirst(Builder $query)
147
    {
148
        $sql = $query->getExpressionGrammar()->compileOrderByPath();
149 459
150
        return $query->orderByRaw($sql);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->orderByRaw($sql) also could return the type Illuminate\Database\Query\Builder which is incompatible with the documented return type Illuminate\Database\Eloquent\Builder.
Loading history...
151 459
    }
152
153 459
    /**
154
     * Add a recursive expression for the relationship to the query.
155 459
     *
156 459
     * @param \Illuminate\Database\Eloquent\Builder $query
157 459
     * @param string $direction
158 459
     * @param callable $constraint
159
     * @param int $initialDepth
160 459
     * @param string|null $from
161
     * @param int|null $maxDepth
162 459
     * @return \Illuminate\Database\Eloquent\Builder
163
     */
164 459
    public function scopeWithRelationshipExpression(Builder $query, $direction, callable $constraint, $initialDepth, $from = null, $maxDepth = null)
165
    {
166
        $from = $from ?: $this->getTable();
0 ignored issues
show
Bug introduced by
It seems like getTable() 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

166
        $from = $from ?: $this->/** @scrutinizer ignore-call */ getTable();
Loading history...
167
168
        $grammar = $query->getExpressionGrammar();
169
170
        $expression = $this->getInitialQuery($grammar, $constraint, $initialDepth, $from)
0 ignored issues
show
Bug introduced by
It seems like $grammar can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $grammar of Staudenmeir\LaravelAdjac...opes::getInitialQuery() does only seem to accept Staudenmeir\LaravelAdjac...mmars\ExpressionGrammar, 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

170
        $expression = $this->getInitialQuery(/** @scrutinizer ignore-type */ $grammar, $constraint, $initialDepth, $from)
Loading history...
171
            ->unionAll(
172
                $this->getRecursiveQuery($grammar, $direction, $from, $maxDepth)
0 ignored issues
show
Bug introduced by
It seems like $grammar can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $grammar of Staudenmeir\LaravelAdjac...es::getRecursiveQuery() does only seem to accept Staudenmeir\LaravelAdjac...mmars\ExpressionGrammar, 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

172
                $this->getRecursiveQuery(/** @scrutinizer ignore-type */ $grammar, $direction, $from, $maxDepth)
Loading history...
173
            );
174
175
        $name = $this->getExpressionName();
0 ignored issues
show
Bug introduced by
It seems like getExpressionName() 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

175
        /** @scrutinizer ignore-call */ 
176
        $name = $this->getExpressionName();
Loading history...
176 459
177
        $query->getModel()->setTable($name);
178 459
179
        return $query->withRecursiveExpression($name, $expression)->from($name);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->withRecur...xpression)->from($name) also could return the type Illuminate\Database\Query\Builder which is incompatible with the documented return type Illuminate\Database\Eloquent\Builder.
Loading history...
180 459
    }
181 459
182 459
    /**
183 459
     * Get the initial query for a relationship expression.
184
     *
185 459
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar|\Illuminate\Database\Grammar $grammar
186 459
     * @param callable $constraint
187 459
     * @param int $initialDepth
188 459
     * @param string $from
189 459
     * @return \Illuminate\Database\Eloquent\Builder $query
190
     */
191 459
    protected function getInitialQuery(ExpressionGrammar $grammar, callable $constraint, $initialDepth, $from)
192 454
    {
193 454
        $depth = $grammar->wrap($this->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

193
        /** @scrutinizer ignore-call */ 
194
        $depth = $grammar->wrap($this->getDepthName());
Loading history...
194 454
195
        $initialPath = $grammar->compileInitialPath(
196
            $this->getLocalKeyName(),
197 459
            $this->getPathName()
0 ignored issues
show
Bug introduced by
It seems like getPathName() 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

197
            $this->/** @scrutinizer ignore-call */ 
198
                   getPathName()
Loading history...
198
        );
199 459
200
        $query = $this->newModelQuery()
0 ignored issues
show
Bug introduced by
It seems like newModelQuery() 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

200
        $query = $this->/** @scrutinizer ignore-call */ newModelQuery()
Loading history...
201
            ->select('*')
202
            ->selectRaw($initialDepth.' as '.$depth)
203
            ->selectRaw($initialPath)
204
            ->from($from);
205
206
        foreach ($this->getCustomPaths() as $path) {
0 ignored issues
show
Bug introduced by
It seems like getCustomPaths() 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

206
        foreach ($this->/** @scrutinizer ignore-call */ getCustomPaths() as $path) {
Loading history...
207
            $query->selectRaw(
208
                $grammar->compileInitialPath($path['column'], $path['name'])
209
            );
210
        }
211 459
212
        $constraint($query);
213 459
214
        return $query;
215 459
    }
216
217 459
    /**
218
     * Get the recursive query for a relationship expression.
219 459
     *
220 459
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar|\Illuminate\Database\Grammar $grammar
221 459
     * @param string $direction
222 459
     * @param string $from
223 459
     * @param int|null $maxDepth
224 459
     * @return \Illuminate\Database\Eloquent\Builder $query
225 459
     */
226 459
    protected function getRecursiveQuery(ExpressionGrammar $grammar, $direction, $from, $maxDepth = null)
227 459
    {
228 459
        $name = $this->getExpressionName();
229
230 459
        $table = explode(' as ', $from)[1] ?? $from;
231 60
232
        $depth = $grammar->wrap($this->getDepthName());
233 405
234
        $joinColumns = [
235
            'asc' => [
236 459
                $name.'.'.$this->getParentKeyName(),
237 459
                $this->getQualifiedLocalKeyName(),
0 ignored issues
show
Bug introduced by
It seems like getQualifiedLocalKeyName() 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

237
                $this->/** @scrutinizer ignore-call */ 
238
                       getQualifiedLocalKeyName(),
Loading history...
238 459
            ],
239 459
            'desc' => [
240
                $name.'.'.$this->getLocalKeyName(),
241 459
                $this->qualifyColumn($this->getParentKeyName()),
0 ignored issues
show
Bug introduced by
It seems like qualifyColumn() 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

241
                $this->/** @scrutinizer ignore-call */ 
242
                       qualifyColumn($this->getParentKeyName()),
Loading history...
242
            ],
243 459
        ];
244 459
245 459
        if ($direction === 'both') {
246 459
            $recursiveDepth = "$depth + (case when {$joinColumns['desc'][1]}={$joinColumns['desc'][0]} then 1 else -1 end)";
247 459
        } else {
248
            $recursiveDepth = $depth.' '.($direction === 'asc' ? '-' : '+').' 1';
249 459
        }
250 454
251 454
        $recursivePath = $grammar->compileRecursivePath(
252 454
            $this->getQualifiedLocalKeyName(),
253 454
            $this->getPathName()
254 454
        );
255 454
256 454
        $recursivePathBindings = $grammar->getRecursivePathBindings($this->getPathSeparator());
0 ignored issues
show
Bug introduced by
It seems like getPathSeparator() 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

256
        $recursivePathBindings = $grammar->getRecursivePathBindings($this->/** @scrutinizer ignore-call */ getPathSeparator());
Loading history...
257 454
258
        $query = $this->newModelQuery()
259
            ->select($table.'.*')
260 459
            ->selectRaw($recursiveDepth.' as '.$depth)
261
            ->selectRaw($recursivePath, $recursivePathBindings)
262 459
            ->from($from);
263 20
264
        foreach ($this->getCustomPaths() as $path) {
265
            $query->selectRaw(
266 459
                $grammar->compileRecursivePath(
267
                    is_string($path['column']) ? $this->qualifyColumn($path['column']) : $path['column'],
268
                    $path['name'],
269
                    $path['reverse'] ?? false,
270
                ),
271
                $grammar->getRecursivePathBindings($path['separator'])
272
            );
273
        }
274
275
        $this->addRecursiveQueryJoinsAndConstraints($query, $direction, $name, $joinColumns);
276
277
        if (!is_null($maxDepth)) {
278 459
            $query->where($this->getDepthName(), '<', $maxDepth);
279
        }
280 459
281 60
        return $query;
282 60
    }
283 60
284 60
    /**
285
     * Add join and where clauses to the recursive query for a relationship expression.
286 60
     *
287
     * @param \Illuminate\Database\Eloquent\Builder $query
288 60
     * @param string $direction
289 60
     * @param string $name
290 60
     * @param array $joinColumns
291 60
     * @return void
292 60
     */
293 60
    protected function addRecursiveQueryJoinsAndConstraints(Builder $query, $direction, $name, array $joinColumns)
294 60
    {
295 60
        if ($direction === 'both') {
296 60
            $query->join($name, function (JoinClause $join) use ($joinColumns) {
297 60
                $join->on($joinColumns['asc'][0], '=', $joinColumns['asc'][1])
298 60
                    ->orOn($joinColumns['desc'][0], '=', $joinColumns['desc'][1]);
299
            });
300 405
301
            $depth = $this->getDepthName();
302
303 459
            $query->where(function (Builder  $query) use ($depth, $joinColumns) {
304 20
                $query->where($depth, '=', 0)
305
                    ->orWhere(function (Builder $query) use ($depth, $joinColumns) {
306
                        $query->whereColumn($joinColumns['asc'][0], '=', $joinColumns['asc'][1])
307
                            ->where($depth, '<', 0);
308
                    })
309
                    ->orWhere(function (Builder $query) use ($depth, $joinColumns) {
310
                        $query->whereColumn($joinColumns['desc'][0], '=', $joinColumns['desc'][1])
311
                            ->where($depth, '>', 0);
312
                    });
313
            });
314
        } else {
315
            $query->join($name, $joinColumns[$direction][0], '=', $joinColumns[$direction][1]);
316
        }
317
318
        if (static::$recursiveQueryConstraint) {
319
            (static::$recursiveQueryConstraint)($query);
320
        }
321
    }
322
}
323