Completed
Push — master ( 3ba87a...c1c280 )
by Freek
9s
created

QueryBuilder::findFilter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
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
        if ($this->request->sorts()) {
50
            $this->allowedSorts('*');
51
        }
52
    }
53
54
    /**
55
     * Add the model, scopes, eager loaded relationships, local macro's and onDelete callback
56
     * from the $builder to this query builder.
57
     *
58
     * @param \Illuminate\Database\Eloquent\Builder $builder
59
     */
60
    protected function initializeFromBuilder(Builder $builder)
61
    {
62
        $this->setModel($builder->getModel())
63
            ->setEagerLoads($builder->getEagerLoads());
64
65
        $builder->macro('getProtected', function (Builder $builder, string $property) {
66
            return $builder->{$property};
67
        });
68
69
        $this->scopes = $builder->getProtected('scopes');
70
71
        $this->localMacros = $builder->getProtected('localMacros');
72
73
        $this->onDelete = $builder->getProtected('onDelete');
74
    }
75
76
    /**
77
     * Create a new QueryBuilder for a request and model.
78
     *
79
     * @param string|\Illuminate\Database\Query\Builder $baseQuery Model class or base query builder
80
     * @param Request $request
81
     *
82
     * @return \Spatie\QueryBuilder\QueryBuilder
83
     */
84
    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...
85
    {
86
        if (is_string($baseQuery)) {
87
            $baseQuery = ($baseQuery)::query();
88
        }
89
90
        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...
91
    }
92
93
    public function allowedFilters($filters) : self
94
    {
95
        $filters = is_array($filters) ? $filters : func_get_args();
96
        $this->allowedFilters = collect($filters)->map(function ($filter) {
97
            if ($filter instanceof Filter) {
98
                return $filter;
99
            }
100
101
            return Filter::partial($filter);
102
        });
103
104
        $this->guardAgainstUnknownFilters();
105
106
        $this->addFiltersToQuery($this->request->filters());
107
108
        return $this;
109
    }
110
111
    public function defaultSort($sort) : self
112
    {
113
        $this->defaultSort = $sort;
114
115
        $this->addSortsToQuery($this->request->sorts($this->defaultSort));
116
117
        return $this;
118
    }
119
120
    public function allowedSorts($sorts) : self
121
    {
122
        $sorts = is_array($sorts) ? $sorts : func_get_args();
123
        if (! $this->request->sorts()) {
124
            return $this;
125
        }
126
127
        $this->allowedSorts = collect($sorts);
128
129
        if (! $this->allowedSorts->contains('*')) {
130
            $this->guardAgainstUnknownSorts();
131
        }
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 $sort) {
227
                $descending = $sort[0] === '-';
228
229
                $key = ltrim($sort, '-');
230
231
                $this->orderBy($key, $descending ? 'desc' : 'asc');
0 ignored issues
show
Bug introduced by
The method orderBy() does not exist on Spatie\QueryBuilder\QueryBuilder. Did you maybe mean enforceOrderBy()?

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...
232
            });
233
    }
234
235
    protected function filterDuplicates(Collection $sorts): Collection
236
    {
237
        if (! is_array($orders = $this->getQuery()->orders)) {
238
            return $sorts;
239
        }
240
241
        return $sorts->reject(function (string $sort) use ($orders) {
242
            $toSort = [
243
                'column' => ltrim($sort, '-'),
244
                'direction' => ($sort[0] === '-') ? 'desc' : 'asc',
245
            ];
246
            foreach ($orders as $order) {
247
                if ($order === $toSort) {
248
                    return true;
249
                }
250
            }
251
        });
252
    }
253
254
    protected function addIncludesToQuery(Collection $includes)
255
    {
256
        $includes
257
            ->map('camel_case')
258
            ->map(function (string $include) {
259
                return collect(explode('.', $include));
260
            })
261
            ->flatMap(function (Collection $relatedTables) {
262
                return $relatedTables
263
                    ->mapWithKeys(function ($table, $key) use ($relatedTables) {
264
                        $fields = $this->getFieldsForRelatedTable(snake_case($table));
265
266
                        $fullRelationName = $relatedTables->slice(0, $key + 1)->implode('.');
267
268
                        if (empty($fields)) {
269
                            return [$fullRelationName];
270
                        }
271
272
                        return [$fullRelationName => function ($query) use ($fields) {
273
                            $query->select($fields);
274
                        }];
275
                    });
276
            })
277
            ->pipe(function (Collection $withs) {
278
                $this->with($withs->all());
279
            });
280
    }
281
282
    public function setAppendsToResult($result)
283
    {
284
        $result->map(function ($item) {
285
            $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...
286
287
            return $item;
288
        });
289
290
        return $result;
291
    }
292
293
    protected function guardAgainstUnknownFilters()
294
    {
295
        $filterNames = $this->request->filters()->keys();
296
297
        $allowedFilterNames = $this->allowedFilters->map->getProperty();
298
299
        $diff = $filterNames->diff($allowedFilterNames);
300
301
        if ($diff->count()) {
302
            throw InvalidFilterQuery::filtersNotAllowed($diff, $allowedFilterNames);
303
        }
304
    }
305
306
    protected function guardAgainstUnknownSorts()
307
    {
308
        $sorts = $this->request->sorts()->map(function ($sort) {
309
            return ltrim($sort, '-');
310
        });
311
312
        $diff = $sorts->diff($this->allowedSorts);
313
314
        if ($diff->count()) {
315
            throw InvalidSortQuery::sortsNotAllowed($diff, $this->allowedSorts);
316
        }
317
    }
318
319
    protected function guardAgainstUnknownIncludes()
320
    {
321
        $includes = $this->request->includes();
322
323
        $diff = $includes->diff($this->allowedIncludes);
324
325
        if ($diff->count()) {
326
            throw InvalidIncludeQuery::includesNotAllowed($diff, $this->allowedIncludes);
327
        }
328
    }
329
330
    protected function guardAgainstUnknownAppends()
331
    {
332
        $appends = $this->request->appends();
333
334
        $diff = $appends->diff($this->allowedAppends);
335
336
        if ($diff->count()) {
337
            throw InvalidAppendQuery::appendsNotAllowed($diff, $this->allowedAppends);
338
        }
339
    }
340
341
    public function get($columns = ['*'])
342
    {
343
        $result = parent::get($columns);
344
345
        if (count($this->appends) > 0) {
346
            $result = $this->setAppendsToResult($result);
347
        }
348
349
        return $result;
350
    }
351
}
352