Completed
Push — develop ( db9bd8...90d90f )
by Neomerx
04:13 queued 02:35
created

QueryParser::parseFilterLink()   C

Complexity

Conditions 11
Paths 9

Size

Total Lines 39
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 11

Importance

Changes 0
Metric Value
dl 0
loc 39
ccs 22
cts 22
cp 1
rs 5.2653
c 0
b 0
f 0
cc 11
eloc 24
nc 9
nop 0
crap 11

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Limoncello\Flute\Exceptions\InvalidQueryParametersException;
23
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;
24
use Neomerx\JsonApi\Encoder\Parameters\EncodingParameters;
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 28
    public function __construct(PaginationStrategyInterface $paginationStrategy, array $messages = null)
79
    {
80 28
        $parameters = [];
81 28
        parent::__construct($parameters, $messages);
82
83 28
        $this->paginationStrategy = $paginationStrategy;
84
85 28
        $this->clear();
86
    }
87
88
    /**
89
     * @inheritdoc
90
     */
91 10
    public function withAllowedFilterFields(array $fields): QueryParserInterface
92
    {
93
        // debug check all fields are strings
94 10
        assert(
95 10
            (function () use ($fields) {
96 10
                $allAreStrings = !empty($fields);
97 10
                foreach ($fields as $field) {
98 10
                    $allAreStrings = $allAreStrings === true && is_string($field) === true && empty($field) === false;
99
                }
100
101 10
                return $allAreStrings;
102 10
            })() === true
103
        );
104
105 10
        $this->allowedFilterFields = $fields;
106
107 10
        return $this;
108
    }
109
110
    /**
111
     * @inheritdoc
112
     */
113 11
    public function withAllAllowedFilterFields(): QueryParserInterface
114
    {
115 11
        $this->allowedFilterFields = null;
116
117 11
        return $this;
118
    }
119
120
    /**
121
     * @inheritdoc
122
     */
123 28
    public function withNoAllowedFilterFields(): QueryParserInterface
124
    {
125 28
        $this->allowedFilterFields = [];
126
127 28
        return $this;
128
    }
129
130
    /**
131
     * @inheritdoc
132
     */
133 9
    public function withAllowedSortFields(array $fields): QueryParserInterface
134
    {
135
        // debug check all fields are strings
136 9
        assert(
137 9
            (function () use ($fields) {
138 9
                $allAreStrings = !empty($fields);
139 9
                foreach ($fields as $field) {
140 9
                    $allAreStrings = $allAreStrings === true && is_string($field) === true && empty($field) === false;
141
                }
142
143 9
                return $allAreStrings;
144 9
            })() === true
145
        );
146
147 9
        $this->allowedSortFields = $fields;
148
149 9
        return $this;
150
    }
151
152
    /**
153
     * @inheritdoc
154
     */
155 11
    public function withAllAllowedSortFields(): QueryParserInterface
156
    {
157 11
        $this->allowedSortFields = null;
158
159 11
        return $this;
160
    }
161
162
    /**
163
     * @inheritdoc
164
     */
165 28
    public function withNoAllowedSortFields(): QueryParserInterface
166
    {
167 28
        $this->allowedSortFields = [];
168
169 28
        return $this;
170
    }
171
172
    /**
173
     * @inheritdoc
174
     */
175 12
    public function withAllowedIncludePaths(array $paths): QueryParserInterface
176
    {
177
        // debug check all fields are strings
178 12
        assert(
179 12
            (function () use ($paths) {
180 12
                $allAreStrings = !empty($paths);
181 12
                foreach ($paths as $path) {
182 12
                    $allAreStrings = $allAreStrings === true && is_string($path) === true && empty($path) === false;
183
                }
184
185 12
                return $allAreStrings;
186 12
            })() === true
187
        );
188
189 12
        $this->allowedIncludePaths = $paths;
190
191 12
        return $this;
192
    }
193
194
    /**
195
     * @inheritdoc
196
     */
197 11
    public function withAllAllowedIncludePaths(): QueryParserInterface
198
    {
199 11
        $this->allowedIncludePaths = null;
200
201 11
        return $this;
202
    }
203
204
    /**
205
     * @inheritdoc
206
     */
207 28
    public function withNoAllowedIncludePaths(): QueryParserInterface
208
    {
209 28
        $this->allowedIncludePaths = [];
210
211 28
        return $this;
212
    }
213
214
    /**
215
     * @inheritdoc
216
     */
217 27
    public function parse(array $parameters): QueryParserInterface
218
    {
219 27
        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...
220
221 27
        $this->parsePagingParameters()->parseFilterLink();
222
223 23
        return $this;
224
    }
225
226
    /**
227
     * @inheritdoc
228
     */
229 18
    public function areFiltersWithAnd(): bool
230
    {
231 18
        return $this->areFiltersWithAnd;
232
    }
233
234
    /**
235
     * @inheritdoc
236
     */
237 19
    public function getFilters(): iterable
238
    {
239 19
        foreach ($this->getFilterParameters() as $field => $operationsWithArgs) {
240 11
            if (is_string($field) === false || empty($field) === true ||
241 11
                is_array($operationsWithArgs) === false || empty($operationsWithArgs) === true
242
            ) {
243 1
                throw new InvalidQueryParametersException($this->createParameterError(static::PARAM_FILTER));
244
            }
245
246 10
            if ($this->allowedFilterFields === null || in_array($field, $this->allowedFilterFields) === true) {
247 10
                yield $field => $this->parseOperationsAndArguments(static::PARAM_FILTER, $operationsWithArgs);
248
            }
249
        }
250
    }
251
252
    /**
253
     * @inheritdoc
254
     */
255 16
    public function getSorts(): iterable
256
    {
257 16
        foreach (parent::getSorts() as $field => $isAsc) {
258 6
            if ($this->allowedSortFields === null || in_array($field, $this->allowedSortFields) === true) {
259 6
                yield $field => $isAsc;
260
            }
261
        }
262
    }
263
264
    /**
265
     * @inheritdoc
266
     */
267 17
    public function getIncludes(): iterable
268
    {
269 17
        foreach (parent::getIncludes() as $path => $split) {
270 5
            if ($this->allowedIncludePaths === null || in_array($path, $this->allowedIncludePaths) === true) {
271 5
                yield $path => $split;
272
            }
273
        }
274
    }
275
276
    /**
277
     * @inheritdoc
278
     */
279 15
    public function getPagingOffset(): ?int
280
    {
281 15
        return $this->pagingOffset;
282
    }
283
284
    /**
285
     * @inheritdoc
286
     */
287 15
    public function getPagingLimit(): ?int
288
    {
289 15
        return $this->pagingLimit;
290
    }
291
292
    /**
293
     * @inheritdoc
294
     */
295 16
    public function createEncodingParameters(): EncodingParametersInterface
296
    {
297 16
        $paths = null;
298 16
        foreach ($this->getIncludes() as $path => $links) {
299 3
            $paths[] = $path;
300
        }
301
302 16
        $fields = $this->deepReadIterable($this->getParameters()[static::PARAM_FIELDS] ?? []);
303
304
        // encoder uses only these parameters and the rest are ignored
305 16
        return new EncodingParameters($paths, empty($fields) === true ? null : $fields);
306
    }
307
308
    /**
309
     * @return self
310
     */
311 27
    private function parsePagingParameters(): self
312
    {
313 27
        $pagingParams = $this->getParameters()[static::PARAM_PAGE] ?? null;
314
315 27
        list ($this->pagingOffset, $this->pagingLimit) = $this->getPaginationStrategy()->parseParameters($pagingParams);
316 27
        assert(is_int($this->pagingOffset) === true && $this->pagingOffset >= 0);
317 27
        assert(is_int($this->pagingLimit) === true && $this->pagingLimit > 0);
318
319 27
        return $this;
320
    }
321
322
    /**
323
     * Pre-parsing for filter parameters.
324
     *
325
     * @return self
326
     */
327 27
    private function parseFilterLink(): self
328
    {
329 27
        if (array_key_exists(static::PARAM_FILTER, $this->getParameters()) === false) {
330 12
            $this->setFiltersWithAnd()->setFilterParameters([]);
331
332 12
            return $this;
333
        }
334
335 15
        $filterSection = $this->getParameters()[static::PARAM_FILTER];
336 15
        if (is_array($filterSection) === false || empty($filterSection) === true) {
337 2
            throw new InvalidQueryParametersException($this->createParameterError(static::PARAM_FILTER));
338
        }
339
340 13
        $isWithAnd = true;
341 13
        reset($filterSection);
342
343
        // check if top level element is `AND` or `OR`
344 13
        $firstKey   = key($filterSection);
345 13
        $firstLcKey = strtolower(trim($firstKey));
346 13
        if (($hasOr = ($firstLcKey === 'or')) || $firstLcKey === 'and') {
347 4
            if (count($filterSection) > 1 ||
348 4
                empty($filterSection = $filterSection[$firstKey]) === true ||
349 4
                is_array($filterSection) === false
350
            ) {
351 2
                throw new InvalidQueryParametersException($this->createParameterError(static::PARAM_FILTER));
352
            } else {
353 2
                $this->setFilterParameters($filterSection);
354 2
                if ($hasOr === true) {
355 2
                    $isWithAnd = false;
356
                }
357
            }
358
        } else {
359 9
            $this->setFilterParameters($filterSection);
360
        }
361
362 11
        $isWithAnd === true ? $this->setFiltersWithAnd() : $this->setFiltersWithOr();
363
364 11
        return $this;
365
    }
366
367
    /**
368
     * @param array $values
369
     *
370
     * @return self
371
     */
372 23
    private function setFilterParameters(array $values): self
373
    {
374 23
        $this->filterParameters = $values;
375
376 23
        return $this;
377
    }
378
379
    /**
380
     * @return array
381
     */
382 19
    private function getFilterParameters(): array
383
    {
384 19
        return $this->filterParameters;
385
    }
386
387
    /**
388
     * @return self
389
     */
390 21
    private function setFiltersWithAnd(): self
391
    {
392 21
        $this->areFiltersWithAnd = true;
393
394 21
        return $this;
395
    }
396
397
    /**
398
     * @return self
399
     */
400 2
    private function setFiltersWithOr(): self
401
    {
402 2
        $this->areFiltersWithAnd = false;
403
404 2
        return $this;
405
    }
406
407
    /**
408
     * @return PaginationStrategyInterface
409
     */
410 27
    private function getPaginationStrategy(): PaginationStrategyInterface
411
    {
412 27
        return $this->paginationStrategy;
413
    }
414
415
    /**
416
     * @return self
417
     */
418 28
    private function clear(): self
419
    {
420 28
        $this->filterParameters  = [];
421 28
        $this->areFiltersWithAnd = true;
422 28
        $this->pagingOffset      = null;
423 28
        $this->pagingLimit       = null;
424
425 28
        $this->withNoAllowedFilterFields()->withNoAllowedSortFields()->withNoAllowedIncludePaths();
426
427 28
        return $this;
428
    }
429
430
    /**
431
     * @param string $parameterName
432
     * @param array  $value
433
     *
434
     * @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...
435
     */
436 9
    private function parseOperationsAndArguments(string $parameterName, array $value): iterable
437
    {
438
        // in this case we interpret it as an [operation => 'comma separated argument(s)']
439 9
        foreach ($value as $operationName => $arguments) {
440 9
            if (is_string($operationName) === false || empty($operationName) === true ||
441 9
                is_string($arguments) === false || empty($arguments) === true
442
            ) {
443 1
                $title = static::MSG_ERR_INVALID_OPERATION_ARGUMENTS;
444 1
                $error = $this->createQueryError($parameterName, $title);
445 1
                throw new InvalidQueryParametersException($error);
446
            }
447
448 8
            yield $operationName => $this->splitCommaSeparatedStringAndCheckNoEmpties($parameterName, $arguments);
449
        }
450
    }
451
452
    /**
453
     * @param iterable $input
454
     *
455
     * @return array
456
     */
457 16
    private function deepReadIterable(iterable $input): array
458
    {
459 16
        $result = [];
460
461 16
        foreach ($input as $key => $value) {
462 1
            $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...
463
        }
464
465 16
        return $result;
466
    }
467
}
468