Passed
Push — master ( 6ed66a...c3d33f )
by Jonas
06:02
created

HasManyOfDescendants::getRelationExistenceQuery()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

183
                $table.'.'./** @scrutinizer ignore-type */ $first,
Loading history...
184
                '=',
185
                $this->parent->getQualifiedLocalKeyName()
186
            );
187
        };
188
189
        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

189
        return $this->addExpression($constraint, /** @scrutinizer ignore-type */ $query->select($columns), $alias);
Loading history...
190
    }
191
192
    /**
193
     * Add a recursive expression to the query.
194
     *
195
     * @param callable $constraint
196
     * @param \Illuminate\Database\Eloquent\Builder|null $query
197
     * @param string|null $alias
198
     * @param bool $selectPath
199
     * @return \Illuminate\Database\Eloquent\Builder
200
     */
201
    protected function addExpression(callable $constraint, Builder $query = null, $alias = null, $selectPath = false)
202
    {
203
        $name = $this->parent->getExpressionName();
204
205
        $query = $query ?: $this->query;
206
207
        $grammar = $this->getExpressionGrammar();
208
209
        $expression = $this->getInitialQuery($grammar, $constraint, $alias, $selectPath)
210
            ->unionAll(
211
                $this->getRecursiveQuery($grammar, $selectPath)
212
            );
213
214
        $query->withRecursiveExpression($name, $expression);
215
216
        $query->withGlobalScope('HasManyOfDescendants', function (Builder $query) use ($name) {
217
            $query->whereIn(
218
                $this->foreignKey,
219
                (new $this->parent)->setTable($name)->newQuery()->select($this->localKey)
220
            );
221
        });
222
223
        return $query;
224
    }
225
226
    /**
227
     * Get the initial query for a relationship expression.
228
     *
229
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
230
     * @param callable $constraint
231
     * @param string|null $alias
232
     * @param bool $selectPath
233
     * @return \Illuminate\Database\Eloquent\Builder $query
234
     */
235
    protected function getInitialQuery(ExpressionGrammar $grammar, callable $constraint, $alias, $selectPath)
236
    {
237
        $model = new $this->parent;
238
        $query = $model->newModelQuery();
239
240
        if ($alias) {
241
            $query->from($query->getQuery()->from, $alias);
242
        }
243
244
        $constraint($query);
245
246
        if ($selectPath) {
247
            $initialPath = $grammar->compileInitialPath(
248
                $this->localKey,
249
                $model->getPathName()
250
            );
251
252
            $query->select('*')->selectRaw($initialPath);
253
        }
254
255
        return $query;
256
    }
257
258
    /**
259
     * Get the recursive query for a relationship expression.
260
     *
261
     * @param \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar $grammar
262
     * @param bool $selectPath
263
     * @return \Illuminate\Database\Eloquent\Builder $query
264
     */
265
    protected function getRecursiveQuery(ExpressionGrammar $grammar, $selectPath)
266
    {
267
        $model = new $this->parent;
268
        $name = $model->getExpressionName();
269
        $query = $model->newModelQuery();
270
271
        $query->select($query->getQuery()->from.'.*')
272
            ->join(
273
                $name,
274
                $name.'.'.$model->getLocalKeyName(),
275
                '=',
276
                $query->qualifyColumn($model->getParentKeyName())
277
            );
278
279
        if ($selectPath) {
280
            $recursivePath = $grammar->compileRecursivePath(
281
                $model->qualifyColumn($this->localKey),
282
                $model->getPathName()
283
            );
284
285
            $recursivePathBindings = $grammar->getRecursivePathBindings($model->getPathSeparator());
286
287
            $query->selectRaw($recursivePath, $recursivePathBindings);
288
        }
289
290
        return $query;
291
    }
292
293
    /**
294
     * Get the expression grammar
295
     *
296
     * @return \Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar
297
     */
298
    protected function getExpressionGrammar()
299
    {
300
        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...
301
    }
302
303
    /**
304
     * Get the path list separator.
305
     *
306
     * @return string
307
     */
308
    protected function getPathListSeparator()
309
    {
310
        return str_repeat(
311
            $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 $input 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

311
            /** @scrutinizer ignore-type */ $this->parent->getPathSeparator(),
Loading history...
312
            2
313
        );
314
    }
315
316
    /**
317
     * Include trashed descendants in the query.
318
     *
319
     * @return $this
320
     */
321
    public function withTrashedDescendants()
322
    {
323
        $table = $this->parent->getExpressionName();
324
325
        $this->query->withoutGlobalScope('HasManyOfDescendants')
326
            ->whereIn(
327
                $this->foreignKey,
328
                (new $this->parent)->setTable($table)->newModelQuery()->select($this->localKey)
329
            );
330
331
        return $this;
332
    }
333
}
334