Passed
Push — master ( a1f161...76823e )
by Jonas
06:13
created

HasManyOfDescendants::withTrashedDescendants()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

79
        $whereIn = $this->whereInMethod($this->parent, /** @scrutinizer ignore-type */ $localKey);
Loading history...
80
81 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

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

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

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

336
            /** @scrutinizer ignore-type */ $this->parent->getPathSeparator(),
Loading history...
337 16
            2
338
        );
339
    }
340
}
341