Completed
Pull Request — master (#50)
by
unknown
01:29
created

QueryBuilder   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 303
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 8

Importance

Changes 0
Metric Value
wmc 38
lcom 2
cbo 8
dl 0
loc 303
rs 8.3999
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 2
A initializeFromBuilder() 0 15 1
A for() 0 8 2
A allowedFilters() 0 17 3
A defaultSort() 0 8 1
A allowedSorts() 0 17 4
A allowedIncludes() 0 12 2
A allowedAppends() 0 12 2
A parseSelectedFields() 0 14 2
A getFieldsForRelatedTable() 0 10 2
A addFiltersToQuery() 0 8 1
A findFilter() 0 7 1
A addSortsToQuery() 0 11 2
B addIncludesToQuery() 0 27 2
A setAppendsToResult() 0 9 1
A guardAgainstUnknownFilters() 0 12 2
A guardAgainstUnknownSorts() 0 12 2
A guardAgainstUnknownIncludes() 0 10 2
A guardAgainstUnknownAppends() 0 10 2
A get() 0 10 2
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
use Spatie\QueryBuilder\Exceptions\InvalidAppendQuery;
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
144
        $this->guardAgainstUnknownIncludes();
145
146
        $this->addIncludesToQuery($this->request->includes());
147
148
        return $this;
149
    }
150
151
    public function allowedAppends($appends) : self
152
    {
153
        $appends = is_array($appends) ? $appends : func_get_args();
154
155
        $this->allowedAppends = collect($appends);
156
157
        $this->guardAgainstUnknownAppends();
158
159
        $this->appends = $this->request->appends();
160
161
        return $this;
162
    }
163
164
    protected function parseSelectedFields()
165
    {
166
        $this->fields = $this->request->fields();
167
168
        $modelFields = $this->fields->get(
169
            $this->getModel()->getTable()
170
        );
171
172
        if (! $modelFields) {
173
            return;
174
        }
175
176
        $this->select(explode(',', $modelFields));
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...
177
    }
178
179
    protected function getFieldsForRelatedTable(string $relation): array
180
    {
181
        $fields = $this->fields->get($relation);
182
183
        if (! $fields) {
184
            return [];
185
        }
186
187
        return explode(',', $fields);
188
    }
189
190
    protected function addFiltersToQuery(Collection $filters)
191
    {
192
        $filters->each(function ($value, $property) {
193
            $filter = $this->findFilter($property);
194
195
            $filter->filter($this, $value);
196
        });
197
    }
198
199
    protected function findFilter(string $property) : ? Filter
200
    {
201
        return $this->allowedFilters
202
            ->first(function (Filter $filter) use ($property) {
203
                return $filter->isForProperty($property);
204
            });
205
    }
206
207
    protected function addSortsToQuery(Collection $sorts)
208
    {
209
        $sorts
210
            ->each(function (string $sort) {
211
                $descending = $sort[0] === '-';
212
213
                $key = ltrim($sort, '-');
214
215
                $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...
216
            });
217
    }
218
219
    protected function addIncludesToQuery(Collection $includes)
220
    {
221
        $includes
222
            ->map('camel_case')
223
            ->map(function (string $include) {
224
                return collect(explode('.', $include));
225
            })
226
            ->flatMap(function (Collection $relatedTables) {
227
                return $relatedTables
228
                    ->mapWithKeys(function ($table, $key) use ($relatedTables) {
229
                        $fields = $this->getFieldsForRelatedTable(snake_case($table));
230
231
                        $fullRelationName = $relatedTables->slice(0, $key + 1)->implode('.');
232
233
                        if (empty($fields)) {
234
                            return [$fullRelationName];
235
                        }
236
237
                        return [$fullRelationName => function ($query) use ($fields) {
238
                            $query->select($fields);
239
                        }];
240
                    });
241
            })
242
            ->pipe(function (Collection $withs) {
243
                $this->with($withs->all());
244
            });
245
    }
246
247
    public function setAppendsToResult($result)
248
    {
249
        $result->map(function ($item) {
250
            $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...
251
            return $item;
252
        });
253
            
254
        return $result;
255
    }
256
257
    protected function guardAgainstUnknownFilters()
258
    {
259
        $filterNames = $this->request->filters()->keys();
260
261
        $allowedFilterNames = $this->allowedFilters->map->getProperty();
262
263
        $diff = $filterNames->diff($allowedFilterNames);
264
265
        if ($diff->count()) {
266
            throw InvalidFilterQuery::filtersNotAllowed($diff, $allowedFilterNames);
267
        }
268
    }
269
270
    protected function guardAgainstUnknownSorts()
271
    {
272
        $sorts = $this->request->sorts()->map(function ($sort) {
273
            return ltrim($sort, '-');
274
        });
275
276
        $diff = $sorts->diff($this->allowedSorts);
277
278
        if ($diff->count()) {
279
            throw InvalidSortQuery::sortsNotAllowed($diff, $this->allowedSorts);
280
        }
281
    }
282
283
    protected function guardAgainstUnknownIncludes()
284
    {
285
        $includes = $this->request->includes();
286
287
        $diff = $includes->diff($this->allowedIncludes);
288
289
        if ($diff->count()) {
290
            throw InvalidIncludeQuery::includesNotAllowed($diff, $this->allowedIncludes);
291
        }
292
    }
293
294
    protected function guardAgainstUnknownAppends()
295
    {
296
        $appends = $this->request->appends();
297
298
        $diff = $appends->diff($this->allowedAppends);
299
300
        if ($diff->count()) {
301
            throw InvalidAppendQuery::appendsNotAllowed($diff, $this->allowedAppends);
302
        }
303
    }
304
305
    public function get($columns = ['*'])
306
    {
307
        $result = parent::get($columns);
308
309
        if (count($this->appends) > 0) {
310
            $result = $this->setAppendsToResult($result);
311
        }
312
313
        return $result;
314
    }
315
}
316