Passed
Push — master ( a1f161...76823e )
by Jonas
06:13
created

HasRecursiveRelationshipScopes::scopeTree()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 7
ccs 5
cts 5
cp 1
rs 10
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
namespace Staudenmeir\LaravelAdjacencyList\Eloquent;
4
5
use Illuminate\Database\Eloquent\Builder;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Staudenmeir\LaravelAdjacencyList\Eloquent\Builder. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
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 28
    public function scopeTree(Builder $query, $maxDepth = null)
19
    {
20 28
        $constraint = function (Builder $query) {
21 28
            $query->isRoot();
22 28
        };
23
24 28
        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 36
    public function scopeTreeOf(Builder $query, callable $constraint, $maxDepth = null)
36
    {
37 36
        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 4
    public function scopeHasChildren(Builder $query)
47
    {
48 4
        $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 4
            ->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 4
            ->hasParent();
51
52 4
        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

52
        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...
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 12
    public function scopeHasParent(Builder $query)
62
    {
63 12
        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 4
    public function scopeIsLeaf(Builder $query)
73
    {
74 4
        $keys = (new static())->newQuery()
75 4
            ->select($this->getParentKeyName())
76 4
            ->hasParent();
77
78 4
        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 54
    public function scopeIsRoot(Builder $query)
88
    {
89 54
        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 16
    public function scopeWhereDepth(Builder $query, $operator, $value = null)
101
    {
102 16
        $arguments = array_slice(func_get_args(), 1);
103
104 16
        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 16
    public function scopeBreadthFirst(Builder $query)
114
    {
115 16
        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 8
    public function scopeDepthFirst(Builder $query)
125
    {
126 8
        return $query->orderBy($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

126
        return $query->orderBy($this->/** @scrutinizer ignore-call */ getPathName());
Loading history...
Bug Best Practice introduced by
The expression return $query->orderBy($this->getPathName()) also could return the type Illuminate\Database\Query\Builder which is incompatible with the documented return type Illuminate\Database\Eloquent\Builder.
Loading history...
127
    }
128
129
    /**
130
     * Add a recursive expression for the relationship to the query.
131
     *
132
     * @param \Illuminate\Database\Eloquent\Builder $query
133
     * @param string $direction
134
     * @param callable $constraint
135
     * @param int $initialDepth
136
     * @param string|null $from
137
     * @param int|null $maxDepth
138
     * @return \Illuminate\Database\Eloquent\Builder
139
     */
140 202
    public function scopeWithRelationshipExpression(Builder $query, $direction, callable $constraint, $initialDepth, $from = null, $maxDepth = null)
141
    {
142 202
        $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

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

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

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

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

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

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

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

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

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

232
        $recursivePathBindings = $grammar->getRecursivePathBindings($this->/** @scrutinizer ignore-call */ getPathSeparator());
Loading history...
233
234 202
        $query = $this->newModelQuery()
235 202
            ->select($table.'.*')
236 202
            ->selectRaw($recursiveDepth.' as '.$depth)
237 202
            ->selectRaw($recursivePath, $recursivePathBindings)
238 202
            ->from($from);
239
240 202
        foreach ($this->getCustomPaths() as $path) {
241 202
            $query->selectRaw(
242 202
                $grammar->compileRecursivePath(
243 202
                    $this->qualifyColumn($path['column']),
244 202
                    $path['name']
245
                ),
246 202
                $grammar->getRecursivePathBindings($path['separator'])
247
            );
248
        }
249
250 202
        if ($direction === 'both') {
251 22
            $query->join($name, function (JoinClause $join) use ($joinColumns) {
252 22
                $join->on($joinColumns['asc'][0], '=', $joinColumns['asc'][1])
253 22
                    ->orOn($joinColumns['desc'][0], '=', $joinColumns['desc'][1]);
254 22
            });
255
256 22
            $depth = $this->getDepthName();
257
258 22
            $query->where(function (Builder  $query) use ($depth, $joinColumns) {
259 22
                $query->where($depth, '=', 0)
260 22
                    ->orWhere(function (Builder $query) use ($depth, $joinColumns) {
261 22
                        $query->whereColumn($joinColumns['asc'][0], '=', $joinColumns['asc'][1])
262 22
                            ->where($depth, '<', 0);
263 22
                    })
264 22
                    ->orWhere(function (Builder $query) use ($depth, $joinColumns) {
265 22
                        $query->whereColumn($joinColumns['desc'][0], '=', $joinColumns['desc'][1])
266 22
                            ->where($depth, '>', 0);
267 22
                    });
268 22
            });
269
        } else {
270 183
            $query->join($name, $joinColumns[$direction][0], '=', $joinColumns[$direction][1]);
271
        }
272
273 202
        if (!is_null($maxDepth)) {
274 12
            $query->where($this->getDepthName(), '<', $maxDepth);
275
        }
276
277 202
        return $query;
278
    }
279
}
280