Completed
Push — master ( 00b628...f9d136 )
by
unknown
05:53 queued 04:22
created

QueryBuilder::initializeFromBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 8
nc 1
nop 1
1
<?php
2
3
namespace Spatie\QueryBuilder;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Http\Request;
7
use Illuminate\Support\Collection;
8
use Spatie\QueryBuilder\Exceptions\InvalidFilterQuery;
9
use Spatie\QueryBuilder\Exceptions\InvalidIncludeQuery;
10
use Spatie\QueryBuilder\Exceptions\InvalidSortQuery;
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\Support\Collection */
27
    protected $fields;
28
29
    /** @var \Illuminate\Http\Request */
30
    protected $request;
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
        $this->parseSelectedFields();
41
42
        if ($this->request->sorts()) {
43
            $this->allowedSorts('*');
44
        }
45
    }
46
47
    /**
48
     * Add the model, scopes, eager loaded relationships, local macro's and onDelete callback
49
     * from the $builder to this query builder.
50
     *
51
     * @param \Illuminate\Database\Eloquent\Builder $builder
52
     */
53
    protected function initializeFromBuilder(Builder $builder)
54
    {
55
        $this->setModel($builder->getModel())
56
            ->setEagerLoads($builder->getEagerLoads());
57
58
        $builder->macro('getProtected', function (Builder $builder, string $property) {
59
            return $builder->{$property};
60
        });
61
62
        $this->scopes = $builder->getProtected('scopes');
63
64
        $this->localMacros = $builder->getProtected('localMacros');
65
66
        $this->onDelete = $builder->getProtected('onDelete');
67
    }
68
69
    /**
70
     * Create a new QueryBuilder for a request and model.
71
     *
72
     * @param string|\Illuminate\Database\Query\Builder $baseQuery Model class or base query builder
73
     * @param Request $request
74
     *
75
     * @return \Spatie\QueryBuilder\QueryBuilder
76
     */
77
    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...
78
    {
79
        if (is_string($baseQuery)) {
80
            $baseQuery = ($baseQuery)::query();
81
        }
82
83
        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...
84
    }
85
86
    public function allowedFilters($filters) : self
87
    {
88
        $filters = is_array($filters) ? $filters : func_get_args();
89
        $this->allowedFilters = collect($filters)->map(function ($filter) {
90
            if ($filter instanceof Filter) {
91
                return $filter;
92
            }
93
94
            return Filter::partial($filter);
95
        });
96
97
        $this->guardAgainstUnknownFilters();
98
99
        $this->addFiltersToQuery($this->request->filters());
100
101
        return $this;
102
    }
103
104
    public function defaultSort($sort) : self
105
    {
106
        $this->defaultSort = $sort;
107
108
        $this->addSortsToQuery($this->request->sorts($this->defaultSort));
109
110
        return $this;
111
    }
112
113
    public function allowedSorts($sorts) : self
114
    {
115
        $sorts = is_array($sorts) ? $sorts : func_get_args();
116
        if (!$this->request->sorts()) {
117
            return $this;
118
        }
119
120
        $this->allowedSorts = collect($sorts);
121
122
        if (!$this->allowedSorts->contains('*')) {
123
            $this->guardAgainstUnknownSorts();
124
        }
125
126
        $this->addSortsToQuery($this->request->sorts($this->defaultSort));
127
128
        return $this;
129
    }
130
131
    public function allowedIncludes($includes) : self
132
    {
133
        $includes = is_array($includes) ? $includes : func_get_args();
134
135
        $this->allowedIncludes = collect($includes);
136
137
        $this->guardAgainstUnknownIncludes();
138
139
        $this->addIncludesToQuery($this->request->includes());
140
141
        return $this;
142
    }
143
144
    protected function parseSelectedFields()
145
    {
146
        $this->fields = $this->request->fields();
147
148
        $modelFields = $this->fields->get(
149
            $this->getModel()->getTable()
150
        );
151
152
        if (! $modelFields) {
153
            return;
154
        }
155
156
        $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...
157
    }
158
159
    protected function getFieldsForRelatedTable(string $relation): array
160
    {
161
        $fields = $this->fields->get($relation);
162
163
        if (! $fields) {
164
            return [];
165
        }
166
167
        return explode(',', $fields);
168
    }
169
170
    protected function addFiltersToQuery(Collection $filters)
171
    {
172
        $filters->each(function ($value, $property) {
173
            $filter = $this->findFilter($property);
174
175
            $filter->filter($this, $value);
176
        });
177
    }
178
179
    protected function findFilter(string $property) : ? Filter
180
    {
181
        return $this->allowedFilters
182
            ->first(function (Filter $filter) use ($property) {
183
                return $filter->isForProperty($property);
184
            });
185
    }
186
187
    protected function addSortsToQuery(Collection $sorts)
188
    {
189
        $sorts
190
            ->each(function (string $sort) {
191
                $descending = $sort[0] === '-';
192
193
                $key = ltrim($sort, '-');
194
195
                $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...
196
            });
197
    }
198
199
    protected function addIncludesToQuery(Collection $includes)
200
    {
201
        $includes
202
            ->map('camel_case')
203
            ->map(function (string $include) {
204
                return collect(explode('.', $include));
205
            })
206
            ->flatMap(function (Collection $relatedTables) {
207
                return $relatedTables
208
                    ->mapWithKeys(function ($table, $key) use ($relatedTables) {
209
                        $fields = $this->getFieldsForRelatedTable(snake_case($table));
210
211
                        $fullRelationName = $relatedTables->slice(0, $key + 1)->implode('.');
212
213
                        if (empty($fields)) {
214
                            return [$fullRelationName];
215
                        }
216
217
                        return [$fullRelationName => function ($query) use ($fields) {
218
                            $query->select($fields);
219
                        }];
220
                    });
221
            })
222
            ->pipe(function (Collection $withs) {
223
                $this->with($withs->all());
224
            });
225
    }
226
227
    protected function guardAgainstUnknownFilters()
228
    {
229
        $filterNames = $this->request->filters()->keys();
230
231
        $allowedFilterNames = $this->allowedFilters->map->getProperty();
232
233
        $diff = $filterNames->diff($allowedFilterNames);
234
235
        if ($diff->count()) {
236
            throw InvalidFilterQuery::filtersNotAllowed($diff, $allowedFilterNames);
237
        }
238
    }
239
240
    protected function guardAgainstUnknownSorts()
241
    {
242
        $sorts = $this->request->sorts()->map(function ($sort) {
243
            return ltrim($sort, '-');
244
        });
245
246
        $diff = $sorts->diff($this->allowedSorts);
247
248
        if ($diff->count()) {
249
            throw InvalidSortQuery::sortsNotAllowed($diff, $this->allowedSorts);
250
        }
251
    }
252
253
    protected function guardAgainstUnknownIncludes()
254
    {
255
        $includes = $this->request->includes();
256
257
        $diff = $includes->diff($this->allowedIncludes);
258
259
        if ($diff->count()) {
260
            throw InvalidIncludeQuery::includesNotAllowed($diff, $this->allowedIncludes);
261
        }
262
    }
263
}
264