Passed
Push — master ( 2d9671...2ebaad )
by Jonas
05:47
created

scopeDoesntHaveChildren()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
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 5
    public function scopeDoesntHaveChildren(Builder $query)
69
    {
70 5
        return $query->isLeaf();
71
    }
72
73
    /**
74
     * Limit the query to models with a parent.
75
     *
76
     * @param \Illuminate\Database\Eloquent\Builder $query
77
     * @return \Illuminate\Database\Eloquent\Builder
78
     */
79 20
    public function scopeHasParent(Builder $query)
80
    {
81 20
        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...
82
    }
83
84
    /**
85
     * Limit the query to leaf models.
86
     *
87
     * @param \Illuminate\Database\Eloquent\Builder $query
88
     * @return \Illuminate\Database\Eloquent\Builder
89
     */
90 10
    public function scopeIsLeaf(Builder $query)
91
    {
92 10
        $keys = (new static())->newQuery()
93 10
            ->select($this->getParentKeyName())
94 10
            ->hasParent();
95
96 10
        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...
97
    }
98
99
    /**
100
     * Limit the query to root models.
101
     *
102
     * @param \Illuminate\Database\Eloquent\Builder $query
103
     * @return \Illuminate\Database\Eloquent\Builder
104
     */
105 83
    public function scopeIsRoot(Builder $query)
106
    {
107 83
        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...
108
    }
109
110
    /**
111
     * Limit the query by depth.
112
     *
113
     * @param \Illuminate\Database\Eloquent\Builder $query
114
     * @param mixed $operator
115
     * @param mixed|null $value
116
     * @return \Illuminate\Database\Eloquent\Builder
117
     */
118 50
    public function scopeWhereDepth(Builder $query, $operator, $value = null)
119
    {
120 50
        $arguments = array_slice(func_get_args(), 1);
121
122 50
        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

122
        return $query->where($this->/** @scrutinizer ignore-call */ getDepthName(), ...$arguments);
Loading history...
123
    }
124
125
    /**
126
     * Order the query breadth-first.
127
     *
128
     * @param \Illuminate\Database\Eloquent\Builder $query
129
     * @return \Illuminate\Database\Eloquent\Builder
130
     */
131 20
    public function scopeBreadthFirst(Builder $query)
132
    {
133 20
        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...
134
    }
135
136
    /**
137
     * Order the query depth-first.
138
     *
139
     * @param \Illuminate\Database\Eloquent\Builder $query
140
     * @return \Illuminate\Database\Eloquent\Builder
141
     */
142 18
    public function scopeDepthFirst(Builder $query)
143
    {
144 18
        $sql = $query->getExpressionGrammar()->compileOrderByPath();
145
146 18
        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...
147
    }
148
149
    /**
150
     * Add a recursive expression for the relationship to the query.
151
     *
152
     * @param \Illuminate\Database\Eloquent\Builder $query
153
     * @param string $direction
154
     * @param callable $constraint
155
     * @param int $initialDepth
156
     * @param string|null $from
157
     * @param int|null $maxDepth
158
     * @return \Illuminate\Database\Eloquent\Builder
159
     */
160 459
    public function scopeWithRelationshipExpression(Builder $query, $direction, callable $constraint, $initialDepth, $from = null, $maxDepth = null)
161
    {
162 459
        $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

162
        $from = $from ?: $this->/** @scrutinizer ignore-call */ getTable();
Loading history...
163
164 459
        $grammar = $query->getExpressionGrammar();
165
166 459
        $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

166
        $expression = $this->getInitialQuery(/** @scrutinizer ignore-type */ $grammar, $constraint, $initialDepth, $from)
Loading history...
167 459
            ->unionAll(
168 459
                $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

168
                $this->getRecursiveQuery(/** @scrutinizer ignore-type */ $grammar, $direction, $from, $maxDepth)
Loading history...
169 459
            );
170
171 459
        $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

171
        /** @scrutinizer ignore-call */ 
172
        $name = $this->getExpressionName();
Loading history...
172
173 459
        $query->getModel()->setTable($name);
174
175 459
        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...
176
    }
177
178
    /**
179
     * Get the initial query for a relationship expression.
180
     *
181
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar|\Illuminate\Database\Grammar $grammar
182
     * @param callable $constraint
183
     * @param int $initialDepth
184
     * @param string $from
185
     * @return \Illuminate\Database\Eloquent\Builder $query
186
     */
187 459
    protected function getInitialQuery(ExpressionGrammar $grammar, callable $constraint, $initialDepth, $from)
188
    {
189 459
        $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

189
        /** @scrutinizer ignore-call */ 
190
        $depth = $grammar->wrap($this->getDepthName());
Loading history...
190
191 459
        $initialPath = $grammar->compileInitialPath(
192 459
            $this->getLocalKeyName(),
193 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

193
            $this->/** @scrutinizer ignore-call */ 
194
                   getPathName()
Loading history...
194 459
        );
195
196 459
        $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

196
        $query = $this->/** @scrutinizer ignore-call */ newModelQuery()
Loading history...
197 459
            ->select('*')
198 459
            ->selectRaw($initialDepth.' as '.$depth)
199 459
            ->selectRaw($initialPath)
200 459
            ->from($from);
201
202 459
        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

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

233
                $this->/** @scrutinizer ignore-call */ 
234
                       getQualifiedLocalKeyName(),
Loading history...
234 459
            ],
235 459
            'desc' => [
236 459
                $name.'.'.$this->getLocalKeyName(),
237 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

237
                $this->/** @scrutinizer ignore-call */ 
238
                       qualifyColumn($this->getParentKeyName()),
Loading history...
238 459
            ],
239 459
        ];
240
241 459
        if ($direction === 'both') {
242 60
            $recursiveDepth = "$depth + (case when {$joinColumns['desc'][1]}={$joinColumns['desc'][0]} then 1 else -1 end)";
243
        } else {
244 405
            $recursiveDepth = $depth.' '.($direction === 'asc' ? '-' : '+').' 1';
245
        }
246
247 459
        $recursivePath = $grammar->compileRecursivePath(
248 459
            $this->getQualifiedLocalKeyName(),
249 459
            $this->getPathName()
250 459
        );
251
252 459
        $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

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