Completed
Push — master ( b6f561...db9bd8 )
by Neomerx
03:16
created

QueryParser::getFilters()   B

Complexity

Conditions 8
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 8.1867

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 6
cts 7
cp 0.8571
rs 7.7777
c 0
b 0
f 0
cc 8
eloc 7
nc 4
nop 0
crap 8.1867
1
<?php namespace Limoncello\Flute\Http\Query;
2
3
/**
4
 * Copyright 2015-2017 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use Generator;
20
use Limoncello\Flute\Contracts\Adapters\PaginationStrategyInterface;
21
use Limoncello\Flute\Contracts\Http\Query\QueryParserInterface;
22
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;
23
use Neomerx\JsonApi\Encoder\Parameters\EncodingParameters;
24
use Neomerx\JsonApi\Exceptions\JsonApiException;
25
26
/**
27
 * @package Limoncello\Flute
28
 */
29
class QueryParser extends BaseQueryParser implements QueryParserInterface
30
{
31
    /** Message */
32
    public const MSG_ERR_INVALID_OPERATION_ARGUMENTS = 'Invalid Operation Arguments.';
33
34
    /**
35
     * @var PaginationStrategyInterface
36
     */
37
    private $paginationStrategy;
38
39
    /**
40
     * @var array
41
     */
42
    private $filterParameters;
43
44
    /**
45
     * @var bool
46
     */
47
    private $areFiltersWithAnd;
48
49
    /**
50
     * @var int|null
51
     */
52
    private $pagingOffset;
53
54
    /**
55
     * @var int|null
56
     */
57
    private $pagingLimit;
58
59
    /**
60
     * @var string[]|null
61
     */
62
    private $allowedFilterFields;
63
64
    /**
65
     * @var string[]|null
66
     */
67
    private $allowedSortFields;
68
69
    /**
70
     * @var string[]|null
71
     */
72
    private $allowedIncludePaths;
73
74
    /**
75
     * @param PaginationStrategyInterface $paginationStrategy
76
     * @param string[]|null               $messages
77
     */
78 25
    public function __construct(PaginationStrategyInterface $paginationStrategy, array $messages = null)
79
    {
80 25
        $parameters = [];
81 25
        parent::__construct($parameters, $messages);
82
83 25
        $this->paginationStrategy = $paginationStrategy;
84
85 25
        $this->clear();
86
    }
87
88
    /**
89
     * @inheritdoc
90
     */
91
    public function withAllowedFilterFields(array $fields): QueryParserInterface
92
    {
93
        // debug check all fields are strings
94 10
        assert((function () use ($fields) {
95 10
                $allAreStrings = !empty($fields);
96 10
                foreach ($fields as $field) {
97 10
                    $allAreStrings = $allAreStrings === true && is_string($field) === true && empty($field) === false;
98
                }
99
100 10
                return $allAreStrings;
101 10
            })() === true);
102
103 10
        $this->allowedFilterFields = $fields;
104
105 10
        return $this;
106
    }
107
108
    /**
109
     * @inheritdoc
110
     */
111 8
    public function withAllAllowedFilterFields(): QueryParserInterface
112
    {
113 8
        $this->allowedFilterFields = null;
114
115 8
        return $this;
116
    }
117
118
    /**
119
     * @inheritdoc
120
     */
121 25
    public function withNoAllowedFilterFields(): QueryParserInterface
122
    {
123 25
        $this->allowedFilterFields = [];
124
125 25
        return $this;
126
    }
127
128
    /**
129
     * @inheritdoc
130
     */
131
    public function withAllowedSortFields(array $fields): QueryParserInterface
132
    {
133
        // debug check all fields are strings
134 9
        assert((function () use ($fields) {
135 9
                $allAreStrings = !empty($fields);
136 9
                foreach ($fields as $field) {
137 9
                    $allAreStrings = $allAreStrings === true && is_string($field) === true && empty($field) === false;
138
                }
139
140 9
                return $allAreStrings;
141 9
            })() === true);
142
143 9
        $this->allowedSortFields = $fields;
144
145 9
        return $this;
146
    }
147
148
    /**
149
     * @inheritdoc
150
     */
151 8
    public function withAllAllowedSortFields(): QueryParserInterface
152
    {
153 8
        $this->allowedSortFields = null;
154
155 8
        return $this;
156
    }
157
158
    /**
159
     * @inheritdoc
160
     */
161 25
    public function withNoAllowedSortFields(): QueryParserInterface
162
    {
163 25
        $this->allowedSortFields = [];
164
165 25
        return $this;
166
    }
167
168
    /**
169
     * @inheritdoc
170
     */
171
    public function withAllowedIncludePaths(array $paths): QueryParserInterface
172
    {
173
        // debug check all fields are strings
174 12
        assert((function () use ($paths) {
175 12
                $allAreStrings = !empty($paths);
176 12
                foreach ($paths as $path) {
177 12
                    $allAreStrings = $allAreStrings === true && is_string($path) === true && empty($path) === false;
178
                }
179
180 12
                return $allAreStrings;
181 12
            })() === true);
182
183 12
        $this->allowedIncludePaths = $paths;
184
185 12
        return $this;
186
    }
187
188
    /**
189
     * @inheritdoc
190
     */
191 8
    public function withAllAllowedIncludePaths(): QueryParserInterface
192
    {
193 8
        $this->allowedIncludePaths = null;
194
195 8
        return $this;
196
    }
197
198
    /**
199
     * @inheritdoc
200
     */
201 25
    public function withNoAllowedIncludePaths(): QueryParserInterface
202
    {
203 25
        $this->allowedIncludePaths = [];
204
205 25
        return $this;
206
    }
207
208
    /**
209
     * @inheritdoc
210
     */
211 24
    public function parse(array $parameters): QueryParserInterface
212
    {
213 24
        parent::setParameters($parameters);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (setParameters() instead of parse()). Are you sure this is correct? If so, you might want to change this to $this->setParameters().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
214
215 24
        $this->parsePagingParameters()->parseFilterLink();
216
217 20
        return $this;
218
    }
219
220
    /**
221
     * @inheritdoc
222
     */
223 18
    public function areFiltersWithAnd(): bool
224
    {
225 18
        return $this->areFiltersWithAnd;
226
    }
227
228
    /**
229
     * @inheritdoc
230
     */
231 17
    public function getFilters(): iterable
232
    {
233 17
        foreach ($this->getFilterParameters() as $field => $operationsWithArgs) {
234 9
            if (is_string($field) === false || empty($field) === true ||
235 9
                is_array($operationsWithArgs) === false || empty($operationsWithArgs) === true
236
            ) {
237
                throw new JsonApiException($this->createParameterError(static::PARAM_FILTER));
238
            }
239
240 9
            if ($this->allowedFilterFields === null || in_array($field, $this->allowedFilterFields) === true) {
241 9
                yield $field => $this->parseOperationsAndArguments(static::PARAM_FILTER, $operationsWithArgs);
242
            }
243
        }
244
    }
245
246
    /**
247
     * @inheritdoc
248
     */
249 16
    public function getSorts(): iterable
250
    {
251 16
        foreach (parent::getSorts() as $field => $isAsc) {
252 6
            if ($this->allowedSortFields === null || in_array($field, $this->allowedSortFields) === true) {
253 6
                yield $field => $isAsc;
254
            }
255
        }
256
    }
257
258
    /**
259
     * @inheritdoc
260
     */
261 16
    public function getIncludes(): iterable
262
    {
263 16
        foreach (parent::getIncludes() as $path => $split) {
264 5
            if ($this->allowedIncludePaths === null || in_array($path, $this->allowedIncludePaths) === true) {
265 5
                yield $path => $split;
266
            }
267
        }
268
    }
269
270
    /**
271
     * @inheritdoc
272
     */
273 15
    public function getPagingOffset(): ?int
274
    {
275 15
        return $this->pagingOffset;
276
    }
277
278
    /**
279
     * @inheritdoc
280
     */
281 15
    public function getPagingLimit(): ?int
282
    {
283 15
        return $this->pagingLimit;
284
    }
285
286
    /**
287
     * @inheritdoc
288
     */
289 15
    public function createEncodingParameters(): EncodingParametersInterface
290
    {
291 15
        $paths = null;
292 15
        foreach ($this->getIncludes() as $path => $links) {
293 3
            $paths[] = $path;
294
        }
295
296 15
        $fields = $this->deepReadIterable($this->getParameters()[static::PARAM_FIELDS] ?? []);
297
298
        // encoder uses only these parameters and the rest are ignored
299 15
        return new EncodingParameters($paths, empty($fields) === true ? null : $fields);
300
    }
301
302
    /**
303
     * @return self
304
     */
305 24
    private function parsePagingParameters(): self
306
    {
307 24
        $pagingParams = $this->getParameters()[static::PARAM_PAGE] ?? null;
308
309 24
        list ($this->pagingOffset, $this->pagingLimit) = $this->getPaginationStrategy()->parseParameters($pagingParams);
310 24
        assert(is_int($this->pagingOffset) === true && $this->pagingOffset >= 0);
311 24
        assert(is_int($this->pagingLimit) === true && $this->pagingLimit > 0);
312
313 24
        return $this;
314
    }
315
316
    /**
317
     * Pre-parsing for filter parameters.
318
     *
319
     * @return self
320
     */
321 24
    private function parseFilterLink(): self
322
    {
323 24
        if (array_key_exists(static::PARAM_FILTER, $this->getParameters()) === false) {
324 11
            $this->setFiltersWithAnd()->setFilterParameters([]);
325
326 11
            return $this;
327
        }
328
329 13
        $filterSection = $this->getParameters()[static::PARAM_FILTER];
330 13
        if (is_array($filterSection) === false || empty($filterSection) === true) {
331 2
            throw new JsonApiException($this->createParameterError(static::PARAM_FILTER));
332
        }
333
334 11
        $isWithAnd = true;
335 11
        reset($filterSection);
336
337
        // check if top level element is `AND` or `OR`
338 11
        $firstKey   = key($filterSection);
339 11
        $firstLcKey = strtolower(trim($firstKey));
340 11
        if (($hasOr = ($firstLcKey === 'or')) || $firstLcKey === 'and') {
341 4
            if (count($filterSection) > 1 ||
342 4
                empty($filterSection = $filterSection[$firstKey]) === true ||
343 4
                is_array($filterSection) === false
344
            ) {
345 2
                throw new JsonApiException($this->createParameterError(static::PARAM_FILTER));
346
            } else {
347 2
                $this->setFilterParameters($filterSection);
348 2
                if ($hasOr === true) {
349 2
                    $isWithAnd = false;
350
                }
351
            }
352
        } else {
353 7
            $this->setFilterParameters($filterSection);
354
        }
355
356 9
        $isWithAnd === true ? $this->setFiltersWithAnd() : $this->setFiltersWithOr();
357
358 9
        return $this;
359
    }
360
361
    /**
362
     * @param array $values
363
     *
364
     * @return self
365
     */
366 20
    private function setFilterParameters(array $values): self
367
    {
368 20
        $this->filterParameters = $values;
369
370 20
        return $this;
371
    }
372
373
    /**
374
     * @return array
375
     */
376 17
    private function getFilterParameters(): array
377
    {
378 17
        return $this->filterParameters;
379
    }
380
381
    /**
382
     * @return self
383
     */
384 18
    private function setFiltersWithAnd(): self
385
    {
386 18
        $this->areFiltersWithAnd = true;
387
388 18
        return $this;
389
    }
390
391
    /**
392
     * @return self
393
     */
394 2
    private function setFiltersWithOr(): self
395
    {
396 2
        $this->areFiltersWithAnd = false;
397
398 2
        return $this;
399
    }
400
401
    /**
402
     * @return PaginationStrategyInterface
403
     */
404 24
    private function getPaginationStrategy(): PaginationStrategyInterface
405
    {
406 24
        return $this->paginationStrategy;
407
    }
408
409
    /**
410
     * @return self
411
     */
412 25
    private function clear(): self
413
    {
414 25
        $this->filterParameters  = [];
415 25
        $this->areFiltersWithAnd = true;
416 25
        $this->pagingOffset      = null;
417 25
        $this->pagingLimit       = null;
418
419 25
        $this->withNoAllowedFilterFields()->withNoAllowedSortFields()->withNoAllowedIncludePaths();
420
421 25
        return $this;
422
    }
423
424
    /**
425
     * @param string $parameterName
426
     * @param array  $value
427
     *
428
     * @return iterable
0 ignored issues
show
Documentation introduced by
Should the return type not be Generator?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
429
     */
430 8
    private function parseOperationsAndArguments(string $parameterName, array $value): iterable
431
    {
432
        // in this case we interpret it as an [operation => 'comma separated argument(s)']
433 8
        foreach ($value as $operationName => $arguments) {
434 8
            if (is_string($operationName) === false || empty($operationName) === true ||
435 8
                is_string($arguments) === false || empty($arguments) === true
436
            ) {
437
                $title = static::MSG_ERR_INVALID_OPERATION_ARGUMENTS;
438
                $error = $this->createQueryError($parameterName, $title);
439
                throw new JsonApiException($error);
440
            }
441
442 8
            yield $operationName => $this->splitCommaSeparatedStringAndCheckNoEmpties($parameterName, $arguments);
443
        }
444
    }
445
446
    /**
447
     * @param iterable $input
448
     *
449
     * @return array
450
     */
451 15
    private function deepReadIterable(iterable $input): array
452
    {
453 15
        $result = [];
454
455 15
        foreach ($input as $key => $value) {
456
            $result[$key] = $value instanceof Generator ? $this->deepReadIterable($value) : $value;
0 ignored issues
show
Documentation introduced by
$value is of type object<Generator>, but the function expects a object<Limoncello\Flute\Http\Query\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
457
        }
458
459 15
        return $result;
460
    }
461
}
462