Passed
Push — master ( aa1f26...3776bc )
by Jonas
06:48
created

IsOfDescendantsRelation::addExpression()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 16
c 1
b 0
f 0
dl 0
loc 27
ccs 18
cts 18
cp 1
rs 9.7333
cc 2
nc 1
nop 4
crap 2
1
<?php
2
3
namespace Staudenmeir\LaravelAdjacencyList\Eloquent\Relations;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Collection;
7
use Illuminate\Database\Eloquent\Model;
8
use Illuminate\Support\Str;
9
use Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar;
10
11
trait IsOfDescendantsRelation
12
{
13
    use TracksIntermediateScopes;
14
15
    /**
16
     * Whether to include the parent model.
17
     *
18
     * @var bool
19
     */
20
    protected $andSelf;
21
22
    /**
23
     * The path list column alias.
24
     *
25
     * @var string
26
     */
27
    protected $pathListAlias = 'laravel_paths';
28
29
    /**
30
     * Set the base constraints on the relation query.
31
     *
32
     * @return void
33
     */
34 220
    public function addConstraints()
35
    {
36 220
        if (static::$constraints) {
37 136
            $constraint = function (Builder $query) {
38 136
                $this->addExpressionWhereConstraints($query);
39 136
            };
40
41 136
            $this->addExpression($constraint);
42
        }
43 220
    }
44
45
    /**
46
     * Set the where clause on the recursive expression query.
47
     *
48
     * @param \Illuminate\Database\Eloquent\Builder $query
49
     * @return void
50
     */
51
    abstract public function addExpressionWhereConstraints(Builder $query);
52
53
    /**
54
     * Set the constraints for an eager load of the relation.
55
     *
56
     * @param array $models
57
     * @return void
58
     */
59 48
    public function addEagerConstraints(array $models)
60
    {
61 48
        $constraint = function (Builder $query) use ($models) {
62 48
            $this->addEagerExpressionWhereConstraints($query, $models);
63 48
        };
64
65 48
        $grammar = $this->getExpressionGrammar();
66
67 48
        $pathSeparator = $this->parent->getPathSeparator();
68 48
        $listSeparator = $this->getPathListSeparator();
69
70 48
        $pathList = $grammar->selectPathList(
71 48
            $this->query->getQuery()->newQuery(),
72 48
            $this->parent->getExpressionName(),
73 48
            $this->parent->getPathName(),
74
            $pathSeparator,
75
            $listSeparator
76
        );
77
78 48
        $this->addExpression($constraint, null, null, true)
79 48
            ->select($this->query->getQuery()->from.'.*')
80 48
            ->selectSub($pathList, $this->pathListAlias);
81 48
    }
82
83
    /**
84
     * Set the where clause on the recursive expression query for an eager load of the relation.
85
     *
86
     * @param \Illuminate\Database\Eloquent\Builder $query
87
     * @param array $models
88
     * @return void
89
     */
90 48
    public function addEagerExpressionWhereConstraints(Builder $query, array $models)
91
    {
92 48
        $localKeyName = $this->getEagerLoadingLocalKeyName();
93
94 48
        $whereIn = $this->whereInMethod($this->parent, $localKeyName);
0 ignored issues
show
Bug introduced by
It seems like whereInMethod() 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

94
        /** @scrutinizer ignore-call */ 
95
        $whereIn = $this->whereInMethod($this->parent, $localKeyName);
Loading history...
95
96 48
        $keys = $this->getKeys($models, $localKeyName);
0 ignored issues
show
Bug introduced by
It seems like getKeys() 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

96
        /** @scrutinizer ignore-call */ 
97
        $keys = $this->getKeys($models, $localKeyName);
Loading history...
97
98 48
        $query->$whereIn($localKeyName, $keys);
99 48
    }
100
101
    /**
102
     * Build model dictionary.
103
     *
104
     * @param \Illuminate\Database\Eloquent\Collection $results
105
     * @return array
106
     */
107 48
    protected function buildDictionary(Collection $results)
108
    {
109 48
        $dictionary = [];
110
111 48
        $paths = explode(
112 48
            $this->getPathListSeparator(),
113 48
            $results[0]->{$this->pathListAlias}
114
        );
115
116 48
        $foreignKeyName = $this->getEagerLoadingForeignKeyName();
117 48
        $accessor = $this->getEagerLoadingAccessor();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $accessor is correct as $this->getEagerLoadingAccessor() targeting Staudenmeir\LaravelAdjac...tEagerLoadingAccessor() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
118 48
        $pathSeparator = $this->parent->getPathSeparator();
119
120 48
        foreach ($results as $result) {
121 48
            foreach ($paths as $path) {
122 48
                if (!$this->pathMatches($result, $foreignKeyName, $accessor, $pathSeparator, $path)) {
123 48
                    continue;
124
                }
125
126 48
                $keys = explode($pathSeparator, $path);
127
128 48
                if (!$this->andSelf) {
129 24
                    array_pop($keys);
130
                }
131
132 48
                foreach ($keys as $key) {
133 48
                    if (!isset($dictionary[$key])) {
134 48
                        $dictionary[$key] = new Collection();
135
                    }
136
137 48
                    if (!$dictionary[$key]->contains($result)) {
138 48
                        $dictionary[$key][] = $result;
139
                    }
140
                }
141
            }
142
143 48
            unset($result->{$this->pathListAlias});
144
        }
145
146 48
        foreach ($dictionary as $key => $results) {
147 48
            $dictionary[$key] = $results->all();
148
        }
149
150 48
        return $dictionary;
151
    }
152
153
    /**
154
     * Determine whether a path belongs to a result.
155
     *
156
     * @param \Illuminate\Database\Eloquent\Model $result
157
     * @param string $foreignKeyName
158
     * @param string $accessor
159
     * @param string $pathSeparator
160
     * @param string $path
161
     * @return bool
162
     */
163 48
    protected function pathMatches(Model $result, $foreignKeyName, $accessor, $pathSeparator, $path)
164
    {
165 48
        $foreignKey = (string) ($accessor ? $result->$accessor : $result)->$foreignKeyName;
166
167 48
        $isDescendant = Str::endsWith($path, $pathSeparator.$foreignKey);
168
169 48
        if ($this->andSelf) {
170 24
            if ($isDescendant || $path === $foreignKey) {
171 24
                return true;
172
            }
173 24
        } elseif ($isDescendant) {
174 24
            return true;
175
        }
176
177 48
        return false;
178
    }
179
180
    /**
181
     * Get the local key name for an eager load of the relation.
182
     *
183
     * @return string
184
     */
185
    abstract public function getEagerLoadingLocalKeyName();
186
187
    /**
188
     * Get the foreign key name for an eager load of the relation.
189
     *
190
     * @return string
191
     */
192
    abstract public function getEagerLoadingForeignKeyName();
193
194
    /**
195
     * Get the accessor for an eager load of the relation.
196
     *
197
     * @return string|null
198
     */
199 16
    public function getEagerLoadingAccessor()
200
    {
201 16
        return null;
202
    }
203
204
    /**
205
     * Add a recursive expression to the query.
206
     *
207
     * @param callable $constraint
208
     * @param \Illuminate\Database\Eloquent\Builder|null $query
209
     * @param string|null $alias
210
     * @param bool $selectPath
211
     * @return \Illuminate\Database\Eloquent\Builder
212
     */
213 220
    protected function addExpression(callable $constraint, Builder $query = null, $alias = null, $selectPath = false)
214
    {
215 220
        $name = $this->parent->getExpressionName();
216
217 220
        $query = $query ?: $this->query;
218
219 220
        $grammar = $this->getExpressionGrammar();
220
221 220
        $expression = $this->getInitialQuery($grammar, $constraint, $alias, $selectPath)
222 220
            ->unionAll(
223 220
                $this->getRecursiveQuery($grammar, $selectPath)
224
            );
225
226 220
        $query->withRecursiveExpression($name, $expression);
227
228 220
        $query->withGlobalScope(get_class(), function (Builder $query) use ($name) {
229 196
            $query->whereIn(
230 196
                $this->getExpressionForeignKeyName(),
231 196
                (new $this->parent())->setTable($name)
232 196
                    ->newQuery()
233 196
                    ->select($this->getExpressionLocalKeyName())
234 196
                    ->withGlobalScopes($this->intermediateScopes)
235 196
                    ->withoutGlobalScopes($this->removedIntermediateScopes)
236
            );
237 220
        });
238
239 220
        return $query;
240
    }
241
242
    /**
243
     * Get the initial query for a relationship expression.
244
     *
245
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
246
     * @param callable $constraint
247
     * @param string|null $alias
248
     * @param bool $selectPath
249
     * @return \Illuminate\Database\Eloquent\Builder $query
250
     */
251 220
    protected function getInitialQuery(ExpressionGrammar $grammar, callable $constraint, $alias, $selectPath)
252
    {
253 220
        $model = new $this->parent();
254 220
        $query = $model->newModelQuery();
255
256 220
        if ($alias) {
257 18
            $query->from($query->getQuery()->from, $alias);
258
        }
259
260 220
        $constraint($query);
261
262 220
        if ($selectPath) {
263 48
            $initialPath = $grammar->compileInitialPath(
264 48
                $this->getExpressionLocalKeyName(),
265 48
                $model->getPathName()
266
            );
267
268 48
            $query->select('*')->selectRaw($initialPath);
269
        }
270
271 220
        return $query;
272
    }
273
274
    /**
275
     * Get the recursive query for a relationship expression.
276
     *
277
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
278
     * @param bool $selectPath
279
     * @return \Illuminate\Database\Eloquent\Builder $query
280
     */
281 220
    protected function getRecursiveQuery(ExpressionGrammar $grammar, $selectPath)
282
    {
283 220
        $model = new $this->parent();
284 220
        $name = $model->getExpressionName();
285 220
        $query = $model->newModelQuery();
286
287 220
        $query->select($query->getQuery()->from.'.*')
288 220
            ->join(
289 220
                $name,
290 220
                $name.'.'.$model->getLocalKeyName(),
291 220
                '=',
292 220
                $query->qualifyColumn($model->getParentKeyName())
293
            );
294
295 220
        if ($selectPath) {
296 48
            $recursivePath = $grammar->compileRecursivePath(
297 48
                $model->qualifyColumn(
298 48
                    $this->getExpressionLocalKeyName()
299
                ),
300 48
                $model->getPathName()
301
            );
302
303 48
            $recursivePathBindings = $grammar->getRecursivePathBindings($model->getPathSeparator());
304
305 48
            $query->selectRaw($recursivePath, $recursivePathBindings);
306
        }
307
308 220
        return $query;
309
    }
310
311
    /**
312
     * Get the local key name for the recursion expression.
313
     *
314
     * @return string
315
     */
316
    abstract public function getExpressionLocalKeyName();
317
318
    /**
319
     * Get the foreign key name for the recursion expression.
320
     *
321
     * @return string
322
     */
323
    abstract public function getExpressionForeignKeyName();
324
325
    /**
326
     * Add the constraints for a relationship query.
327
     *
328
     * @param \Illuminate\Database\Eloquent\Builder $query
329
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
330
     * @param array|mixed $columns
331
     * @return \Illuminate\Database\Eloquent\Builder
332
     */
333 36
    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
334
    {
335 36
        $table = (new $this->parent())->getTable();
336
337 36
        if ($table === $parentQuery->getQuery()->from) {
338 18
            $table = $alias = $this->getRelationCountHash();
0 ignored issues
show
Bug introduced by
It seems like getRelationCountHash() 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

338
            $table = $alias = $this->/** @scrutinizer ignore-call */ getRelationCountHash();
Loading history...
339
        } else {
340 18
            $alias = null;
341
        }
342
343 36
        $constraint = function (Builder $query) use ($table) {
344 36
            $this->addExistenceExpressionWhereConstraints($query, $table);
345 36
        };
346
347 36
        return $this->addExpression($constraint, $query->select($columns), $alias);
0 ignored issues
show
Bug introduced by
It seems like $query->select($columns) can also be of type Illuminate\Database\Query\Builder; however, parameter $query of Staudenmeir\LaravelAdjac...lation::addExpression() does only seem to accept Illuminate\Database\Eloquent\Builder|null, 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

347
        return $this->addExpression($constraint, /** @scrutinizer ignore-type */ $query->select($columns), $alias);
Loading history...
348
    }
349
350
    /**
351
     * Set the where clause on the recursive expression query for an existence query.
352
     *
353
     * @param \Illuminate\Database\Eloquent\Builder $query
354
     * @param string $table
355
     * @return void
356
     */
357 36
    public function addExistenceExpressionWhereConstraints(Builder $query, $table)
358
    {
359 36
        $first = $this->andSelf
360 18
            ? $this->parent->getLocalKeyName()
361 18
            : $this->parent->getParentKeyName();
362
363 36
        $query->whereColumn(
364 36
            $table.'.'.$first,
365 36
            '=',
366 36
            $this->parent->getQualifiedLocalKeyName()
367
        );
368 36
    }
369
370
    /**
371
     * Get the expression grammar.
372
     *
373
     * @return \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar
374
     */
375 220
    protected function getExpressionGrammar()
376
    {
377 220
        return $this->parent->newQuery()->getExpressionGrammar();
378
    }
379
380
    /**
381
     * Get the path list separator.
382
     *
383
     * @return string
384
     */
385 48
    protected function getPathListSeparator()
386
    {
387 48
        return str_repeat(
388 48
            $this->parent->getPathSeparator(),
389 48
            2
390
        );
391
    }
392
}
393