scopeWithRelationshipExpression()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 16
ccs 9
cts 9
cp 1
rs 10
cc 2
nc 1
nop 6
crap 2
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 62
    public function scopeTree(Builder $query, $maxDepth = null)
20
    {
21 62
        $constraint = function (Builder $query) {
22 62
            $query->isRoot();
23 62
        };
24
25 62
        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 86
    public function scopeTreeOf(Builder $query, callable|Model $constraint, $maxDepth = null)
37
    {
38 86
        if ($constraint instanceof Model) {
39 12
            $constraint = fn ($query) => $query->whereKey($constraint->getKey());
40 12
        }
41 12
42
        return $query->withRelationshipExpression('desc', $constraint, 0, null, $maxDepth);
43
    }
44 86
45
    /**
46
     * Limit the query to models with children.
47
     *
48
     * @param \Illuminate\Database\Eloquent\Builder $query
49
     * @return \Illuminate\Database\Eloquent\Builder
50
     */
51
    public function scopeHasChildren(Builder $query)
52
    {
53 6
        $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

53
        $keys = (new static())->/** @scrutinizer ignore-call */ newQuery()
Loading history...
54
            ->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

54
            ->select($this->/** @scrutinizer ignore-call */ getParentKeyName())
Loading history...
55 6
            ->hasParent();
56 6
57 6
        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

57
        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...
58
    }
59 6
60
    /**
61
     * Limit the query to models without children.
62
     *
63
     * @param \Illuminate\Database\Eloquent\Builder $query
64
     * @return \Illuminate\Database\Eloquent\Builder
65
     */
66
    public function scopeDoesntHaveChildren(Builder $query)
67
    {
68 6
        return $query->isLeaf();
69
    }
70 6
71
    /**
72
     * Limit the query to models with a parent.
73
     *
74
     * @param \Illuminate\Database\Eloquent\Builder $query
75
     * @return \Illuminate\Database\Eloquent\Builder
76
     */
77
    public function scopeHasParent(Builder $query)
78
    {
79 24
        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...
80
    }
81 24
82
    /**
83
     * Limit the query to leaf models.
84
     *
85
     * @param \Illuminate\Database\Eloquent\Builder $query
86
     * @return \Illuminate\Database\Eloquent\Builder
87
     */
88
    public function scopeIsLeaf(Builder $query)
89
    {
90 12
        $keys = (new static())->newQuery()
91
            ->select($this->getParentKeyName())
92 12
            ->hasParent();
93 12
94 12
        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...
95
    }
96 12
97
    /**
98
     * Limit the query to root models.
99
     *
100
     * @param \Illuminate\Database\Eloquent\Builder $query
101
     * @return \Illuminate\Database\Eloquent\Builder
102
     */
103
    public function scopeIsRoot(Builder $query)
104
    {
105 96
        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...
106
    }
107 96
108
    /**
109
     * Limit the query by depth.
110
     *
111
     * @param \Illuminate\Database\Eloquent\Builder $query
112
     * @param mixed $operator
113
     * @param mixed|null $value
114
     * @return \Illuminate\Database\Eloquent\Builder
115
     */
116
    public function scopeWhereDepth(Builder $query, $operator, $value = null)
117
    {
118 56
        $arguments = array_slice(func_get_args(), 1);
119
120 56
        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

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

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

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

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

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

189
        /** @scrutinizer ignore-call */ 
190
        $depth = $grammar->wrap($this->getDepthName());
Loading history...
190
191 523
        $initialPath = $grammar->compileInitialPath(
192 523
            $this->getLocalKeyName(),
193 523
            $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 523
        );
195
196 523
        $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 523
            ->select("$table.*")
198 523
            ->selectRaw($initialDepth.' as '.$depth)
199 523
            ->selectRaw($initialPath)
200 523
            ->from($from);
201
202 523
        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 517
            $query->selectRaw(
204 517
                $grammar->compileInitialPath($path['column'], $path['name'])
205 517
            );
206
        }
207
208 523
        $constraint($query);
209
210 523
        if (static::$initialQueryConstraint) {
211
            (static::$initialQueryConstraint)($query);
212
        }
213
214
        return $query;
215
    }
216
217
    /**
218
     * Get the recursive query for a relationship expression.
219
     *
220
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar|\Illuminate\Database\Grammar $grammar
221
     * @param string $direction
222 523
     * @param string $from
223
     * @param int|null $maxDepth
224 523
     * @return \Illuminate\Database\Eloquent\Builder $query
225
     */
226 523
    protected function getRecursiveQuery(ExpressionGrammar $grammar, $direction, $from, $maxDepth = null)
227
    {
228 523
        $name = $this->getExpressionName();
229
230 523
        $table = explode(' as ', $from)[1] ?? $from;
231 523
232 523
        $depth = $grammar->wrap($this->getDepthName());
233 523
234 523
        $joinColumns = [
235 523
            'asc' => [
236 523
                $name.'.'.$this->getParentKeyName(),
237 523
                $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 523
            ],
239 523
            'desc' => [
240
                $name.'.'.$this->getLocalKeyName(),
241 523
                $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 68
            ],
243
        ];
244 461
245
        if ($direction === 'both') {
246
            $left = $grammar->wrap($joinColumns['desc'][1]);
247 523
            $right = $grammar->wrap($joinColumns['desc'][0]);
248 523
249 523
            $recursiveDepth = "$depth + (case when $left=$right then 1 else -1 end)";
250 523
        } else {
251
            $recursiveDepth = $depth.' '.($direction === 'asc' ? '-' : '+').' 1';
252 523
        }
253
254 523
        $recursivePath = $grammar->compileRecursivePath(
255 523
            $this->getQualifiedLocalKeyName(),
256 523
            $this->getPathName()
257 523
        );
258 523
259
        $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

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