-
-
Notifications
You must be signed in to change notification settings - Fork 469
Expand file tree
/
Copy pathPaginatedModelsLoader.php
More file actions
212 lines (177 loc) · 8.13 KB
/
Copy pathPaginatedModelsLoader.php
File metadata and controls
212 lines (177 loc) · 8.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
<?php declare(strict_types=1);
namespace Nuwave\Lighthouse\Execution\ModelsLoader;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Pagination\LengthAwarePaginator;
use Nuwave\Lighthouse\Pagination\PaginationArgs;
use Nuwave\Lighthouse\Pagination\ZeroPerPageLengthAwarePaginator;
use Nuwave\Lighthouse\Support\Utils;
use function Safe\json_encode;
class PaginatedModelsLoader implements ModelsLoader
{
public function __construct(
protected string $relation,
protected \Closure $decorateBuilder,
protected PaginationArgs $paginationArgs,
) {}
public function load(EloquentCollection $parents): void
{
$parents->loadCount([$this->relation => $this->decorateBuilder]);
$relation = $this->relationInstance($parents);
$relatedModels = $this->loadRelatedModels($parents);
$this->hydratePivotRelation($relation, $relatedModels);
$this->loadDefaultWith($relatedModels);
$this->associateRelationModels($parents, $relatedModels);
$this->convertRelationToPaginator($parents);
}
public function extract(Model $model): mixed
{
return $model->getRelation($this->relation);
}
/**
* @param \Illuminate\Database\Eloquent\Collection<array-key, \Illuminate\Database\Eloquent\Model> $parents
*
* @return \Illuminate\Database\Eloquent\Collection<array-key, \Illuminate\Database\Eloquent\Model>
*/
protected function loadRelatedModels(EloquentCollection $parents): EloquentCollection
{
if ($this->paginationArgs->first === null) {
$relation = $this->relationInstance($parents);
$relation->addEagerConstraints($parents->all());
($this->decorateBuilder)($relation);
if ($relation instanceof BelongsToMany || $relation instanceof HasManyThrough) {
$select = Utils::callProtected($relation, 'shouldSelect', ['*']);
$relation->addSelect($select);
}
return $relation->get();
}
$relations = $parents->toBase()
->map(function (Model $model) use ($parents): Relation {
$relation = $this->relationInstance($parents);
$relation->addEagerConstraints([$model]);
($this->decorateBuilder)($relation, $model);
if ($relation instanceof BelongsToMany || $relation instanceof HasManyThrough) {
$select = Utils::callProtected($relation, 'shouldSelect', ['*']);
$relation->addSelect($select);
}
$relation->initRelation([$model], $this->relation);
// @phpstan-ignore-next-line Builder mixin is not understood
return $relation->forPage($this->paginationArgs->page, $this->paginationArgs->first);
});
// Merge all the relation queries into a single query with UNION ALL.
$firstRelation = $relations->shift();
// @phpstan-ignore function.impossibleType,instanceof.alwaysFalse
assert($firstRelation instanceof Relation, 'Non-null because only non-empty lists of parents are passed into this loader.');
// Use ->getQuery() to respect model scopes, such as soft deletes
$mergedRelationQuery = $relations->reduce(
static fn (EloquentBuilder $builder, Relation $relation): EloquentBuilder => $builder->unionAll(
$relation->getQuery(),
),
$firstRelation->getQuery(),
);
$relatedModels = $mergedRelationQuery->get();
return $relatedModels->unique(
// Compare all attributes because there might not be a unique primary key
// or there could be differing pivot attributes.
static fn (Model $relatedModel): string => json_encode($relatedModel->getRawOriginal()),
);
}
/**
* Use the underlying model to instantiate a relation by name.
*
* @param \Illuminate\Database\Eloquent\Collection<array-key, \Illuminate\Database\Eloquent\Model> $parents
*
* @return \Illuminate\Database\Eloquent\Relations\Relation<\Illuminate\Database\Eloquent\Model>
*/
protected function relationInstance(EloquentCollection $parents): Relation
{
return $this->newModelQuery($parents)
->getRelation($this->relation);
}
/**
* Return a fresh instance of a query builder for the underlying model.
*
* @param \Illuminate\Database\Eloquent\Collection<array-key, \Illuminate\Database\Eloquent\Model> $parents
*
* @return \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>
*/
protected function newModelQuery(EloquentCollection $parents): EloquentBuilder
{
$anyModelInstance = $parents->first();
assert($anyModelInstance instanceof Model);
// @phpstan-ignore-next-line Laravel 9 defines this as Builder|Model
return $anyModelInstance->newModelQuery();
}
/**
* Ensure the pivot relation is hydrated too, if it exists.
*
* @param \Illuminate\Database\Eloquent\Relations\Relation<\Illuminate\Database\Eloquent\Model> $relation
* @param \Illuminate\Database\Eloquent\Collection<array-key, \Illuminate\Database\Eloquent\Model> $relatedModels
*/
protected function hydratePivotRelation(Relation $relation, EloquentCollection $relatedModels): void
{
/**
* @see BelongsToMany::hydratePivotRelation()
*/
if ($relation instanceof BelongsToMany) {
Utils::callProtected($relation, 'hydratePivotRelation', $relatedModels->all());
}
}
/**
* Ensure the models default relations are loaded.
*
* This is necessary because we load models in a non-standard way in @see loadRelatedModels()
*
* @param \Illuminate\Database\Eloquent\Collection<array-key, \Illuminate\Database\Eloquent\Model> $models
*/
protected function loadDefaultWith(EloquentCollection $models): void
{
$model = $models->first();
if ($model === null) {
return;
}
$unloadedWiths = array_filter(
Utils::accessProtected($model, 'with'),
static fn (string $relation): bool => ! $model->relationLoaded($relation),
);
if ($unloadedWiths !== []) {
$models->load($unloadedWiths);
}
}
/**
* Associate the collection of all fetched relationModels back with their parents.
*
* @param \Illuminate\Database\Eloquent\Collection<array-key, \Illuminate\Database\Eloquent\Model> $parents
* @param \Illuminate\Database\Eloquent\Collection<array-key, \Illuminate\Database\Eloquent\Model> $relatedModels
*/
protected function associateRelationModels(EloquentCollection $parents, EloquentCollection $relatedModels): void
{
$this->relationInstance($parents)
->match(
$parents->all(),
$relatedModels,
$this->relation,
);
}
/** @param \Illuminate\Database\Eloquent\Collection<array-key, \Illuminate\Database\Eloquent\Model> $parents */
protected function convertRelationToPaginator(EloquentCollection $parents): void
{
$first = $this->paginationArgs->first;
$page = $this->paginationArgs->page;
foreach ($parents as $model) {
$total = CountModelsLoader::extractCount($model, $this->relation);
if ($first === null) {
$paginator = new LengthAwarePaginator($model->getRelation($this->relation), $total, max(1, $total), $page);
} elseif ($first === 0) {
$paginator = new ZeroPerPageLengthAwarePaginator($total, $page);
} else {
$paginator = new LengthAwarePaginator($model->getRelation($this->relation), $total, $first, $page);
}
$model->setRelation($this->relation, $paginator);
}
}
}