Passed
Push — master ( a0a8dd...2952d5 )
by Jonas
10:46
created

scopeWithRelationshipExpression()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 23
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 23
ccs 9
cts 9
cp 1
rs 10
cc 2
nc 1
nop 7
crap 2
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
     * Limit the query by depth.
13
     *
14
     * @param \Illuminate\Database\Eloquent\Builder $query
15
     * @param mixed $operator
16
     * @param mixed|null $value
17
     * @return \Illuminate\Database\Eloquent\Builder
18
     */
19 16
    public function scopeWhereDepth(Builder $query, mixed $operator, mixed $value = null): Builder
20
    {
21 16
        $arguments = array_slice(func_get_args(), 1);
22
23 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

23
        return $query->where($this->/** @scrutinizer ignore-call */ getDepthName(), ...$arguments);
Loading history...
24
    }
25
26
    /**
27
     * Order the query breadth-first.
28
     *
29
     * @param \Illuminate\Database\Eloquent\Builder $query
30
     * @return \Illuminate\Database\Eloquent\Builder
31
     */
32 4
    public function scopeBreadthFirst(Builder $query): Builder
33
    {
34 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...
35
    }
36
37
    /**
38
     * Order the query depth-first.
39
     *
40
     * @param \Illuminate\Database\Eloquent\Builder $query
41
     * @return \Illuminate\Database\Eloquent\Builder
42
     */
43 4
    public function scopeDepthFirst(Builder $query): Builder
44
    {
45 4
        $sql = $query->getExpressionGrammar()->compileOrderByPath();
46
47 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...
48
    }
49
50
    /**
51
     * Add a recursive expression for the relationship to the query.
52
     *
53
     * @param \Illuminate\Database\Eloquent\Builder $query
54
     * @param string $direction
55
     * @param callable $constraint
56
     * @param int $initialDepth
57
     * @param string|null $from
58
     * @param int|null $maxDepth
59
     * @param string $union
60
     * @return \Illuminate\Database\Eloquent\Builder
61
     */
62 198
    public function scopeWithRelationshipExpression(
63
        Builder $query,
64
        string $direction,
65
        callable $constraint,
66
        int $initialDepth,
67
        string $from = null,
68
        int $maxDepth = null,
69
        string $union = 'unionAll'
70
    ): Builder {
71 198
        $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

71
        $from = $from ?: $this->/** @scrutinizer ignore-call */ getTable();
Loading history...
72
73 198
        $grammar = $query->getExpressionGrammar();
74
75 198
        $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

75
        $expression = $this->getInitialQuery(/** @scrutinizer ignore-type */ $grammar, $constraint, $initialDepth, $from)
Loading history...
76 198
            ->$union(
77 198
                $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

77
                $this->getRecursiveQuery(/** @scrutinizer ignore-type */ $grammar, $direction, $from, $maxDepth)
Loading history...
78
            );
79
80 198
        $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

80
        /** @scrutinizer ignore-call */ 
81
        $name = $this->getExpressionName();
Loading history...
81
82 198
        $query->getModel()->setTable($name);
83
84 198
        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...
85
    }
86
87
    /**
88
     * Get the initial query for a relationship expression.
89
     *
90
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
91
     * @param callable $constraint
92
     * @param int $initialDepth
93
     * @param array $pivotColumns
94
     * @param string $from
95
     * @return \Illuminate\Database\Eloquent\Builder $query
96
     */
97 198
    protected function getInitialQuery(
98
        ExpressionGrammar $grammar,
99
        callable $constraint,
100
        int $initialDepth,
101
        string $from
102
    ): Builder {
103 198
        $table = explode(' as ', $from)[1] ?? $from;
104
105 198
        $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

105
        /** @scrutinizer ignore-call */ 
106
        $pivotTable = $this->getPivotTableName();
Loading history...
106
107 198
        $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

107
        /** @scrutinizer ignore-call */ 
108
        $depth = $grammar->wrap(
Loading history...
108 198
            $this->getDepthName()
109
        );
110
111 198
        $initialPath = $grammar->compileInitialPath(
112 198
            $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

112
            $this->/** @scrutinizer ignore-call */ 
113
                   getQualifiedLocalKeyName(),
Loading history...
113 198
            $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

113
            $this->/** @scrutinizer ignore-call */ 
114
                   getPathName()
Loading history...
114
        );
115
116 198
        $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

116
        $query = $this->/** @scrutinizer ignore-call */ newModelQuery()
Loading history...
117 198
            ->select("$table.*")
118 198
            ->selectRaw($initialDepth . ' as ' . $depth)
119 198
            ->selectRaw($initialPath)
120 198
            ->from($from);
121
122 198
        $this->addInitialQueryCustomPaths($query, $grammar);
123
124 198
        $this->addInitialQueryPivotColumns($query, $grammar, $pivotTable, $initialDepth);
125
126 198
        $this->addInitialQueryCycleDetection($query, $grammar);
127
128 198
        $this->addInitialQueryJoins($query, $pivotTable, $initialDepth);
129
130 198
        $constraint($query);
131
132 198
        return $query;
133
    }
134
135
    /**
136
     * Add custom paths to the initial query.
137
     *
138
     * @param \Illuminate\Database\Eloquent\Builder $query
139
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
140
     * @return void
141
     */
142 198
    protected function addInitialQueryCustomPaths(Builder $query, ExpressionGrammar $grammar): void
143
    {
144 198
        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

144
        foreach ($this->/** @scrutinizer ignore-call */ getCustomPaths() as $path) {
Loading history...
145 198
            $query->selectRaw(
146 198
                $grammar->compileInitialPath(
147 198
                    $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

147
                    $this->/** @scrutinizer ignore-call */ 
148
                           qualifyColumn($path['column']),
Loading history...
148 198
                    $path['name']
149
                )
150
            );
151
        }
152
    }
153
154
    /**
155
     * Add pivot columns to the initial query for a relationship expression.
156
     *
157
     * @param \Illuminate\Database\Eloquent\Builder $query
158
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
159
     * @param string $pivotTable
160
     * @param int $initialDepth
161
     * @return void
162
     */
163 198
    protected function addInitialQueryPivotColumns(
164
        Builder $query,
165
        ExpressionGrammar $grammar,
166
        string $pivotTable,
167
        int $initialDepth
168
    ): void {
169 198
        $columns = [$this->getParentKeyName(), $this->getChildKeyName(), ...$this->getPivotColumns()];
0 ignored issues
show
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

169
        $columns = [$this->getParentKeyName(), $this->/** @scrutinizer ignore-call */ getChildKeyName(), ...$this->getPivotColumns()];
Loading history...
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

169
        $columns = [$this->getParentKeyName(), $this->getChildKeyName(), ...$this->/** @scrutinizer ignore-call */ 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

169
        $columns = [$this->/** @scrutinizer ignore-call */ getParentKeyName(), $this->getChildKeyName(), ...$this->getPivotColumns()];
Loading history...
170
171 198
        if ($initialDepth === 0) {
172 88
            if (!class_exists('Doctrine\DBAL\Schema\Column')) {
173
                throw new RuntimeException(
174
                    'This feature requires the "doctrine/dbal" package, please run "composer require doctrine/dbal".'
175
                ); // @codeCoverageIgnore
176
            }
177
178 88
            $localKeyType = $query->getConnection()->getSchemaBuilder()->getColumnType(
179 88
                (new $this())->getTable(),
180 88
                $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

180
                $this->/** @scrutinizer ignore-call */ 
181
                       getLocalKeyName()
Loading history...
181
            );
182 88
            foreach ($columns as $i => $column) {
183 88
                if ($i < 2) {
184 88
                    $type = $localKeyType;
185
                } else {
186 88
                    $type = $query->getConnection()->getSchemaBuilder()->getColumnType($pivotTable, $column);
187
                }
188
189 88
                $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

189
                $null = $grammar->compilePivotColumnNullValue(/** @scrutinizer ignore-type */ $type);
Loading history...
190
191 88
                $query->selectRaw("$null as " . $grammar->wrap("pivot_$column"));
192
            }
193
        } else {
194 122
            foreach ($columns as $column) {
195 122
                $query->addSelect("$pivotTable.$column as pivot_$column");
196
            }
197
        }
198
    }
199
200
    /**
201
     * Add cycle detection to the initial query for a relationship expression.
202
     *
203
     * @param \Illuminate\Database\Eloquent\Builder $query
204
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
205
     * @return void
206
     */
207 198
    protected function addInitialQueryCycleDetection(Builder $query, ExpressionGrammar $grammar): void
208
    {
209 198
        if ($this->enableCycleDetection() && $this->includeCycleStart()) {
0 ignored issues
show
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

209
        if ($this->/** @scrutinizer ignore-call */ enableCycleDetection() && $this->includeCycleStart()) {
Loading history...
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

209
        if ($this->enableCycleDetection() && $this->/** @scrutinizer ignore-call */ includeCycleStart()) {
Loading history...
210 38
            $query->selectRaw(
211 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

211
                'false as ' . $grammar->wrap($this->/** @scrutinizer ignore-call */ getCycleDetectionColumnName())
Loading history...
212
            );
213
        }
214
    }
215
216
    /**
217
     * Add join clauses to the initial query for a relationship expression.
218
     *
219
     * @param \Illuminate\Database\Eloquent\Builder $query
220
     * @param string $pivotTable
221
     * @param int $initialDepth
222
     * @return void
223
     */
224 198
    protected function addInitialQueryJoins(Builder $query, string $pivotTable, int $initialDepth): void
225
    {
226 198
        if ($initialDepth < 0) {
227 58
            $query->join(
228
                $pivotTable,
229 58
                $this->getQualifiedLocalKeyName(),
230
                '=',
231 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

231
                $this->/** @scrutinizer ignore-call */ 
232
                       getQualifiedParentKeyName()
Loading history...
232
            );
233 161
        } elseif ($initialDepth > 0) {
234 76
            $query->join(
235
                $pivotTable,
236 76
                $this->getQualifiedLocalKeyName(),
237
                '=',
238 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

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

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