Completed
Push — master ( 210649...ef3b6a )
by Neomerx
05:34
created

DataParser::formatMessage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
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\JsonApiDataParserInterface;
23
use Limoncello\Flute\Contracts\Validation\JsonApiDataRulesSerializerInterface;
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\Captures\CaptureAggregator;
29
use Limoncello\Validation\Contracts\Captures\CaptureAggregatorInterface;
30
use Limoncello\Validation\Contracts\Errors\ErrorAggregatorInterface;
31
use Limoncello\Validation\Contracts\Errors\ErrorInterface;
32
use Limoncello\Validation\Contracts\Execution\ContextStorageInterface;
33
use Limoncello\Validation\Errors\ErrorAggregator;
34
use Limoncello\Validation\Execution\BlockInterpreter;
35
use Neomerx\JsonApi\Contracts\Document\DocumentInterface as DI;
36
use Neomerx\JsonApi\Exceptions\JsonApiException;
37
38
/**
39
 * @package Limoncello\Flute
40
 *
41
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
42
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
43
 */
44
class DataParser implements JsonApiDataParserInterface
45
{
46
    use RelationshipRulesTrait;
47
48
    /** Rule description index */
49
    const RULE_INDEX = 0;
50
51
    /** Rule description index */
52
    const RULE_ATTRIBUTES = self::RULE_INDEX + 1;
53
54
    /** Rule description index */
55
    const RULE_TO_ONE = self::RULE_ATTRIBUTES + 1;
56
57
    /** Rule description index */
58
    const RULE_TO_MANY = self::RULE_TO_ONE + 1;
59
60
    /** Rule description index */
61
    const RULE_UNLISTED_ATTRIBUTE = self::RULE_TO_MANY + 1;
62
63
    /** Rule description index */
64
    const RULE_UNLISTED_RELATIONSHIP = self::RULE_UNLISTED_ATTRIBUTE + 1;
65
66
    /**
67
     * NOTE: Despite the type it is just a string so only static methods can be called from the interface.
68
     *
69
     * @var JsonApiDataRulesSerializerInterface|string
70
     */
71
    private $serializerClass;
72
73
    /**
74
     * @var int|null
75
     */
76
    private $errorStatus;
77
78
    /**
79
     * @var ContextStorageInterface
80
     */
81
    private $context;
82
83
    /**
84
     * @var JsonApiErrorCollection
85
     */
86
    private $jsonApiErrors;
87
88
    /**
89
     * @var array
90
     */
91
    private $blocks;
92
93
    /**
94
     * @var array
95
     */
96
    private $idRule;
97
98
    /**
99
     * @var array
100
     */
101
    private $typeRule;
102
103
    /**
104
     * @var int[]
105
     */
106
    private $attributeRules;
107
108
    /**
109
     * @var int[]
110
     */
111
    private $toOneRules;
112
113
    /**
114
     * @var int[]
115
     */
116
    private $toManyRules;
117
118
    /**
119
     * @var bool
120
     */
121
    private $isIgnoreUnknowns;
122
123
    /**
124
     * @var FormatterInterface|null
125
     */
126
    private $formatter;
127
128
    /**
129
     * @var FormatterFactoryInterface
130
     */
131
    private $formatterFactory;
132
133
    /**
134
     * @var ErrorAggregatorInterface
135
     */
136
    private $errorAggregator;
137
138
    /**
139
     * @var CaptureAggregatorInterface
140
     */
141
    private $captureAggregator;
142
143
    /**
144
     * @param string                    $rulesClass
145
     * @param string                    $serializerClass
146
     * @param array                     $serializedData
147
     * @param ContextStorageInterface   $context
148
     * @param JsonApiErrorCollection    $jsonErrors
149
     * @param FormatterFactoryInterface $formatterFactory
150
     *
151
     * @SuppressWarnings(PHPMD.StaticAccess)
152
     */
153 27
    public function __construct(
154
        string $rulesClass,
155
        string $serializerClass,
156
        array $serializedData,
157
        ContextStorageInterface $context,
158
        JsonApiErrorCollection $jsonErrors,
159
        FormatterFactoryInterface $formatterFactory
160
    ) {
161
        $this
162 27
            ->setSerializerClass($serializerClass)
163 27
            ->setContext($context)
164 27
            ->setJsonApiErrors($jsonErrors)
165 27
            ->setFormatterFactory($formatterFactory);
166
167 27
        $this->blocks      = $this->getSerializer()::readBlocks($serializedData);
168 27
        $ruleSet           = $this->getSerializer()::readRules($rulesClass, $serializedData);
169 27
        $this->idRule      = $this->getSerializer()::readIdRuleIndexes($ruleSet);
170 27
        $this->typeRule    = $this->getSerializer()::readTypeRuleIndexes($ruleSet);
171 27
        $this->errorStatus = null;
172
173
        $this
174 27
            ->setAttributeRules($this->getSerializer()::readAttributeRulesIndexes($ruleSet))
175 27
            ->setToOneIndexes($this->getSerializer()::readToOneRulesIndexes($ruleSet))
176 27
            ->setToManyIndexes($this->getSerializer()::readToManyRulesIndexes($ruleSet))
177 27
            ->disableIgnoreUnknowns();
178
179 27
        $this->errorAggregator   = new ErrorAggregator();
180 27
        $this->captureAggregator = new CaptureAggregator();
181
    }
182
183
    /**
184
     * @inheritdoc
185
     */
186 12
    public function assert(array $jsonData): JsonApiDataParserInterface
187
    {
188 12
        if ($this->parse($jsonData) === false) {
189 5
            throw new JsonApiException($this->getJsonApiErrorCollection(), $this->getErrorStatus());
190
        }
191
192 7
        return $this;
193
    }
194
195
    /**
196
     * @inheritdoc
197
     *
198
     * @SuppressWarnings(PHPMD.ElseExpression)
199
     */
200 16
    public function parse(array $input): bool
201
    {
202 16
        $this->resetAggregators();
203
204
        $this
205 16
            ->validateType($input)
206 16
            ->validateId($input)
207 16
            ->validateAttributes($input)
208 16
            ->validateRelationships($input);
209
210 16
        $hasNoErrors = $this->getJsonApiErrorCollection()->count() <= 0;
211
212 16
        return $hasNoErrors;
213
    }
214
215
    /**
216
     * @inheritdoc
217
     *
218
     * @SuppressWarnings(PHPMD.ElseExpression)
219
     */
220 7
    public function parseRelationship(string $index, string $name, array $jsonData): bool
221
    {
222 7
        $this->resetAggregators();
223
224 7
        $isFoundInToOne  = array_key_exists($name, $this->getSerializer()::readRulesIndexes($this->getToOneRules()));
225 7
        $isFoundInToMany = $isFoundInToOne === false &&
226 7
            array_key_exists($name, $this->getSerializer()::readRulesIndexes($this->getToManyRules()));
227
228 7
        if ($isFoundInToOne === false && $isFoundInToMany === false) {
229 1
            $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
230 1
            $details = $this->formatMessage(ErrorCodes::UNKNOWN_RELATIONSHIP);
231 1
            $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
232 1
            $this->getJsonApiErrorCollection()->addRelationshipError($name, $title, $details, $status);
233 1
            $this->addErrorStatus($status);
234
        } else {
235 6
            assert($isFoundInToOne xor $isFoundInToMany);
236 6
            $ruleIndexes = $this->getSerializer()::readSingleRuleIndexes(
237 6
                $isFoundInToOne === true ? $this->getToOneRules() : $this->getToManyRules(),
238 6
                $name
239
            );
240
241
            // now execute validation rules
242 6
            $this->executeStarts($this->getSerializer()::readRuleStartIndexes($ruleIndexes));
243 6
            $ruleIndex = $this->getSerializer()::readRuleIndex($ruleIndexes);
244 6
            $isFoundInToOne === true ?
245 2
                $this->validateAsToOneRelationship($ruleIndex, $name, $jsonData) :
246 4
                $this->validateAsToManyRelationship($ruleIndex, $name, $jsonData);
247 6
            $this->executeEnds($this->getSerializer()::readRuleEndIndexes($ruleIndexes));
248
249 6
            if (count($this->getErrorAggregator()) > 0) {
250 1
                $status  = JsonApiResponse::HTTP_CONFLICT;
251 1
                foreach ($this->getErrorAggregator()->get() as $error) {
252 1
                    $this->getJsonApiErrorCollection()->addValidationRelationshipError($error, $status);
253 1
                    $this->addErrorStatus($status);
254
                }
255 1
                $this->getErrorAggregator()->clear();
256
            }
257
        }
258
259 7
        $hasNoErrors = count($this->getJsonApiErrorCollection()) <= 0;
260
261 7
        return $hasNoErrors;
262
    }
263
264
    /**
265
     * @inheritdoc
266
     */
267 7
    public function assertRelationship(
268
        string $index,
269
        string $name,
270
        array $jsonData
271
    ): JsonApiDataParserInterface {
272 7
        if ($this->parseRelationship($index, $name, $jsonData) === false) {
273 2
            $status = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
274 2
            throw new JsonApiException($this->getJsonApiErrorCollection(), $status);
275
        }
276
277 5
        return $this;
278
    }
279
280
    /**
281
     * @inheritdoc
282
     */
283 6
    public function getErrors(): array
284
    {
285 6
        return $this->getJsonApiErrorCollection()->getArrayCopy();
286
    }
287
288
    /**
289
     * @inheritdoc
290
     */
291 15
    public function getCaptures(): array
292
    {
293 15
        return $this->getCaptureAggregator()->get();
294
    }
295
296
    /**
297
     * @return self
298
     */
299 23
    protected function resetAggregators(): self
300
    {
301 23
        $this->getCaptureAggregator()->clear();
302 23
        $this->getErrorAggregator()->clear();
303 23
        $this->getContext()->clear();
304
305 23
        return $this;
306
    }
307
308
    /**
309
     * @param string $serializerClass
310
     *
311
     * @return self
312
     */
313 27
    protected function setSerializerClass(string $serializerClass): self
314
    {
315 27
        assert(
316 27
            class_exists($serializerClass) === true &&
317 27
            in_array(JsonApiDataRulesSerializerInterface::class, class_implements($serializerClass)) === true
318
        );
319
320 27
        $this->serializerClass = $serializerClass;
321
322 27
        return $this;
323
    }
324
325
    /**
326
     * @return JsonApiDataRulesSerializerInterface|string
327
     */
328 27
    protected function getSerializer()
329
    {
330 27
        return $this->serializerClass;
331
    }
332
333
    /**
334
     * @param array $jsonData
335
     *
336
     * @return self
337
     *
338
     * @SuppressWarnings(PHPMD.StaticAccess)
339
     * @SuppressWarnings(PHPMD.ElseExpression)
340
     */
341 16
    private function validateType(array $jsonData): self
342
    {
343
        // execute start(s)
344 16
        $starts = $this->getSerializer()::readRuleStartIndexes($this->getTypeRule());
345 16
        $this->executeStarts($starts);
346
347 16
        if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true &&
348 16
            array_key_exists(DI::KEYWORD_TYPE, $data = $jsonData[DI::KEYWORD_DATA]) === true
349
        ) {
350
            // execute main validation block(s)
351 15
            $index = $this->getSerializer()::readRuleIndex($this->getTypeRule());
352 15
            $this->executeBlock($data[DI::KEYWORD_TYPE], $index);
353
        } else {
354 1
            $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
355 1
            $details = $this->formatMessage(ErrorCodes::TYPE_MISSING);
356 1
            $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
357 1
            $this->getJsonApiErrorCollection()->addDataTypeError($title, $details, $status);
358 1
            $this->addErrorStatus($status);
359
        }
360
361
        // execute end(s)
362 16
        $ends = $this->getSerializer()::readRuleEndIndexes($this->getTypeRule());
363 16
        $this->executeEnds($ends);
364
365 16
        if (count($this->getErrorAggregator()) > 0) {
366 1
            $title = $this->formatMessage(ErrorCodes::INVALID_VALUE);
367 1
            $status  = JsonApiResponse::HTTP_CONFLICT;
368 1
            foreach ($this->getErrorAggregator()->get() as $error) {
369 1
                $details = $this->getMessage($error);
370 1
                $this->getJsonApiErrorCollection()->addDataTypeError($title, $details, $status);
371
            }
372 1
            $this->addErrorStatus($status);
373 1
            $this->getErrorAggregator()->clear();
374
        }
375
376 16
        return $this;
377
    }
378
379
    /**
380
     * @param array $jsonData
381
     *
382
     * @return self
383
     *
384
     * @SuppressWarnings(PHPMD.StaticAccess)
385
     */
386 16
    private function validateId(array $jsonData): self
387
    {
388
        // execute start(s)
389 16
        $starts = $this->getSerializer()::readRuleStartIndexes($this->getIdRule());
390 16
        $this->executeStarts($starts);
391
392
        // execute main validation block(s)
393 16
        if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true &&
394 16
            array_key_exists(DI::KEYWORD_ID, $data = $jsonData[DI::KEYWORD_DATA]) === true
395
        ) {
396 13
            $index = $this->getSerializer()::readRuleIndex($this->getIdRule());
397 13
            $this->executeBlock($data[DI::KEYWORD_ID], $index);
398
        }
399
400
        // execute end(s)
401 16
        $ends = $this->getSerializer()::readRuleEndIndexes($this->getIdRule());
402 16
        $this->executeEnds($ends);
403
404 16
        if (count($this->getErrorAggregator()) > 0) {
405 3
            $title  = $this->formatMessage(ErrorCodes::INVALID_VALUE);
406 3
            $status = JsonApiResponse::HTTP_CONFLICT;
407 3
            foreach ($this->getErrorAggregator()->get() as $error) {
408 3
                $details = $this->getMessage($error);
409 3
                $this->getJsonApiErrorCollection()->addDataIdError($title, $details, $status);
410
            }
411 3
            $this->addErrorStatus($status);
412 3
            $this->getErrorAggregator()->clear();
413
        }
414
415 16
        return $this;
416
    }
417
418
    /**
419
     * @param array $jsonData
420
     *
421
     * @return self
422
     *
423
     * @SuppressWarnings(PHPMD.StaticAccess)
424
     * @SuppressWarnings(PHPMD.ElseExpression)
425
     */
426 16
    private function validateAttributes(array $jsonData): self
427
    {
428
        // execute start(s)
429 16
        $starts = $this->getSerializer()::readRulesStartIndexes($this->getAttributeRules());
430 16
        $this->executeStarts($starts);
431
432 16
        if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true &&
433 16
            array_key_exists(DI::KEYWORD_ATTRIBUTES, $data = $jsonData[DI::KEYWORD_DATA]) === true
434
        ) {
435 15
            if (is_array($attributes = $data[DI::KEYWORD_ATTRIBUTES]) === false) {
436 2
                $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
437 2
                $details = $this->formatMessage(ErrorCodes::INVALID_ATTRIBUTES);
438 2
                $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
439 2
                $this->getJsonApiErrorCollection()->addAttributesError($title, $details, $status);
440 2
                $this->addErrorStatus($status);
441
            } else {
442
                // execute main validation block(s)
443 13
                foreach ($attributes as $name => $value) {
444 10
                    if (($index = $this->getAttributeIndex($name)) !== null) {
445 9
                        $this->executeBlock($value, $index);
446 1
                    } elseif ($this->isIgnoreUnknowns() === false) {
447 1
                        $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
448 1
                        $details = $this->formatMessage(ErrorCodes::UNKNOWN_ATTRIBUTE);
449 1
                        $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
450 1
                        $this->getJsonApiErrorCollection()->addDataAttributeError($name, $title, $details, $status);
451 10
                        $this->addErrorStatus($status);
452
                    }
453
                }
454
            }
455
        }
456
457
        // execute end(s)
458 16
        $ends = $this->getSerializer()::readRulesEndIndexes($this->getAttributeRules());
459 16
        $this->executeEnds($ends);
460
461 16
        if (count($this->getErrorAggregator()) > 0) {
462 2
            $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
463 2
            foreach ($this->getErrorAggregator()->get() as $error) {
464 2
                $this->getJsonApiErrorCollection()->addValidationAttributeError($error, $status);
465
            }
466 2
            $this->addErrorStatus($status);
467 2
            $this->getErrorAggregator()->clear();
468
        }
469
470 16
        return $this;
471
    }
472
473
    /**
474
     * @param array $jsonData
475
     *
476
     * @return self
477
     *
478
     * @SuppressWarnings(PHPMD.StaticAccess)
479
     * @SuppressWarnings(PHPMD.ElseExpression)
480
     */
481 16
    private function validateRelationships(array $jsonData): self
482
    {
483
        // execute start(s)
484 16
        $starts = array_merge(
485 16
            $this->getSerializer()::readRulesStartIndexes($this->getToOneRules()),
486 16
            $this->getSerializer()::readRulesStartIndexes($this->getToManyRules())
487
        );
488 16
        $this->executeStarts($starts);
489
490 16
        if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true &&
491 16
            array_key_exists(DI::KEYWORD_RELATIONSHIPS, $data = $jsonData[DI::KEYWORD_DATA]) === true
492
        ) {
493 10
            if (is_array($relationships = $data[DI::KEYWORD_RELATIONSHIPS]) === false) {
494 1
                $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
495 1
                $details = $this->formatMessage(ErrorCodes::INVALID_RELATIONSHIP_TYPE);
496 1
                $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
497 1
                $this->getJsonApiErrorCollection()->addRelationshipsError($title, $details, $status);
498 1
                $this->addErrorStatus($status);
499
            } else {
500
                // ok we got to something that could be null or a valid relationship
501 9
                $toOneIndexes  = $this->getSerializer()::readRulesIndexes($this->getToOneRules());
502 9
                $toManyIndexes = $this->getSerializer()::readRulesIndexes($this->getToManyRules());
503
504 9
                foreach ($relationships as $name => $relationship) {
505 9
                    if (array_key_exists($name, $toOneIndexes) === true) {
506
                        // it might be to1 relationship
507 8
                        $this->validateAsToOneRelationship($toOneIndexes[$name], $name, $relationship);
508 9
                    } elseif (array_key_exists($name, $toManyIndexes) === true) {
509
                        // it might be toMany relationship
510 8
                        $this->validateAsToManyRelationship($toManyIndexes[$name], $name, $relationship);
511
                    } else {
512
                        // unknown relationship
513 1
                        $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
514 1
                        $details = $this->formatMessage(ErrorCodes::UNKNOWN_RELATIONSHIP);
515 1
                        $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
516 1
                        $this->getJsonApiErrorCollection()->addRelationshipError($name, $title, $details, $status);
517 9
                        $this->addErrorStatus($status);
518
                    }
519
                }
520
            }
521
        }
522
523
        // execute end(s)
524 16
        $ends = array_merge(
525 16
            $this->getSerializer()::readRulesEndIndexes($this->getToOneRules()),
526 16
            $this->getSerializer()::readRulesEndIndexes($this->getToManyRules())
527
        );
528 16
        $this->executeEnds($ends);
529
530 16
        if (count($this->getErrorAggregator()) > 0) {
531 3
            $status  = JsonApiResponse::HTTP_CONFLICT;
532 3
            foreach ($this->getErrorAggregator()->get() as $error) {
533 3
                $this->getJsonApiErrorCollection()->addValidationRelationshipError($error, $status);
534
            }
535 3
            $this->addErrorStatus($status);
536 3
            $this->getErrorAggregator()->clear();
537
        }
538
539 16
        return $this;
540
    }
541
542
    /**
543
     * @param int    $index
544
     * @param string $name
545
     * @param mixed  $mightBeRelationship
546
     *
547
     * @return void
548
     *
549
     * @SuppressWarnings(PHPMD.ElseExpression)
550
     */
551 10
    private function validateAsToOneRelationship(int $index, string $name, $mightBeRelationship): void
552
    {
553 10
        if (is_array($mightBeRelationship) === true &&
554 10
            array_key_exists(DI::KEYWORD_DATA, $mightBeRelationship) === true &&
555 10
            ($parsed = $this->parseSingleRelationship($mightBeRelationship[DI::KEYWORD_DATA])) !== false
556
        ) {
557
            // All right we got something. Now pass it to a validation rule.
558 8
            $this->executeBlock($parsed, $index);
559
        } else {
560 2
            $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
561 2
            $details = $this->formatMessage(ErrorCodes::INVALID_RELATIONSHIP);
562 2
            $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
563 2
            $this->getJsonApiErrorCollection()->addRelationshipError($name, $title, $details, $status);
564 2
            $this->addErrorStatus($status);
565
        }
566
    }
567
568
    /**
569
     * @param int    $index
570
     * @param string $name
571
     * @param mixed  $mightBeRelationship
572
     *
573
     * @return void
574
     *
575
     * @SuppressWarnings(PHPMD.ElseExpression)
576
     */
577 12
    private function validateAsToManyRelationship(int $index, string $name, $mightBeRelationship): void
578
    {
579 12
        $isParsed       = true;
580 12
        $collectedPairs = [];
581 12
        if (is_array($mightBeRelationship) === true &&
582 12
            array_key_exists(DI::KEYWORD_DATA, $mightBeRelationship) === true &&
583 12
            is_array($data = $mightBeRelationship[DI::KEYWORD_DATA]) === true
584
        ) {
585 10
            foreach ($data as $mightTypeAndId) {
586
                // we accept only pairs of type and id (no `null`s are accepted).
587 9
                if (is_array($parsed = $this->parseSingleRelationship($mightTypeAndId)) === true) {
588 8
                    $collectedPairs[] = $parsed;
589
                } else {
590 1
                    $isParsed = false;
591 10
                    break;
592
                }
593
            }
594
        } else {
595 2
            $isParsed = false;
596
        }
597
598 12
        if ($isParsed === true) {
599
            // All right we got something. Now pass it to a validation rule.
600 9
            $this->executeBlock($collectedPairs, $index);
601
        } else {
602 3
            $title   = $this->formatMessage(ErrorCodes::INVALID_VALUE);
603 3
            $details = $this->formatMessage(ErrorCodes::INVALID_RELATIONSHIP);
604 3
            $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
605 3
            $this->getJsonApiErrorCollection()->addRelationshipError($name, $title, $details, $status);
606 3
            $this->addErrorStatus($status);
607
        }
608
    }
609
610
    /**
611
     * @param mixed $data
612
     *
613
     * @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...
614
     *
615
     * @SuppressWarnings(PHPMD.ElseExpression)
616
     */
617 13
    private function parseSingleRelationship($data)
618
    {
619 13
        if ($data === null) {
620 2
            $result = null;
621 11
        } elseif (is_array($data) === true &&
622 11
            array_key_exists(DI::KEYWORD_TYPE, $data) === true &&
623 11
            array_key_exists(DI::KEYWORD_ID, $data) === true &&
624 11
            is_scalar($type = $data[DI::KEYWORD_TYPE]) === true &&
625 11
            is_scalar($index = $data[DI::KEYWORD_ID]) === true
626
        ) {
627 10
            $result = [$type => $index];
628
        } else {
629 1
            $result = false;
630
        }
631
632 13
        return $result;
633
    }
634
635
    /**
636
     * @param mixed $input
637
     * @param int   $index
638
     *
639
     * @return void
640
     *
641
     * @SuppressWarnings(PHPMD.StaticAccess)
642
     */
643 21
    private function executeBlock($input, int $index): void
644
    {
645 21
        BlockInterpreter::executeBlock(
646 21
            $input,
647 21
            $index,
648 21
            $this->getBlocks(),
649 21
            $this->getContext(),
650 21
            $this->getCaptureAggregator(),
651 21
            $this->getErrorAggregator()
652
        );
653
    }
654
655
    /**
656
     * @param array $indexes
657
     *
658
     * @return void
659
     *
660
     * @SuppressWarnings(PHPMD.StaticAccess)
661
     */
662 22
    private function executeStarts(array $indexes): void
663
    {
664 22
        BlockInterpreter::executeStarts(
665 22
            $indexes,
666 22
            $this->getBlocks(),
667 22
            $this->getContext(),
668 22
            $this->getErrorAggregator()
669
        );
670
    }
671
672
    /**
673
     * @param array $indexes
674
     *
675
     * @return void
676
     *
677
     * @SuppressWarnings(PHPMD.StaticAccess)
678
     */
679 22
    private function executeEnds(array $indexes): void
680
    {
681 22
        BlockInterpreter::executeEnds(
682 22
            $indexes,
683 22
            $this->getBlocks(),
684 22
            $this->getContext(),
685 22
            $this->getErrorAggregator()
686
        );
687
    }
688
689
    /**
690
     * @param ErrorInterface $error
691
     *
692
     * @return string
693
     */
694 3
    private function getMessage(ErrorInterface $error): string
695
    {
696 3
        $context = $error->getMessageContext();
697 3
        $args    = $context === null ? [] : $context;
698 3
        $message = $this->formatMessage($error->getMessageCode(), $args);
699
700 3
        return $message;
701
    }
702
703
    /**
704
     * @return array
705
     */
706 16
    protected function getIdRule(): array
707
    {
708 16
        return $this->idRule;
709
    }
710
711
    /**
712
     * @return array
713
     */
714 16
    protected function getTypeRule(): array
715
    {
716 16
        return $this->typeRule;
717
    }
718
719
    /**
720
     * @return ContextStorageInterface
721
     */
722 23
    protected function getContext(): ContextStorageInterface
723
    {
724 23
        return $this->context;
725
    }
726
727
    /**
728
     * @param ContextStorageInterface $context
729
     *
730
     * @return self
731
     */
732 27
    protected function setContext(ContextStorageInterface $context): self
733
    {
734 27
        $this->context = $context;
735
736 27
        return $this;
737
    }
738
739
    /**
740
     * @return JsonApiErrorCollection
741
     */
742 23
    protected function getJsonApiErrorCollection(): JsonApiErrorCollection
743
    {
744 23
        return $this->jsonApiErrors;
745
    }
746
747
    /**
748
     * @param JsonApiErrorCollection $errors
749
     *
750
     * @return self
751
     */
752 27
    protected function setJsonApiErrors(JsonApiErrorCollection $errors): self
753
    {
754 27
        $this->jsonApiErrors = $errors;
755
756 27
        return $this;
757
    }
758
759
    /**
760
     * @return bool
761
     */
762 1
    protected function isIgnoreUnknowns(): bool
763
    {
764 1
        return $this->isIgnoreUnknowns;
765
    }
766
767
    /**
768
     * @return self
769
     */
770 1
    protected function enableIgnoreUnknowns(): self
771
    {
772 1
        $this->isIgnoreUnknowns = true;
773
774 1
        return $this;
775
    }
776
777
    /**
778
     * @return self
779
     */
780 27
    protected function disableIgnoreUnknowns(): self
781
    {
782 27
        $this->isIgnoreUnknowns = false;
783
784 27
        return $this;
785
    }
786
787
    /**
788
     * @return int
789
     */
790 5
    private function getErrorStatus(): int
791
    {
792 5
        assert($this->errorStatus !== null, 'Check error code was set');
793
794 5
        return $this->errorStatus;
795
    }
796
797
    /**
798
     * @param int $status
799
     */
800 11
    private function addErrorStatus(int $status): void
801
    {
802
        // Currently (at the moment of writing) the spec is vague about how error status should be set.
803
        // On the one side it says, for example, 'A server MUST return 409 Conflict when processing a POST
804
        // request to create a resource with a client-generated ID that already exists.'
805
        // So you might think 'simple, that should be HTTP status, right?'
806
        // But on the other
807
        // - 'it [server] MAY continue processing and encounter multiple problems.'
808
        // - 'When a server encounters multiple problems for a single request, the most generally applicable
809
        //    HTTP error code SHOULD be used in the response. For instance, 400 Bad Request might be appropriate
810
        //    for multiple 4xx errors'
811
812
        // So, as we might return multiple errors, we have to figure out what is the best status for response.
813
814
        // The strategy is the following: for the first error its error code becomes the Response's status.
815
        // If any following error code do not match the previous the status becomes generic 400.
816 11
        if ($this->errorStatus === null) {
817 11
            $this->errorStatus = $status;
818 6
        } elseif ($this->errorStatus !== JsonApiResponse::HTTP_BAD_REQUEST && $this->errorStatus !== $status) {
819 3
            $this->errorStatus = JsonApiResponse::HTTP_BAD_REQUEST;
820
        }
821
    }
822
823
    /**
824
     * @param array $rules
825
     *
826
     * @return self
827
     */
828 27
    private function setAttributeRules(array $rules): self
829
    {
830 27
        assert($this->debugCheckIndexesExist($rules));
831
832 27
        $this->attributeRules = $rules;
833
834 27
        return $this;
835
    }
836
837
    /**
838
     * @param array $rules
839
     *
840
     * @return self
841
     */
842 27
    private function setToOneIndexes(array $rules): self
843
    {
844 27
        assert($this->debugCheckIndexesExist($rules));
845
846 27
        $this->toOneRules = $rules;
847
848 27
        return $this;
849
    }
850
851
    /**
852
     * @param array $rules
853
     *
854
     * @return self
855
     */
856 27
    private function setToManyIndexes(array $rules): self
857
    {
858 27
        assert($this->debugCheckIndexesExist($rules));
859
860 27
        $this->toManyRules = $rules;
861
862 27
        return $this;
863
    }
864
865
    /**
866
     * @return int[]
867
     */
868 16
    protected function getAttributeRules(): array
869
    {
870 16
        return $this->attributeRules;
871
    }
872
873
    /**
874
     * @return int[]
875
     */
876 23
    protected function getToOneRules(): array
877
    {
878 23
        return $this->toOneRules;
879
    }
880
881
    /**
882
     * @return int[]
883
     */
884 21
    protected function getToManyRules(): array
885
    {
886 21
        return $this->toManyRules;
887
    }
888
889
    /**
890
     * @return array
891
     */
892 25
    private function getBlocks(): array
893
    {
894 25
        return $this->blocks;
895
    }
896
897
    /**
898
     * @return FormatterInterface
899
     */
900 9
    protected function getFormatter(): FormatterInterface
901
    {
902 9
        if ($this->formatter === null) {
903 9
            $this->formatter = $this->formatterFactory->createFormatter(FluteSettings::VALIDATION_NAMESPACE);
904
        }
905
906 9
        return $this->formatter;
907
    }
908
909
    /**
910
     * @param FormatterFactoryInterface $formatterFactory
911
     *
912
     * @return self
913
     */
914 27
    protected function setFormatterFactory(FormatterFactoryInterface $formatterFactory): self
915
    {
916 27
        $this->formatterFactory = $formatterFactory;
917
918 27
        return $this;
919
    }
920
921
    /**
922
     * @param string $name
923
     *
924
     * @return int|null
925
     *
926
     * @SuppressWarnings(PHPMD.StaticAccess)
927
     */
928 10
    private function getAttributeIndex(string $name): ?int
929
    {
930 10
        $indexes = $this->getSerializer()::readRulesIndexes($this->getAttributeRules());
931 10
        $index   = $indexes[$name] ?? null;
932
933 10
        return $index;
934
    }
935
936
    /**
937
     * @param int   $messageId
938
     * @param array $args
939
     *
940
     * @return string
941
     */
942 9
    private function formatMessage(int $messageId, array $args = []): string
943
    {
944 9
        $message = $this->getFormatter()->formatMessage($messageId, $args);
945
946 9
        return $message;
947
    }
948
949
    /**
950
     * @return ErrorAggregatorInterface
951
     */
952 23
    private function getErrorAggregator(): ErrorAggregatorInterface
953
    {
954 23
        return $this->errorAggregator;
955
    }
956
957
    /**
958
     * @return CaptureAggregatorInterface
959
     */
960 23
    private function getCaptureAggregator(): CaptureAggregatorInterface
961
    {
962 23
        return $this->captureAggregator;
963
    }
964
965
    /**
966
     * @param array $rules
967
     *
968
     * @return bool
969
     *
970
     * @SuppressWarnings(PHPMD.StaticAccess)
971
     */
972 27
    private function debugCheckIndexesExist(array $rules): bool
973
    {
974 27
        $allOk = true;
975
976 27
        $indexes = array_merge(
977 27
            $this->getSerializer()::readRulesIndexes($rules),
978 27
            $this->getSerializer()::readRulesStartIndexes($rules),
979 27
            $this->getSerializer()::readRulesEndIndexes($rules)
980
        );
981
982 27
        foreach ($indexes as $index) {
983 23
            $allOk = $allOk && is_int($index) && $this->getSerializer()::hasRule($index, $this->getBlocks());
984
        }
985
986 27
        return $allOk;
987
    }
988
}
989