Completed
Push — develop ( 6173d9...1863cd )
by Neomerx
09:44
created

DataParser::parseRelationship()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 8

Importance

Changes 0
Metric Value
dl 0
loc 40
ccs 26
cts 26
cp 1
rs 8.0355
c 0
b 0
f 0
cc 8
nc 10
nop 2
crap 8
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 Limoncello\Contracts\L10n\FormatterFactoryInterface;
20
use Limoncello\Contracts\L10n\FormatterInterface;
21
use Limoncello\Flute\Contracts\Validation\ErrorCodes;
22
use Limoncello\Flute\Contracts\Validation\JsonApiDataRulesSerializerInterface;
23
use Limoncello\Flute\Contracts\Validation\JsonApiDataValidatingParserInterface;
24
use Limoncello\Flute\Http\JsonApiResponse;
25
use Limoncello\Flute\Package\FluteSettings;
26
use Limoncello\Flute\Validation\JsonApi\Execution\JsonApiErrorCollection;
27
use Limoncello\Flute\Validation\Rules\RelationshipRulesTrait;
28
use Limoncello\Validation\Contracts\Errors\ErrorInterface;
29
use Limoncello\Validation\Contracts\Execution\ContextStorageInterface;
30
use Limoncello\Validation\Execution\BlockInterpreter;
31
use Limoncello\Validation\Validator\BaseValidator;
32
use Neomerx\JsonApi\Contracts\Document\DocumentInterface as DI;
33
use Neomerx\JsonApi\Exceptions\JsonApiException;
34
35
/**
36
 * @package Limoncello\Flute
37
 *
38
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
39
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
40
 */
41
class DataParser extends BaseValidator implements JsonApiDataValidatingParserInterface
42
{
43
    use RelationshipRulesTrait;
44
45
    /** Rule description index */
46
    const RULE_INDEX = 0;
47
48
    /** Rule description index */
49
    const RULE_ATTRIBUTES = self::RULE_INDEX + 1;
50
51
    /** Rule description index */
52
    const RULE_TO_ONE = self::RULE_ATTRIBUTES + 1;
53
54
    /** Rule description index */
55
    const RULE_TO_MANY = self::RULE_TO_ONE + 1;
56
57
    /** Rule description index */
58
    const RULE_UNLISTED_ATTRIBUTE = self::RULE_TO_MANY + 1;
59
60
    /** Rule description index */
61
    const RULE_UNLISTED_RELATIONSHIP = self::RULE_UNLISTED_ATTRIBUTE + 1;
62
63
    /**
64
     * NOTE: Despite the type it is just a string so only static methods can be called from the interface.
65
     *
66
     * @var JsonApiDataRulesSerializerInterface|string
67
     */
68
    private $serializerClass;
69
70
    /**
71
     * @var int
72
     */
73
    private $errorStatus;
74
75
    /**
76
     * @var ContextStorageInterface
77
     */
78
    private $context;
79
80
    /**
81
     * @var JsonApiErrorCollection
82
     */
83
    private $jsonApiErrors;
84
85
    /**
86
     * @var array
87
     */
88
    private $blocks;
89
90
    /**
91
     * @var array
92
     */
93
    private $idRule;
94
95
    /**
96
     * @var array
97
     */
98
    private $typeRule;
99
100
    /**
101
     * @var int[]
102
     */
103
    private $attributeRules;
104
105
    /**
106
     * @var int[]
107
     */
108
    private $toOneRules;
109
110
    /**
111
     * @var int[]
112
     */
113
    private $toManyRules;
114
115
    /**
116
     * @var bool
117
     */
118
    private $isIgnoreUnknowns;
119
120
    /**
121
     * @var FormatterInterface|null
122
     */
123
    private $formatter;
124
125
    /**
126
     * @var FormatterFactoryInterface
127
     */
128
    private $formatterFactory;
129
130
    /**
131
     * @param string                    $rulesClass
132
     * @param string                    $serializerClass
133
     * @param array                     $serializedData
134
     * @param ContextStorageInterface   $context
135
     * @param JsonApiErrorCollection    $jsonErrors
136
     * @param FormatterFactoryInterface $formatterFactory
137
     * @param int                       $errorStatus
138
     *
139
     * @SuppressWarnings(PHPMD.StaticAccess)
140
     */
141 26
    public function __construct(
142
        string $rulesClass,
143
        string $serializerClass,
144
        array $serializedData,
145
        ContextStorageInterface $context,
146
        JsonApiErrorCollection $jsonErrors,
147
        FormatterFactoryInterface $formatterFactory,
148
        int $errorStatus = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY
149
    ) {
150
        $this
151 26
            ->setSerializerClass($serializerClass)
152 26
            ->setContext($context)
153 26
            ->setJsonApiErrors($jsonErrors)
154 26
            ->setFormatterFactory($formatterFactory);
155
156 26
        $this->blocks      = $this->getSerializer()::readBlocks($serializedData);
157 26
        $ruleSet           = $this->getSerializer()::readRules($rulesClass, $serializedData);
158 26
        $this->idRule      = $this->getSerializer()::readIdRuleIndexes($ruleSet);
159 26
        $this->typeRule    = $this->getSerializer()::readTypeRuleIndexes($ruleSet);
160 26
        $this->errorStatus = $errorStatus;
161
162
        $this
163 26
            ->setAttributeRules($this->getSerializer()::readAttributeRulesIndexes($ruleSet))
164 26
            ->setToOneIndexes($this->getSerializer()::readToOneRulesIndexes($ruleSet))
165 26
            ->setToManyIndexes($this->getSerializer()::readToManyRulesIndexes($ruleSet))
166 26
            ->disableIgnoreUnknowns();
167
168 26
        parent::__construct();
169
    }
170
171
    /**
172
     * @inheritdoc
173
     */
174 11
    public function assert($jsonData): JsonApiDataValidatingParserInterface
175
    {
176 11
        if ($this->validate($jsonData) === false) {
177 6
            throw new JsonApiException($this->getJsonApiErrorCollection(), $this->getErrorStatus());
178
        }
179
180 5
        return $this;
181
    }
182
183
    /**
184
     * @inheritdoc
185
     */
186 11
    public function validate($input): bool
187
    {
188 11
        if (is_array($input) === true) {
189 10
            return $this->parse($input);
190
        }
191
192 1
        $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
193 1
        $details = $this->formatMessage(ErrorCodes::INVALID_VALUE);
194 1
        $this->getJsonApiErrorCollection()->addDataError($title, $details, $this->getErrorStatus());
195
196 1
        return false;
197
    }
198
199
    /**
200
     * @inheritdoc
201
     *
202
     * @SuppressWarnings(PHPMD.ElseExpression)
203
     */
204 14
    public function parse(array $input): bool
205
    {
206 14
        $this->reInitAggregatorsIfNeeded();
207
208
        $this
209 14
            ->validateType($input)
210 14
            ->validateId($input)
211 14
            ->validateAttributes($input)
212 14
            ->validateRelationships($input);
213
214 14
        $hasNoErrors = $this->getJsonApiErrorCollection()->count() <= 0;
215
216 14
        return $hasNoErrors;
217
    }
218
219
    /**
220
     * @inheritdoc
221
     *
222
     * @SuppressWarnings(PHPMD.ElseExpression)
223
     */
224 7
    public function parseRelationship(string $name, array $jsonData): bool
225
    {
226 7
        $this->reInitAggregatorsIfNeeded();
227
228 7
        $isFoundInToOne  = array_key_exists($name, $this->getSerializer()::readRulesIndexes($this->getToOneRules()));
229 7
        $isFoundInToMany = $isFoundInToOne === false &&
230 7
            array_key_exists($name, $this->getSerializer()::readRulesIndexes($this->getToManyRules()));
231
232 7
        if ($isFoundInToOne === false && $isFoundInToMany === false) {
233 1
            $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
234 1
            $details = $this->formatMessage(ErrorCodes::UNKNOWN_RELATIONSHIP);
235 1
            $status  = $this->getErrorStatus();
236 1
            $this->getJsonApiErrorCollection()->addRelationshipError($name, $title, $details, $status);
237
        } else {
238 6
            assert($isFoundInToOne xor $isFoundInToMany);
239 6
            $ruleIndexes = $this->getSerializer()::readSingleRuleIndexes(
240 6
                $isFoundInToOne === true ? $this->getToOneRules() : $this->getToManyRules(),
241 6
                $name
242
            );
243
244
            // now execute validation rules
245 6
            $this->executeStarts($this->getSerializer()::readRuleStartIndexes($ruleIndexes));
246 6
            $ruleIndex = $this->getSerializer()::readRuleIndex($ruleIndexes);
247 6
            $isFoundInToOne === true ?
248 2
                $this->validateAsToOneRelationship($ruleIndex, $name, $jsonData) :
249 4
                $this->validateAsToManyRelationship($ruleIndex, $name, $jsonData);
250 6
            $this->executeEnds($this->getSerializer()::readRuleEndIndexes($ruleIndexes));
251
252 6
            if (count($this->getErrorAggregator()) > 0) {
253 1
                foreach ($this->getErrorAggregator()->get() as $error) {
254 1
                    $this->getJsonApiErrorCollection()->addValidationRelationshipError($error);
255
                }
256 1
                $this->getErrorAggregator()->clear();
257
            }
258
        }
259
260 7
        $hasNoErrors = count($this->getJsonApiErrorCollection()) <= 0;
261
262 7
        return $hasNoErrors;
263
    }
264
265
    /**
266
     * @inheritdoc
267
     */
268 7
    public function assertRelationship(string $name, array $jsonData): JsonApiDataValidatingParserInterface
269
    {
270 7
        if ($this->parseRelationship($name, $jsonData) === false) {
271 2
            throw new JsonApiException($this->getJsonApiErrorCollection(), $this->getErrorStatus());
272
        }
273
274 5
        return $this;
275
    }
276
277
    /**
278
     * @inheritdoc
279
     */
280 6
    public function getJsonApiErrors(): array
281
    {
282 6
        return $this->getJsonApiErrorCollection()->getArrayCopy();
283
    }
284
285
    /**
286
     * @inheritdoc
287
     */
288 13
    public function getJsonApiCaptures(): array
289
    {
290 13
        return $this->getCaptures();
291
    }
292
293
    /**
294
     * @return BaseValidator
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use DataParser.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
295
     */
296 26
    protected function resetAggregators(): BaseValidator
297
    {
298 26
        $self = parent::resetAggregators();
299
300 26
        $this->getContext()->clear();
301
302 26
        return $self;
303
    }
304
305
    /**
306
     * @param string $serializerClass
307
     *
308
     * @return self
309
     */
310 26
    protected function setSerializerClass(string $serializerClass): self
311
    {
312 26
        assert(
313 26
            class_exists($serializerClass) === true &&
314 26
            in_array(JsonApiDataRulesSerializerInterface::class, class_implements($serializerClass)) === true
315
        );
316
317 26
        $this->serializerClass = $serializerClass;
318
319 26
        return $this;
320
    }
321
322
    /**
323
     * @return JsonApiDataRulesSerializerInterface|string
324
     */
325 26
    protected function getSerializer()
326
    {
327 26
        return $this->serializerClass;
328
    }
329
330
    /**
331
     * @param array $jsonData
332
     *
333
     * @return self
334
     *
335
     * @SuppressWarnings(PHPMD.StaticAccess)
336
     * @SuppressWarnings(PHPMD.ElseExpression)
337
     */
338 14
    private function validateType(array $jsonData): self
339
    {
340
        // execute start(s)
341 14
        $starts = $this->getSerializer()::readRuleStartIndexes($this->getTypeRule());
342 14
        $this->executeStarts($starts);
343
344 14
        if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true &&
345 14
            array_key_exists(DI::KEYWORD_TYPE, $data = $jsonData[DI::KEYWORD_DATA]) === true
346
        ) {
347
            // execute main validation block(s)
348 13
            $index = $this->getSerializer()::readRuleIndex($this->getTypeRule());
349 13
            $this->executeBlock($data[DI::KEYWORD_TYPE], $index);
350
        } else {
351 1
            $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
352 1
            $details = $this->formatMessage(ErrorCodes::TYPE_MISSING);
353 1
            $this->getJsonApiErrorCollection()->addDataTypeError($title, $details, $this->getErrorStatus());
354
        }
355
356
        // execute end(s)
357 14
        $ends = $this->getSerializer()::readRuleEndIndexes($this->getTypeRule());
358 14
        $this->executeEnds($ends);
359
360 14
        if (count($this->getErrorAggregator()) > 0) {
361 1
            $title = $this->formatMessage(ErrorCodes::INVALID_VALUE);
362 1
            foreach ($this->getErrorAggregator()->get() as $error) {
363 1
                $this->getJsonApiErrorCollection()
364 1
                    ->addDataTypeError($title, $this->getMessage($error), $this->getErrorStatus());
365
            }
366 1
            $this->getErrorAggregator()->clear();
367
        }
368
369 14
        return $this;
370
    }
371
372
    /**
373
     * @param array $jsonData
374
     *
375
     * @return self
376
     *
377
     * @SuppressWarnings(PHPMD.StaticAccess)
378
     */
379 14
    private function validateId(array $jsonData): self
380
    {
381
        // execute start(s)
382 14
        $starts = $this->getSerializer()::readRuleStartIndexes($this->getIdRule());
383 14
        $this->executeStarts($starts);
384
385
        // execute main validation block(s)
386 14
        if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true &&
387 14
            array_key_exists(DI::KEYWORD_ID, $data = $jsonData[DI::KEYWORD_DATA]) === true
388
        ) {
389 12
            $index = $this->getSerializer()::readRuleIndex($this->getIdRule());
390 12
            $this->executeBlock($data[DI::KEYWORD_ID], $index);
391
        }
392
393
        // execute end(s)
394 14
        $ends = $this->getSerializer()::readRuleEndIndexes($this->getIdRule());
395 14
        $this->executeEnds($ends);
396
397 14
        if (count($this->getErrorAggregator()) > 0) {
398 3
            $title = $this->formatMessage(ErrorCodes::INVALID_VALUE);
399 3
            foreach ($this->getErrorAggregator()->get() as $error) {
400 3
                $this->getJsonApiErrorCollection()
401 3
                    ->addDataIdError($title, $this->getMessage($error), $this->getErrorStatus());
402
            }
403 3
            $this->getErrorAggregator()->clear();
404
        }
405
406 14
        return $this;
407
    }
408
409
    /**
410
     * @param array $jsonData
411
     *
412
     * @return self
413
     *
414
     * @SuppressWarnings(PHPMD.StaticAccess)
415
     * @SuppressWarnings(PHPMD.ElseExpression)
416
     */
417 14
    private function validateAttributes(array $jsonData): self
418
    {
419
        // execute start(s)
420 14
        $starts = $this->getSerializer()::readRulesStartIndexes($this->getAttributeRules());
421 14
        $this->executeStarts($starts);
422
423 14
        if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true &&
424 14
            array_key_exists(DI::KEYWORD_ATTRIBUTES, $data = $jsonData[DI::KEYWORD_DATA]) === true
425
        ) {
426 13
            if (is_array($attributes = $data[DI::KEYWORD_ATTRIBUTES]) === false) {
427 2
                $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
428 2
                $details = $this->formatMessage(ErrorCodes::INVALID_ATTRIBUTES);
429 2
                $this->getJsonApiErrorCollection()->addAttributesError($title, $details, $this->getErrorStatus());
430
            } else {
431
                // execute main validation block(s)
432 11
                foreach ($attributes as $name => $value) {
433 8
                    if (($index = $this->getAttributeIndex($name)) !== null) {
434 7
                        $this->executeBlock($value, $index);
435 1
                    } elseif ($this->isIgnoreUnknowns() === false) {
436 1
                        $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
437 1
                        $details = $this->formatMessage(ErrorCodes::UNKNOWN_ATTRIBUTE);
438 1
                        $status  = $this->getErrorStatus();
439 8
                        $this->getJsonApiErrorCollection()->addDataAttributeError($name, $title, $details, $status);
440
                    }
441
                }
442
            }
443
        }
444
445
        // execute end(s)
446 14
        $ends = $this->getSerializer()::readRulesEndIndexes($this->getAttributeRules());
447 14
        $this->executeEnds($ends);
448
449 14
        if (count($this->getErrorAggregator()) > 0) {
450 2
            foreach ($this->getErrorAggregator()->get() as $error) {
451 2
                $this->getJsonApiErrorCollection()->addValidationAttributeError($error);
452
            }
453 2
            $this->getErrorAggregator()->clear();
454
        }
455
456 14
        return $this;
457
    }
458
459
    /**
460
     * @param array $jsonData
461
     *
462
     * @return self
463
     *
464
     * @SuppressWarnings(PHPMD.StaticAccess)
465
     * @SuppressWarnings(PHPMD.ElseExpression)
466
     */
467 14
    private function validateRelationships(array $jsonData): self
468
    {
469
        // execute start(s)
470 14
        $starts = array_merge(
471 14
            $this->getSerializer()::readRulesStartIndexes($this->getToOneRules()),
472 14
            $this->getSerializer()::readRulesStartIndexes($this->getToManyRules())
473
        );
474 14
        $this->executeStarts($starts);
475
476 14
        if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true &&
477 14
            array_key_exists(DI::KEYWORD_RELATIONSHIPS, $data = $jsonData[DI::KEYWORD_DATA]) === true
478
        ) {
479 10
            if (is_array($relationships = $data[DI::KEYWORD_RELATIONSHIPS]) === false) {
480 1
                $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
481 1
                $details = $this->formatMessage(ErrorCodes::INVALID_RELATIONSHIP_TYPE);
482 1
                $this->getJsonApiErrorCollection()->addRelationshipsError($title, $details, $this->getErrorStatus());
483
            } else {
484
                // ok we got to something that could be null or a valid relationship
485 9
                $toOneIndexes  = $this->getSerializer()::readRulesIndexes($this->getToOneRules());
486 9
                $toManyIndexes = $this->getSerializer()::readRulesIndexes($this->getToManyRules());
487
488 9
                foreach ($relationships as $name => $relationship) {
489 9
                    if (array_key_exists($name, $toOneIndexes) === true) {
490
                        // it might be to1 relationship
491 8
                        $this->validateAsToOneRelationship($toOneIndexes[$name], $name, $relationship);
492 9
                    } elseif (array_key_exists($name, $toManyIndexes) === true) {
493
                        // it might be toMany relationship
494 8
                        $this->validateAsToManyRelationship($toManyIndexes[$name], $name, $relationship);
495
                    } else {
496
                        // unknown relationship
497 1
                        $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
498 1
                        $details = $this->formatMessage(ErrorCodes::UNKNOWN_RELATIONSHIP);
499 1
                        $status  = $this->getErrorStatus();
500 9
                        $this->getJsonApiErrorCollection()->addRelationshipError($name, $title, $details, $status);
501
                    }
502
                }
503
            }
504
        }
505
506
        // execute end(s)
507 14
        $ends = array_merge(
508 14
            $this->getSerializer()::readRulesEndIndexes($this->getToOneRules()),
509 14
            $this->getSerializer()::readRulesEndIndexes($this->getToManyRules())
510
        );
511 14
        $this->executeEnds($ends);
512
513 14
        if (count($this->getErrorAggregator()) > 0) {
514 3
            foreach ($this->getErrorAggregator()->get() as $error) {
515 3
                $this->getJsonApiErrorCollection()->addValidationRelationshipError($error);
516
            }
517 3
            $this->getErrorAggregator()->clear();
518
        }
519
520 14
        return $this;
521
    }
522
523
    /**
524
     * @param int    $index
525
     * @param string $name
526
     * @param mixed  $mightBeRelationship
527
     *
528
     * @return void
529
     *
530
     * @SuppressWarnings(PHPMD.ElseExpression)
531
     */
532 10
    private function validateAsToOneRelationship(int $index, string $name, $mightBeRelationship): void
533
    {
534 10
        if (is_array($mightBeRelationship) === true &&
535 10
            array_key_exists(DI::KEYWORD_DATA, $mightBeRelationship) === true &&
536 10
            ($parsed = $this->parseSingleRelationship($mightBeRelationship[DI::KEYWORD_DATA])) !== false
537
        ) {
538
            // All right we got something. Now pass it to a validation rule.
539 8
            $this->executeBlock($parsed, $index);
540
        } else {
541 2
            $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
542 2
            $details = $this->formatMessage(ErrorCodes::INVALID_RELATIONSHIP);
543 2
            $this->getJsonApiErrorCollection()->addRelationshipError($name, $title, $details, $this->getErrorStatus());
544
        }
545
    }
546
547
    /**
548
     * @param int    $index
549
     * @param string $name
550
     * @param mixed  $mightBeRelationship
551
     *
552
     * @return void
553
     *
554
     * @SuppressWarnings(PHPMD.ElseExpression)
555
     */
556 12
    private function validateAsToManyRelationship(int $index, string $name, $mightBeRelationship): void
557
    {
558 12
        $isParsed       = true;
559 12
        $collectedPairs = [];
560 12
        if (is_array($mightBeRelationship) === true &&
561 12
            array_key_exists(DI::KEYWORD_DATA, $mightBeRelationship) === true &&
562 12
            is_array($data = $mightBeRelationship[DI::KEYWORD_DATA]) === true
563
        ) {
564 10
            foreach ($data as $mightTypeAndId) {
565
                // we accept only pairs of type and id (no `null`s are accepted).
566 9
                if (is_array($parsed = $this->parseSingleRelationship($mightTypeAndId)) === true) {
567 8
                    $collectedPairs[] = $parsed;
568
                } else {
569 1
                    $isParsed = false;
570 10
                    break;
571
                }
572
            }
573
        } else {
574 2
            $isParsed = false;
575
        }
576
577 12
        if ($isParsed === true) {
578
            // All right we got something. Now pass it to a validation rule.
579 9
            $this->executeBlock($collectedPairs, $index);
580
        } else {
581 3
            $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
582 3
            $details = $this->formatMessage(ErrorCodes::INVALID_RELATIONSHIP);
583 3
            $this->getJsonApiErrorCollection()->addRelationshipError($name, $title, $details, $this->getErrorStatus());
584
        }
585
    }
586
587
    /**
588
     * @param mixed $data
589
     *
590
     * @return array|null|false Either `array` ($type => $id), or `null`, or `false` on error.
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<*,integer|double|string|boolean>|false|null.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
591
     *
592
     * @SuppressWarnings(PHPMD.ElseExpression)
593
     */
594 13
    private function parseSingleRelationship($data)
595
    {
596 13
        if ($data === null) {
597 2
            $result = null;
598 11
        } elseif (is_array($data) === true &&
599 11
            array_key_exists(DI::KEYWORD_TYPE, $data) === true &&
600 11
            array_key_exists(DI::KEYWORD_ID, $data) === true &&
601 11
            is_scalar($type = $data[DI::KEYWORD_TYPE]) === true &&
602 11
            is_scalar($index = $data[DI::KEYWORD_ID]) === true
603
        ) {
604 10
            $result = [$type => $index];
605
        } else {
606 1
            $result = false;
607
        }
608
609 13
        return $result;
610
    }
611
612
    /**
613
     * Re-initializes internal aggregators for captures, errors, etc.
614
     */
615 21
    private function reInitAggregatorsIfNeeded(): void
616
    {
617 21
        $this->areAggregatorsDirty() === false ?: $this->resetAggregators();
618
    }
619
620
    /**
621
     * @param mixed $input
622
     * @param int   $index
623
     *
624
     * @return void
625
     *
626
     * @SuppressWarnings(PHPMD.StaticAccess)
627
     */
628 19
    private function executeBlock($input, int $index): void
629
    {
630 19
        BlockInterpreter::executeBlock(
631 19
            $input,
632 19
            $index,
633 19
            $this->getBlocks(),
634 19
            $this->getContext(),
635 19
            $this->getCaptureAggregator(),
636 19
            $this->getErrorAggregator()
637
        );
638
    }
639
640
    /**
641
     * @param array $indexes
642
     *
643
     * @return void
644
     *
645
     * @SuppressWarnings(PHPMD.StaticAccess)
646
     */
647 20
    private function executeStarts(array $indexes): void
648
    {
649 20
        BlockInterpreter::executeStarts(
650 20
            $indexes,
651 20
            $this->getBlocks(),
652 20
            $this->getContext(),
653 20
            $this->getErrorAggregator()
654
        );
655
    }
656
657
    /**
658
     * @param array $indexes
659
     *
660
     * @return void
661
     *
662
     * @SuppressWarnings(PHPMD.StaticAccess)
663
     */
664 20
    private function executeEnds(array $indexes): void
665
    {
666 20
        BlockInterpreter::executeEnds(
667 20
            $indexes,
668 20
            $this->getBlocks(),
669 20
            $this->getContext(),
670 20
            $this->getErrorAggregator()
671
        );
672
    }
673
674
    /**
675
     * @param ErrorInterface $error
676
     *
677
     * @return string
678
     */
679 3
    private function getMessage(ErrorInterface $error): string
680
    {
681 3
        $context = $error->getMessageContext();
682 3
        $args    = $context === null ? [] : $context;
683 3
        $message = $this->formatMessage($error->getMessageCode(), $args);
684
685 3
        return $message;
686
    }
687
688
    /**
689
     * @return array
690
     */
691 14
    protected function getIdRule(): array
692
    {
693 14
        return $this->idRule;
694
    }
695
696
    /**
697
     * @return array
698
     */
699 14
    protected function getTypeRule(): array
700
    {
701 14
        return $this->typeRule;
702
    }
703
704
    /**
705
     * @return ContextStorageInterface
706
     */
707 26
    protected function getContext(): ContextStorageInterface
708
    {
709 26
        return $this->context;
710
    }
711
712
    /**
713
     * @param ContextStorageInterface $context
714
     *
715
     * @return self
716
     */
717 26
    protected function setContext(ContextStorageInterface $context): self
718
    {
719 26
        $this->context = $context;
720
721 26
        return $this;
722
    }
723
724
    /**
725
     * @return JsonApiErrorCollection
726
     */
727 22
    protected function getJsonApiErrorCollection(): JsonApiErrorCollection
728
    {
729 22
        return $this->jsonApiErrors;
730
    }
731
732
    /**
733
     * @param JsonApiErrorCollection $errors
734
     *
735
     * @return self
736
     */
737 26
    protected function setJsonApiErrors(JsonApiErrorCollection $errors): self
738
    {
739 26
        $this->jsonApiErrors = $errors;
740
741 26
        return $this;
742
    }
743
744
    /**
745
     * @return int
746
     */
747 12
    protected function getErrorStatus(): int
748
    {
749 12
        return $this->errorStatus;
750
    }
751
752
    /**
753
     * @return bool
754
     */
755 1
    protected function isIgnoreUnknowns(): bool
756
    {
757 1
        return $this->isIgnoreUnknowns;
758
    }
759
760
    /**
761
     * @return self
762
     */
763 1
    protected function enableIgnoreUnknowns(): self
764
    {
765 1
        $this->isIgnoreUnknowns = true;
766
767 1
        return $this;
768
    }
769
770
    /**
771
     * @return self
772
     */
773 26
    protected function disableIgnoreUnknowns(): self
774
    {
775 26
        $this->isIgnoreUnknowns = false;
776
777 26
        return $this;
778
    }
779
780
    /**
781
     * @param array $rules
782
     *
783
     * @return self
784
     */
785 26
    private function setAttributeRules(array $rules): self
786
    {
787 26
        assert($this->debugCheckIndexesExist($rules));
788
789 26
        $this->attributeRules = $rules;
790
791 26
        return $this;
792
    }
793
794
    /**
795
     * @param array $rules
796
     *
797
     * @return self
798
     */
799 26
    private function setToOneIndexes(array $rules): self
800
    {
801 26
        assert($this->debugCheckIndexesExist($rules));
802
803 26
        $this->toOneRules = $rules;
804
805 26
        return $this;
806
    }
807
808
    /**
809
     * @param array $rules
810
     *
811
     * @return self
812
     */
813 26
    private function setToManyIndexes(array $rules): self
814
    {
815 26
        assert($this->debugCheckIndexesExist($rules));
816
817 26
        $this->toManyRules = $rules;
818
819 26
        return $this;
820
    }
821
822
    /**
823
     * @return int[]
824
     */
825 14
    protected function getAttributeRules(): array
826
    {
827 14
        return $this->attributeRules;
828
    }
829
830
    /**
831
     * @return int[]
832
     */
833 21
    protected function getToOneRules(): array
834
    {
835 21
        return $this->toOneRules;
836
    }
837
838
    /**
839
     * @return int[]
840
     */
841 19
    protected function getToManyRules(): array
842
    {
843 19
        return $this->toManyRules;
844
    }
845
846
    /**
847
     * @return array
848
     */
849 23
    private function getBlocks(): array
850
    {
851 23
        return $this->blocks;
852
    }
853
854
    /**
855
     * @return FormatterInterface
856
     */
857 10
    protected function getFormatter(): FormatterInterface
858
    {
859 10
        if ($this->formatter === null) {
860 10
            $this->formatter = $this->formatterFactory->createFormatter(FluteSettings::VALIDATION_NAMESPACE);
861
        }
862
863 10
        return $this->formatter;
864
    }
865
866
    /**
867
     * @param FormatterFactoryInterface $formatterFactory
868
     *
869
     * @return self
870
     */
871 26
    protected function setFormatterFactory(FormatterFactoryInterface $formatterFactory): self
872
    {
873 26
        $this->formatterFactory = $formatterFactory;
874
875 26
        return $this;
876
    }
877
878
    /**
879
     * @param string $name
880
     *
881
     * @return int|null
882
     *
883
     * @SuppressWarnings(PHPMD.StaticAccess)
884
     */
885 8
    private function getAttributeIndex(string $name): ?int
886
    {
887 8
        $indexes = $this->getSerializer()::readRulesIndexes($this->getAttributeRules());
888 8
        $index   = $indexes[$name] ?? null;
889
890 8
        return $index;
891
    }
892
893
    /**
894
     * @param int   $messageId
895
     * @param array $args
896
     *
897
     * @return string
898
     */
899 10
    private function formatMessage(int $messageId, array $args = []): string
900
    {
901 10
        $message = $this->getFormatter()->formatMessage($messageId, $args);
902
903 10
        return $message;
904
    }
905
906
    /**
907
     * @param array $rules
908
     *
909
     * @return bool
910
     *
911
     * @SuppressWarnings(PHPMD.StaticAccess)
912
     */
913 26
    private function debugCheckIndexesExist(array $rules): bool
914
    {
915 26
        $allOk = true;
916
917 26
        $indexes = array_merge(
918 26
            $this->getSerializer()::readRulesIndexes($rules),
919 26
            $this->getSerializer()::readRulesStartIndexes($rules),
920 26
            $this->getSerializer()::readRulesEndIndexes($rules)
921
        );
922
923 26
        foreach ($indexes as $index) {
924 21
            $allOk = $allOk && is_int($index) && $this->getSerializer()::hasRule($index, $this->getBlocks());
925
        }
926
927 26
        return $allOk;
928
    }
929
}
930