Completed
Pull Request — master (#39)
by
unknown
01:28
created

QueryBuilder::addFiltersToQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
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\InvalidFilterQuery;
10
use Spatie\QueryBuilder\Exceptions\InvalidIncludeQuery;
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\Http\Request */
27
    protected $request;
28
29
    public function __construct(Builder $builder, ?Request $request = null)
30
    {
31
        parent::__construct(clone $builder->getQuery());
32
33
        $this->initializeFromBuilder($builder);
34
35
        $this->request = $request ?? request();
36
37
        if ($this->request->sorts()) {
38
            $this->allowedSorts('*');
39
        }
40
    }
41
42
    /**
43
     * Add the model, scopes, eager loaded relationships, local macro's and onDelete callback
44
     * from the $builder to this query builder.
45
     *
46
     * @param \Illuminate\Database\Eloquent\Builder $builder
47
     */
48
    protected function initializeFromBuilder(Builder $builder)
49
    {
50
        $this->setModel($builder->getModel())
51
            ->setEagerLoads($builder->getEagerLoads());
52
53
        $builder->macro('getProtected', function (Builder $builder, string $property) {
54
            return $builder->{$property};
55
        });
56
57
        $this->scopes = $builder->getProtected('scopes');
58
59
        $this->localMacros = $builder->getProtected('localMacros');
60
61
        $this->onDelete = $builder->getProtected('onDelete');
62
    }
63
64
    /**
65
     * Create a new QueryBuilder for a request and model.
66
     *
67
     * @param string|\Illuminate\Database\Query\Builder $baseQuery Model class or base query builder
68
     * @param Request $request
69
     *
70
     * @return \Spatie\QueryBuilder\QueryBuilder
71
     */
72
    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...
73
    {
74
        if (is_string($baseQuery)) {
75
            $baseQuery = ($baseQuery)::query();
76
        }
77
78
        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...
79
    }
80
81
    public function allowedFilters($filters): self
82
    {
83
        $filters = is_array($filters) ? $filters : func_get_args();
84
        $this->allowedFilters = collect($filters)->map(function ($filter) {
85
            if ($filter instanceof Filter) {
86
                return $filter;
87
            }
88
89
            return Filter::partial($filter);
90
        });
91
92
        $this->guardAgainstUnknownFilters();
93
94
        $this->addFiltersToQuery($this->request->filters());
95
96
        return $this;
97
    }
98
99
    public function defaultSort($sort): self
100
    {
101
        $this->defaultSort = $sort;
102
103
        $this->addSortsToQuery($this->request->sorts($this->defaultSort));
104
105
        return $this;
106
    }
107
108
    public function allowedSorts($sorts): self
109
    {
110
        $sorts = is_array($sorts) ? $sorts : func_get_args();
111
        if (! $this->request->sorts()) {
112
            return $this;
113
        }
114
115
        $this->allowedSorts = collect($sorts);
116
117
        if (! $this->allowedSorts->contains('*')) {
118
            $this->guardAgainstUnknownSorts();
119
        }
120
121
        $this->addSortsToQuery($this->request->sorts($this->defaultSort));
122
123
        return $this;
124
    }
125
126
    public function allowedIncludes($includes): self
127
    {
128
        $includes = is_array($includes) ? $includes : func_get_args();
129
        $this->allowedIncludes = collect($includes);
130
131
        $this->guardAgainstUnknownIncludes();
132
133
        $this->addIncludesToQuery($this->request->includes());
134
135
        return $this;
136
    }
137
138
    public function allowedSearches($columns): self
139
    {
140
        $columns = is_array($columns) ? $columns : func_get_args();
141
142
        $columns = collect($columns);
143
144
        $this->addSearchesToQuery($columns);
145
146
        return $this;
147
    }
148
149
    protected function addSearchesToQuery(Collection $columns)
150
    {
151
        if (! $search = $this->request->search()) {
152
            return;
153
        }
154
155
        $this->where(function ($query) use ($columns, $search) {
156
            foreach ($columns as $column) {
157
                $query->orWhere($column, 'LIKE', "%{$search}%");
158
            }
159
        });
160
    }
161
162
    protected function addFiltersToQuery(Collection $filters)
163
    {
164
        $filters->each(function ($value, $property) {
165
            $filter = $this->findFilter($property);
166
167
            $filter->filter($this, $value);
168
        });
169
    }
170
171
    protected function findFilter(string $property) : ?Filter
172
    {
173
        return $this->allowedFilters
174
            ->first(function (Filter $filter) use ($property) {
175
                return $filter->isForProperty($property);
176
            });
177
    }
178
179
    protected function addSortsToQuery(Collection $sorts)
180
    {
181
        $sorts
182
            ->each(function (string $sort) {
183
                $descending = $sort[0] === '-';
184
185
                $key = ltrim($sort, '-');
186
187
                $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...
188
            });
189
    }
190
191
    protected function addIncludesToQuery(Collection $includes)
192
    {
193
        $includes
194
            ->map(function (string $include) {
195
                return camel_case($include);
196
            })
197
            ->each(function (string $include) {
198
                $this->with($include);
199
            });
200
    }
201
202
    protected function guardAgainstUnknownFilters()
203
    {
204
        $filterNames = $this->request->filters()->keys();
205
206
        $allowedFilterNames = $this->allowedFilters->map->getProperty();
207
208
        $diff = $filterNames->diff($allowedFilterNames);
209
210
        if ($diff->count()) {
211
            throw InvalidFilterQuery::filtersNotAllowed($diff, $allowedFilterNames);
212
        }
213
    }
214
215
    protected function guardAgainstUnknownSorts()
216
    {
217
        $sorts = $this->request->sorts()->map(function ($sort) {
218
            return ltrim($sort, '-');
219
        });
220
221
        $diff = $sorts->diff($this->allowedSorts);
222
223
        if ($diff->count()) {
224
            throw InvalidSortQuery::sortsNotAllowed($diff, $this->allowedSorts);
225
        }
226
    }
227
228
    protected function guardAgainstUnknownIncludes()
229
    {
230
        $includes = $this->request->includes();
231
232
        $diff = $includes->diff($this->allowedIncludes);
233
234
        if ($diff->count()) {
235
            throw InvalidIncludeQuery::includesNotAllowed($diff, $this->allowedIncludes);
236
        }
237
    }
238
}
239