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

HasRecursiveRelationshipScopes::scopeIsRoot()   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\Query\JoinClause;
7
use Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar;
8
9
trait HasRecursiveRelationshipScopes
10
{
11
    /**
12
     * Add a recursive expression for the whole tree to the query.
13
     *
14
     * @param \Illuminate\Database\Eloquent\Builder $query
15
     * @param int|null $maxDepth
16
     * @return \Illuminate\Database\Eloquent\Builder
17
     */
18 53
    public function scopeTree(Builder $query, $maxDepth = null)
19
    {
20 53
        $constraint = function (Builder $query) {
21 53
            $query->isRoot();
22
        };
23
24 53
        return $query->treeOf($constraint, $maxDepth);
25
    }
26
27
    /**
28
     * Add a recursive expression for a custom tree to the query.
29
     *
30
     * @param \Illuminate\Database\Eloquent\Builder $query
31
     * @param callable $constraint
32
     * @param int|null $maxDepth
33
     * @return \Illuminate\Database\Eloquent\Builder
34
     */
35 63
    public function scopeTreeOf(Builder $query, callable $constraint, $maxDepth = null)
36
    {
37 63
        return $query->withRelationshipExpression('desc', $constraint, 0, null, $maxDepth);
38
    }
39
40
    /**
41
     * Limit the query to models with children.
42
     *
43
     * @param \Illuminate\Database\Eloquent\Builder $query
44
     * @return \Illuminate\Database\Eloquent\Builder
45
     */
46 5
    public function scopeHasChildren(Builder $query)
47
    {
48 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

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

49
            ->select($this->/** @scrutinizer ignore-call */ getParentKeyName())
Loading history...
50 5
            ->hasParent();
51
52 5
        return $query->whereIn($this->getLocalKeyName(), $keys);
0 ignored issues
show
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...
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

52
        return $query->whereIn($this->/** @scrutinizer ignore-call */ getLocalKeyName(), $keys);
Loading history...
53
    }
54
55
    /**
56
     * Limit the query to models with a parent.
57
     *
58
     * @param \Illuminate\Database\Eloquent\Builder $query
59
     * @return \Illuminate\Database\Eloquent\Builder
60
     */
61 15
    public function scopeHasParent(Builder $query)
62
    {
63 15
        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...
64
    }
65
66
    /**
67
     * Limit the query to leaf models.
68
     *
69
     * @param \Illuminate\Database\Eloquent\Builder $query
70
     * @return \Illuminate\Database\Eloquent\Builder
71
     */
72 5
    public function scopeIsLeaf(Builder $query)
73
    {
74 5
        $keys = (new static())->newQuery()
75 5
            ->select($this->getParentKeyName())
76 5
            ->hasParent();
77
78 5
        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...
79
    }
80
81
    /**
82
     * Limit the query to root models.
83
     *
84
     * @param \Illuminate\Database\Eloquent\Builder $query
85
     * @return \Illuminate\Database\Eloquent\Builder
86
     */
87 83
    public function scopeIsRoot(Builder $query)
88
    {
89 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...
90
    }
91
92
    /**
93
     * Limit the query by depth.
94
     *
95
     * @param \Illuminate\Database\Eloquent\Builder $query
96
     * @param mixed $operator
97
     * @param mixed $value
98
     * @return \Illuminate\Database\Eloquent\Builder
99
     */
100 40
    public function scopeWhereDepth(Builder $query, $operator, $value = null)
101
    {
102 40
        $arguments = array_slice(func_get_args(), 1);
103
104 40
        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

104
        return $query->where($this->/** @scrutinizer ignore-call */ getDepthName(), ...$arguments);
Loading history...
105
    }
106
107
    /**
108
     * Order the query breadth-first.
109
     *
110
     * @param \Illuminate\Database\Eloquent\Builder $query
111
     * @return \Illuminate\Database\Eloquent\Builder
112
     */
113 20
    public function scopeBreadthFirst(Builder $query)
114
    {
115 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...
116
    }
117
118
    /**
119
     * Order the query depth-first.
120
     *
121
     * @param \Illuminate\Database\Eloquent\Builder $query
122
     * @return \Illuminate\Database\Eloquent\Builder
123
     */
124 18
    public function scopeDepthFirst(Builder $query)
125
    {
126 18
        $sql = $query->getExpressionGrammar()->compileOrderByPath();
127
128 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...
129
    }
130
131
    /**
132
     * Add a recursive expression for the relationship to the query.
133
     *
134
     * @param \Illuminate\Database\Eloquent\Builder $query
135
     * @param string $direction
136
     * @param callable $constraint
137
     * @param int $initialDepth
138
     * @param string|null $from
139
     * @param int|null $maxDepth
140
     * @return \Illuminate\Database\Eloquent\Builder
141
     */
142 294
    public function scopeWithRelationshipExpression(Builder $query, $direction, callable $constraint, $initialDepth, $from = null, $maxDepth = null)
143
    {
144 294
        $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

144
        $from = $from ?: $this->/** @scrutinizer ignore-call */ getTable();
Loading history...
145
146 294
        $grammar = $query->getExpressionGrammar();
147
148 294
        $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

148
        $expression = $this->getInitialQuery(/** @scrutinizer ignore-type */ $grammar, $constraint, $initialDepth, $from)
Loading history...
149 294
            ->unionAll(
150 294
                $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

150
                $this->getRecursiveQuery(/** @scrutinizer ignore-type */ $grammar, $direction, $from, $maxDepth)
Loading history...
151
            );
152
153 294
        $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

153
        /** @scrutinizer ignore-call */ 
154
        $name = $this->getExpressionName();
Loading history...
154
155 294
        $query->getModel()->setTable($name);
156
157 294
        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...
158
    }
159
160
    /**
161
     * Get the initial query for a relationship expression.
162
     *
163
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar|\Illuminate\Database\Grammar $grammar
164
     * @param callable $constraint
165
     * @param int $initialDepth
166
     * @param string $from
167
     * @return \Illuminate\Database\Eloquent\Builder $query
168
     */
169 294
    protected function getInitialQuery(ExpressionGrammar $grammar, callable $constraint, $initialDepth, $from)
170
    {
171 294
        $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

171
        /** @scrutinizer ignore-call */ 
172
        $depth = $grammar->wrap($this->getDepthName());
Loading history...
172
173 294
        $initialPath = $grammar->compileInitialPath(
174 294
            $this->getLocalKeyName(),
175 294
            $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

175
            $this->/** @scrutinizer ignore-call */ 
176
                   getPathName()
Loading history...
176
        );
177
178 294
        $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

178
        $query = $this->/** @scrutinizer ignore-call */ newModelQuery()
Loading history...
179 294
            ->select('*')
180 294
            ->selectRaw($initialDepth.' as '.$depth)
181 294
            ->selectRaw($initialPath)
182 294
            ->from($from);
183
184 294
        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

184
        foreach ($this->/** @scrutinizer ignore-call */ getCustomPaths() as $path) {
Loading history...
185 289
            $query->selectRaw(
186 289
                $grammar->compileInitialPath($path['column'], $path['name'])
187
            );
188
        }
189
190 294
        $constraint($query);
191
192 294
        return $query;
193
    }
194
195
    /**
196
     * Get the recursive query for a relationship expression.
197
     *
198
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar|\Illuminate\Database\Grammar $grammar
199
     * @param string $direction
200
     * @param string $from
201
     * @param int|null $maxDepth
202
     * @return \Illuminate\Database\Eloquent\Builder $query
203
     */
204 294
    protected function getRecursiveQuery(ExpressionGrammar $grammar, $direction, $from, $maxDepth = null)
205
    {
206 294
        $name = $this->getExpressionName();
207
208 294
        $table = explode(' as ', $from)[1] ?? $from;
209
210 294
        $depth = $grammar->wrap($this->getDepthName());
211
212 294
        $joinColumns = [
213
            'asc' => [
214 294
                $name.'.'.$this->getParentKeyName(),
215 294
                $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

215
                $this->/** @scrutinizer ignore-call */ 
216
                       getQualifiedLocalKeyName(),
Loading history...
216
            ],
217
            'desc' => [
218 294
                $name.'.'.$this->getLocalKeyName(),
219 294
                $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

219
                $this->/** @scrutinizer ignore-call */ 
220
                       qualifyColumn($this->getParentKeyName()),
Loading history...
220
            ],
221
        ];
222
223 294
        if ($direction === 'both') {
224 29
            $recursiveDepth = "$depth + (case when {$joinColumns['desc'][1]}={$joinColumns['desc'][0]} then 1 else -1 end)";
225
        } else {
226 268
            $recursiveDepth = $depth.' '.($direction === 'asc' ? '-' : '+').' 1';
227
        }
228
229 294
        $recursivePath = $grammar->compileRecursivePath(
230 294
            $this->getQualifiedLocalKeyName(),
231 294
            $this->getPathName()
232
        );
233
234 294
        $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

234
        $recursivePathBindings = $grammar->getRecursivePathBindings($this->/** @scrutinizer ignore-call */ getPathSeparator());
Loading history...
235
236 294
        $query = $this->newModelQuery()
237 294
            ->select($table.'.*')
238 294
            ->selectRaw($recursiveDepth.' as '.$depth)
239 294
            ->selectRaw($recursivePath, $recursivePathBindings)
240 294
            ->from($from);
241
242 294
        foreach ($this->getCustomPaths() as $path) {
243 289
            $query->selectRaw(
244 289
                $grammar->compileRecursivePath(
245 289
                    $this->qualifyColumn($path['column']),
246 289
                    $path['name']
247
                ),
248 289
                $grammar->getRecursivePathBindings($path['separator'])
249
            );
250
        }
251
252 294
        $this->addRecursiveQueryJoinsAndConstraints($query, $direction, $name, $joinColumns);
253
254 294
        if (!is_null($maxDepth)) {
255 15
            $query->where($this->getDepthName(), '<', $maxDepth);
256
        }
257
258 294
        return $query;
259
    }
260
261
    /**
262
     * Add join and where clauses to the recursive query for a relationship expression.
263
     *
264
     * @param \Illuminate\Database\Eloquent\Builder $query
265
     * @param string $direction
266
     * @param string $name
267
     * @param array $joinColumns
268
     * @return void
269
     */
270 294
    protected function addRecursiveQueryJoinsAndConstraints(Builder $query, $direction, $name, array $joinColumns)
271
    {
272 294
        if ($direction === 'both') {
273 29
            $query->join($name, function (JoinClause $join) use ($joinColumns) {
274 29
                $join->on($joinColumns['asc'][0], '=', $joinColumns['asc'][1])
275 29
                    ->orOn($joinColumns['desc'][0], '=', $joinColumns['desc'][1]);
276
            });
277
278 29
            $depth = $this->getDepthName();
279
280 29
            $query->where(function (Builder  $query) use ($depth, $joinColumns) {
281 29
                $query->where($depth, '=', 0)
282 29
                    ->orWhere(function (Builder $query) use ($depth, $joinColumns) {
283 29
                        $query->whereColumn($joinColumns['asc'][0], '=', $joinColumns['asc'][1])
284 29
                            ->where($depth, '<', 0);
285
                    })
286 29
                    ->orWhere(function (Builder $query) use ($depth, $joinColumns) {
287 29
                        $query->whereColumn($joinColumns['desc'][0], '=', $joinColumns['desc'][1])
288 29
                            ->where($depth, '>', 0);
289
                    });
290
            });
291
        } else {
292 268
            $query->join($name, $joinColumns[$direction][0], '=', $joinColumns[$direction][1]);
293
        }
294
295 294
        if (static::$recursiveQueryConstraint) {
296 10
            (static::$recursiveQueryConstraint)($query);
297
        }
298
    }
299
}
300