Completed
Pull Request — master (#35)
by
unknown
01:44
created

QueryBuilder   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 246
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 6

Importance

Changes 0
Metric Value
wmc 31
lcom 2
cbo 6
dl 0
loc 246
rs 9.8
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 16 3
A addSelectedColumns() 0 10 2
A getSelectForRelation() 0 10 1
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 11 2
A addFiltersToQuery() 0 8 1
A findFilter() 0 7 1
A addSortsToQuery() 0 11 2
A addIncludesToQuery() 0 18 2
A guardAgainstUnknownFilters() 0 12 2
A guardAgainstUnknownSorts() 0 12 2
A guardAgainstUnknownIncludes() 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
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
    /** @var \Illuminate\Support\Collection */
30
    protected $columns;
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
        if ($this->columns = $this->request->column()) {
41
            $this->addSelectedColumns($this->columns);
42
        }
43
44
        if ($this->request->sorts()) {
45
            $this->allowedSorts('*');
46
        }
47
    }
48
49
    /**
50
     * Saves selects as a collection
51
     *
52
     * @param array|string $select
0 ignored issues
show
Bug introduced by
There is no parameter named $select. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
53
     * @return void
54
     */
55
    protected function addSelectedColumns($columns)
56
    {
57
        $columns = $columns->filter(function ($item) {
58
            return strpos($item, '.') === false;
59
        });
60
61
        if ($columns->count() > 0) {
62
            $this->select($columns->all());
0 ignored issues
show
Bug introduced by
The method select() does not exist on Spatie\QueryBuilder\QueryBuilder. Did you maybe mean addSelectedColumns()?

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...
63
        }
64
    }
65
66
    protected function getSelectForRelation($relation)
67
    {
68
        return $this->columns
69
            ->filter(function ($column) use ($relation) {
70
                return strpos($column, "{$relation}.") !== false;
71
            })
72
            ->map(function ($column) use ($relation) {
73
                return str_replace("{$relation}.", '', $column);
74
            });
75
    }
76
77
    /**
78
     * Add the model, scopes, eager loaded relationships, local macro's and onDelete callback
79
     * from the $builder to this query builder.
80
     *
81
     * @param \Illuminate\Database\Eloquent\Builder $builder
82
     */
83
    protected function initializeFromBuilder(Builder $builder)
84
    {
85
        $this->setModel($builder->getModel())
86
            ->setEagerLoads($builder->getEagerLoads());
87
88
        $builder->macro('getProtected', function (Builder $builder, string $property) {
89
            return $builder->{$property};
90
        });
91
92
        $this->scopes = $builder->getProtected('scopes');
93
94
        $this->localMacros = $builder->getProtected('localMacros');
95
96
        $this->onDelete = $builder->getProtected('onDelete');
97
    }
98
99
    /**
100
     * Create a new QueryBuilder for a request and model.
101
     *
102
     * @param string|\Illuminate\Database\Query\Builder $baseQuery Model class or base query builder
103
     * @param Request $request
104
     *
105
     * @return \Spatie\QueryBuilder\QueryBuilder
106
     */
107
    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...
108
    {
109
        if (is_string($baseQuery)) {
110
            $baseQuery = ($baseQuery)::query();
111
        }
112
113
        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...
114
    }
115
116
    public function allowedFilters($filters): self
117
    {
118
        $filters = is_array($filters) ? $filters : func_get_args();
119
        $this->allowedFilters = collect($filters)->map(function ($filter) {
120
            if ($filter instanceof Filter) {
121
                return $filter;
122
            }
123
124
            return Filter::partial($filter);
125
        });
126
127
        $this->guardAgainstUnknownFilters();
128
129
        $this->addFiltersToQuery($this->request->filters());
130
131
        return $this;
132
    }
133
134
    public function defaultSort($sort): self
135
    {
136
        $this->defaultSort = $sort;
137
138
        $this->addSortsToQuery($this->request->sorts($this->defaultSort));
139
140
        return $this;
141
    }
142
143
    public function allowedSorts($sorts): self
144
    {
145
        $sorts = is_array($sorts) ? $sorts : func_get_args();
146
        if (! $this->request->sorts()) {
147
            return $this;
148
        }
149
150
        $this->allowedSorts = collect($sorts);
151
152
        if (! $this->allowedSorts->contains('*')) {
153
            $this->guardAgainstUnknownSorts();
154
        }
155
156
        $this->addSortsToQuery($this->request->sorts($this->defaultSort));
157
158
        return $this;
159
    }
160
161
    public function allowedIncludes($includes): self
162
    {
163
        $includes = is_array($includes) ? $includes : func_get_args();
164
        $this->allowedIncludes = collect($includes);
165
166
        $this->guardAgainstUnknownIncludes();
167
168
        $this->addIncludesToQuery($this->request->includes());
169
170
        return $this;
171
    }
172
173
    protected function addFiltersToQuery(Collection $filters)
174
    {
175
        $filters->each(function ($value, $property) {
176
            $filter = $this->findFilter($property);
177
178
            $filter->filter($this, $value);
179
        });
180
    }
181
182
    protected function findFilter(string $property) : ?Filter
183
    {
184
        return $this->allowedFilters
185
            ->first(function (Filter $filter) use ($property) {
186
                return $filter->isForProperty($property);
187
            });
188
    }
189
190
    protected function addSortsToQuery(Collection $sorts)
191
    {
192
        $sorts
193
            ->each(function (string $sort) {
194
                $descending = $sort[0] === '-';
195
196
                $key = ltrim($sort, '-');
197
198
                $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...
199
            });
200
    }
201
202
    protected function addIncludesToQuery(Collection $includes)
203
    {
204
        $includes
205
            ->map(function (string $include) {
206
                return camel_case($include);
207
            })
208
            ->each(function (string $include) {
209
                $selects = $this->getSelectForRelation(kebab_case($include));
210
211
                if (! $selects->count()) {
212
                    return $this->with($include);
213
                }
214
215
                $this->with([$include => function ($query) use ($selects) {
216
                    $query->select($selects->all());
217
                }]);
218
            });
219
    }
220
221
    protected function guardAgainstUnknownFilters()
222
    {
223
        $filterNames = $this->request->filters()->keys();
224
225
        $allowedFilterNames = $this->allowedFilters->map->getProperty();
226
227
        $diff = $filterNames->diff($allowedFilterNames);
228
229
        if ($diff->count()) {
230
            throw InvalidFilterQuery::filtersNotAllowed($diff, $allowedFilterNames);
231
        }
232
    }
233
234
    protected function guardAgainstUnknownSorts()
235
    {
236
        $sorts = $this->request->sorts()->map(function ($sort) {
237
            return ltrim($sort, '-');
238
        });
239
240
        $diff = $sorts->diff($this->allowedSorts);
241
242
        if ($diff->count()) {
243
            throw InvalidSortQuery::sortsNotAllowed($diff, $this->allowedSorts);
244
        }
245
    }
246
247
    protected function guardAgainstUnknownIncludes()
248
    {
249
        $includes = $this->request->includes();
250
251
        $diff = $includes->diff($this->allowedIncludes);
252
253
        if ($diff->count()) {
254
            throw InvalidIncludeQuery::includesNotAllowed($diff, $this->allowedIncludes);
255
        }
256
    }
257
}
258