Completed
Pull Request — master (#91)
by
unknown
05:30
created

QueryBuilder::getQuery()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 3
nc 2
nop 0
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\InvalidAppendQuery;
10
use Spatie\QueryBuilder\Exceptions\InvalidFilterQuery;
11
use Spatie\QueryBuilder\Exceptions\InvalidIncludeQuery;
12
13
class QueryBuilder extends Builder
14
{
15
    /** @var \Illuminate\Support\Collection */
16
    protected $allowedFilters;
17
18
    /** @var string|null */
19
    protected $defaultSort;
20
21
    /** @var \Illuminate\Support\Collection */
22
    protected $allowedSorts;
23
24
    /** @var \Illuminate\Support\Collection */
25
    protected $allowedIncludes;
26
27
    /** @var \Illuminate\Support\Collection */
28
    protected $allowedAppends;
29
30
    /** @var \Illuminate\Support\Collection */
31
    protected $fields;
32
33
    /** @var array */
34
    protected $appends = [];
35
36
    /** @var \Illuminate\Http\Request */
37
    protected $request;
38
39
    public function __construct(Builder $builder, ? Request $request = null)
40
    {
41
        parent::__construct(clone $builder->getQuery());
42
43
        $this->initializeFromBuilder($builder);
44
45
        $this->request = $request ?? request();
46
47
        $this->parseSelectedFields();
48
    }
49
50
    /**
51
     * Add the model, scopes, eager loaded relationships, local macro's and onDelete callback
52
     * from the $builder to this query builder.
53
     *
54
     * @param \Illuminate\Database\Eloquent\Builder $builder
55
     */
56
    protected function initializeFromBuilder(Builder $builder)
57
    {
58
        $this->setModel($builder->getModel())
59
            ->setEagerLoads($builder->getEagerLoads());
60
61
        $builder->macro('getProtected', function (Builder $builder, string $property) {
62
            return $builder->{$property};
63
        });
64
65
        $this->scopes = $builder->getProtected('scopes');
66
67
        $this->localMacros = $builder->getProtected('localMacros');
68
69
        $this->onDelete = $builder->getProtected('onDelete');
70
    }
71
72
    /**
73
     * Create a new QueryBuilder for a request and model.
74
     *
75
     * @param string|\Illuminate\Database\Query\Builder $baseQuery Model class or base query builder
76
     * @param Request $request
77
     *
78
     * @return \Spatie\QueryBuilder\QueryBuilder
79
     */
80
    public static function for($baseQuery, ? Request $request = null) : self
0 ignored issues
show
Coding Style introduced by
Possible parse error: non-abstract method defined as abstract
Loading history...
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
81
    {
82
        if (is_string($baseQuery)) {
83
            $baseQuery = ($baseQuery)::query();
84
        }
85
86
        return new static($baseQuery, $request ?? request());
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
Coding Style introduced by
The visibility should be declared for property $baseQuery.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
87
    }
88
89
    public function allowedFilters($filters) : self
90
    {
91
        $filters = is_array($filters) ? $filters : func_get_args();
92
        $this->allowedFilters = collect($filters)->map(function ($filter) {
93
            if ($filter instanceof Filter) {
94
                return $filter;
95
            }
96
97
            return Filter::partial($filter);
98
        });
99
100
        $this->guardAgainstUnknownFilters();
101
102
        $this->addFiltersToQuery($this->request->filters());
103
104
        return $this;
105
    }
106
107
    public function defaultSort($sort) : self
108
    {
109
        $this->defaultSort = $sort;
110
111
        $this->addSortsToQuery($this->request->sorts($this->defaultSort));
112
113
        return $this;
114
    }
115
116
    public function allowedSorts($sorts) : self
117
    {
118
        $sorts = is_array($sorts) ? $sorts : func_get_args();
119
        if (! $this->request->sorts()) {
120
            return $this;
121
        }
122
123
        $this->allowedSorts = collect($sorts)->map(function ($sort) {
124
            if ($sort instanceof Sort) {
125
                return $sort;
126
            }
127
128
            return Sort::field(ltrim($sort, '-'));
129
        });
130
131
        $this->guardAgainstUnknownSorts();
132
133
        $this->addSortsToQuery($this->request->sorts($this->defaultSort));
134
135
        return $this;
136
    }
137
138
    public function allowedIncludes($includes) : self
139
    {
140
        $includes = is_array($includes) ? $includes : func_get_args();
141
142
        $this->allowedIncludes = collect($includes)
143
            ->flatMap(function ($include) {
144
                return collect(explode('.', $include))
145
                    ->reduce(function ($collection, $include) {
146
                        if ($collection->isEmpty()) {
147
                            return $collection->push($include);
148
                        }
149
150
                        return $collection->push("{$collection->last()}.{$include}");
151
                    }, collect());
152
            });
153
154
        $this->guardAgainstUnknownIncludes();
155
156
        $this->addIncludesToQuery($this->request->includes());
157
158
        return $this;
159
    }
160
161
    public function allowedAppends($appends) : self
162
    {
163
        $appends = is_array($appends) ? $appends : func_get_args();
164
165
        $this->allowedAppends = collect($appends);
166
167
        $this->guardAgainstUnknownAppends();
168
169
        $this->appends = $this->request->appends();
170
171
        return $this;
172
    }
173
174
    protected function parseSelectedFields()
175
    {
176
        $this->fields = $this->request->fields();
177
178
        $modelTableName = $this->getModel()->getTable();
179
        $modelFields = $this->fields->get($modelTableName);
180
181
        if (! $modelFields) {
182
            $modelFields = '*';
183
        }
184
185
        $this->select($this->prependFieldsWithTableName(explode(',', $modelFields), $modelTableName));
0 ignored issues
show
Bug introduced by
The method select() does not exist on Spatie\QueryBuilder\QueryBuilder. Did you maybe mean parseSelectedFields()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
186
    }
187
188
    protected function prependFieldsWithTableName(array $fields, string $tableName): array
189
    {
190
        return array_map(function ($field) use ($tableName) {
191
            return "{$tableName}.{$field}";
192
        }, $fields);
193
    }
194
195
    protected function getFieldsForRelatedTable(string $relation): array
196
    {
197
        $fields = $this->fields->get($relation);
198
199
        if (! $fields) {
200
            return [];
201
        }
202
203
        return explode(',', $fields);
204
    }
205
206
    protected function addFiltersToQuery(Collection $filters)
207
    {
208
        $filters->each(function ($value, $property) {
209
            $filter = $this->findFilter($property);
210
211
            $filter->filter($this, $value);
212
        });
213
    }
214
215
    protected function findFilter(string $property) : ? Filter
216
    {
217
        return $this->allowedFilters
218
            ->first(function (Filter $filter) use ($property) {
219
                return $filter->isForProperty($property);
220
            });
221
    }
222
223
    protected function addSortsToQuery(Collection $sorts)
224
    {
225
        $this->filterDuplicates($sorts)
226
            ->each(function (string $property) {
227
                $descending = $property[0] === '-';
228
229
                $key = ltrim($property, '-');
230
231
                $sort = $this->findSort($key);
232
233
                $sort->sort($this, $descending);
234
            });
235
    }
236
237
    protected function filterDuplicates(Collection $sorts): Collection
238
    {
239
        if (! is_array($orders = $this->getQuery()->orders)) {
240
            return $sorts;
241
        }
242
243
        return $sorts->reject(function (string $sort) use ($orders) {
244
            $toSort = [
245
                'column' => ltrim($sort, '-'),
246
                'direction' => ($sort[0] === '-') ? 'desc' : 'asc',
247
            ];
248
            foreach ($orders as $order) {
249
                if ($order === $toSort) {
250
                    return true;
251
                }
252
            }
253
        });
254
    }
255
256
    protected function findSort(string $property) : ? Sort
257
    {
258
        return $this->allowedSorts
259
            ->first(function (Sort $sort) use ($property) {
260
                return $sort->isForProperty($property);
261
            });
262
    }
263
264
    protected function addDefaultSorts()
265
    {
266
        $this->allowedSorts = collect($this->request->sorts($this->defaultSort))->map(function ($sort) {
267
            if ($sort instanceof Sort) {
268
                return $sort;
269
            }
270
271
            return Sort::field(ltrim($sort, '-'));
272
        });
273
274
        $this->addSortsToQuery($this->request->sorts($this->defaultSort));
275
    }
276
277
    protected function addIncludesToQuery(Collection $includes)
278
    {
279
        $includes
280
            ->map('camel_case')
281
            ->map(function (string $include) {
282
                return collect(explode('.', $include));
283
            })
284
            ->flatMap(function (Collection $relatedTables) {
285
                return $relatedTables
286
                    ->mapWithKeys(function ($table, $key) use ($relatedTables) {
287
                        $fields = $this->getFieldsForRelatedTable(snake_case($table));
288
289
                        $fullRelationName = $relatedTables->slice(0, $key + 1)->implode('.');
290
291
                        if (empty($fields)) {
292
                            return [$fullRelationName];
293
                        }
294
295
                        return [$fullRelationName => function ($query) use ($fields) {
296
                            $query->select($fields);
297
                        }];
298
                    });
299
            })
300
            ->pipe(function (Collection $withs) {
301
                $this->with($withs->all());
302
            });
303
    }
304
305
    public function setAppendsToResult($result)
306
    {
307
        $result->map(function ($item) {
308
            $item->append($this->appends->toArray());
0 ignored issues
show
Bug introduced by
The method toArray cannot be called on $this->appends (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
309
310
            return $item;
311
        });
312
313
        return $result;
314
    }
315
316
    protected function guardAgainstUnknownFilters()
317
    {
318
        $filterNames = $this->request->filters()->keys();
319
320
        $allowedFilterNames = $this->allowedFilters->map->getProperty();
321
322
        $diff = $filterNames->diff($allowedFilterNames);
323
324
        if ($diff->count()) {
325
            throw InvalidFilterQuery::filtersNotAllowed($diff, $allowedFilterNames);
326
        }
327
    }
328
329
    protected function guardAgainstUnknownSorts()
330
    {
331
        $sortNames = $this->request->sorts()->map(function ($sort) {
332
            return ltrim($sort, '-');
333
        });
334
335
        $allowedSortNames = $this->allowedSorts->map->getProperty();
336
337
        $diff = $sortNames->diff($allowedSortNames);
338
339
        if ($diff->count()) {
340
            throw InvalidSortQuery::sortsNotAllowed($diff, $allowedSortNames);
341
        }
342
    }
343
344
    protected function guardAgainstUnknownIncludes()
345
    {
346
        $includes = $this->request->includes();
347
348
        $diff = $includes->diff($this->allowedIncludes);
349
350
        if ($diff->count()) {
351
            throw InvalidIncludeQuery::includesNotAllowed($diff, $this->allowedIncludes);
352
        }
353
    }
354
355
    protected function guardAgainstUnknownAppends()
356
    {
357
        $appends = $this->request->appends();
358
359
        $diff = $appends->diff($this->allowedAppends);
360
361
        if ($diff->count()) {
362
            throw InvalidAppendQuery::appendsNotAllowed($diff, $this->allowedAppends);
363
        }
364
    }
365
366
    public function getQuery()
367
    {
368
        if ($this->request->sorts() && ! $this->allowedSorts instanceof Collection) {
369
            $this->addDefaultSorts();
370
        }
371
372
        return parent::getQuery();
373
    }
374
375
    public function get($columns = ['*'])
376
    {
377
        if ($this->request->sorts() && ! $this->allowedSorts instanceof Collection) {
378
            $this->addDefaultSorts();
379
        }
380
381
        $result = parent::get($columns);
382
383
        if (count($this->appends) > 0) {
384
            $result = $this->setAppendsToResult($result);
385
        }
386
387
        return $result;
388
    }
389
390
    public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
391
    {
392
        if ($this->request->sorts() && ! $this->allowedSorts instanceof Collection) {
393
            $this->addDefaultSorts();
394
        }
395
396
        return parent::paginate($perPage, $columns, $pageName, $page);
397
    }
398
399
    public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
400
    {
401
        if ($this->request->sorts() && ! $this->allowedSorts instanceof Collection) {
402
            $this->addDefaultSorts();
403
        }
404
405
        return parent::simplePaginate($perPage, $columns, $pageName, $page);
406
    }
407
}
408