Passed
Pull Request — master (#209)
by
unknown
11:40
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 15
    public function scopeDoesntHaveChildren(Builder $query)
69
    {
70 15
        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 5
    public function scopeHasParent(Builder $query)
80
    {
81 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...
82 5
    }
83 5
84
    /**
85 5
     * Limit the query to leaf models.
86
     *
87
     * @param \Illuminate\Database\Eloquent\Builder $query
88
     * @return \Illuminate\Database\Eloquent\Builder
89
     */
90
    public function scopeIsLeaf(Builder $query)
91
    {
92
        $keys = (new static())->newQuery()
93
            ->select($this->getParentKeyName())
94 83
            ->hasParent();
95
96 83
        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
    public function scopeIsRoot(Builder $query)
106
    {
107 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...
108
    }
109 50
110
    /**
111 50
     * 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
    public function scopeWhereDepth(Builder $query, $operator, $value = null)
119
    {
120 20
        $arguments = array_slice(func_get_args(), 1);
121
122 20
        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 18
    public function scopeBreadthFirst(Builder $query)
132
    {
133 18
        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 18
136
    /**
137
     * Order the query depth-first.
138
     *
139
     * @param \Illuminate\Database\Eloquent\Builder $query
140
     * @return \Illuminate\Database\Eloquent\Builder
141
     */
142
    public function scopeDepthFirst(Builder $query)
143
    {
144
        $sql = $query->getExpressionGrammar()->compileOrderByPath();
145
146
        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 459
    /**
150
     * Add a recursive expression for the relationship to the query.
151 459
     *
152
     * @param \Illuminate\Database\Eloquent\Builder $query
153 459
     * @param string $direction
154
     * @param callable $constraint
155 459
     * @param int $initialDepth
156 459
     * @param string|null $from
157 459
     * @param int|null $maxDepth
158 459
     * @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
        $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
            ->unionAll(
168
                $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
            );
170
171
        $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
        $query->getModel()->setTable($name);
174
175
        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 459
    }
177
178 459
    /**
179
     * Get the initial query for a relationship expression.
180 459
     *
181 459
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar|\Illuminate\Database\Grammar $grammar
182 459
     * @param callable $constraint
183 459
     * @param int $initialDepth
184
     * @param string $from
185 459
     * @return \Illuminate\Database\Eloquent\Builder $query
186 459
     */
187 459
    protected function getInitialQuery(ExpressionGrammar $grammar, callable $constraint, $initialDepth, $from)
188 459
    {
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 454
            $this->getLocalKeyName(),
193 454
            $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 454
        );
195
196
        $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
            ->selectRaw($initialDepth.' as '.$depth)
199 459
            ->selectRaw($initialPath)
200
            ->from($from);
201
202
        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
            $query->selectRaw(
204
                $grammar->compileInitialPath($path['column'], $path['name'])
205
            );
206
        }
207
208
        $constraint($query);
209
210
        return $query;
211 459
    }
212
213 459
    /**
214
     * Get the recursive query for a relationship expression.
215 459
     *
216
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar|\Illuminate\Database\Grammar $grammar
217 459
     * @param string $direction
218
     * @param string $from
219 459
     * @param int|null $maxDepth
220 459
     * @return \Illuminate\Database\Eloquent\Builder $query
221 459
     */
222 459
    protected function getRecursiveQuery(ExpressionGrammar $grammar, $direction, $from, $maxDepth = null)
223 459
    {
224 459
        $name = $this->getExpressionName();
225 459
226 459
        $table = explode(' as ', $from)[1] ?? $from;
227 459
228 459
        $depth = $grammar->wrap($this->getDepthName());
229
230 459
        $joinColumns = [
231 60
            'asc' => [
232
                $name.'.'.$this->getParentKeyName(),
233 405
                $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
            ],
235
            '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
            $recursiveDepth = "$depth + (case when {$joinColumns['desc'][1]}={$joinColumns['desc'][0]} then 1 else -1 end)";
243 459
        } else {
244 459
            $recursiveDepth = $depth.' '.($direction === 'asc' ? '-' : '+').' 1';
245 459
        }
246 459
247 459
        $recursivePath = $grammar->compileRecursivePath(
248
            $this->getQualifiedLocalKeyName(),
249 459
            $this->getPathName()
250 454
        );
251 454
252 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

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