Passed
Push — master ( 81d2d7...2ca0db )
by Jonas
10:40
created

HasManyDeep::getFarParent()   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 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Staudenmeir\EloquentHasManyDeep;
4
5
use Exception;
6
use Illuminate\Contracts\Pagination\Paginator;
7
use Illuminate\Database\Eloquent\Builder;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
10
use Illuminate\Database\Eloquent\SoftDeletes;
11
use Illuminate\Pagination\CursorPaginator;
12
use Illuminate\Support\Collection;
13
use Staudenmeir\EloquentHasManyDeep\Traits\RetrievesIntermediateTables;
14
15
/**
16
 * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
17
 * @extends \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel>
18
 */
19
class HasManyDeep extends HasManyThrough
20
{
21
    use HasEagerLimit;
0 ignored issues
show
Bug introduced by
The trait Staudenmeir\EloquentHasManyDeep\HasEagerLimit requires the property $exists which is not provided by Staudenmeir\EloquentHasManyDeep\HasManyDeep.
Loading history...
22
    use RetrievesIntermediateTables;
23
24
    /**
25
     * The "through" parent model instances.
26
     *
27
     * @var \Illuminate\Database\Eloquent\Model[]
28
     */
29
    protected $throughParents;
30
31
    /**
32
     * The foreign keys on the relationship.
33
     *
34
     * @var array
35
     */
36
    protected $foreignKeys;
37
38
    /**
39
     * The local keys on the relationship.
40
     *
41
     * @var array
42
     */
43
    protected $localKeys;
44
45
    /**
46
     * Create a new has many deep relationship instance.
47
     *
48
     * @param \Illuminate\Database\Eloquent\Builder $query
49
     * @param \Illuminate\Database\Eloquent\Model $farParent
50
     * @param \Illuminate\Database\Eloquent\Model[] $throughParents
51
     * @param array $foreignKeys
52
     * @param array $localKeys
53
     * @return void
54
     */
55 54
    public function __construct(Builder $query, Model $farParent, array $throughParents, array $foreignKeys, array $localKeys)
56
    {
57 54
        $this->throughParents = $throughParents;
58 54
        $this->foreignKeys = $foreignKeys;
59 54
        $this->localKeys = $localKeys;
60
61 54
        $firstKey = is_array($foreignKeys[0]) ? $foreignKeys[0][1] : $foreignKeys[0];
62
63 54
        parent::__construct($query, $farParent, $throughParents[0], $firstKey, $foreignKeys[1], $localKeys[0], $localKeys[1]);
64
    }
65
66
    /**
67
     * Set the base constraints on the relation query.
68
     *
69
     * @return void
70
     */
71 54
    public function addConstraints()
72
    {
73 54
        parent::addConstraints();
74
75 54
        if (static::$constraints) {
76 36
            if (is_array($this->foreignKeys[0])) {
77 2
                $column = $this->throughParent->qualifyColumn($this->foreignKeys[0][0]);
78
79 2
                $this->query->where($column, '=', $this->farParent->getMorphClass());
80
            }
81
        }
82
    }
83
84
    /**
85
     * Set the join clauses on the query.
86
     *
87
     * @param \Illuminate\Database\Eloquent\Builder|null $query
88
     * @return void
89
     */
90 54
    protected function performJoin(Builder $query = null)
91
    {
92 54
        $query = $query ?: $this->query;
93
94 54
        $throughParents = array_reverse($this->throughParents);
95 54
        $foreignKeys = array_reverse($this->foreignKeys);
96 54
        $localKeys = array_reverse($this->localKeys);
97
98 54
        $segments = explode(' as ', $query->getQuery()->from);
99
100 54
        $alias = $segments[1] ?? null;
101
102 54
        foreach ($throughParents as $i => $throughParent) {
103 54
            $predecessor = $throughParents[$i - 1] ?? $this->related;
104
105 54
            $prefix = $i === 0 && $alias ? $alias.'.' : '';
106
107 54
            $this->joinThroughParent($query, $throughParent, $predecessor, $foreignKeys[$i], $localKeys[$i], $prefix);
108
        }
109
    }
110
111
    /**
112
     * Join a through parent table.
113
     *
114
     * @param \Illuminate\Database\Eloquent\Builder $query
115
     * @param \Illuminate\Database\Eloquent\Model $throughParent
116
     * @param \Illuminate\Database\Eloquent\Model $predecessor
117
     * @param array|string $foreignKey
118
     * @param array|string $localKey
119
     * @param string $prefix
120
     * @return void
121
     */
122 54
    protected function joinThroughParent(Builder $query, Model $throughParent, Model $predecessor, $foreignKey, $localKey, $prefix)
123
    {
124 54
        if (is_array($localKey)) {
125 5
            $query->where($throughParent->qualifyColumn($localKey[0]), '=', $predecessor->getMorphClass());
126
127 5
            $localKey = $localKey[1];
128
        }
129
130 54
        $first = $throughParent->qualifyColumn($localKey);
131
132 54
        if (is_array($foreignKey)) {
133 5
            $query->where($predecessor->qualifyColumn($foreignKey[0]), '=', $throughParent->getMorphClass());
134
135 5
            $foreignKey = $foreignKey[1];
136
        }
137
138 54
        $second = $predecessor->qualifyColumn($prefix.$foreignKey);
139
140 54
        $query->join($throughParent->getTable(), $first, '=', $second);
141
142 54
        if ($this->throughParentInstanceSoftDeletes($throughParent)) {
143 32
            $column= $throughParent->getQualifiedDeletedAtColumn();
144
145 32
            $query->withGlobalScope(__CLASS__ . ":$column", function (Builder $query) use ($column) {
146 29
                $query->whereNull($column);
147
            });
148
        }
149
    }
150
151
    /**
152
     * Determine whether a "through" parent instance of the relation uses SoftDeletes.
153
     *
154
     * @param \Illuminate\Database\Eloquent\Model $instance
155
     * @return bool
156
     */
157 54
    public function throughParentInstanceSoftDeletes(Model $instance)
158
    {
159 54
        return in_array(SoftDeletes::class, class_uses_recursive($instance));
160
    }
161
162
    /**
163
     * Set the constraints for an eager load of the relation.
164
     *
165
     * @param array $models
166
     * @return void
167
     */
168 9
    public function addEagerConstraints(array $models)
169
    {
170 9
        parent::addEagerConstraints($models);
171
172 9
        if (is_array($this->foreignKeys[0])) {
173 1
            $column = $this->throughParent->qualifyColumn($this->foreignKeys[0][0]);
174
175 1
            $this->query->where($column, '=', $this->farParent->getMorphClass());
176
        }
177
    }
178
179
    /**
180
     * Execute the query as a "select" statement.
181
     *
182
     * @param array $columns
183
     * @return \Illuminate\Database\Eloquent\Collection
184
     */
185 41
    public function get($columns = ['*'])
186
    {
187 41
        $models = parent::get($columns);
188
189 41
        $this->hydrateIntermediateRelations($models->all());
190
191 41
        return $models;
192
    }
193
194
    /**
195
     * Get a paginator for the "select" statement.
196
     *
197
     * @param int $perPage
198
     * @param array $columns
199
     * @param string $pageName
200
     * @param int $page
201
     * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
202
     */
203 1
    public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
204
    {
205 1
        $columns = $this->shouldSelect($columns);
206
207 1
        $columns = array_diff($columns, [$this->getQualifiedFirstKeyName().' as laravel_through_key']);
208
209 1
        $this->query->addSelect($columns);
210
211 1
        return tap($this->query->paginate($perPage, $columns, $pageName, $page), function (Paginator $paginator) {
212 1
            $this->hydrateIntermediateRelations($paginator->items());
213
        });
214
    }
215
216
    /**
217
     * Paginate the given query into a simple paginator.
218
     *
219
     * @param int $perPage
220
     * @param array $columns
221
     * @param string $pageName
222
     * @param int|null $page
223
     * @return \Illuminate\Contracts\Pagination\Paginator
224
     */
225 1
    public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
226
    {
227 1
        $columns = $this->shouldSelect($columns);
228
229 1
        $columns = array_diff($columns, [$this->getQualifiedFirstKeyName().' as laravel_through_key']);
230
231 1
        $this->query->addSelect($columns);
232
233 1
        return tap($this->query->simplePaginate($perPage, $columns, $pageName, $page), function (Paginator $paginator) {
234 1
            $this->hydrateIntermediateRelations($paginator->items());
235
        });
236
    }
237
238
    /**
239
     * Paginate the given query into a cursor paginator.
240
     *
241
     * @param  int|null  $perPage
242
     * @param  array  $columns
243
     * @param  string  $cursorName
244
     * @param  string|null  $cursor
245
     * @return \Illuminate\Contracts\Pagination\CursorPaginator
246
     */
247 1
    public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
248
    {
249 1
        $columns = $this->shouldSelect($columns);
250
251 1
        $columns = array_diff($columns, [$this->getQualifiedFirstKeyName().' as laravel_through_key']);
252
253 1
        $this->query->addSelect($columns);
254
255 1
        return tap($this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor), function (CursorPaginator $paginator) {
256 1
            $this->hydrateIntermediateRelations($paginator->items());
257
        });
258
    }
259
260
    /**
261
     * Set the select clause for the relation query.
262
     *
263
     * @param array $columns
264
     * @return array
265
     */
266 45
    protected function shouldSelect(array $columns = ['*'])
267
    {
268 45
        return array_merge(parent::shouldSelect($columns), $this->intermediateColumns());
269
    }
270
271
    /**
272
     * Chunk the results of the query.
273
     *
274
     * @param int $count
275
     * @param callable $callback
276
     * @return bool
277
     */
278 1
    public function chunk($count, callable $callback)
279
    {
280 1
        return $this->prepareQueryBuilder()->chunk($count, function (Collection $results) use ($callback) {
281 1
            $this->hydrateIntermediateRelations($results->all());
282
283 1
            return $callback($results);
284
        });
285
    }
286
287
    /**
288
     * Add the constraints for a relationship query.
289
     *
290
     * @param \Illuminate\Database\Eloquent\Builder $query
291
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
292
     * @param array|mixed $columns
293
     * @return \Illuminate\Database\Eloquent\Builder
294
     */
295 9
    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
296
    {
297 9
        foreach ($this->throughParents as $throughParent) {
298 9
            if ($throughParent->getTable() === $parentQuery->getQuery()->from) {
299 2
                if (!in_array(HasTableAlias::class, class_uses_recursive($throughParent))) {
300 1
                    $traitClass = HasTableAlias::class;
301 1
                    $parentClass = get_class($throughParent);
302
303 1
                    throw new Exception(
304
                        <<<EOT
305
This query requires an additional trait. Please add the $traitClass trait to $parentClass.
306
See https://github.com/staudenmeir/eloquent-has-many-deep/issues/137 for details.
307
EOT
308
                    );
309
                }
310
311 1
                $table = $throughParent->getTable() . ' as ' . $this->getRelationCountHash();
312
313 1
                $throughParent->setTable($table);
314
315 1
                break;
316
            }
317
        }
318
319 8
        $query = parent::getRelationExistenceQuery($query, $parentQuery, $columns);
320
321 8
        if (is_array($this->foreignKeys[0])) {
322 2
            $column = $this->throughParent->qualifyColumn($this->foreignKeys[0][0]);
323
324 2
            $query->where($column, '=', $this->farParent->getMorphClass());
325
        }
326
327 8
        return $query;
328
    }
329
330
    /**
331
     * Add the constraints for a relationship query on the same table.
332
     *
333
     * @param \Illuminate\Database\Eloquent\Builder $query
334
     * @param \Illuminate\Database\Eloquent\Builder $parentQuery
335
     * @param array|mixed $columns
336
     * @return \Illuminate\Database\Eloquent\Builder
337
     */
338 2
    public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
339
    {
340 2
        $hash = $this->getRelationCountHash();
341
342 2
        $query->from($query->getModel()->getTable().' as '.$hash);
343
344 2
        $this->performJoin($query);
345
346 2
        $query->getModel()->setTable($hash);
347
348 2
        return $query->select($columns)->whereColumn(
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->select($c...ualifiedFirstKeyName()) also could return the type Illuminate\Database\Query\Builder which is incompatible with the documented return type Illuminate\Database\Eloquent\Builder.
Loading history...
349 2
            $parentQuery->getQuery()->from.'.'.$this->localKey,
350
            '=',
351 2
            $this->getQualifiedFirstKeyName()
352
        );
353
    }
354
355
    /**
356
     * Restore soft-deleted models.
357
     *
358
     * @param array|string ...$columns
359
     * @return $this
360
     */
361 3
    public function withTrashed(...$columns)
362
    {
363 3
        if (empty($columns)) {
364 1
            $this->query->withTrashed();
365
366 1
            return $this;
367
        }
368
369 2
        if (is_array($columns[0])) {
370 1
            $columns = $columns[0];
371
        }
372
373 2
        foreach ($columns as $column) {
374 2
            $this->query->withoutGlobalScope(__CLASS__ . ":$column");
375
        }
376
377 2
        return $this;
378
    }
379
380
    /**
381
     * Get the far parent model instance.
382
     *
383
     * @return \Illuminate\Database\Eloquent\Model
384
     */
385 5
    public function getFarParent(): Model
386
    {
387 5
        return $this->farParent;
388
    }
389
390
    /**
391
     * Get the "through" parent model instances.
392
     *
393
     * @return \Illuminate\Database\Eloquent\Model[]
394
     */
395 7
    public function getThroughParents()
396
    {
397 7
        return $this->throughParents;
398
    }
399
400
    /**
401
     * Get the foreign keys on the relationship.
402
     *
403
     * @return array
404
     */
405 7
    public function getForeignKeys()
406
    {
407 7
        return $this->foreignKeys;
408
    }
409
410
    /**
411
     * Get the local keys on the relationship.
412
     *
413
     * @return array
414
     */
415 7
    public function getLocalKeys()
416
    {
417 7
        return $this->localKeys;
418
    }
419
}
420