Completed
Push — master ( 1a67d2...dfab50 )
by Neomerx
11:35 queued 22s
created

QueryParser::hasFilters()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php namespace Limoncello\Flute\Validation\JsonApi;
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\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
 */
48
class QueryParser implements JsonApiQueryValidatingParserInterface
49
{
50
    use BaseQueryParserTrait {
51
        BaseQueryParserTrait::getFields as getFieldsImpl;
52
        BaseQueryParserTrait::getIncludes as getIncludesImpl;
53
        BaseQueryParserTrait::getSorts as getSortsImpl;
54
    }
55
56
    /** Message */
57
    public const MSG_ERR_INVALID_PARAMETER = 'Invalid Parameter.';
58
59
    /**
60
     * @var array
61
     */
62
    private $parameters;
63
64
    /**
65
     * @var string[]|null
66
     */
67
    private $messages;
68
69
    /**
70
     * @var array
71
     */
72
    private $filterParameters;
73
74
    /**
75
     * @var bool
76
     */
77
    private $areFiltersWithAnd;
78
79
    /**
80
     * @var int|null
81
     */
82
    private $pagingOffset;
83
84
    /**
85
     * @var int|null
86
     */
87
    private $pagingLimit;
88
89
    /**
90
     * NOTE: Despite the type it is just a string so only static methods can be called from the interface.
91
     *
92
     * @var JsonApiQueryRulesSerializerInterface|string
93
     */
94
    private $serializerClass;
95
96
    /**
97
     * @var array
98
     */
99
    private $serializedRuleSet;
100
101
    /**
102
     * @var array
103
     */
104
    private $validationBlocks;
105
106
    /**
107
     * @var ContextStorageInterface
108
     */
109
    private $context;
110
111
    /**
112
     * @var CaptureAggregatorInterface
113
     */
114
    private $captures;
115
116
    /**
117
     * @var ErrorAggregatorInterface
118
     */
119
    private $validationErrors;
120
121
    /**
122
     * @var JsonApiErrorCollection
123
     */
124
    private $jsonErrors;
125
126
    /**
127
     * @var null|array
128
     */
129
    private $cachedFilters = null;
130
131
    /**
132
     * @var null|array
133
     */
134
    private $cachedFields = null;
135
136
    /**
137
     * @var null|array
138
     */
139
    private $cachedSorts = null;
140
141
    /**
142
     * @var null|array
143
     */
144
    private $cachedIncludes = null;
145
146
    /**
147
     * @var FormatterFactoryInterface
148
     */
149
    private $formatterFactory;
150
151
    /**
152
     * @var FormatterInterface|null
153
     */
154
    private $formatter;
155
156
    /**
157
     * @param string                     $rulesClass
158
     * @param string                     $serializerClass
159
     * @param array                      $serializedData
160
     * @param ContextStorageInterface    $context
161
     * @param CaptureAggregatorInterface $captures
162
     * @param ErrorAggregatorInterface   $validationErrors
163
     * @param JsonApiErrorCollection     $jsonErrors
164
     * @param FormatterFactoryInterface  $formatterFactory
165
     * @param string[]|null              $messages
166
     */
167 32
    public function __construct(
168
        string $rulesClass,
169
        string $serializerClass,
170
        array $serializedData,
171
        ContextStorageInterface $context,
172
        CaptureAggregatorInterface $captures,
173
        ErrorAggregatorInterface $validationErrors,
174
        JsonApiErrorCollection $jsonErrors,
175
        FormatterFactoryInterface $formatterFactory,
176
        array $messages = null
177
    ) {
178 32
        assert(
179 32
            in_array(JsonApiQueryRulesSerializerInterface::class, class_implements($serializerClass)),
180 32
            "`$serializerClass` should implement interface `" . JsonApiQueryRulesSerializerInterface::class . '`.'
181
        );
182
183 32
        $parameters = [];
184 32
        $this->setParameters($parameters)->setMessages($messages);
185 32
        $this->serializerClass  = $serializerClass;
186 32
        $this->context          = $context;
187 32
        $this->captures         = $captures;
188 32
        $this->validationErrors = $validationErrors;
189 32
        $this->jsonErrors       = $jsonErrors;
190 32
        $this->formatterFactory = $formatterFactory;
191
192 32
        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...
193 32
        $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...
194 32
        $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...
195
196 32
        $this->clear();
197
    }
198
199
    /**
200
     * @inheritdoc
201
     */
202 31
    public function parse(array $parameters): JsonApiQueryValidatingParserInterface
203
    {
204 31
        $this->clear();
205
206 31
        $this->setParameters($parameters);
207
208 31
        $this->parsePagingParameters()->parseFilterLink();
209
210 27
        return $this;
211
    }
212
213
    /**
214
     * @inheritdoc
215
     */
216 17
    public function areFiltersWithAnd(): bool
217
    {
218 17
        return $this->areFiltersWithAnd;
219
    }
220
221
    /**
222
     * @inheritdoc
223
     */
224 1
    public function hasFilters(): bool
225
    {
226 1
        return $this->hasParameter(static::PARAM_FILTER);
227
    }
228
229
    /**
230
     * @inheritdoc
231
     */
232 14
    public function hasFields(): bool
233
    {
234 14
        return $this->hasParameter(static::PARAM_FIELDS);
235
    }
236
237
    /**
238
     * @inheritdoc
239
     */
240 14
    public function hasIncludes(): bool
241
    {
242 14
        return $this->hasParameter(static::PARAM_INCLUDE);
243
    }
244
245
    /**
246
     * @inheritdoc
247
     */
248 1
    public function hasSorts(): bool
249
    {
250 1
        return $this->hasParameter(static::PARAM_SORT);
251
    }
252
253
    /**
254
     * @inheritdoc
255
     */
256 1
    public function hasPaging(): bool
257
    {
258 1
        return $this->hasParameter(static::PARAM_PAGE);
259
    }
260
261
    /**
262
     * @inheritdoc
263
     */
264 22
    public function getFilters(): array
265
    {
266 22
        if ($this->cachedFilters === null) {
267 22
            $this->cachedFilters = $this->iterableToArray($this->getValidatedFilters());
268
        }
269
270 18
        return $this->cachedFilters;
271
    }
272
273
    /**
274
     * @inheritdoc
275
     */
276 2
    public function getFields(): array
277
    {
278 2
        if ($this->cachedFields === null) {
279 2
            $fields = $this->getFieldsImpl($this->getParameters(), $this->getInvalidParamMessage());
280 2
            $this->cachedFields = $this->iterableToArray($this->getValidatedFields($fields));
281
        }
282
283 2
        return $this->cachedFields;
284
    }
285
286
    /**
287
     * @inheritdoc
288
     */
289 16
    public function getSorts(): array
290
    {
291 16
        if ($this->cachedSorts === null) {
292 16
            $sorts = $this->getSortsImpl($this->getParameters(), $this->getInvalidParamMessage());
293 16
            $this->cachedSorts = $this->iterableToArray($this->getValidatedSorts($sorts));
294
        }
295
296 16
        return $this->cachedSorts;
297
    }
298
299
    /**
300
     * @inheritdoc
301
     */
302 16
    public function getIncludes(): iterable
303
    {
304 16
        if ($this->cachedIncludes === null) {
305 16
            $includes = $this->getIncludesImpl($this->getParameters(), $this->getInvalidParamMessage());
306 16
            $this->cachedIncludes = $this->iterableToArray($this->getValidatedIncludes($includes));
307
        }
308
309 16
        return $this->cachedIncludes;
310
    }
311
312
    /**
313
     * @inheritdoc
314
     */
315 17
    public function getPagingOffset(): ?int
316
    {
317 17
        return $this->pagingOffset;
318
    }
319
320
    /**
321
     * @inheritdoc
322
     */
323 17
    public function getPagingLimit(): ?int
324
    {
325 17
        return $this->pagingLimit;
326
    }
327
328
    /**
329
     * @param array $parameters
330
     *
331
     * @return self
332
     */
333 32
    protected function setParameters(array $parameters): self
334
    {
335 32
        $this->parameters = $parameters;
336
337 32
        return $this;
338
    }
339
340
    /**
341
     * @return array
342
     */
343 31
    protected function getParameters(): array
344
    {
345 31
        return $this->parameters;
346
    }
347
348
    /**
349
     * @param array $messages
350
     *
351
     * @return self
352
     */
353 32
    protected function setMessages(?array $messages): self
354
    {
355 32
        $this->messages = $messages;
356
357 32
        return $this;
358
    }
359
360
    /**
361
     * @param string $message
362
     *
363
     * @return string
364
     */
365 25
    protected function getMessage(string $message): string
366
    {
367 25
        $hasTranslation = $this->messages !== null && array_key_exists($message, $this->messages) === false;
368
369 25
        return $hasTranslation === true ? $this->messages[$message] : $message;
370
    }
371
372
    /**
373
     * @return JsonApiErrorCollection
374
     */
375 6
    protected function getJsonErrors(): JsonApiErrorCollection
376
    {
377 6
        return $this->jsonErrors;
378
    }
379
380
    /**
381
     * @return ErrorAggregatorInterface
382
     */
383 32
    protected function getValidationErrors(): ErrorAggregatorInterface
384
    {
385 32
        return $this->validationErrors;
386
    }
387
388
    /**
389
     * @return CaptureAggregatorInterface
390
     */
391 32
    protected function getCaptures(): CaptureAggregatorInterface
392
    {
393 32
        return $this->captures;
394
    }
395
396
    /**
397
     * @return ContextStorageInterface
398
     */
399 30
    protected function getContext(): ContextStorageInterface
400
    {
401 30
        return $this->context;
402
    }
403
404
    /**
405
     * @return array
406
     */
407 30
    protected function getValidationBlocks(): array
408
    {
409 30
        return $this->validationBlocks;
410
    }
411
412
    /**
413
     * @return array
414
     */
415 31
    protected function getSerializedRuleSet(): array
416
    {
417 31
        return $this->serializedRuleSet;
418
    }
419
420
    /**
421
     * @return FormatterFactoryInterface
422
     */
423 1
    protected function getFormatterFactory(): FormatterFactoryInterface
424
    {
425 1
        return $this->formatterFactory;
426
    }
427
428
    /**
429
     * @return FormatterInterface
430
     */
431 1
    protected function getFormatter(): FormatterInterface
432
    {
433 1
        if ($this->formatter === null) {
434 1
            $this->formatter = $this->getFormatterFactory()->createFormatter(FluteSettings::VALIDATION_NAMESPACE);
435
        }
436
437 1
        return $this->formatter;
438
    }
439
440
    /**
441
     * @param string $name
442
     *
443
     * @return bool
444
     */
445 17
    protected function hasParameter(string $name): bool
446
    {
447 17
        return array_key_exists($name, $this->getParameters());
448
    }
449
450
    /**
451
     * @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...
452
     *
453
     * @SuppressWarnings(PHPMD.ElseExpression)
454
     */
455 22
    private function getValidatedFilters(): iterable
456
    {
457 22
        $ruleIndexes = $this->serializerClass::readFilterRulesIndexes($this->getSerializedRuleSet());
458
459 22
        if ($ruleIndexes === null) {
460
            // without validation
461 1
            foreach ($this->getFilterParameters() as $field => $operationsWithArgs) {
462 1
                yield $field => $this->parseOperationsAndArguments(static::PARAM_FILTER, $operationsWithArgs);
463
            }
464
        } else {
465
            // with validation
466 21
            $mainIndexes = $this->serializerClass::readRuleMainIndexes($ruleIndexes);
467 21
            $this->validationStarts(static::PARAM_FILTER, $ruleIndexes);
468 21
            foreach ($this->getFilterParameters() as $field => $operationsWithArgs) {
469 13
                if (is_string($field) === false || empty($field) === true ||
470 13
                    is_array($operationsWithArgs) === false || empty($operationsWithArgs) === true
471
                ) {
472 1
                    throw new InvalidQueryParametersException($this->createParameterError(
473 1
                        static::PARAM_FILTER,
474 1
                        $this->getInvalidParamMessage()
475
                    ));
476
                }
477
478 12
                if (array_key_exists($field, $mainIndexes) === false) {
479
                    // unknown field set type
480 2
                    $this->getValidationErrors()->add(
481 2
                        new Error(static::PARAM_FILTER, $field, ErrorCodes::INVALID_VALUE, null)
482
                    );
483
                } else {
484
                    // for field a validation rule is defined so input value will be validated
485 10
                    $ruleIndex = $mainIndexes[$field];
486 10
                    $parsed    = $this->parseOperationsAndArguments(static::PARAM_FILTER, $operationsWithArgs);
487
488 12
                    yield $field => $this->validateFilterArguments($ruleIndex, $parsed);
489
                }
490
            }
491 19
            $this->validateEnds(static::PARAM_FILTER, $ruleIndexes);
492
        }
493
    }
494
495
    /**
496
     * @param iterable $fieldsFromParent
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.StaticAccess)
501
     * @SuppressWarnings(PHPMD.ElseExpression)
502
     */
503 2
    private function getValidatedFields(iterable $fieldsFromParent): iterable
504
    {
505 2
        $ruleIndexes = $this->serializerClass::readFieldSetRulesIndexes($this->getSerializedRuleSet());
506
507 2
        if ($ruleIndexes === null) {
508
            // without validation
509 1
            foreach ($fieldsFromParent as $type => $fieldList) {
510 1
                yield $type => $fieldList;
511
            }
512
        } else {
513
            // with validation
514 1
            $mainIndexes = $this->serializerClass::readRuleMainIndexes($ruleIndexes);
515 1
            $this->validationStarts(static::PARAM_FIELDS, $ruleIndexes);
516 1
            foreach ($fieldsFromParent as $type => $fieldList) {
517 1
                if (array_key_exists($type, $mainIndexes) === true) {
518 1
                    yield $type => $this->validateValues($mainIndexes[$type], $fieldList);
519
                } else {
520
                    // unknown field set type
521 1
                    $this->getValidationErrors()->add(
522 1
                        new Error(static::PARAM_FIELDS, $type, ErrorCodes::INVALID_VALUE, null)
523
                    );
524
                }
525
            }
526 1
            $this->validateEnds(static::PARAM_FIELDS, $ruleIndexes);
527
        }
528
    }
529
530
    /**
531
     * @param iterable $sortsFromParent
532
     *
533
     * @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...
534
     *
535
     * @SuppressWarnings(PHPMD.StaticAccess)
536
     * @SuppressWarnings(PHPMD.ElseExpression)
537
     */
538 16
    private function getValidatedSorts(iterable $sortsFromParent): iterable
539
    {
540 16
        $ruleIndexes = $this->serializerClass::readSortsRuleIndexes($this->getSerializedRuleSet());
541
542 16
        if ($ruleIndexes === null) {
543
            // without validation
544 1
            foreach ($sortsFromParent as $field => $isAsc) {
545 1
                yield $field => $isAsc;
546
            }
547
        } else {
548
            // with validation
549 15
            $ruleIndex = $this->serializerClass::readRuleMainIndex($ruleIndexes);
550 15
            $this->validationStarts(static::PARAM_SORT, $ruleIndexes);
551 15
            foreach ($sortsFromParent as $field => $isAsc) {
552 5
                $this->getCaptures()->clear();
553 5
                $this->validateAndAccumulateError($field, $ruleIndex);
554 5
                if ($this->getCaptures()->count() > 0) {
555 5
                    yield $this->readSingleCapturedValue() => $isAsc;
556
                }
557
            }
558 15
            $this->validateEnds(static::PARAM_SORT, $ruleIndexes);
559
        }
560
    }
561
562
    /**
563
     * @param iterable $includesFromParent
564
     *
565
     * @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...
566
     *
567
     * @SuppressWarnings(PHPMD.StaticAccess)
568
     * @SuppressWarnings(PHPMD.ElseExpression)
569
     */
570 16
    private function getValidatedIncludes(iterable $includesFromParent): iterable
571
    {
572 16
        $ruleIndexes = $this->serializerClass::readIncludesRuleIndexes($this->getSerializedRuleSet());
573
574 16
        if ($ruleIndexes === null) {
575
            // without validation
576 1
            foreach ($includesFromParent as $path => $split) {
577 1
                yield $path => $split;
578
            }
579
        } else {
580
            // with validation
581 15
            $ruleIndex = $this->serializerClass::readRuleMainIndex($ruleIndexes);
582 15
            $this->validationStarts(static::PARAM_INCLUDE, $ruleIndexes);
583 15
            foreach ($includesFromParent as $path => $split) {
584 4
                $this->getCaptures()->clear();
585 4
                $this->validateAndAccumulateError($path, $ruleIndex);
586 4
                if ($this->getCaptures()->count() > 0) {
587 4
                    yield $this->readSingleCapturedValue() => $split;
588
                }
589
            }
590 15
            $this->validateEnds(static::PARAM_INCLUDE, $ruleIndexes);
591
        }
592
    }
593
594
    /**
595
     * @param iterable $iterable
596
     *
597
     * @return array
598
     */
599 25
    private function iterableToArray(iterable $iterable): array
600
    {
601 25
        $result = [];
602
603 25
        foreach ($iterable as $key => $value) {
604 19
            $result[$key] = $value instanceof Generator ? $this->iterableToArray($value) : $value;
605
        }
606
607 21
        return $result;
608
    }
609
610
    /**
611
     * @param mixed $value
612
     *
613
     * @return int
614
     */
615 31
    private function validatePageOffset($value): int
616
    {
617 31
        $ruleIndexes    = $this->serializerClass::readPageOffsetRuleIndexes($this->getSerializedRuleSet());
618 31
        $validatedValue = $this->validatePaginationValue($value, $ruleIndexes);
619
620 31
        return $validatedValue;
621
    }
622
623
    /**
624
     * @param mixed $value
625
     *
626
     * @return int
627
     */
628 31
    private function validatePageLimit($value): int
629
    {
630 31
        $ruleIndexes    = $this->serializerClass::readPageLimitRuleIndexes($this->getSerializedRuleSet());
631 31
        $validatedValue = $this->validatePaginationValue($value, $ruleIndexes);
632
633 31
        return $validatedValue;
634
    }
635
636
    /**
637
     * @param mixed $value
638
     * @param array $ruleIndexes
639
     *
640
     * @return int
641
     */
642 31
    private function validatePaginationValue($value, ?array $ruleIndexes): int
643
    {
644
        // no validation rule means we should accept any input value
645 31
        if ($ruleIndexes === null) {
646 1
            return is_numeric($value) === true ? (int)$value : 0;
647
        }
648
649 30
        $ruleIndex = $this->serializerClass::readRuleMainIndex($ruleIndexes);
650
651 30
        $this->validationStarts(static::PARAM_PAGE, $ruleIndexes);
652 30
        $this->validateAndThrowOnError(static::PARAM_PAGE, $value, $ruleIndex);
653 30
        $this->validateEnds(static::PARAM_PAGE, $ruleIndexes);
654
655 30
        $validatedValue = $this->readSingleCapturedValue();
656
657 30
        return (int)$validatedValue;
658
    }
659
660
    /**
661
     * @param string $paramName
662
     * @param array  $ruleIndexes
663
     *
664
     * @return void
665
     *
666
     * @SuppressWarnings(PHPMD.StaticAccess)
667
     */
668 30
    private function validationStarts(string $paramName, array $ruleIndexes): void
669
    {
670 30
        $this->getCaptures()->clear();
671 30
        $this->getValidationErrors()->clear();
672
673 30
        BlockInterpreter::executeStarts(
674 30
            $this->serializerClass::readRuleStartIndexes($ruleIndexes),
675 30
            $this->getValidationBlocks(),
676 30
            $this->getContext(),
677 30
            $this->getValidationErrors()
678
        );
679 30
        $this->checkValidationQueueErrors($paramName);
680
    }
681
682
    /**
683
     * @param string $paramName
684
     * @param mixed  $value
685
     * @param int    $ruleIndex
686
     *
687
     * @return void
688
     *
689
     * @SuppressWarnings(PHPMD.StaticAccess)
690
     */
691 30
    private function validateAndThrowOnError(string $paramName, $value, int $ruleIndex): void
692
    {
693 30
        BlockInterpreter::executeBlock(
694 30
            $value,
695 30
            $ruleIndex,
696 30
            $this->getValidationBlocks(),
697 30
            $this->getContext(),
698 30
            $this->getCaptures(),
699 30
            $this->getValidationErrors()
700
        );
701 30
        $this->checkValidationQueueErrors($paramName);
702
    }
703
704
    /**
705
     * @param mixed $value
706
     * @param int   $ruleIndex
707
     *
708
     * @return bool
709
     *
710
     * @SuppressWarnings(PHPMD.StaticAccess)
711
     */
712 16
    private function validateAndAccumulateError($value, int $ruleIndex): bool
713
    {
714 16
        return BlockInterpreter::executeBlock(
715 16
            $value,
716 16
            $ruleIndex,
717 16
            $this->getValidationBlocks(),
718 16
            $this->getContext(),
719 16
            $this->getCaptures(),
720 16
            $this->getValidationErrors()
721
        );
722
    }
723
724
    /**
725
     * @param string $paramName
726
     * @param array  $ruleIndexes
727
     *
728
     * @return void
729
     *
730
     * @SuppressWarnings(PHPMD.StaticAccess)
731
     */
732 30
    private function validateEnds(string $paramName, array $ruleIndexes): void
733
    {
734 30
        BlockInterpreter::executeEnds(
735 30
            $this->serializerClass::readRuleEndIndexes($ruleIndexes),
736 30
            $this->getValidationBlocks(),
737 30
            $this->getContext(),
738 30
            $this->getValidationErrors()
739
        );
740 30
        $this->checkValidationQueueErrors($paramName);
741
    }
742
743
    /**
744
     * @return mixed
745
     */
746 30
    private function readSingleCapturedValue()
747
    {
748 30
        assert(count($this->getCaptures()->get()) === 1, 'Expected that only one value would be captured.');
749 30
        $value = current($this->getCaptures()->get());
750
751 30
        return $value;
752
    }
753
754
    /**
755
     * @param int      $ruleIndex
756
     * @param iterable $values
757
     *
758
     * @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...
759
     */
760 10
    private function validateValues(int $ruleIndex, iterable $values): iterable
761
    {
762 10
        foreach ($values as $key => $value) {
763 9
            $this->getCaptures()->clear();
764 9
            $this->validateAndAccumulateError($value, $ruleIndex);
765 9
            if ($this->getCaptures()->count() > 0) {
766 9
                yield $key => $this->readSingleCapturedValue();
767
            }
768
        }
769
    }
770
771
    /**
772
     * @param int      $ruleIndex
773
     * @param iterable $opsAndArgs
774
     *
775
     * @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...
776
     */
777 10
    private function validateFilterArguments(int $ruleIndex, iterable $opsAndArgs): iterable
778
    {
779 10
        foreach ($opsAndArgs as $operation => $arguments) {
780 9
            yield $operation => $this->validateValues($ruleIndex, $arguments);
781
        }
782
    }
783
784
    /**
785
     * @return self
786
     */
787 31
    private function parsePagingParameters(): self
788
    {
789 31
        $parameters    = $this->getParameters();
790 31
        $mightBeOffset = $parameters[static::PARAM_PAGE][static::PARAM_PAGING_OFFSET] ?? null;
791 31
        $mightBeLimit  = $parameters[static::PARAM_PAGE][static::PARAM_PAGING_LIMIT] ?? null;
792
793 31
        $this->pagingOffset = $this->validatePageOffset($mightBeOffset);
794 31
        $this->pagingLimit  = $this->validatePageLimit($mightBeLimit);
795
796 31
        assert(is_int($this->pagingOffset) === true && $this->pagingOffset >= 0);
797 31
        assert(is_int($this->pagingLimit) === true && $this->pagingLimit > 0);
798
799 31
        return $this;
800
    }
801
802
    /**
803
     * @param string $paramName
804
     *
805
     * @return void
806
     *
807
     * @throws JsonApiException
808
     */
809 30
    private function checkValidationQueueErrors(string $paramName): void
810
    {
811 30
        if ($this->getValidationErrors()->count() > 0) {
812 6
            foreach ($this->getValidationErrors()->get() as $error) {
813 6
                $this->getJsonErrors()->addValidationQueryError($paramName, $error);
814
            }
815
816 6
            throw new JsonApiException($this->getJsonErrors());
817
        }
818
    }
819
820
    /**
821
     * @param array $values
822
     *
823
     * @return self
824
     */
825 27
    private function setFilterParameters(array $values): self
826
    {
827 27
        $this->filterParameters = $values;
828
829 27
        return $this;
830
    }
831
832
    /**
833
     * @return array
834
     */
835 22
    private function getFilterParameters(): array
836
    {
837 22
        return $this->filterParameters;
838
    }
839
840
    /**
841
     * @return self
842
     */
843 25
    private function setFiltersWithAnd(): self
844
    {
845 25
        $this->areFiltersWithAnd = true;
846
847 25
        return $this;
848
    }
849
850
    /**
851
     * @return self
852
     */
853 2
    private function setFiltersWithOr(): self
854
    {
855 2
        $this->areFiltersWithAnd = false;
856
857 2
        return $this;
858
    }
859
860
    /**
861
     * @return self
862
     */
863 32
    private function clear(): self
864
    {
865 32
        $this->filterParameters  = [];
866 32
        $this->areFiltersWithAnd = true;
867 32
        $this->pagingOffset      = null;
868 32
        $this->pagingLimit       = null;
869
870 32
        $this->cachedFilters  = null;
871 32
        $this->cachedFields   = null;
872 32
        $this->cachedIncludes = null;
873 32
        $this->cachedSorts    = null;
874
875 32
        $this->getCaptures()->clear();
876 32
        $this->getValidationErrors()->clear();
877
878 32
        return $this;
879
    }
880
881
    /**
882
     * Pre-parsing for filter parameters.
883
     *
884
     * @return self
885
     *
886
     * @SuppressWarnings(PHPMD.ElseExpression)
887
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
888
     */
889 31
    private function parseFilterLink(): self
890
    {
891 31
        if (array_key_exists(static::PARAM_FILTER, $this->getParameters()) === false) {
892 13
            $this->setFiltersWithAnd()->setFilterParameters([]);
893
894 13
            return $this;
895
        }
896
897 18
        $filterSection = $this->getParameters()[static::PARAM_FILTER];
898 18
        if (is_array($filterSection) === false || empty($filterSection) === true) {
899 2
            throw new InvalidQueryParametersException($this->createParameterError(
900 2
                static::PARAM_FILTER,
901 2
                $this->getInvalidParamMessage()
902
            ));
903
        }
904
905 16
        $isWithAnd = true;
906 16
        reset($filterSection);
907
908
        // check if top level element is `AND` or `OR`
909 16
        $firstKey   = key($filterSection);
910 16
        $firstLcKey = strtolower(trim($firstKey));
911 16
        if (($hasOr = ($firstLcKey === 'or')) || $firstLcKey === 'and') {
912 4
            if (count($filterSection) > 1 ||
913 4
                empty($filterSection = $filterSection[$firstKey]) === true ||
914 4
                is_array($filterSection) === false
915
            ) {
916 2
                throw new InvalidQueryParametersException($this->createParameterError(
917 2
                    static::PARAM_FILTER,
918 2
                    $this->getInvalidParamMessage()
919
                ));
920
            } else {
921 2
                $this->setFilterParameters($filterSection);
922 2
                if ($hasOr === true) {
923 2
                    $isWithAnd = false;
924
                }
925
            }
926
        } else {
927 12
            $this->setFilterParameters($filterSection);
928
        }
929
930 14
        $isWithAnd === true ? $this->setFiltersWithAnd() : $this->setFiltersWithOr();
931
932 14
        return $this;
933
    }
934
935
    /**
936
     * @param string $parameterName
937
     * @param array  $value
938
     *
939
     * @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...
940
     *
941
     * @SuppressWarnings(PHPMD.ElseExpression)
942
     */
943 11
    private function parseOperationsAndArguments(string $parameterName, array $value): iterable
944
    {
945
        // in this case we interpret it as an [operation => 'comma separated argument(s)']
946 11
        foreach ($value as $operationName => $arguments) {
947 11
            if (is_string($operationName) === false || empty($operationName) === true ||
948 11
                is_string($arguments) === false
949
            ) {
950 1
                $title = $this->getFormatter()->formatMessage(Validation::INVALID_OPERATION_ARGUMENTS);
951 1
                $error = $this->createQueryError($parameterName, $title);
952 1
                throw new InvalidQueryParametersException($error);
953
            }
954
955 10
            if ($arguments === '') {
956 1
                yield $operationName => [];
957
            } else {
958 9
                yield $operationName => $this->splitCommaSeparatedStringAndCheckNoEmpties(
959 9
                    $parameterName,
960 9
                    $arguments,
961 10
                    $this->getInvalidParamMessage()
962
                );
963
            }
964
        }
965
    }
966
967
    /**
968
     * @param string $paramName
969
     * @param string $errorTitle
970
     *
971
     * @return ErrorInterface
972
     */
973 1
    private function createQueryError(string $paramName, string $errorTitle): ErrorInterface
974
    {
975 1
        $source = [ErrorInterface::SOURCE_PARAMETER => $paramName];
976 1
        $error  = new JsonApiError(null, null, null, null, $errorTitle, null, $source);
977
978 1
        return $error;
979
    }
980
981
    /**
982
     *
983
     * @return string
984
     */
985 25
    private function getInvalidParamMessage(): string
986
    {
987 25
        return $this->getMessage(static::MSG_ERR_INVALID_PARAMETER);
988
    }
989
}
990