Passed
Pull Request — master (#77)
by
unknown
13:35
created

HasRecursiveRelationshipScopes   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 302
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 22
eloc 95
c 2
b 0
f 0
dl 0
loc 302
ccs 104
cts 104
cp 1
rs 10

14 Methods

Rating   Name   Duplication   Size   Complexity  
A scopeBreadthFirst() 0 3 1
A scopeHasChildren() 0 7 1
A scopeWhereDepth() 0 5 1
A scopeTree() 0 7 1
A scopeIsLeaf() 0 7 1
A scopeIsRoot() 0 3 1
A scopeHasParent() 0 3 1
A scopeTreeOf() 0 3 1
A getInitialQuery() 0 24 2
A scopeDepthFirst() 0 5 1
A scopeWithRelationshipExpression() 0 16 2
A addRecursiveQueryJoinsAndConstraints() 0 23 2
A getRecursiveQuery() 0 57 5
A callRecursiveQueryDecoratingFunction() 0 7 2
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 43
    public function scopeTree(Builder $query, $maxDepth = null)
19
    {
20 43
        $constraint = function (Builder $query) {
21 43
            $query->isRoot();
22 43
        };
23
24 43
        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 53
    public function scopeTreeOf(Builder $query, callable $constraint, $maxDepth = null)
36
    {
37 53
        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 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());
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);
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 73
    public function scopeIsRoot(Builder $query)
88
    {
89 73
        return $query->whereNull($this->getParentKeyName());
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());
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);
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 272
    public function scopeWithRelationshipExpression(Builder $query, $direction, callable $constraint, $initialDepth, $from = null, $maxDepth = null)
143
    {
144 272
        $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 272
        $grammar = $query->getExpressionGrammar();
147
148 272
        $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 272
            ->unionAll(
150 272
                $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 272
        $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 272
        $query->getModel()->setTable($name);
156
157 272
        return $query->withRecursiveExpression($name, $expression)->from($name);
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 272
    protected function getInitialQuery(ExpressionGrammar $grammar, callable $constraint, $initialDepth, $from)
170
    {
171 272
        $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 272
        $initialPath = $grammar->compileInitialPath(
174 272
            $this->getLocalKeyName(),
175 272
            $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 272
        $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 272
            ->select('*')
180 272
            ->selectRaw($initialDepth.' as '.$depth)
181 272
            ->selectRaw($initialPath)
182 272
            ->from($from);
183
184 272
        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 267
            $query->selectRaw(
186 267
                $grammar->compileInitialPath($path['column'], $path['name'])
187
            );
188
        }
189
190 272
        $constraint($query);
191
192 272
        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 272
    protected function getRecursiveQuery(ExpressionGrammar $grammar, $direction, $from, $maxDepth = null)
205
    {
206 272
        $name = $this->getExpressionName();
207
208 272
        $table = explode(' as ', $from)[1] ?? $from;
209
210 272
        $depth = $grammar->wrap($this->getDepthName());
211
212 272
        $joinColumns = [
213
            'asc' => [
214 272
                $name.'.'.$this->getParentKeyName(),
215 272
                $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 272
                $name.'.'.$this->getLocalKeyName(),
219 272
                $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 272
        if ($direction === 'both') {
224 29
            $recursiveDepth = "$depth + (case when {$joinColumns['desc'][1]}={$joinColumns['desc'][0]} then 1 else -1 end)";
225
        } else {
226 246
            $recursiveDepth = $depth.' '.($direction === 'asc' ? '-' : '+').' 1';
227
        }
228
229 272
        $recursivePath = $grammar->compileRecursivePath(
230 272
            $this->getQualifiedLocalKeyName(),
231 272
            $this->getPathName()
232
        );
233
234 272
        $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 272
        $query = $this->newModelQuery()
237 272
            ->select($table.'.*')
238 272
            ->selectRaw($recursiveDepth.' as '.$depth)
239 272
            ->selectRaw($recursivePath, $recursivePathBindings)
240 272
            ->from($from);
241
242 272
        foreach ($this->getCustomPaths() as $path) {
243 267
            $query->selectRaw(
244 267
                $grammar->compileRecursivePath(
245 267
                    $this->qualifyColumn($path['column']),
246 267
                    $path['name']
247
                ),
248 267
                $grammar->getRecursivePathBindings($path['separator'])
249
            );
250
        }
251
252 272
        $this->addRecursiveQueryJoinsAndConstraints($query, $direction, $name, $joinColumns);
253
254 272
        if (!is_null($maxDepth)) {
255 15
            $query->where($this->getDepthName(), '<', $maxDepth);
256
        }
257
258 272
        $this->callRecursiveQueryDecoratingFunction($query);
259
260
        return $query;
261
    }
262
263
    /**
264
     * Add join and where clauses to the recursive query for a relationship expression.
265
     *
266
     * @param \Illuminate\Database\Eloquent\Builder $query
267
     * @param string $direction
268
     * @param string $name
269
     * @param array $joinColumns
270 272
     * @return void
271
     */
272 272
    protected function addRecursiveQueryJoinsAndConstraints(Builder $query, $direction, $name, array $joinColumns)
273 29
    {
274 29
        if ($direction === 'both') {
275 29
            $query->join($name, function (JoinClause $join) use ($joinColumns) {
276 29
                $join->on($joinColumns['asc'][0], '=', $joinColumns['asc'][1])
277
                    ->orOn($joinColumns['desc'][0], '=', $joinColumns['desc'][1]);
278 29
            });
279
280 29
            $depth = $this->getDepthName();
281 29
282 29
            $query->where(function (Builder  $query) use ($depth, $joinColumns) {
283 29
                $query->where($depth, '=', 0)
284 29
                    ->orWhere(function (Builder $query) use ($depth, $joinColumns) {
285 29
                        $query->whereColumn($joinColumns['asc'][0], '=', $joinColumns['asc'][1])
286 29
                            ->where($depth, '<', 0);
287 29
                    })
288 29
                    ->orWhere(function (Builder $query) use ($depth, $joinColumns) {
289 29
                        $query->whereColumn($joinColumns['desc'][0], '=', $joinColumns['desc'][1])
290 29
                            ->where($depth, '>', 0);
291
                    });
292 246
            });
293
        } else {
294 272
            $query->join($name, $joinColumns[$direction][0], '=', $joinColumns[$direction][1]);
295
        }
296
    }
297
298
    /**
299
     * Call decorating function for recursive builder if set
300
     *
301
     * @param \Illuminate\Database\Eloquent\Builder $query
302
     * @return \Illuminate\Database\Eloquent\Builder
303
     */
304
    protected function callRecursiveQueryDecoratingFunction(Builder $query) {
305
        if (is_callable(static::$recursiveQueryDecoratingFunction)) {
306
            (static::$recursiveQueryDecoratingFunction)($query);
307
            static::$recursiveQueryDecoratingFunction = null;
0 ignored issues
show
Bug Best Practice introduced by
The property recursiveQueryDecoratingFunction does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
308
        }
309
310
        return $query;
311
    }
312
}
313