1
|
|
|
<?php |
|
|
|
|
2
|
|
|
|
3
|
|
|
namespace Spatie\QueryBuilder; |
4
|
|
|
|
5
|
|
|
use Illuminate\Http\Request; |
6
|
|
|
use Illuminate\Support\Collection; |
7
|
|
|
use Illuminate\Database\Eloquent\Builder; |
8
|
|
|
use Spatie\QueryBuilder\Exceptions\InvalidSortQuery; |
9
|
|
|
use Spatie\QueryBuilder\Exceptions\InvalidFilterQuery; |
10
|
|
|
use Spatie\QueryBuilder\Exceptions\InvalidIncludeQuery; |
11
|
|
|
|
12
|
|
|
class QueryBuilder extends Builder |
|
|
|
|
13
|
|
|
{ |
14
|
|
|
/** @var \Illuminate\Support\Collection */ |
15
|
|
|
protected $allowedFilters; |
16
|
|
|
|
17
|
|
|
/** @var string|null */ |
18
|
|
|
protected $defaultSort; |
19
|
|
|
|
20
|
|
|
/** @var \Illuminate\Support\Collection */ |
21
|
|
|
protected $allowedSorts; |
22
|
|
|
|
23
|
|
|
/** @var \Illuminate\Support\Collection */ |
24
|
|
|
protected $allowedIncludes; |
25
|
|
|
|
26
|
|
|
/** @var \Illuminate\Http\Request */ |
27
|
|
|
protected $request; |
28
|
|
|
|
29
|
|
|
/** @var \Illuminate\Support\Collection */ |
30
|
|
|
protected $columns; |
31
|
|
|
|
32
|
|
|
public function __construct(Builder $builder, ?Request $request = null) |
33
|
|
|
{ |
34
|
|
|
parent::__construct(clone $builder->getQuery()); |
35
|
|
|
|
36
|
|
|
$this->initializeFromBuilder($builder); |
37
|
|
|
|
38
|
|
|
$this->request = $request ?? request(); |
39
|
|
|
|
40
|
|
|
if ($this->columns = $this->request->column()) { |
41
|
|
|
$this->addSelectedColumns($this->columns); |
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
if ($this->request->sorts()) { |
45
|
|
|
$this->allowedSorts('*'); |
46
|
|
|
} |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Saves selects as a collection |
51
|
|
|
* |
52
|
|
|
* @param array|string $select |
53
|
|
|
* @return void |
54
|
|
|
*/ |
55
|
|
|
protected function addSelectedColumns($columns) |
56
|
|
|
{ |
57
|
|
|
$columns = $columns->filter(function ($item) { |
58
|
|
|
return strpos($item, '.') === false; |
59
|
|
|
}); |
60
|
|
|
|
61
|
|
|
if ($columns->count() > 0) { |
62
|
|
|
$this->select($columns->all()); |
63
|
|
|
} |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
protected function getSelectForRelation($relation) |
67
|
|
|
{ |
68
|
|
|
return $this->columns |
69
|
|
|
->filter(function ($column) use ($relation) { |
70
|
|
|
return strpos($column, "{$relation}.") !== false; |
71
|
|
|
}) |
72
|
|
|
->map(function ($column) use ($relation) { |
73
|
|
|
return str_replace("{$relation}.", '', $column); |
74
|
|
|
}); |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Add the model, scopes, eager loaded relationships, local macro's and onDelete callback |
78
|
|
|
* from the $builder to this query builder. |
79
|
|
|
* |
80
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $builder |
81
|
|
|
*/ |
82
|
|
|
protected function initializeFromBuilder(Builder $builder) |
|
|
|
|
83
|
|
|
{ |
84
|
|
|
$this->setModel($builder->getModel()) |
85
|
|
|
->setEagerLoads($builder->getEagerLoads()); |
86
|
|
|
|
87
|
|
|
$builder->macro('getProtected', function (Builder $builder, string $property) { |
88
|
|
|
return $builder->{$property}; |
89
|
|
|
}); |
90
|
|
|
|
91
|
|
|
$this->scopes = $builder->getProtected('scopes'); |
92
|
|
|
|
93
|
|
|
$this->localMacros = $builder->getProtected('localMacros'); |
94
|
|
|
|
95
|
|
|
$this->onDelete = $builder->getProtected('onDelete'); |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Create a new QueryBuilder for a request and model. |
100
|
|
|
* |
101
|
|
|
* @param string|\Illuminate\Database\Query\Builder $baseQuery Model class or base query builder |
102
|
|
|
* @param Request $request |
103
|
|
|
* |
104
|
|
|
* @return \Spatie\QueryBuilder\QueryBuilder |
105
|
|
|
*/ |
106
|
|
|
public static function for($baseQuery, ?Request $request = null): self |
107
|
|
|
{ |
108
|
|
|
if (is_string($baseQuery)) { |
109
|
|
|
$baseQuery = ($baseQuery)::query(); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
return new static($baseQuery, $request ?? request()); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
public function allowedFilters($filters): self |
116
|
|
|
{ |
117
|
|
|
$filters = is_array($filters) ? $filters : func_get_args(); |
118
|
|
|
$this->allowedFilters = collect($filters)->map(function ($filter) { |
119
|
|
|
if ($filter instanceof Filter) { |
120
|
|
|
return $filter; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
return Filter::partial($filter); |
124
|
|
|
}); |
125
|
|
|
|
126
|
|
|
$this->guardAgainstUnknownFilters(); |
127
|
|
|
|
128
|
|
|
$this->addFiltersToQuery($this->request->filters()); |
129
|
|
|
|
130
|
|
|
return $this; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
public function defaultSort($sort): self |
134
|
|
|
{ |
135
|
|
|
$this->defaultSort = $sort; |
136
|
|
|
|
137
|
|
|
$this->addSortsToQuery($this->request->sorts($this->defaultSort)); |
138
|
|
|
|
139
|
|
|
return $this; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
public function allowedSorts($sorts): self |
143
|
|
|
{ |
144
|
|
|
$sorts = is_array($sorts) ? $sorts : func_get_args(); |
145
|
|
|
if (! $this->request->sorts()) { |
146
|
|
|
return $this; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
$this->allowedSorts = collect($sorts); |
150
|
|
|
|
151
|
|
|
if (! $this->allowedSorts->contains('*')) { |
152
|
|
|
$this->guardAgainstUnknownSorts(); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
$this->addSortsToQuery($this->request->sorts($this->defaultSort)); |
156
|
|
|
|
157
|
|
|
return $this; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
public function allowedIncludes($includes): self |
161
|
|
|
{ |
162
|
|
|
$includes = is_array($includes) ? $includes : func_get_args(); |
163
|
|
|
$this->allowedIncludes = collect($includes); |
164
|
|
|
|
165
|
|
|
$this->guardAgainstUnknownIncludes(); |
166
|
|
|
|
167
|
|
|
$this->addIncludesToQuery($this->request->includes()); |
168
|
|
|
|
169
|
|
|
return $this; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
protected function addFiltersToQuery(Collection $filters) |
173
|
|
|
{ |
174
|
|
|
$filters->each(function ($value, $property) { |
175
|
|
|
$filter = $this->findFilter($property); |
176
|
|
|
|
177
|
|
|
$filter->filter($this, $value); |
178
|
|
|
}); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
protected function findFilter(string $property) : ?Filter |
182
|
|
|
{ |
183
|
|
|
return $this->allowedFilters |
184
|
|
|
->first(function (Filter $filter) use ($property) { |
185
|
|
|
return $filter->isForProperty($property); |
186
|
|
|
}); |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
protected function addSortsToQuery(Collection $sorts) |
190
|
|
|
{ |
191
|
|
|
$sorts |
192
|
|
|
->each(function (string $sort) { |
193
|
|
|
$descending = $sort[0] === '-'; |
194
|
|
|
|
195
|
|
|
$key = ltrim($sort, '-'); |
196
|
|
|
|
197
|
|
|
$this->orderBy($key, $descending ? 'desc' : 'asc'); |
198
|
|
|
}); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
protected function addIncludesToQuery(Collection $includes) |
202
|
|
|
{ |
203
|
|
|
$includes |
204
|
|
|
->map(function (string $include) { |
205
|
|
|
return camel_case($include); |
206
|
|
|
}) |
207
|
|
|
->each(function (string $include) { |
208
|
|
|
$selects = $this->getSelectForRelation(kebab_case($include)); |
209
|
|
|
|
210
|
|
|
if (! $selects->count()) { |
211
|
|
|
return $this->with($include); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
$this->with([$include => function ($query) use ($selects) { |
215
|
|
|
$query->select($selects->all()); |
216
|
|
|
}]); |
217
|
|
|
}); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
protected function guardAgainstUnknownFilters() |
221
|
|
|
{ |
222
|
|
|
$filterNames = $this->request->filters()->keys(); |
223
|
|
|
|
224
|
|
|
$allowedFilterNames = $this->allowedFilters->map->getProperty(); |
225
|
|
|
|
226
|
|
|
$diff = $filterNames->diff($allowedFilterNames); |
227
|
|
|
|
228
|
|
|
if ($diff->count()) { |
229
|
|
|
throw InvalidFilterQuery::filtersNotAllowed($diff, $allowedFilterNames); |
230
|
|
|
} |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
protected function guardAgainstUnknownSorts() |
234
|
|
|
{ |
235
|
|
|
$sorts = $this->request->sorts()->map(function ($sort) { |
236
|
|
|
return ltrim($sort, '-'); |
237
|
|
|
}); |
238
|
|
|
|
239
|
|
|
$diff = $sorts->diff($this->allowedSorts); |
240
|
|
|
|
241
|
|
|
if ($diff->count()) { |
242
|
|
|
throw InvalidSortQuery::sortsNotAllowed($diff, $this->allowedSorts); |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
protected function guardAgainstUnknownIncludes() |
247
|
|
|
{ |
248
|
|
|
$includes = $this->request->includes(); |
249
|
|
|
|
250
|
|
|
$diff = $includes->diff($this->allowedIncludes); |
251
|
|
|
|
252
|
|
|
if ($diff->count()) { |
253
|
|
|
throw InvalidIncludeQuery::includesNotAllowed($diff, $this->allowedIncludes); |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
} |
257
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.