DataParser   F
last analyzed

Complexity

Total Complexity 103

Size/Duplication

Total Lines 938
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 103
lcom 1
cbo 13
dl 0
loc 938
ccs 321
cts 321
cp 1
rs 1.662
c 0
b 0
f 0

46 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 29 1
A assert() 0 8 2
A parse() 0 14 1
B parseRelationship() 0 43 8
A assertRelationship() 0 12 2
A getErrors() 0 4 1
A getCaptures() 0 4 1
A resetAggregators() 0 8 1
A setSerializerClass() 0 8 1
A getSerializer() 0 4 1
A validateType() 0 35 5
A validateId() 0 29 5
B validateAttributes() 0 45 9
B validateRelationships() 0 59 9
A validateAsToOneRelationship() 0 16 4
B validateAsToManyRelationship() 0 32 7
B parseSingleRelationship() 0 17 7
A executeBlock() 0 11 1
A executeStarts() 0 11 2
A executeEnds() 0 11 2
A getMessage() 0 6 1
A getIdRule() 0 4 1
A getTypeRule() 0 4 1
A getContext() 0 4 1
A setContext() 0 6 1
A getJsonApiErrorCollection() 0 4 1
A setJsonApiErrors() 0 6 1
A isIgnoreUnknowns() 0 4 1
A enableIgnoreUnknowns() 0 6 1
A disableIgnoreUnknowns() 0 6 1
A getErrorStatus() 0 6 1
A addErrorStatus() 0 22 4
A setAttributeRules() 0 8 1
A setToOneIndexes() 0 8 1
A setToManyIndexes() 0 8 1
A getAttributeRules() 0 4 1
A getToOneRules() 0 4 1
A getToManyRules() 0 4 1
A getBlocks() 0 4 1
A getFormatter() 0 8 2
A setFormatterFactory() 0 6 1
A getAttributeIndex() 0 7 1
A formatMessage() 0 6 1
A getErrorAggregator() 0 4 1
A getCaptureAggregator() 0 4 1
A debugCheckIndexesExist() 0 16 4

How to fix   Complexity   

Complex Class

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

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

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

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