Completed
Push — master ( f480ca...1da237 )
by
unknown
01:23
created

QueryBuilder::defaultSorts()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
cc 3
nc 2
nop 1
1
<?php
2
3
namespace Spatie\QueryBuilder;
4
5
use Illuminate\Support\Str;
6
use Illuminate\Http\Request;
7
use Illuminate\Support\Collection;
8
use Illuminate\Database\Eloquent\Builder;
9
use Spatie\QueryBuilder\Exceptions\InvalidSortQuery;
10
use Spatie\QueryBuilder\Exceptions\InvalidFieldQuery;
11
use Spatie\QueryBuilder\Exceptions\InvalidAppendQuery;
12
use Spatie\QueryBuilder\Exceptions\InvalidFilterQuery;
13
use Spatie\QueryBuilder\Exceptions\InvalidIncludeQuery;
14
15
class QueryBuilder extends Builder
16
{
17
    /** @var \Illuminate\Support\Collection */
18
    protected $allowedFilters;
19
20
    /** @var \Illuminate\Support\Collection */
21
    protected $allowedFields;
22
23
    /** @var \Illuminate\Support\Collection */
24
    protected $defaultSorts;
25
26
    /** @var \Illuminate\Support\Collection */
27
    protected $allowedSorts;
28
29
    /** @var \Illuminate\Support\Collection */
30
    protected $allowedIncludes;
31
32
    /** @var \Illuminate\Support\Collection */
33
    protected $allowedAppends;
34
35
    /** @var \Illuminate\Http\Request */
36
    protected $request;
37
38
    public function __construct(Builder $builder, ? Request $request = null)
39
    {
40
        parent::__construct(clone $builder->getQuery());
41
42
        $this->initializeFromBuilder($builder);
43
44
        $this->request = $request ?? request();
45
46
        $this->parseFields();
47
    }
48
49
    /**
50
     * Create a new QueryBuilder for a request and model.
51
     *
52
     * @param string|\Illuminate\Database\Query\Builder $baseQuery Model class or base query builder
53
     * @param \Illuminate\Http\Request                  $request
54
     *
55
     * @return \Spatie\QueryBuilder\QueryBuilder
56
     */
57
    public static function for($baseQuery, ? Request $request = null): self
58
    {
59
        if (is_string($baseQuery)) {
60
            $baseQuery = ($baseQuery)::query();
61
        }
62
63
        return new static($baseQuery, $request ?? request());
64
    }
65
66
    public function allowedFilters($filters): self
67
    {
68
        $filters = is_array($filters) ? $filters : func_get_args();
69
        $this->allowedFilters = collect($filters)->map(function ($filter) {
70
            if ($filter instanceof Filter) {
71
                return $filter;
72
            }
73
74
            return Filter::partial($filter);
75
        });
76
77
        $this->guardAgainstUnknownFilters();
78
79
        $this->addFiltersToQuery($this->request->filters());
80
81
        return $this;
82
    }
83
84
    public function allowedFields($fields): self
85
    {
86
        $fields = is_array($fields) ? $fields : func_get_args();
87
88
        $this->allowedFields = collect($fields)
89
            ->map(function (string $fieldName) {
90
                if (! Str::contains($fieldName, '.')) {
91
                    $modelTableName = $this->getModel()->getTable();
0 ignored issues
show
Bug introduced by
The method getTable does only exist in Illuminate\Database\Eloquent\Model, but not in Illuminate\Database\Eloquent\Builder.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
92
93
                    return "{$modelTableName}.{$fieldName}";
94
                }
95
96
                return $fieldName;
97
            });
98
99
        if (! $this->allowedFields->contains('*')) {
100
            $this->guardAgainstUnknownFields();
101
        }
102
103
        return $this;
104
    }
105
106
    public function parseFields()
107
    {
108
        $this->addFieldsToQuery($this->request->fields());
109
    }
110
111
    public function allowedIncludes($includes): self
112
    {
113
        $includes = is_array($includes) ? $includes : func_get_args();
114
115
        $this->allowedIncludes = collect($includes)
116
            ->flatMap(function ($include) {
117
                return collect(explode('.', $include))
118
                    ->reduce(function ($collection, $include) {
119
                        if ($collection->isEmpty()) {
120
                            return $collection->push($include);
121
                        }
122
123
                        return $collection->push("{$collection->last()}.{$include}");
124
                    }, collect());
125
            });
126
127
        $this->guardAgainstUnknownIncludes();
128
129
        $this->addIncludesToQuery($this->request->includes());
130
131
        return $this;
132
    }
133
134
    public function allowedAppends($appends): self
135
    {
136
        $appends = is_array($appends) ? $appends : func_get_args();
137
138
        $this->allowedAppends = collect($appends);
139
140
        $this->guardAgainstUnknownAppends();
141
142
        return $this;
143
    }
144
145
    public function allowedSorts($sorts): self
146
    {
147
        $sorts = is_array($sorts) ? $sorts : func_get_args();
148
149
        if (! $this->request->sorts()) {
150
            return $this;
151
        }
152
153
        $this->allowedSorts = collect($sorts)->map(function ($sort) {
154
            if ($sort instanceof Sort) {
155
                return $sort;
156
            }
157
158
            return Sort::field(ltrim($sort, '-'));
159
        });
160
161
        $this->guardAgainstUnknownSorts();
162
163
        $this->parseSorts();
164
165
        return $this;
166
    }
167
168
    /**
169
     * @param array|string|\Spatie\QueryBuilder\Sort $sorts
170
     *
171
     * @return \Spatie\QueryBuilder\QueryBuilder
172
     */
173
    public function defaultSort($sorts): self
0 ignored issues
show
Unused Code introduced by
The parameter $sorts is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
174
    {
175
        return $this->defaultSorts(func_get_args());
176
    }
177
178
    /**
179
     * @param array|string|\Spatie\QueryBuilder\Sort $sorts
180
     *
181
     * @return \Spatie\QueryBuilder\QueryBuilder
182
     */
183
    public function defaultSorts($sorts): self
184
    {
185
        $sorts = is_array($sorts) ? $sorts : func_get_args();
186
187
        $this->defaultSorts = collect($sorts)->map(function ($sort) {
188
            if (is_string($sort)) {
189
                return Sort::field($sort);
190
            }
191
192
            return $sort;
193
        });
194
195
        $this->parseSorts();
196
197
        return $this;
198
    }
199
200
    public function getQuery()
201
    {
202
        if ($this->request->sorts() && ! $this->allowedSorts instanceof Collection) {
203
            $this->addDefaultSorts();
204
        }
205
206
        return parent::getQuery();
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212
    public function get($columns = ['*'])
213
    {
214
        if ($this->request->sorts() && ! $this->allowedSorts instanceof Collection) {
215
            $this->addDefaultSorts();
216
        }
217
218
        $results = parent::get($columns);
219
220
        if ($this->request->appends()->isNotEmpty()) {
221
            $results = $this->addAppendsToResults($results);
0 ignored issues
show
Bug introduced by
It seems like $results can also be of type array<integer,object<Ill...base\Eloquent\Builder>>; however, Spatie\QueryBuilder\Quer...::addAppendsToResults() does only seem to accept object<Illuminate\Support\Collection>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
222
        }
223
224
        return $results;
225
    }
226
227
    public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
228
    {
229
        if ($this->request->sorts() && ! $this->allowedSorts instanceof Collection) {
230
            $this->addDefaultSorts();
231
        }
232
233
        return parent::paginate($perPage, $columns, $pageName, $page);
234
    }
235
236
    public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
237
    {
238
        if ($this->request->sorts() && ! $this->allowedSorts instanceof Collection) {
239
            $this->addDefaultSorts();
240
        }
241
242
        return parent::simplePaginate($perPage, $columns, $pageName, $page);
243
    }
244
245
    /**
246
     * Add the model, scopes, eager loaded relationships, local macro's and onDelete callback
247
     * from the $builder to this query builder.
248
     *
249
     * @param \Illuminate\Database\Eloquent\Builder $builder
250
     */
251
    protected function initializeFromBuilder(Builder $builder)
252
    {
253
        $this->setModel($builder->getModel())
0 ignored issues
show
Bug introduced by
It seems like $builder->getModel() targeting Illuminate\Database\Eloquent\Builder::getModel() can also be of type object<Illuminate\Database\Eloquent\Builder>; however, Illuminate\Database\Eloquent\Builder::setModel() does only seem to accept object<Illuminate\Database\Eloquent\Model>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
254
            ->setEagerLoads($builder->getEagerLoads());
255
256
        $builder->macro('getProtected', function (Builder $builder, string $property) {
257
            return $builder->{$property};
258
        });
259
260
        $this->scopes = $builder->getProtected('scopes');
261
262
        $this->localMacros = $builder->getProtected('localMacros');
263
264
        $this->onDelete = $builder->getProtected('onDelete');
265
    }
266
267
    protected function addFieldsToQuery(Collection $fields)
268
    {
269
        $modelTableName = $this->getModel()->getTable();
0 ignored issues
show
Bug introduced by
The method getTable does only exist in Illuminate\Database\Eloquent\Model, but not in Illuminate\Database\Eloquent\Builder.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
270
271
        if ($modelFields = $fields->get($modelTableName)) {
272
            $this->select($this->prependFieldsWithTableName($modelFields, $modelTableName));
0 ignored issues
show
Bug introduced by
The method select() does not exist on Spatie\QueryBuilder\QueryBuilder. Did you maybe mean createSelectWithConstraint()?

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...
273
        }
274
    }
275
276
    protected function prependFieldsWithTableName(array $fields, string $tableName): array
277
    {
278
        return array_map(function ($field) use ($tableName) {
279
            return "{$tableName}.{$field}";
280
        }, $fields);
281
    }
282
283
    protected function getFieldsForIncludedTable(string $relation): array
284
    {
285
        return $this->request->fields()->get($relation, []);
286
    }
287
288
    protected function addFiltersToQuery(Collection $filters)
289
    {
290
        $filters->each(function ($value, $property) {
291
            $filter = $this->findFilter($property);
292
293
            $filter->filter($this, $value);
294
        });
295
    }
296
297
    protected function findFilter(string $property): ?Filter
298
    {
299
        return $this->allowedFilters
300
            ->first(function (Filter $filter) use ($property) {
301
                return $filter->isForProperty($property);
302
            });
303
    }
304
305
    protected function parseSorts()
306
    {
307
        $sorts = $this->request->sorts();
308
309
        if ($sorts->isEmpty()) {
310
            optional($this->defaultSorts)->each(function (Sort $sort) {
311
                $sort->sort($this);
312
            });
313
        }
314
315
        $this
316
            ->filterDuplicates($sorts)
317
            ->each(function (string $property) {
318
                $descending = $property[0] === '-';
319
320
                $key = ltrim($property, '-');
321
322
                $sort = $this->findSort($key);
323
324
                $sort->sort($this, $descending);
325
            });
326
    }
327
328
    protected function filterDuplicates(Collection $sorts): Collection
329
    {
330
        if (! is_array($orders = $this->getQuery()->orders)) {
331
            return $sorts;
332
        }
333
334
        return $sorts->reject(function (string $sort) use ($orders) {
335
            $toSort = [
336
                'column' => ltrim($sort, '-'),
337
                'direction' => ($sort[0] === '-') ? 'desc' : 'asc',
338
            ];
339
            foreach ($orders as $order) {
340
                if ($order === $toSort) {
341
                    return true;
342
                }
343
            }
344
        });
345
    }
346
347
    protected function findSort(string $property): ?Sort
348
    {
349
        return $this->allowedSorts
350
            ->merge($this->defaultSorts)
351
            ->first(function (Sort $sort) use ($property) {
352
                return $sort->isForProperty($property);
353
            });
354
    }
355
356
    protected function addDefaultSorts()
357
    {
358
        $this->allowedSorts = collect($this->request->sorts($this->defaultSorts))
359
            ->map(function ($sort) {
360
                if ($sort instanceof Sort) {
361
                    return $sort;
362
                }
363
364
                return Sort::field(ltrim($sort, '-'));
365
            });
366
367
        $this->parseSorts();
368
    }
369
370
    protected function addAppendsToResults(Collection $results)
371
    {
372
        $appends = $this->request->appends();
373
374
        return $results->each->append($appends->toArray());
375
    }
376
377
    protected function addIncludesToQuery(Collection $includes)
378
    {
379
        $includes
380
            ->map([Str::class, 'camel'])
381
            ->map(function (string $include) {
382
                return collect(explode('.', $include));
383
            })
384
            ->flatMap(function (Collection $relatedTables) {
385
                return $relatedTables
386
                    ->mapWithKeys(function ($table, $key) use ($relatedTables) {
387
                        $fields = $this->getFieldsForIncludedTable(Str::snake($table));
388
389
                        $fullRelationName = $relatedTables->slice(0, $key + 1)->implode('.');
390
391
                        if (empty($fields)) {
392
                            return [$fullRelationName];
393
                        }
394
395
                        return [$fullRelationName => function ($query) use ($fields) {
396
                            $query->select($fields);
397
                        }];
398
                    });
399
            })
400
            ->pipe(function (Collection $withs) {
401
                $this->with($withs->all());
402
            });
403
    }
404
405
    protected function guardAgainstUnknownFilters()
406
    {
407
        $filterNames = $this->request->filters()->keys();
408
409
        $allowedFilterNames = $this->allowedFilters->map->getProperty();
410
411
        $diff = $filterNames->diff($allowedFilterNames);
412
413
        if ($diff->count()) {
414
            throw InvalidFilterQuery::filtersNotAllowed($diff, $allowedFilterNames);
415
        }
416
    }
417
418
    protected function guardAgainstUnknownFields()
419
    {
420
        $fields = $this->request->fields()
421
            ->map(function ($fields, $model) {
422
                $tableName = Str::snake(preg_replace('/-/', '_', $model));
423
424
                $fields = array_map([Str::class, 'snake'], $fields);
425
426
                return $this->prependFieldsWithTableName($fields, $tableName);
427
            })
428
            ->flatten()
429
            ->unique();
430
431
        $diff = $fields->diff($this->allowedFields);
432
433
        if ($diff->count()) {
434
            throw InvalidFieldQuery::fieldsNotAllowed($diff, $this->allowedFields);
435
        }
436
    }
437
438
    protected function guardAgainstUnknownSorts()
439
    {
440
        $sortNames = $this->request->sorts()->map(function ($sort) {
441
            return ltrim($sort, '-');
442
        });
443
444
        $allowedSortNames = $this->allowedSorts->map->getProperty();
445
446
        $diff = $sortNames->diff($allowedSortNames);
447
448
        if ($diff->count()) {
449
            throw InvalidSortQuery::sortsNotAllowed($diff, $allowedSortNames);
450
        }
451
    }
452
453
    protected function guardAgainstUnknownIncludes()
454
    {
455
        $includes = $this->request->includes();
456
457
        $diff = $includes->diff($this->allowedIncludes);
458
459
        if ($diff->count()) {
460
            throw InvalidIncludeQuery::includesNotAllowed($diff, $this->allowedIncludes);
461
        }
462
    }
463
464
    protected function guardAgainstUnknownAppends()
465
    {
466
        $appends = $this->request->appends();
467
468
        $diff = $appends->diff($this->allowedAppends);
469
470
        if ($diff->count()) {
471
            throw InvalidAppendQuery::appendsNotAllowed($diff, $this->allowedAppends);
472
        }
473
    }
474
}
475