Passed
Push — master ( 482c07...a1f161 )
by Jonas
05:55
created

HasManyOfDescendants::addConstraints()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 14
ccs 11
cts 11
cp 1
rs 9.9666
cc 3
nc 3
nop 0
crap 3
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\Database\Eloquent\Relations\HasMany;
9
use Illuminate\Database\Eloquent\SoftDeletingScope;
10
use Illuminate\Support\Str;
11
use Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar;
12
13
class HasManyOfDescendants extends HasMany
14
{
15
    use TracksIntermediateScopes;
16
17
    /**
18
     * Whether to include the parent model.
19
     *
20
     * @var bool
21
     */
22
    protected $andSelf;
23
24
    /**
25
     * The path list column alias.
26
     *
27
     * @var string
28
     */
29
    protected $pathListAlias = 'laravel_paths';
30
31
    /**
32
     * Create a new has many of descendants relationship instance.
33
     *
34
     * @param \Illuminate\Database\Eloquent\Builder $query
35
     * @param \Illuminate\Database\Eloquent\Model $parent
36
     * @param string $foreignKey
37
     * @param string $localKey
38
     * @param bool $andSelf
39
     * @return void
40
     */
41 76
    public function __construct(Builder $query, Model $parent, $foreignKey, $localKey, $andSelf)
42
    {
43 76
        $this->andSelf = $andSelf;
44
45 76
        parent::__construct($query, $parent, $foreignKey, $localKey);
46 76
    }
47
48
    /**
49
     * Set the base constraints on the relation query.
50
     *
51
     * @return void
52
     */
53 76
    public function addConstraints()
54
    {
55 76
        if (static::$constraints) {
56 48
            $column = $this->andSelf ? $this->parent->getLocalKeyName() : $this->parent->getParentKeyName();
57
58 48
            $constraint = function (Builder $query) use ($column) {
59 48
                $query->where(
60 48
                    $column,
61 48
                    '=',
62 48
                    $this->parent->{$this->parent->getLocalKeyName()}
63 48
                )->whereNotNull($column);
64 48
            };
65
66 48
            $this->addExpression($constraint);
67
        }
68 76
    }
69
70
    /**
71
     * Set the constraints for an eager load of the relation.
72
     *
73
     * @param array $models
74
     * @return void
75
     */
76 16
    public function addEagerConstraints(array $models)
77
    {
78 16
        $localKey = $this->parent->getLocalKeyName();
79
80 16
        $whereIn = $this->whereInMethod($this->parent, $localKey);
0 ignored issues
show
Bug introduced by
It seems like $localKey can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $key of Illuminate\Database\Eloq...lation::whereInMethod() 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

80
        $whereIn = $this->whereInMethod($this->parent, /** @scrutinizer ignore-type */ $localKey);
Loading history...
81
82 16
        $keys = $this->getKeys($models, $localKey);
0 ignored issues
show
Bug introduced by
It seems like $localKey can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $key of Illuminate\Database\Eloq...ons\Relation::getKeys() does only seem to accept null|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

82
        $keys = $this->getKeys($models, /** @scrutinizer ignore-type */ $localKey);
Loading history...
83
84 16
        $constraint = function (Builder $query) use ($keys, $localKey, $models, $whereIn) {
0 ignored issues
show
Unused Code introduced by
The import $models is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
85 16
            $query->$whereIn($localKey, $keys);
86 16
        };
87
88 16
        $grammar = $this->getExpressionGrammar();
89
90 16
        $pathSeparator = $this->parent->getPathSeparator();
91 16
        $listSeparator = $this->getPathListSeparator();
92
93 16
        $pathList = $grammar->selectPathList(
94 16
            $this->query->getQuery()->newQuery(),
95 16
            $this->parent->getExpressionName(),
96 16
            $this->parent->getPathName(),
97
            $pathSeparator,
98
            $listSeparator
99
        );
100
101 16
        $this->addExpression($constraint, null, null, true)
102 16
            ->select($this->query->getQuery()->from.'.*')
103 16
            ->selectSub($pathList, $this->pathListAlias);
104 16
    }
105
106
    /**
107
     * Build model dictionary.
108
     *
109
     * @param \Illuminate\Database\Eloquent\Collection $results
110
     * @return array
111
     */
112 16
    protected function buildDictionary(Collection $results)
113
    {
114 16
        $dictionary = [];
115
116 16
        $paths = explode(
117 16
            $this->getPathListSeparator(),
118 16
            $results[0]->{$this->pathListAlias}
119
        );
120
121 16
        $foreignKey = $this->getForeignKeyName();
122 16
        $pathSeparator = $this->parent->getPathSeparator();
123
124 16
        foreach ($results as $result) {
125 16
            foreach ($paths as $path) {
126 16
                if (!$this->pathMatches($result, $foreignKey, $pathSeparator, $path)) {
127 16
                    continue;
128
                }
129
130 16
                $keys = explode($pathSeparator, $path);
131
132 16
                if (!$this->andSelf) {
133 8
                    array_pop($keys);
134
                }
135
136 16
                foreach ($keys as $key) {
137 16
                    if (!isset($dictionary[$key])) {
138 16
                        $dictionary[$key] = new Collection();
139
                    }
140
141 16
                    if (!$dictionary[$key]->contains($result)) {
142 16
                        $dictionary[$key][] = $result;
143
                    }
144
                }
145
            }
146
147 16
            unset($result->{$this->pathListAlias});
148
        }
149
150 16
        foreach ($dictionary as $key => $results) {
151 16
            $dictionary[$key] = $results->all();
152
        }
153
154 16
        return $dictionary;
155
    }
156
157
    /**
158
     * Determine whether a path belongs to a result.
159
     *
160
     * @param \Illuminate\Database\Eloquent\Model $result
161
     * @param string $foreignKey
162
     * @param string $pathSeparator
163
     * @param string $path
164
     * @return bool
165
     */
166 16
    protected function pathMatches(Model $result, $foreignKey, $pathSeparator, $path)
167
    {
168 16
        $isDescendant = Str::endsWith($path, $pathSeparator.$result->$foreignKey);
169
170 16
        if ($this->andSelf) {
171 8
            if ($isDescendant || $path === (string) $result->$foreignKey) {
172 8
                return true;
173
            }
174 8
        } elseif ($isDescendant) {
175 8
            return true;
176
        }
177
178 16
        return false;
179
    }
180
181
    /**
182
     * Add the constraints for a relationship query.
183
     *
184
     * @param \Illuminate\Database\Eloquent\Builder $query
185
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
186
     * @param array|mixed $columns
187
     * @return \Illuminate\Database\Eloquent\Builder
188
     */
189 12
    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
190
    {
191 12
        $table = (new $this->parent())->getTable();
192
193 12
        if ($table === $parentQuery->getQuery()->from) {
194 6
            $table = $alias = $this->getRelationCountHash();
195
        } else {
196 6
            $alias = null;
197
        }
198
199 12
        $first = $this->andSelf
200 6
            ? $this->parent->getLocalKeyName()
201 6
            : $this->parent->getParentKeyName();
202
203 12
        $constraint = function (Builder $query) use ($first, $table) {
204 12
            $query->whereColumn(
205 12
                $table.'.'.$first,
0 ignored issues
show
Bug introduced by
Are you sure $first of type Illuminate\Database\Eloquent\Builder|mixed can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

205
                $table.'.'./** @scrutinizer ignore-type */ $first,
Loading history...
206 12
                '=',
207 12
                $this->parent->getQualifiedLocalKeyName()
208
            );
209 12
        };
210
211 12
        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...ndants::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

211
        return $this->addExpression($constraint, /** @scrutinizer ignore-type */ $query->select($columns), $alias);
Loading history...
212
    }
213
214
    /**
215
     * Add a recursive expression to the query.
216
     *
217
     * @param callable $constraint
218
     * @param \Illuminate\Database\Eloquent\Builder|null $query
219
     * @param string|null $alias
220
     * @param bool $selectPath
221
     * @return \Illuminate\Database\Eloquent\Builder
222
     */
223 76
    protected function addExpression(callable $constraint, Builder $query = null, $alias = null, $selectPath = false)
224
    {
225 76
        $name = $this->parent->getExpressionName();
226
227 76
        $query = $query ?: $this->query;
228
229 76
        $grammar = $this->getExpressionGrammar();
230
231 76
        $expression = $this->getInitialQuery($grammar, $constraint, $alias, $selectPath)
232 76
            ->unionAll(
233 76
                $this->getRecursiveQuery($grammar, $selectPath)
234
            );
235
236 76
        $query->withRecursiveExpression($name, $expression);
237
238 76
        $query->withGlobalScope('HasManyOfDescendants', function (Builder $query) use ($name) {
239 68
            $query->whereIn(
240 68
                $this->foreignKey,
241 68
                (new $this->parent())->setTable($name)
242 68
                    ->newQuery()
243 68
                    ->select($this->localKey)
244 68
                    ->withGlobalScopes($this->intermediateScopes)
245 68
                    ->withoutGlobalScopes($this->removedIntermediateScopes)
246
            );
247 76
        });
248
249 76
        return $query;
250
    }
251
252
    /**
253
     * Get the initial query for a relationship expression.
254
     *
255
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
256
     * @param callable $constraint
257
     * @param string|null $alias
258
     * @param bool $selectPath
259
     * @return \Illuminate\Database\Eloquent\Builder $query
260
     */
261 76
    protected function getInitialQuery(ExpressionGrammar $grammar, callable $constraint, $alias, $selectPath)
262
    {
263 76
        $model = new $this->parent();
264 76
        $query = $model->newModelQuery();
265
266 76
        if ($alias) {
267 6
            $query->from($query->getQuery()->from, $alias);
268
        }
269
270 76
        $constraint($query);
271
272 76
        if ($selectPath) {
273 16
            $initialPath = $grammar->compileInitialPath(
274 16
                $this->localKey,
275 16
                $model->getPathName()
276
            );
277
278 16
            $query->select('*')->selectRaw($initialPath);
279
        }
280
281 76
        return $query;
282
    }
283
284
    /**
285
     * Get the recursive query for a relationship expression.
286
     *
287
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
288
     * @param bool $selectPath
289
     * @return \Illuminate\Database\Eloquent\Builder $query
290
     */
291 76
    protected function getRecursiveQuery(ExpressionGrammar $grammar, $selectPath)
292
    {
293 76
        $model = new $this->parent();
294 76
        $name = $model->getExpressionName();
295 76
        $query = $model->newModelQuery();
296
297 76
        $query->select($query->getQuery()->from.'.*')
298 76
            ->join(
299 76
                $name,
300 76
                $name.'.'.$model->getLocalKeyName(),
301 76
                '=',
302 76
                $query->qualifyColumn($model->getParentKeyName())
303
            );
304
305 76
        if ($selectPath) {
306 16
            $recursivePath = $grammar->compileRecursivePath(
307 16
                $model->qualifyColumn($this->localKey),
308 16
                $model->getPathName()
309
            );
310
311 16
            $recursivePathBindings = $grammar->getRecursivePathBindings($model->getPathSeparator());
312
313 16
            $query->selectRaw($recursivePath, $recursivePathBindings);
314
        }
315
316 76
        return $query;
317
    }
318
319
    /**
320
     * Get the expression grammar
321
     *
322
     * @return \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar
323
     */
324 76
    protected function getExpressionGrammar()
325
    {
326 76
        return $this->parent->newQuery()->getExpressionGrammar();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parent->ne...>getExpressionGrammar() also could return the type Illuminate\Database\Eloquent\Builder which is incompatible with the documented return type Staudenmeir\LaravelAdjac...mmars\ExpressionGrammar.
Loading history...
327
    }
328
329
    /**
330
     * Get the path list separator.
331
     *
332
     * @return string
333
     */
334 16
    protected function getPathListSeparator()
335
    {
336 16
        return str_repeat(
337 16
            $this->parent->getPathSeparator(),
0 ignored issues
show
Bug introduced by
It seems like $this->parent->getPathSeparator() can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $string of str_repeat() 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

337
            /** @scrutinizer ignore-type */ $this->parent->getPathSeparator(),
Loading history...
338 16
            2
339
        );
340
    }
341
342
    /**
343
     * Include trashed descendants in the query.
344
     *
345
     * @return $this
346
     */
347 4
    public function withTrashedDescendants()
348
    {
349 4
        return $this->withoutIntermediateScope(SoftDeletingScope::class);
350
    }
351
}
352