Passed
Push — master ( 5a82e5...fe024b )
by Jonas
03:43
created

addRecursiveQueryJoinsAndConstraints()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 15
c 1
b 0
f 0
dl 0
loc 27
ccs 11
cts 11
cp 1
rs 9.7666
cc 3
nc 4
nop 5
crap 3
1
<?php
2
3
namespace Staudenmeir\LaravelAdjacencyList\Eloquent\Traits;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use RuntimeException;
7
use Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar;
8
9
trait HasGraphRelationshipScopes
10
{
11
    /**
12
     * Add a recursive expression for a custom graph to the query.
13
     *
14
     * @param \Illuminate\Database\Eloquent\Builder $query
15
     * @param callable $constraint
16
     * @param int|null $maxDepth
17
     * @return \Illuminate\Database\Eloquent\Builder
18
     */
19 8
    public function scopeGraphOf(Builder $query, callable $constraint, int $maxDepth = null): Builder
20
    {
21 8
        return $query->withRelationshipExpression('desc', $constraint, 0, null, $maxDepth);
22
    }
23
24
    /**
25
     * Limit the query by depth.
26
     *
27
     * @param \Illuminate\Database\Eloquent\Builder $query
28
     * @param mixed $operator
29
     * @param mixed|null $value
30
     * @return \Illuminate\Database\Eloquent\Builder
31
     */
32 16
    public function scopeWhereDepth(Builder $query, mixed $operator, mixed $value = null): Builder
33
    {
34 16
        $arguments = array_slice(func_get_args(), 1);
35
36 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

36
        return $query->where($this->/** @scrutinizer ignore-call */ getDepthName(), ...$arguments);
Loading history...
37
    }
38
39
    /**
40
     * Order the query breadth-first.
41
     *
42
     * @param \Illuminate\Database\Eloquent\Builder $query
43
     * @return \Illuminate\Database\Eloquent\Builder
44
     */
45 4
    public function scopeBreadthFirst(Builder $query): Builder
46
    {
47 4
        return $query->orderBy($this->getDepthName());
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->orderBy($this->getDepthName()) could return the type Illuminate\Database\Query\Builder which is incompatible with the type-hinted return Illuminate\Database\Eloquent\Builder. Consider adding an additional type-check to rule them out.
Loading history...
48
    }
49
50
    /**
51
     * Order the query depth-first.
52
     *
53
     * @param \Illuminate\Database\Eloquent\Builder $query
54
     * @return \Illuminate\Database\Eloquent\Builder
55
     */
56 4
    public function scopeDepthFirst(Builder $query): Builder
57
    {
58 4
        $sql = $query->getExpressionGrammar()->compileOrderByPath();
59
60 4
        return $query->orderByRaw($sql);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->orderByRaw($sql) could return the type Illuminate\Database\Query\Builder which is incompatible with the type-hinted return Illuminate\Database\Eloquent\Builder. Consider adding an additional type-check to rule them out.
Loading history...
61
    }
62
63
    /**
64
     * Add a recursive expression for the relationship to the query.
65
     *
66
     * @param \Illuminate\Database\Eloquent\Builder $query
67
     * @param string $direction
68
     * @param callable $constraint
69
     * @param int $initialDepth
70
     * @param string|null $from
71
     * @param int|null $maxDepth
72
     * @param string $union
73
     * @return \Illuminate\Database\Eloquent\Builder
74
     */
75 206
    public function scopeWithRelationshipExpression(
76
        Builder $query,
77
        string $direction,
78
        callable $constraint,
79
        int $initialDepth,
80
        string $from = null,
81
        int $maxDepth = null,
82
        string $union = 'unionAll'
83
    ): Builder {
84 206
        $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

84
        $from = $from ?: $this->/** @scrutinizer ignore-call */ getTable();
Loading history...
85
86 206
        $grammar = $query->getExpressionGrammar();
87
88 206
        $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

88
        $expression = $this->getInitialQuery(/** @scrutinizer ignore-type */ $grammar, $constraint, $initialDepth, $from)
Loading history...
89 206
            ->$union(
90 206
                $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

90
                $this->getRecursiveQuery(/** @scrutinizer ignore-type */ $grammar, $direction, $from, $maxDepth)
Loading history...
91
            );
92
93 206
        $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

93
        /** @scrutinizer ignore-call */ 
94
        $name = $this->getExpressionName();
Loading history...
94
95 206
        $query->getModel()->setTable($name);
96
97 206
        return $query->withRecursiveExpression($name, $expression)->from($name);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->withRecur...xpression)->from($name) could return the type Illuminate\Database\Query\Builder which is incompatible with the type-hinted return Illuminate\Database\Eloquent\Builder. Consider adding an additional type-check to rule them out.
Loading history...
98
    }
99
100
    /**
101
     * Get the initial query for a relationship expression.
102
     *
103
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
104
     * @param callable $constraint
105
     * @param int $initialDepth
106
     * @param array $pivotColumns
107
     * @param string $from
108
     * @return \Illuminate\Database\Eloquent\Builder $query
109
     */
110 206
    protected function getInitialQuery(
111
        ExpressionGrammar $grammar,
112
        callable $constraint,
113
        int $initialDepth,
114
        string $from
115
    ): Builder {
116 206
        $table = explode(' as ', $from)[1] ?? $from;
117
118 206
        $pivotTable = $this->getPivotTableName();
0 ignored issues
show
Bug introduced by
It seems like getPivotTableName() 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

118
        /** @scrutinizer ignore-call */ 
119
        $pivotTable = $this->getPivotTableName();
Loading history...
119
120 206
        $depth = $grammar->wrap(
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

120
        /** @scrutinizer ignore-call */ 
121
        $depth = $grammar->wrap(
Loading history...
121 206
            $this->getDepthName()
122
        );
123
124 206
        $initialPath = $grammar->compileInitialPath(
125 206
            $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

125
            $this->/** @scrutinizer ignore-call */ 
126
                   getQualifiedLocalKeyName(),
Loading history...
126 206
            $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
            $this->/** @scrutinizer ignore-call */ 
127
                   getPathName()
Loading history...
127
        );
128
129 206
        $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

129
        $query = $this->/** @scrutinizer ignore-call */ newModelQuery()
Loading history...
130 206
            ->select("$table.*")
131 206
            ->selectRaw($initialDepth . ' as ' . $depth)
132 206
            ->selectRaw($initialPath)
133 206
            ->from($from);
134
135 206
        $this->addInitialQueryCustomPaths($query, $grammar);
136
137 206
        $this->addInitialQueryPivotColumns($query, $grammar, $pivotTable, $initialDepth);
138
139 206
        $this->addInitialQueryCycleDetection($query, $grammar);
140
141 206
        $this->addInitialQueryJoins($query, $pivotTable, $initialDepth);
142
143 206
        $constraint($query);
144
145 206
        return $query;
146
    }
147
148
    /**
149
     * Add custom paths to the initial query.
150
     *
151
     * @param \Illuminate\Database\Eloquent\Builder $query
152
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
153
     * @return void
154
     */
155 206
    protected function addInitialQueryCustomPaths(Builder $query, ExpressionGrammar $grammar): void
156
    {
157 206
        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

157
        foreach ($this->/** @scrutinizer ignore-call */ getCustomPaths() as $path) {
Loading history...
158 206
            $query->selectRaw(
159 206
                $grammar->compileInitialPath(
160 206
                    $this->qualifyColumn($path['column']),
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

160
                    $this->/** @scrutinizer ignore-call */ 
161
                           qualifyColumn($path['column']),
Loading history...
161 206
                    $path['name']
162
                )
163
            );
164
        }
165
    }
166
167
    /**
168
     * Add pivot columns to the initial query for a relationship expression.
169
     *
170
     * @param \Illuminate\Database\Eloquent\Builder $query
171
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
172
     * @param string $pivotTable
173
     * @param int $initialDepth
174
     * @return void
175
     */
176 206
    protected function addInitialQueryPivotColumns(
177
        Builder $query,
178
        ExpressionGrammar $grammar,
179
        string $pivotTable,
180
        int $initialDepth
181
    ): void {
182 206
        $columns = [$this->getParentKeyName(), $this->getChildKeyName(), ...$this->getPivotColumns()];
0 ignored issues
show
Bug introduced by
It seems like getPivotColumns() 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
        $columns = [$this->getParentKeyName(), $this->getChildKeyName(), ...$this->/** @scrutinizer ignore-call */ getPivotColumns()];
Loading history...
Bug introduced by
It seems like getChildKeyName() 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
        $columns = [$this->getParentKeyName(), $this->/** @scrutinizer ignore-call */ getChildKeyName(), ...$this->getPivotColumns()];
Loading history...
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

182
        $columns = [$this->/** @scrutinizer ignore-call */ getParentKeyName(), $this->getChildKeyName(), ...$this->getPivotColumns()];
Loading history...
183
184 206
        if ($initialDepth === 0) {
185 96
            if (!$query->getConnection()->isDoctrineAvailable()) {
186
                throw new RuntimeException(
187
                    'This feature requires the doctrine/dbal package. Please run "composer require doctrine/dbal".'
188
                ); // @codeCoverageIgnore
189
            }
190
191 96
            $localKeyType = $query->getConnection()->getSchemaBuilder()->getColumnType(
192 96
                (new $this())->getTable(),
193 96
                $this->getLocalKeyName()
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

193
                $this->/** @scrutinizer ignore-call */ 
194
                       getLocalKeyName()
Loading history...
194
            );
195 96
            foreach ($columns as $i => $column) {
196 96
                if ($i < 2) {
197 96
                    $type = $localKeyType;
198
                } else {
199 96
                    $type = $query->getConnection()->getSchemaBuilder()->getColumnType($pivotTable, $column);
200
                }
201
202 96
                $null = $grammar->compilePivotColumnNullValue($type);
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type Illuminate\Database\ConnectionInterface and Illuminate\Database\Eloquent\Builder; however, parameter $type of Staudenmeir\LaravelAdjac...ePivotColumnNullValue() does only seem to accept string, 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

202
                $null = $grammar->compilePivotColumnNullValue(/** @scrutinizer ignore-type */ $type);
Loading history...
203
204 96
                $query->selectRaw("$null as " . $grammar->wrap("pivot_$column"));
205
            }
206
        } else {
207 122
            foreach ($columns as $column) {
208 122
                $query->addSelect("$pivotTable.$column as pivot_$column");
209
            }
210
        }
211
    }
212
213
    /**
214
     * Add cycle detection to the initial query for a relationship expression.
215
     *
216
     * @param \Illuminate\Database\Eloquent\Builder $query
217
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
218
     * @return void
219
     */
220 206
    protected function addInitialQueryCycleDetection(Builder $query, ExpressionGrammar $grammar): void
221
    {
222 206
        if ($this->enableCycleDetection() && $this->includeCycleStart()) {
0 ignored issues
show
Bug introduced by
It seems like includeCycleStart() 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

222
        if ($this->enableCycleDetection() && $this->/** @scrutinizer ignore-call */ includeCycleStart()) {
Loading history...
Bug introduced by
It seems like enableCycleDetection() 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

222
        if ($this->/** @scrutinizer ignore-call */ enableCycleDetection() && $this->includeCycleStart()) {
Loading history...
223 38
            $query->selectRaw(
224 38
                'false as ' . $grammar->wrap($this->getCycleDetectionColumnName())
0 ignored issues
show
Bug introduced by
It seems like getCycleDetectionColumnName() 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

224
                'false as ' . $grammar->wrap($this->/** @scrutinizer ignore-call */ getCycleDetectionColumnName())
Loading history...
225
            );
226
        }
227
    }
228
229
    /**
230
     * Add join clauses to the initial query for a relationship expression.
231
     *
232
     * @param \Illuminate\Database\Eloquent\Builder $query
233
     * @param string $pivotTable
234
     * @param int $initialDepth
235
     * @return void
236
     */
237 206
    protected function addInitialQueryJoins(Builder $query, string $pivotTable, int $initialDepth): void
238
    {
239 206
        if ($initialDepth < 0) {
240 58
            $query->join(
241
                $pivotTable,
242 58
                $this->getQualifiedLocalKeyName(),
243
                '=',
244 58
                $this->getQualifiedParentKeyName()
0 ignored issues
show
Bug introduced by
It seems like getQualifiedParentKeyName() 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

244
                $this->/** @scrutinizer ignore-call */ 
245
                       getQualifiedParentKeyName()
Loading history...
245
            );
246 169
        } elseif ($initialDepth > 0) {
247 76
            $query->join(
248
                $pivotTable,
249 76
                $this->getQualifiedLocalKeyName(),
250
                '=',
251 76
                $this->getQualifiedChildKeyName()
0 ignored issues
show
Bug introduced by
It seems like getQualifiedChildKeyName() 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

251
                $this->/** @scrutinizer ignore-call */ 
252
                       getQualifiedChildKeyName()
Loading history...
252
            );
253
        }
254
    }
255
256
    /**
257
     * Get the recursive query for a relationship expression.
258
     *
259
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
260
     * @param string $direction
261
     * @param string $from
262
     * @param int|null $maxDepth
263
     * @return \Illuminate\Database\Eloquent\Builder $query
264
     */
265 206
    protected function getRecursiveQuery(
266
        ExpressionGrammar $grammar,
267
        string $direction,
268
        string $from,
269
        int $maxDepth = null
270
    ): Builder {
271 206
        $name = $this->getExpressionName();
272
273 206
        $table = explode(' as ', $from)[1] ?? $from;
274
275 206
        $pivotTable = $this->getPivotTableName();
276
277 206
        $depth = $grammar->wrap($this->getDepthName());
278
279 206
        $joinColumns = [
280
            'asc' => [
281 206
                $name . '.' . $this->getLocalKeyName(),
282 206
                $this->getQualifiedChildKeyName(),
283
            ],
284
            'desc' => [
285 206
                $name . '.' . $this->getLocalKeyName(),
286 206
                $this->getQualifiedParentKeyName(),
287
            ],
288
        ];
289
290 206
        $recursiveDepth = $depth . ' ' . ($direction === 'asc' ? '-' : '+') . ' 1';
291
292 206
        $recursivePath = $grammar->compileRecursivePath(
293 206
            $this->getQualifiedLocalKeyName(),
294 206
            $this->getPathName()
295
        );
296
297 206
        $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

297
        $recursivePathBindings = $grammar->getRecursivePathBindings($this->/** @scrutinizer ignore-call */ getPathSeparator());
Loading history...
298
299 206
        $query = $this->newModelQuery()
300 206
            ->select($table . '.*')
301 206
            ->selectRaw($recursiveDepth . ' as ' . $depth)
302 206
            ->selectRaw($recursivePath, $recursivePathBindings)
303 206
            ->from($from);
304
305 206
        $this->addRecursiveQueryCustomPaths($query, $grammar);
306
307 206
        $this->addRecursiveQueryPivotColumns($query, $pivotTable);
308
309 206
        $this->addRecursiveQueryCycleDetection($query, $grammar);
310
311 206
        $this->addRecursiveQueryJoinsAndConstraints($query, $pivotTable, $direction, $name, $joinColumns);
312
313 206
        if (!is_null($maxDepth)) {
314 4
            $query->where($this->getDepthName(), '<', $maxDepth);
315
        }
316
317 206
        return $query;
318
    }
319
320
    /**
321
     * Add customs path to the recursive query for a relationship expression.
322
     *
323
     * @param \Illuminate\Database\Eloquent\Builder $query
324
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
325
     * @return void
326
     */
327 206
    protected function addRecursiveQueryCustomPaths(Builder $query, ExpressionGrammar $grammar): void
328
    {
329 206
        foreach ($this->getCustomPaths() as $path) {
330 206
            $query->selectRaw(
331 206
                $grammar->compileRecursivePath(
332 206
                    $this->qualifyColumn($path['column']),
333 206
                    $path['name']
334
                ),
335 206
                $grammar->getRecursivePathBindings($path['separator'])
336
            );
337
        }
338
    }
339
340
    /**
341
     * Add pivot columns to the recursive query for a relationship expression.
342
     *
343
     * @param \Illuminate\Database\Eloquent\Builder $query
344
     * @param string $pivotTable
345
     * @return void
346
     */
347 206
    protected function addRecursiveQueryPivotColumns(Builder $query, string $pivotTable): void
348
    {
349 206
        $columns = [$this->getParentKeyName(), $this->getChildKeyName(), ...$this->getPivotColumns()];
350
351 206
        foreach ($columns as $column) {
352 206
            $query->addSelect("$pivotTable.$column as pivot_$column");
353
        }
354
    }
355
356
    /**
357
     * Add cycle detection to the recursive query for a relationship expression.
358
     *
359
     * @param \Illuminate\Database\Eloquent\Builder $query
360
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
361
     * @return void
362
     */
363 206
    protected function addRecursiveQueryCycleDetection(Builder $query, ExpressionGrammar $grammar): void
364
    {
365 206
        if ($this->enableCycleDetection()) {
366 76
            $sql = $grammar->compileCycleDetection(
367 76
                $this->getQualifiedLocalKeyName(),
368 76
                $this->getPathName()
369
            );
370 76
            $bindings = $grammar->getCycleDetectionBindings(
371 76
                $this->getPathSeparator()
372
            );
373
374
            // TODO[L10]: whereNot()
375 76
            if ($this->includeCycleStart()) {
376 38
                $query->selectRaw($sql, $bindings);
377
378 38
                $query->whereRaw(
379 38
                    'not ' . $grammar->wrap($this->getCycleDetectionColumnName())
380
                );
381
            } else {
382 38
                $query->whereRaw("not($sql)", $bindings);
383
            }
384
        }
385
    }
386
387
    /**
388
     * Add join and where clauses to the recursive query for a relationship expression.
389
     *
390
     * @param \Illuminate\Database\Eloquent\Builder $query
391
     * @param string $pivotTable
392
     * @param string $direction
393
     * @param string $name
394
     * @param array $joinColumns
395
     * @return void
396
     */
397 206
    protected function addRecursiveQueryJoinsAndConstraints(
398
        Builder $query,
399
        string $pivotTable,
400
        string $direction,
401
        string $name,
402
        array $joinColumns
403
    ): void {
404 206
        if ($direction === 'desc') {
405 131
            $query->join(
406
                $pivotTable,
407 131
                $this->getQualifiedLocalKeyName(),
408
                '=',
409 131
                $this->getQualifiedChildKeyName()
410
            );
411
        } else {
412 99
            $query->join(
413
                $pivotTable,
414 99
                $this->getQualifiedLocalKeyName(),
415
                '=',
416 99
                $this->getQualifiedParentKeyName()
417
            );
418
        }
419
420 206
        $query->join($name, $joinColumns[$direction][0], '=', $joinColumns[$direction][1]);
421
422 206
        if (static::$recursiveQueryConstraint) {
423 8
            (static::$recursiveQueryConstraint)($query);
424
        }
425
    }
426
}
427