Passed
Push — master ( a09f34...0235c3 )
by Milroy
13:39
created

QueryBuilderAbstract::get()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 7
ccs 1
cts 1
cp 1
crap 2
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ApiChef\RequestToEloquent;
6
7
use ApiChef\RequestQueryHelper\Fields;
8
use ApiChef\RequestQueryHelper\PaginationParams;
9
use ApiChef\RequestQueryHelper\QueryParamBag;
10
use ApiChef\RequestQueryHelper\SortField;
11
use ApiChef\RequestQueryHelper\Sorts;
12
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
13
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
14
use Illuminate\Database\Query\Builder as QueryBuilder;
15
use Illuminate\Http\Request;
16
use Illuminate\Support\Arr;
17
use Illuminate\Support\Str;
18
use RuntimeException;
19
20
abstract class QueryBuilderAbstract
21
{
22
    private Request $request;
23
    protected Fields $fields;
24
    protected QueryParamBag $includes;
25
    protected QueryParamBag $filters;
26
    protected Sorts $sorts;
27
    protected PaginationParams $paginationParams;
28
    protected ?int $defaultPageSize = null;
29
    private array $allowedIncludes = [];
30
    private ?array $allPossibleAvailableIncludes = null;
31
    protected array $availableIncludes = [];
32
    protected array $availableFilters = [];
33
    protected array $availableSorts = [];
34
35
    /** @var EloquentBuilder|QueryBuilder */
36
    private $query;
37
38
    public function __construct(Request $request)
39
    {
40
        $this->request = $request;
41
        $this->fields = $request->fields();
42
        $this->includes = $request->includes();
43
        $this->filters = $request->filters();
44
        $this->sorts = $request->sorts();
45
        $this->paginationParams = $request->paginationParams();
46
        $this->query = $this->init($request);
47
    }
48
49
    /**
50
     * Initialise the query.
51
     *
52
     * @param Request $request
53
     * @return EloquentBuilder|QueryBuilder
54
     */
55 57
    abstract protected function init(Request $request);
56
57 57
    protected function includesMap(): array
58 57
    {
59 57
        return [];
60 57
    }
61 57
62 57
    protected function filtersMap(): array
63 57
    {
64 57
        return [];
65
    }
66
67
    protected function sortsMap(): array
68
    {
69
        return [];
70
    }
71
72
    public function parseAllowedIncludes(array $allowedIncludes): self
73
    {
74
        $this->allowedIncludes = $this->getPossibleRelationshipCombinations($allowedIncludes);
75
76
        return $this;
77
    }
78
79
    private function getPossibleRelationshipCombinations(array $relations): array
80
    {
81
        $combinations = [];
82
83
        foreach ($relations as $relation) {
84
            $combination = null;
85
            foreach (explode('.', $relation) as $part) {
86
                $combination .= is_null($combination) ? $part : ".{$part}";
87
                if (! in_array($combination, $combinations)) {
88
                    array_push($combinations, $combination);
89 21
                }
90
            }
91 21
        }
92
93 21
        return $combinations;
94
    }
95
96 21
    public function query()
97
    {
98 21
        if (! empty($this->allowedIncludes) && $this->includes->filled()) {
99
            $this->loadIncludes($this->includes);
100 21
        }
101 21
102 21
        if ($this->filters->filled()) {
103 21
            $this->applyFilters($this->filters);
104 21
        }
105 21
106
        if ($this->sorts->filled()) {
107
            $this->applySorts($this->sorts);
108
        }
109
110 21
        return $this->query;
111
    }
112
113
    public function get(array $columns = ['*'])
114
    {
115
        if ($this->shouldPaginate()) {
116
            return $this->paginate($columns);
117
        }
118 57
119
        return $this->query()->get($columns);
120 57
    }
121 21
122
    public function paginateWithTotal(array $columns = ['*']): LengthAwarePaginator
123
    {
124 54
        return $this->paginate($columns, true);
125 12
    }
126
127
    private function paginate(array $columns, bool $withTotalCount = false)
128 51
    {
129 12
        $paginate = $withTotalCount ? 'paginate' : 'simplePaginate';
130
131
        return $this->query()->{$paginate}(
132 48
            $this->paginationParams->perPage($this->defaultPageSize),
133
            $columns,
134
            $this->paginationParams->pageName(),
135
            $this->paginationParams->page(1)
136
        );
137
    }
138
139
    private function shouldPaginate(): bool
140
    {
141 33
        return $this->defaultPageSize !== null || $this->paginationParams->filled();
142
    }
143 33
144 6
    public function first(array $columns = ['*'])
145 6
    {
146 4
        return $this->query()->first($columns);
147 6
    }
148 6
149
    private function loadIncludes(QueryParamBag $includes): void
150
    {
151
        $includes->each(function ($params, $relation) {
152 27
            if ($this->isAllowedToInclude($relation)) {
153
                $this->loadRelation($relation, $params);
154
            }
155
        });
156
    }
157
158
    private function isAllowedToInclude($relation): bool
159
    {
160
        return in_array($relation, $this->allowedIncludes);
161 21
    }
162
163 21
    private function loadRelation($relation, $params): void
164
    {
165
        if ($relationAlias = Arr::get($this->includesMap(), $relation)) {
166 21
            $this->query->with($relationAlias);
167
168
            return;
169 21
        }
170 21
171
        $methodName = 'include'.Str::studly(str_replace('.', 'With', $relation));
172 21
        if (method_exists($this, $methodName)) {
173 18
            $this->{$methodName}($this->query, $params);
174
175 21
            return;
176
        }
177 21
178
        if (is_null($this->allPossibleAvailableIncludes)) {
179
            $this->allPossibleAvailableIncludes = $this->getPossibleRelationshipCombinations($this->availableIncludes);
180 21
        }
181
182 21
        if (in_array($relation, $this->allPossibleAvailableIncludes)) {
0 ignored issues
show
Bug introduced by
It seems like $this->allPossibleAvailableIncludes can also be of type null; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

182
        if (in_array($relation, /** @scrutinizer ignore-type */ $this->allPossibleAvailableIncludes)) {
Loading history...
183 3
            $this->query->with($relation);
184
185 3
            return;
186
        }
187
188 18
        throw new RuntimeException("Trying to include non existing relationship {$relation}");
189 18
    }
190 6
191
    private function applyFilters(QueryParamBag $filters): void
192 6
    {
193
        $filters->each(function ($params, $scope) {
194
            $this->applyFilter($scope, $params);
195 12
        });
196 12
    }
197
198
    private function applyFilter($scope, $params): void
199 12
    {
200 9
        if ($filterAlias = Arr::get($this->filtersMap(), $scope)) {
201
            $this->query->{$filterAlias}($params);
202 9
203
            return;
204
        }
205 3
206
        $methodName = 'filterBy'.Str::studly($scope);
207
208 12
        if (method_exists($this, $methodName)) {
209
            $this->{$methodName}($this->query, $params);
210
211 12
            return;
212 12
        }
213 9
214
        if (in_array($scope, $this->availableFilters)) {
215 12
            $this->query->{$scope}($params);
216
217 12
            return;
218 3
        }
219
220 3
        throw new RuntimeException("Trying to filter by non existing filter {$scope}");
221
    }
222
223 9
    private function applySorts(Sorts $sorts): void
224
    {
225 9
        $sorts->each(function (SortField $sortField) {
226 3
            $this->applySort($sortField->getField(), $sortField->getDirection(), $sortField->getParams());
227
        });
228 3
    }
229
230
    private function applySort(string $field, string $direction, string $param = null): void
231 6
    {
232 3
        if ($sortAlias = Arr::get($this->sortsMap(), $field)) {
233
            $this->query->orderBy($sortAlias, $direction);
234 3
235
            return;
236
        }
237 3
238
        $methodName = 'sortBy'.Str::studly($field);
239
240 12
        if (method_exists($this, $methodName)) {
241
            $this->{$methodName}($this->query, $direction, $param);
242
243 12
            return;
244 12
        }
245 9
246
        if (in_array($field, $this->availableSorts)) {
247 12
            $this->query->orderBy($field, $direction);
248
249 12
            return;
250 3
        }
251
252 3
        throw new RuntimeException("Trying to sort by non existing field {$field}");
253
    }
254
}
255