Completed
Push — master ( 6173d9...210649 )
by Neomerx
04:26
created

QueryParser   F

Complexity

Total Complexity 112

Size/Duplication

Total Lines 1010
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 112
lcom 1
cbo 11
dl 0
loc 1010
ccs 317
cts 317
cp 1
rs 1.56
c 0
b 0
f 0

57 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 31 1
A parse() 0 10 1
A areFiltersWithAnd() 0 4 1
A hasFilters() 0 4 1
A hasFields() 0 4 1
A hasIncludes() 0 4 1
A hasSorts() 0 4 1
A hasPaging() 0 4 1
A getIdentity() 0 8 2
A getFilters() 0 8 2
A getFields() 0 9 2
A getSorts() 0 9 2
A getIncludes() 0 9 2
A getPagingOffset() 0 4 1
A getPagingLimit() 0 4 1
A setParameters() 0 6 1
A getParameters() 0 4 1
A setMessages() 0 6 1
A getMessage() 0 6 3
A getJsonErrors() 0 4 1
A getValidationErrors() 0 4 1
A getCaptures() 0 4 1
A getContext() 0 4 1
A getValidationBlocks() 0 4 1
A getSerializedRuleSet() 0 4 1
A getFormatterFactory() 0 4 1
A getFormatter() 0 8 2
A hasParameter() 0 4 1
A getValidatedIdentity() 0 20 2
B getValidatedFilters() 0 39 9
A getValidatedFields() 0 26 5
A getValidatedSorts() 0 23 5
A getValidatedIncludes() 0 23 5
A iterableToArray() 0 10 3
A validatePageOffset() 0 7 1
A validatePageLimit() 0 7 1
A validatePaginationValue() 0 17 3
A validationStarts() 0 13 1
A validateAndThrowOnError() 0 12 1
A validateAndAccumulateError() 0 11 1
A validateEnds() 0 10 1
A readSingleCapturedValue() 0 7 1
A validateValues() 0 10 3
A validateFilterArguments() 0 6 2
A parsePagingParameters() 0 14 3
A checkValidationQueueErrors() 0 10 3
A setIdentityParameter() 0 6 1
A getIdentityParameter() 0 4 1
A setFilterParameters() 0 6 1
A getFilterParameters() 0 4 1
A setFiltersWithAnd() 0 6 1
A setFiltersWithOr() 0 6 1
A clear() 0 19 1
B parseFilterLink() 0 45 11
B parseOperationsAndArguments() 0 23 6
A createQueryError() 0 7 1
A getInvalidParamMessage() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like QueryParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use QueryParser, and based on these observations, apply Extract Interface, too.

1
<?php namespace Limoncello\Flute\Validation\JsonApi;
2
3
/**
4
 * Copyright 2015-2018 [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\Contracts\L10n\FormatterFactoryInterface;
21
use Limoncello\Contracts\L10n\FormatterInterface;
22
use Limoncello\Flute\Contracts\Validation\ErrorCodes;
23
use Limoncello\Flute\Contracts\Validation\JsonApiQueryRulesSerializerInterface;
24
use Limoncello\Flute\Contracts\Validation\JsonApiQueryValidatingParserInterface;
25
use Limoncello\Flute\Exceptions\InvalidQueryParametersException;
26
use Limoncello\Flute\Package\FluteSettings;
27
use Limoncello\Flute\Resources\Messages\En\Validation;
28
use Limoncello\Flute\Validation\JsonApi\Execution\JsonApiErrorCollection;
29
use Limoncello\Validation\Contracts\Captures\CaptureAggregatorInterface;
30
use Limoncello\Validation\Contracts\Errors\ErrorAggregatorInterface;
31
use Limoncello\Validation\Contracts\Execution\ContextStorageInterface;
32
use Limoncello\Validation\Errors\Error;
33
use Limoncello\Validation\Execution\BlockInterpreter;
34
use Neomerx\JsonApi\Contracts\Document\ErrorInterface;
35
use Neomerx\JsonApi\Document\Error as JsonApiError;
36
use Neomerx\JsonApi\Exceptions\JsonApiException;
37
use Neomerx\JsonApi\Http\Query\BaseQueryParserTrait;
38
39
/**
40
 * @package Limoncello\Flute
41
 *
42
 * @SuppressWarnings(PHPMD.TooManyFields)
43
 * @SuppressWarnings(PHPMD.TooManyMethods)
44
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
45
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
46
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
47
 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
48
 */
49
class QueryParser implements JsonApiQueryValidatingParserInterface
50
{
51
    use BaseQueryParserTrait {
52
        BaseQueryParserTrait::getFields as getFieldsImpl;
53
        BaseQueryParserTrait::getIncludes as getIncludesImpl;
54
        BaseQueryParserTrait::getSorts as getSortsImpl;
55
    }
56
57
    /** Message */
58
    public const MSG_ERR_INVALID_PARAMETER = 'Invalid Parameter.';
59
60
    /**
61
     * @var array
62
     */
63
    private $parameters;
64
65
    /**
66
     * @var string[]|null
67
     */
68
    private $messages;
69
70
    /**
71
     * @var null|string
72
     */
73
    private $identityParameter;
74
75
    /**
76
     * @var array
77
     */
78
    private $filterParameters;
79
80
    /**
81
     * @var bool
82
     */
83
    private $areFiltersWithAnd;
84
85
    /**
86
     * @var int|null
87
     */
88
    private $pagingOffset;
89
90
    /**
91
     * @var int|null
92
     */
93
    private $pagingLimit;
94
95
    /**
96
     * NOTE: Despite the type it is just a string so only static methods can be called from the interface.
97
     *
98
     * @var JsonApiQueryRulesSerializerInterface|string
99
     */
100
    private $serializerClass;
101
102
    /**
103
     * @var array
104
     */
105
    private $serializedRuleSet;
106
107
    /**
108
     * @var array
109
     */
110
    private $validationBlocks;
111
112
    /**
113
     * @var ContextStorageInterface
114
     */
115
    private $context;
116
117
    /**
118
     * @var CaptureAggregatorInterface
119
     */
120
    private $captures;
121
122
    /**
123
     * @var ErrorAggregatorInterface
124
     */
125
    private $validationErrors;
126
127
    /**
128
     * @var JsonApiErrorCollection
129
     */
130
    private $jsonErrors;
131
132
    /**
133
     * @var null|mixed
134
     */
135
    private $cachedIdentity = null;
136
137
    /**
138
     * @var null|array
139
     */
140
    private $cachedFilters = null;
141
142
    /**
143
     * @var null|array
144
     */
145
    private $cachedFields = null;
146
147
    /**
148
     * @var null|array
149
     */
150
    private $cachedSorts = null;
151
152
    /**
153
     * @var null|array
154
     */
155
    private $cachedIncludes = null;
156
157
    /**
158
     * @var FormatterFactoryInterface
159
     */
160
    private $formatterFactory;
161
162
    /**
163
     * @var FormatterInterface|null
164
     */
165
    private $formatter;
166
167
    /**
168
     * @param string                     $rulesClass
169
     * @param string                     $serializerClass
170
     * @param array                      $serializedData
171
     * @param ContextStorageInterface    $context
172
     * @param CaptureAggregatorInterface $captures
173
     * @param ErrorAggregatorInterface   $validationErrors
174
     * @param JsonApiErrorCollection     $jsonErrors
175
     * @param FormatterFactoryInterface  $formatterFactory
176
     * @param string[]|null              $messages
177
     */
178 36
    public function __construct(
179
        string $rulesClass,
180
        string $serializerClass,
181
        array $serializedData,
182
        ContextStorageInterface $context,
183
        CaptureAggregatorInterface $captures,
184
        ErrorAggregatorInterface $validationErrors,
185
        JsonApiErrorCollection $jsonErrors,
186
        FormatterFactoryInterface $formatterFactory,
187
        array $messages = null
188
    ) {
189 36
        assert(
190 36
            in_array(JsonApiQueryRulesSerializerInterface::class, class_implements($serializerClass)),
191 36
            "`$serializerClass` should implement interface `" . JsonApiQueryRulesSerializerInterface::class . '`.'
192
        );
193
194 36
        $parameters = [];
195 36
        $this->setParameters($parameters)->setMessages($messages);
0 ignored issues
show
Bug introduced by
It seems like $messages defined by parameter $messages on line 187 can also be of type null; however, Limoncello\Flute\Validat...ryParser::setMessages() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
196 36
        $this->serializerClass  = $serializerClass;
197 36
        $this->context          = $context;
198 36
        $this->captures         = $captures;
199 36
        $this->validationErrors = $validationErrors;
200 36
        $this->jsonErrors       = $jsonErrors;
201 36
        $this->formatterFactory = $formatterFactory;
202
203 36
        assert($this->serializerClass::hasRules($rulesClass, $serializedData));
0 ignored issues
show
Bug introduced by
The method hasRules cannot be called on $this->serializerClass (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
204 36
        $this->serializedRuleSet = $this->serializerClass::readRules($rulesClass, $serializedData);
0 ignored issues
show
Bug introduced by
The method readRules cannot be called on $this->serializerClass (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
205 36
        $this->validationBlocks  = $this->serializerClass::readBlocks($serializedData);
0 ignored issues
show
Bug introduced by
The method readBlocks cannot be called on $this->serializerClass (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
206
207 36
        $this->clear();
208
    }
209
210
    /**
211
     * @inheritdoc
212
     */
213 35
    public function parse(?string $identity, array $parameters = []): JsonApiQueryValidatingParserInterface
214
    {
215 35
        $this->clear();
216
217 35
        $this->setIdentityParameter($identity)->setParameters($parameters);
218
219 35
        $this->parsePagingParameters()->parseFilterLink();
220
221 31
        return $this;
222
    }
223
224
    /**
225
     * @inheritdoc
226
     */
227 17
    public function areFiltersWithAnd(): bool
228
    {
229 17
        return $this->areFiltersWithAnd;
230
    }
231
232
    /**
233
     * @inheritdoc
234
     */
235 1
    public function hasFilters(): bool
236
    {
237 1
        return $this->hasParameter(static::PARAM_FILTER);
238
    }
239
240
    /**
241
     * @inheritdoc
242
     */
243 14
    public function hasFields(): bool
244
    {
245 14
        return $this->hasParameter(static::PARAM_FIELDS);
246
    }
247
248
    /**
249
     * @inheritdoc
250
     */
251 14
    public function hasIncludes(): bool
252
    {
253 14
        return $this->hasParameter(static::PARAM_INCLUDE);
254
    }
255
256
    /**
257
     * @inheritdoc
258
     */
259 1
    public function hasSorts(): bool
260
    {
261 1
        return $this->hasParameter(static::PARAM_SORT);
262
    }
263
264
    /**
265
     * @inheritdoc
266
     */
267 1
    public function hasPaging(): bool
268
    {
269 1
        return $this->hasParameter(static::PARAM_PAGE);
270
    }
271
272
    /**
273
     * @inheritdoc
274
     */
275 5
    public function getIdentity()
276
    {
277 5
        if ($this->cachedIdentity === null) {
278 5
            $this->cachedIdentity = $this->getValidatedIdentity();
279
        }
280
281 5
        return $this->cachedIdentity;
282
    }
283
284
    /**
285
     * @inheritdoc
286
     */
287 22
    public function getFilters(): array
288
    {
289 22
        if ($this->cachedFilters === null) {
290 22
            $this->cachedFilters = $this->iterableToArray($this->getValidatedFilters());
0 ignored issues
show
Documentation introduced by
$this->getValidatedFilters() is of type object<Generator>, but the function expects a object<Limoncello\Flute\...ation\JsonApi\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...
291
        }
292
293 18
        return $this->cachedFilters;
294
    }
295
296
    /**
297
     * @inheritdoc
298
     */
299 2
    public function getFields(): array
300
    {
301 2
        if ($this->cachedFields === null) {
302 2
            $fields = $this->getFieldsImpl($this->getParameters(), $this->getInvalidParamMessage());
303 2
            $this->cachedFields = $this->iterableToArray($this->getValidatedFields($fields));
0 ignored issues
show
Documentation introduced by
$fields is of type object<Generator>, but the function expects a object<Limoncello\Flute\...ation\JsonApi\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...
Documentation introduced by
$this->getValidatedFields($fields) is of type object<Generator>, but the function expects a object<Limoncello\Flute\...ation\JsonApi\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...
304
        }
305
306 2
        return $this->cachedFields;
307
    }
308
309
    /**
310
     * @inheritdoc
311
     */
312 16
    public function getSorts(): array
313
    {
314 16
        if ($this->cachedSorts === null) {
315 16
            $sorts = $this->getSortsImpl($this->getParameters(), $this->getInvalidParamMessage());
316 16
            $this->cachedSorts = $this->iterableToArray($this->getValidatedSorts($sorts));
0 ignored issues
show
Documentation introduced by
$sorts is of type object<Generator>, but the function expects a object<Limoncello\Flute\...ation\JsonApi\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...
Documentation introduced by
$this->getValidatedSorts($sorts) is of type object<Generator>, but the function expects a object<Limoncello\Flute\...ation\JsonApi\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...
317
        }
318
319 16
        return $this->cachedSorts;
320
    }
321
322
    /**
323
     * @inheritdoc
324
     */
325 16
    public function getIncludes(): iterable
326
    {
327 16
        if ($this->cachedIncludes === null) {
328 16
            $includes = $this->getIncludesImpl($this->getParameters(), $this->getInvalidParamMessage());
329 16
            $this->cachedIncludes = $this->iterableToArray($this->getValidatedIncludes($includes));
0 ignored issues
show
Documentation introduced by
$includes is of type object<Generator>, but the function expects a object<Limoncello\Flute\...ation\JsonApi\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...
Documentation introduced by
$this->getValidatedIncludes($includes) is of type object<Generator>, but the function expects a object<Limoncello\Flute\...ation\JsonApi\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...
330
        }
331
332 16
        return $this->cachedIncludes;
333
    }
334
335
    /**
336
     * @inheritdoc
337
     */
338 17
    public function getPagingOffset(): ?int
339
    {
340 17
        return $this->pagingOffset;
341
    }
342
343
    /**
344
     * @inheritdoc
345
     */
346 17
    public function getPagingLimit(): ?int
347
    {
348 17
        return $this->pagingLimit;
349
    }
350
351
    /**
352
     * @param array $parameters
353
     *
354
     * @return self
355
     */
356 36
    protected function setParameters(array $parameters): self
357
    {
358 36
        $this->parameters = $parameters;
359
360 36
        return $this;
361
    }
362
363
    /**
364
     * @return array
365
     */
366 35
    protected function getParameters(): array
367
    {
368 35
        return $this->parameters;
369
    }
370
371
    /**
372
     * @param array $messages
373
     *
374
     * @return self
375
     */
376 36
    protected function setMessages(?array $messages): self
377
    {
378 36
        $this->messages = $messages;
379
380 36
        return $this;
381
    }
382
383
    /**
384
     * @param string $message
385
     *
386
     * @return string
387
     */
388 25
    protected function getMessage(string $message): string
389
    {
390 25
        $hasTranslation = $this->messages !== null && array_key_exists($message, $this->messages) === false;
391
392 25
        return $hasTranslation === true ? $this->messages[$message] : $message;
393
    }
394
395
    /**
396
     * @return JsonApiErrorCollection
397
     */
398 6
    protected function getJsonErrors(): JsonApiErrorCollection
399
    {
400 6
        return $this->jsonErrors;
401
    }
402
403
    /**
404
     * @return ErrorAggregatorInterface
405
     */
406 36
    protected function getValidationErrors(): ErrorAggregatorInterface
407
    {
408 36
        return $this->validationErrors;
409
    }
410
411
    /**
412
     * @return CaptureAggregatorInterface
413
     */
414 36
    protected function getCaptures(): CaptureAggregatorInterface
415
    {
416 36
        return $this->captures;
417
    }
418
419
    /**
420
     * @return ContextStorageInterface
421
     */
422 34
    protected function getContext(): ContextStorageInterface
423
    {
424 34
        return $this->context;
425
    }
426
427
    /**
428
     * @return array
429
     */
430 34
    protected function getValidationBlocks(): array
431
    {
432 34
        return $this->validationBlocks;
433
    }
434
435
    /**
436
     * @return array
437
     */
438 35
    protected function getSerializedRuleSet(): array
439
    {
440 35
        return $this->serializedRuleSet;
441
    }
442
443
    /**
444
     * @return FormatterFactoryInterface
445
     */
446 1
    protected function getFormatterFactory(): FormatterFactoryInterface
447
    {
448 1
        return $this->formatterFactory;
449
    }
450
451
    /**
452
     * @return FormatterInterface
453
     */
454 1
    protected function getFormatter(): FormatterInterface
455
    {
456 1
        if ($this->formatter === null) {
457 1
            $this->formatter = $this->getFormatterFactory()->createFormatter(FluteSettings::VALIDATION_NAMESPACE);
458
        }
459
460 1
        return $this->formatter;
461
    }
462
463
    /**
464
     * @param string $name
465
     *
466
     * @return bool
467
     */
468 17
    protected function hasParameter(string $name): bool
469
    {
470 17
        return array_key_exists($name, $this->getParameters());
471
    }
472
473
    /**
474
     * @return mixed
475
     */
476 5
    private function getValidatedIdentity()
477
    {
478
        // without validation
479 5
        $result = $this->getIdentityParameter();
480
481 5
        $ruleIndexes = $this->serializerClass::readIdentityRuleIndexes($this->getSerializedRuleSet());
482 5
        if ($ruleIndexes !== null) {
483
            // with validation
484 5
            $ruleIndex = $this->serializerClass::readRuleMainIndex($ruleIndexes);
485
486
487 5
            $this->validationStarts(static::PARAM_IDENTITY, $ruleIndexes);
488 5
            $this->validateAndThrowOnError(static::PARAM_IDENTITY, $result, $ruleIndex);
489 5
            $this->validateEnds(static::PARAM_IDENTITY, $ruleIndexes);
490
491 5
            $result = $this->readSingleCapturedValue();
492
        }
493
494 5
        return $result;
495
    }
496
497
    /**
498
     * @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...
499
     *
500
     * @SuppressWarnings(PHPMD.ElseExpression)
501
     */
502 22
    private function getValidatedFilters(): iterable
503
    {
504 22
        $ruleIndexes = $this->serializerClass::readFilterRulesIndexes($this->getSerializedRuleSet());
505
506 22
        if ($ruleIndexes === null) {
507
            // without validation
508 1
            foreach ($this->getFilterParameters() as $field => $operationsWithArgs) {
509 1
                yield $field => $this->parseOperationsAndArguments(static::PARAM_FILTER, $operationsWithArgs);
510
            }
511
        } else {
512
            // with validation
513 21
            $mainIndexes = $this->serializerClass::readRuleMainIndexes($ruleIndexes);
514 21
            $this->validationStarts(static::PARAM_FILTER, $ruleIndexes);
515 21
            foreach ($this->getFilterParameters() as $field => $operationsWithArgs) {
516 13
                if (is_string($field) === false || empty($field) === true ||
517 13
                    is_array($operationsWithArgs) === false || empty($operationsWithArgs) === true
518
                ) {
519 1
                    throw new InvalidQueryParametersException($this->createParameterError(
520 1
                        static::PARAM_FILTER,
521 1
                        $this->getInvalidParamMessage()
522
                    ));
523
                }
524
525 12
                if (array_key_exists($field, $mainIndexes) === false) {
526
                    // unknown field set type
527 2
                    $this->getValidationErrors()->add(
528 2
                        new Error(static::PARAM_FILTER, $field, ErrorCodes::INVALID_VALUE, null)
529
                    );
530
                } else {
531
                    // for field a validation rule is defined so input value will be validated
532 10
                    $ruleIndex = $mainIndexes[$field];
533 10
                    $parsed    = $this->parseOperationsAndArguments(static::PARAM_FILTER, $operationsWithArgs);
534
535 12
                    yield $field => $this->validateFilterArguments($ruleIndex, $parsed);
536
                }
537
            }
538 19
            $this->validateEnds(static::PARAM_FILTER, $ruleIndexes);
539
        }
540
    }
541
542
    /**
543
     * @param iterable $fieldsFromParent
544
     *
545
     * @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...
546
     *
547
     * @SuppressWarnings(PHPMD.StaticAccess)
548
     * @SuppressWarnings(PHPMD.ElseExpression)
549
     */
550 2
    private function getValidatedFields(iterable $fieldsFromParent): iterable
551
    {
552 2
        $ruleIndexes = $this->serializerClass::readFieldSetRulesIndexes($this->getSerializedRuleSet());
553
554 2
        if ($ruleIndexes === null) {
555
            // without validation
556 1
            foreach ($fieldsFromParent as $type => $fieldList) {
557 1
                yield $type => $fieldList;
558
            }
559
        } else {
560
            // with validation
561 1
            $mainIndexes = $this->serializerClass::readRuleMainIndexes($ruleIndexes);
562 1
            $this->validationStarts(static::PARAM_FIELDS, $ruleIndexes);
563 1
            foreach ($fieldsFromParent as $type => $fieldList) {
564 1
                if (array_key_exists($type, $mainIndexes) === true) {
565 1
                    yield $type => $this->validateValues($mainIndexes[$type], $fieldList);
566
                } else {
567
                    // unknown field set type
568 1
                    $this->getValidationErrors()->add(
569 1
                        new Error(static::PARAM_FIELDS, $type, ErrorCodes::INVALID_VALUE, null)
570
                    );
571
                }
572
            }
573 1
            $this->validateEnds(static::PARAM_FIELDS, $ruleIndexes);
574
        }
575
    }
576
577
    /**
578
     * @param iterable $sortsFromParent
579
     *
580
     * @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...
581
     *
582
     * @SuppressWarnings(PHPMD.StaticAccess)
583
     * @SuppressWarnings(PHPMD.ElseExpression)
584
     */
585 16
    private function getValidatedSorts(iterable $sortsFromParent): iterable
586
    {
587 16
        $ruleIndexes = $this->serializerClass::readSortsRuleIndexes($this->getSerializedRuleSet());
588
589 16
        if ($ruleIndexes === null) {
590
            // without validation
591 1
            foreach ($sortsFromParent as $field => $isAsc) {
592 1
                yield $field => $isAsc;
593
            }
594
        } else {
595
            // with validation
596 15
            $ruleIndex = $this->serializerClass::readRuleMainIndex($ruleIndexes);
597 15
            $this->validationStarts(static::PARAM_SORT, $ruleIndexes);
598 15
            foreach ($sortsFromParent as $field => $isAsc) {
599 5
                $this->getCaptures()->clear();
600 5
                $this->validateAndAccumulateError($field, $ruleIndex);
601 5
                if ($this->getCaptures()->count() > 0) {
602 5
                    yield $this->readSingleCapturedValue() => $isAsc;
603
                }
604
            }
605 15
            $this->validateEnds(static::PARAM_SORT, $ruleIndexes);
606
        }
607
    }
608
609
    /**
610
     * @param iterable $includesFromParent
611
     *
612
     * @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...
613
     *
614
     * @SuppressWarnings(PHPMD.StaticAccess)
615
     * @SuppressWarnings(PHPMD.ElseExpression)
616
     */
617 16
    private function getValidatedIncludes(iterable $includesFromParent): iterable
618
    {
619 16
        $ruleIndexes = $this->serializerClass::readIncludesRuleIndexes($this->getSerializedRuleSet());
620
621 16
        if ($ruleIndexes === null) {
622
            // without validation
623 1
            foreach ($includesFromParent as $path => $split) {
624 1
                yield $path => $split;
625
            }
626
        } else {
627
            // with validation
628 15
            $ruleIndex = $this->serializerClass::readRuleMainIndex($ruleIndexes);
629 15
            $this->validationStarts(static::PARAM_INCLUDE, $ruleIndexes);
630 15
            foreach ($includesFromParent as $path => $split) {
631 4
                $this->getCaptures()->clear();
632 4
                $this->validateAndAccumulateError($path, $ruleIndex);
633 4
                if ($this->getCaptures()->count() > 0) {
634 4
                    yield $this->readSingleCapturedValue() => $split;
635
                }
636
            }
637 15
            $this->validateEnds(static::PARAM_INCLUDE, $ruleIndexes);
638
        }
639
    }
640
641
    /**
642
     * @param iterable $iterable
643
     *
644
     * @return array
645
     */
646 25
    private function iterableToArray(iterable $iterable): array
647
    {
648 25
        $result = [];
649
650 25
        foreach ($iterable as $key => $value) {
651 19
            $result[$key] = $value instanceof Generator ? $this->iterableToArray($value) : $value;
652
        }
653
654 21
        return $result;
655
    }
656
657
    /**
658
     * @param mixed $value
659
     *
660
     * @return int
661
     */
662 35
    private function validatePageOffset($value): int
663
    {
664 35
        $ruleIndexes    = $this->serializerClass::readPageOffsetRuleIndexes($this->getSerializedRuleSet());
665 35
        $validatedValue = $this->validatePaginationValue($value, $ruleIndexes);
666
667 35
        return $validatedValue;
668
    }
669
670
    /**
671
     * @param mixed $value
672
     *
673
     * @return int
674
     */
675 35
    private function validatePageLimit($value): int
676
    {
677 35
        $ruleIndexes    = $this->serializerClass::readPageLimitRuleIndexes($this->getSerializedRuleSet());
678 35
        $validatedValue = $this->validatePaginationValue($value, $ruleIndexes);
679
680 35
        return $validatedValue;
681
    }
682
683
    /**
684
     * @param mixed $value
685
     * @param array $ruleIndexes
686
     *
687
     * @return int
688
     */
689 35
    private function validatePaginationValue($value, ?array $ruleIndexes): int
690
    {
691
        // no validation rule means we should accept any input value
692 35
        if ($ruleIndexes === null) {
693 1
            return is_numeric($value) === true ? (int)$value : 0;
694
        }
695
696 34
        $ruleIndex = $this->serializerClass::readRuleMainIndex($ruleIndexes);
697
698 34
        $this->validationStarts(static::PARAM_PAGE, $ruleIndexes);
699 34
        $this->validateAndThrowOnError(static::PARAM_PAGE, $value, $ruleIndex);
700 34
        $this->validateEnds(static::PARAM_PAGE, $ruleIndexes);
701
702 34
        $validatedValue = $this->readSingleCapturedValue();
703
704 34
        return (int)$validatedValue;
705
    }
706
707
    /**
708
     * @param string $paramName
709
     * @param array  $ruleIndexes
710
     *
711
     * @return void
712
     *
713
     * @SuppressWarnings(PHPMD.StaticAccess)
714
     */
715 34
    private function validationStarts(string $paramName, array $ruleIndexes): void
716
    {
717 34
        $this->getCaptures()->clear();
718 34
        $this->getValidationErrors()->clear();
719
720 34
        BlockInterpreter::executeStarts(
721 34
            $this->serializerClass::readRuleStartIndexes($ruleIndexes),
722 34
            $this->getValidationBlocks(),
723 34
            $this->getContext(),
724 34
            $this->getValidationErrors()
725
        );
726 34
        $this->checkValidationQueueErrors($paramName);
727
    }
728
729
    /**
730
     * @param string $paramName
731
     * @param mixed  $value
732
     * @param int    $ruleIndex
733
     *
734
     * @return void
735
     *
736
     * @SuppressWarnings(PHPMD.StaticAccess)
737
     */
738 34
    private function validateAndThrowOnError(string $paramName, $value, int $ruleIndex): void
739
    {
740 34
        BlockInterpreter::executeBlock(
741 34
            $value,
742 34
            $ruleIndex,
743 34
            $this->getValidationBlocks(),
744 34
            $this->getContext(),
745 34
            $this->getCaptures(),
746 34
            $this->getValidationErrors()
747
        );
748 34
        $this->checkValidationQueueErrors($paramName);
749
    }
750
751
    /**
752
     * @param mixed $value
753
     * @param int   $ruleIndex
754
     *
755
     * @return bool
756
     *
757
     * @SuppressWarnings(PHPMD.StaticAccess)
758
     */
759 16
    private function validateAndAccumulateError($value, int $ruleIndex): bool
760
    {
761 16
        return BlockInterpreter::executeBlock(
762 16
            $value,
763 16
            $ruleIndex,
764 16
            $this->getValidationBlocks(),
765 16
            $this->getContext(),
766 16
            $this->getCaptures(),
767 16
            $this->getValidationErrors()
768
        );
769
    }
770
771
    /**
772
     * @param string $paramName
773
     * @param array  $ruleIndexes
774
     *
775
     * @return void
776
     *
777
     * @SuppressWarnings(PHPMD.StaticAccess)
778
     */
779 34
    private function validateEnds(string $paramName, array $ruleIndexes): void
780
    {
781 34
        BlockInterpreter::executeEnds(
782 34
            $this->serializerClass::readRuleEndIndexes($ruleIndexes),
783 34
            $this->getValidationBlocks(),
784 34
            $this->getContext(),
785 34
            $this->getValidationErrors()
786
        );
787 34
        $this->checkValidationQueueErrors($paramName);
788
    }
789
790
    /**
791
     * @return mixed
792
     */
793 34
    private function readSingleCapturedValue()
794
    {
795 34
        assert(count($this->getCaptures()->get()) === 1, 'Expected that only one value would be captured.');
796 34
        $value = current($this->getCaptures()->get());
797
798 34
        return $value;
799
    }
800
801
    /**
802
     * @param int      $ruleIndex
803
     * @param iterable $values
804
     *
805
     * @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...
806
     */
807 10
    private function validateValues(int $ruleIndex, iterable $values): iterable
808
    {
809 10
        foreach ($values as $key => $value) {
810 9
            $this->getCaptures()->clear();
811 9
            $this->validateAndAccumulateError($value, $ruleIndex);
812 9
            if ($this->getCaptures()->count() > 0) {
813 9
                yield $key => $this->readSingleCapturedValue();
814
            }
815
        }
816
    }
817
818
    /**
819
     * @param int      $ruleIndex
820
     * @param iterable $opsAndArgs
821
     *
822
     * @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...
823
     */
824 10
    private function validateFilterArguments(int $ruleIndex, iterable $opsAndArgs): iterable
825
    {
826 10
        foreach ($opsAndArgs as $operation => $arguments) {
827 9
            yield $operation => $this->validateValues($ruleIndex, $arguments);
828
        }
829
    }
830
831
    /**
832
     * @return self
833
     */
834 35
    private function parsePagingParameters(): self
835
    {
836 35
        $parameters    = $this->getParameters();
837 35
        $mightBeOffset = $parameters[static::PARAM_PAGE][static::PARAM_PAGING_OFFSET] ?? null;
838 35
        $mightBeLimit  = $parameters[static::PARAM_PAGE][static::PARAM_PAGING_LIMIT] ?? null;
839
840 35
        $this->pagingOffset = $this->validatePageOffset($mightBeOffset);
841 35
        $this->pagingLimit  = $this->validatePageLimit($mightBeLimit);
842
843 35
        assert(is_int($this->pagingOffset) === true && $this->pagingOffset >= 0);
844 35
        assert(is_int($this->pagingLimit) === true && $this->pagingLimit > 0);
845
846 35
        return $this;
847
    }
848
849
    /**
850
     * @param string $paramName
851
     *
852
     * @return void
853
     *
854
     * @throws JsonApiException
855
     */
856 34
    private function checkValidationQueueErrors(string $paramName): void
857
    {
858 34
        if ($this->getValidationErrors()->count() > 0) {
859 6
            foreach ($this->getValidationErrors()->get() as $error) {
860 6
                $this->getJsonErrors()->addValidationQueryError($paramName, $error);
861
            }
862
863 6
            throw new JsonApiException($this->getJsonErrors());
864
        }
865
    }
866
867
    /**
868
     * @param string|null $value
869
     *
870
     * @return self
871
     */
872 35
    private function setIdentityParameter(?string $value): self
873
    {
874 35
        $this->identityParameter = $value;
875
876 35
        return $this;
877
    }
878
879
    /**
880
     * @return null|string
881
     */
882 5
    private function getIdentityParameter(): ?string
883
    {
884 5
        return $this->identityParameter;
885
    }
886
887
    /**
888
     * @param array $values
889
     *
890
     * @return self
891
     */
892 31
    private function setFilterParameters(array $values): self
893
    {
894 31
        $this->filterParameters = $values;
895
896 31
        return $this;
897
    }
898
899
    /**
900
     * @return array
901
     */
902 22
    private function getFilterParameters(): array
903
    {
904 22
        return $this->filterParameters;
905
    }
906
907
    /**
908
     * @return self
909
     */
910 29
    private function setFiltersWithAnd(): self
911
    {
912 29
        $this->areFiltersWithAnd = true;
913
914 29
        return $this;
915
    }
916
917
    /**
918
     * @return self
919
     */
920 2
    private function setFiltersWithOr(): self
921
    {
922 2
        $this->areFiltersWithAnd = false;
923
924 2
        return $this;
925
    }
926
927
    /**
928
     * @return self
929
     */
930 36
    private function clear(): self
931
    {
932 36
        $this->identityParameter = null;
933 36
        $this->filterParameters  = [];
934 36
        $this->areFiltersWithAnd = true;
935 36
        $this->pagingOffset      = null;
936 36
        $this->pagingLimit       = null;
937
938 36
        $this->cachedIdentity = null;
939 36
        $this->cachedFilters  = null;
940 36
        $this->cachedFields   = null;
941 36
        $this->cachedIncludes = null;
942 36
        $this->cachedSorts    = null;
943
944 36
        $this->getCaptures()->clear();
945 36
        $this->getValidationErrors()->clear();
946
947 36
        return $this;
948
    }
949
950
    /**
951
     * Pre-parsing for filter parameters.
952
     *
953
     * @return self
954
     *
955
     * @SuppressWarnings(PHPMD.ElseExpression)
956
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
957
     */
958 35
    private function parseFilterLink(): self
959
    {
960 35
        if (array_key_exists(static::PARAM_FILTER, $this->getParameters()) === false) {
961 17
            $this->setFiltersWithAnd()->setFilterParameters([]);
962
963 17
            return $this;
964
        }
965
966 18
        $filterSection = $this->getParameters()[static::PARAM_FILTER];
967 18
        if (is_array($filterSection) === false || empty($filterSection) === true) {
968 2
            throw new InvalidQueryParametersException($this->createParameterError(
969 2
                static::PARAM_FILTER,
970 2
                $this->getInvalidParamMessage()
971
            ));
972
        }
973
974 16
        $isWithAnd = true;
975 16
        reset($filterSection);
976
977
        // check if top level element is `AND` or `OR`
978 16
        $firstKey   = key($filterSection);
979 16
        $firstLcKey = strtolower(trim($firstKey));
980 16
        if (($hasOr = ($firstLcKey === 'or')) || $firstLcKey === 'and') {
981 4
            if (count($filterSection) > 1 ||
982 4
                empty($filterSection = $filterSection[$firstKey]) === true ||
983 4
                is_array($filterSection) === false
984
            ) {
985 2
                throw new InvalidQueryParametersException($this->createParameterError(
986 2
                    static::PARAM_FILTER,
987 2
                    $this->getInvalidParamMessage()
988
                ));
989
            } else {
990 2
                $this->setFilterParameters($filterSection);
991 2
                if ($hasOr === true) {
992 2
                    $isWithAnd = false;
993
                }
994
            }
995
        } else {
996 12
            $this->setFilterParameters($filterSection);
997
        }
998
999 14
        $isWithAnd === true ? $this->setFiltersWithAnd() : $this->setFiltersWithOr();
1000
1001 14
        return $this;
1002
    }
1003
1004
    /**
1005
     * @param string $parameterName
1006
     * @param array  $value
1007
     *
1008
     * @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...
1009
     *
1010
     * @SuppressWarnings(PHPMD.ElseExpression)
1011
     */
1012 11
    private function parseOperationsAndArguments(string $parameterName, array $value): iterable
1013
    {
1014
        // in this case we interpret it as an [operation => 'comma separated argument(s)']
1015 11
        foreach ($value as $operationName => $arguments) {
1016 11
            if (is_string($operationName) === false || empty($operationName) === true ||
1017 11
                is_string($arguments) === false
1018
            ) {
1019 1
                $title = $this->getFormatter()->formatMessage(Validation::INVALID_OPERATION_ARGUMENTS);
1020 1
                $error = $this->createQueryError($parameterName, $title);
1021 1
                throw new InvalidQueryParametersException($error);
1022
            }
1023
1024 10
            if ($arguments === '') {
1025 1
                yield $operationName => [];
1026
            } else {
1027 9
                yield $operationName => $this->splitCommaSeparatedStringAndCheckNoEmpties(
1028 9
                    $parameterName,
1029 9
                    $arguments,
1030 10
                    $this->getInvalidParamMessage()
1031
                );
1032
            }
1033
        }
1034
    }
1035
1036
    /**
1037
     * @param string $paramName
1038
     * @param string $errorTitle
1039
     *
1040
     * @return ErrorInterface
1041
     */
1042 1
    private function createQueryError(string $paramName, string $errorTitle): ErrorInterface
1043
    {
1044 1
        $source = [ErrorInterface::SOURCE_PARAMETER => $paramName];
1045 1
        $error  = new JsonApiError(null, null, null, null, $errorTitle, null, $source);
1046
1047 1
        return $error;
1048
    }
1049
1050
    /**
1051
     *
1052
     * @return string
1053
     */
1054 25
    private function getInvalidParamMessage(): string
1055
    {
1056 25
        return $this->getMessage(static::MSG_ERR_INVALID_PARAMETER);
1057
    }
1058
}
1059