This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
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
|
|||
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 |
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.