|
1
|
|
|
<?php namespace Limoncello\Flute\Validation; |
|
2
|
|
|
|
|
3
|
|
|
/** |
|
4
|
|
|
* Copyright 2015-2017 [email protected] |
|
5
|
|
|
* |
|
6
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
7
|
|
|
* you may not use this file except in compliance with the License. |
|
8
|
|
|
* You may obtain a copy of the License at |
|
9
|
|
|
* |
|
10
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0 |
|
11
|
|
|
* |
|
12
|
|
|
* Unless required by applicable law or agreed to in writing, software |
|
13
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, |
|
14
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
15
|
|
|
* See the License for the specific language governing permissions and |
|
16
|
|
|
* limitations under the License. |
|
17
|
|
|
*/ |
|
18
|
|
|
|
|
19
|
|
|
use Limoncello\Contracts\L10n\FormatterFactoryInterface; |
|
20
|
|
|
use Limoncello\Contracts\L10n\FormatterInterface; |
|
21
|
|
|
use Limoncello\Flute\Contracts\Validation\ErrorCodes; |
|
22
|
|
|
use Limoncello\Flute\Contracts\Validation\JsonApiValidatorInterface; |
|
23
|
|
|
use Limoncello\Flute\Http\JsonApiResponse; |
|
24
|
|
|
use Limoncello\Flute\Validation\Execution\ContextStorage; |
|
25
|
|
|
use Limoncello\Flute\Validation\Execution\JsonApiErrorCollection; |
|
26
|
|
|
use Limoncello\Flute\Validation\Execution\JsonApiRuleSerializer; |
|
27
|
|
|
use Limoncello\Flute\Validation\Rules\RelationshipsTrait; |
|
28
|
|
|
use Limoncello\Validation\Contracts\Errors\ErrorInterface; |
|
29
|
|
|
use Limoncello\Validation\Contracts\Execution\ContextStorageInterface; |
|
30
|
|
|
use Limoncello\Validation\Execution\BlockInterpreter; |
|
31
|
|
|
use Limoncello\Validation\Validator\BaseValidator; |
|
32
|
|
|
use Neomerx\JsonApi\Contracts\Document\DocumentInterface as DI; |
|
33
|
|
|
use Neomerx\JsonApi\Exceptions\JsonApiException; |
|
34
|
|
|
use Psr\Container\ContainerInterface; |
|
35
|
|
|
|
|
36
|
|
|
/** |
|
37
|
|
|
* @package Limoncello\Flute |
|
38
|
|
|
* |
|
39
|
|
|
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) |
|
40
|
|
|
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity) |
|
41
|
|
|
*/ |
|
42
|
|
|
class Validator extends BaseValidator implements JsonApiValidatorInterface |
|
43
|
|
|
{ |
|
44
|
|
|
use RelationshipsTrait; |
|
45
|
|
|
|
|
46
|
|
|
/** |
|
47
|
|
|
* Namespace for string resources. |
|
48
|
|
|
*/ |
|
49
|
|
|
const RESOURCES_NAMESPACE = 'Limoncello.Flute.Validation'; |
|
50
|
|
|
|
|
51
|
|
|
/** Rule description index */ |
|
52
|
|
|
const RULE_INDEX = 0; |
|
53
|
|
|
|
|
54
|
|
|
/** Rule description index */ |
|
55
|
|
|
const RULE_ATTRIBUTES = self::RULE_INDEX + 1; |
|
56
|
|
|
|
|
57
|
|
|
/** Rule description index */ |
|
58
|
|
|
const RULE_TO_ONE = self::RULE_ATTRIBUTES + 1; |
|
59
|
|
|
|
|
60
|
|
|
/** Rule description index */ |
|
61
|
|
|
const RULE_TO_MANY = self::RULE_TO_ONE + 1; |
|
62
|
|
|
|
|
63
|
|
|
/** Rule description index */ |
|
64
|
|
|
const RULE_UNLISTED_ATTRIBUTE = self::RULE_TO_MANY + 1; |
|
65
|
|
|
|
|
66
|
|
|
/** Rule description index */ |
|
67
|
|
|
const RULE_UNLISTED_RELATIONSHIP = self::RULE_UNLISTED_ATTRIBUTE + 1; |
|
68
|
|
|
|
|
69
|
|
|
/** |
|
70
|
|
|
* @var ContainerInterface |
|
71
|
|
|
*/ |
|
72
|
|
|
private $container; |
|
73
|
|
|
|
|
74
|
|
|
/** |
|
75
|
|
|
* @var int |
|
76
|
|
|
*/ |
|
77
|
|
|
private $errorStatus; |
|
78
|
|
|
|
|
79
|
|
|
/** |
|
80
|
|
|
* @var ContextStorageInterface |
|
81
|
|
|
*/ |
|
82
|
|
|
private $contextStorage; |
|
83
|
|
|
|
|
84
|
|
|
/** |
|
85
|
|
|
* @var JsonApiErrorCollection |
|
86
|
|
|
*/ |
|
87
|
|
|
private $jsonApiErrors; |
|
88
|
|
|
|
|
89
|
|
|
/** |
|
90
|
|
|
* @var array |
|
91
|
|
|
*/ |
|
92
|
|
|
private $blocks; |
|
93
|
|
|
|
|
94
|
|
|
/** |
|
95
|
|
|
* @var array |
|
96
|
|
|
*/ |
|
97
|
|
|
private $idRule; |
|
98
|
|
|
|
|
99
|
|
|
/** |
|
100
|
|
|
* @var array |
|
101
|
|
|
*/ |
|
102
|
|
|
private $typeRule; |
|
103
|
|
|
|
|
104
|
|
|
/** |
|
105
|
|
|
* @var int[] |
|
106
|
|
|
*/ |
|
107
|
|
|
private $attributeRules; |
|
108
|
|
|
|
|
109
|
|
|
/** |
|
110
|
|
|
* @var int[] |
|
111
|
|
|
*/ |
|
112
|
|
|
private $toOneRules; |
|
113
|
|
|
|
|
114
|
|
|
/** |
|
115
|
|
|
* @var int[] |
|
116
|
|
|
*/ |
|
117
|
|
|
private $toManyRules; |
|
118
|
|
|
|
|
119
|
|
|
/** |
|
120
|
|
|
* @var bool |
|
121
|
|
|
*/ |
|
122
|
|
|
private $isIgnoreUnknowns; |
|
123
|
|
|
|
|
124
|
|
|
/** |
|
125
|
|
|
* @var FormatterInterface|null |
|
126
|
|
|
*/ |
|
127
|
|
|
private $messageFormatter; |
|
128
|
|
|
|
|
129
|
|
|
/** |
|
130
|
|
|
* @param string $name |
|
131
|
|
|
* @param array $data |
|
132
|
|
|
* @param ContainerInterface $container |
|
133
|
|
|
* @param int $errorStatus |
|
134
|
|
|
* |
|
135
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
|
136
|
|
|
*/ |
|
137
|
|
|
public function __construct( |
|
138
|
|
|
string $name, |
|
139
|
|
|
array $data, |
|
140
|
|
|
ContainerInterface $container, |
|
141
|
|
|
int $errorStatus = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY |
|
142
|
|
|
) { |
|
143
|
|
|
$ruleSet = JsonApiRuleSerializer::extractRuleSet($name, $data); |
|
144
|
|
|
$this->blocks = JsonApiRuleSerializer::extractBlocks($data); |
|
145
|
|
|
$this->container = $container; |
|
146
|
|
|
$this->idRule = JsonApiRuleSerializer::getIdRule($ruleSet); |
|
147
|
|
|
$this->typeRule = JsonApiRuleSerializer::getTypeRule($ruleSet); |
|
148
|
|
|
$this->errorStatus = $errorStatus; |
|
149
|
|
|
|
|
150
|
|
|
$this |
|
151
|
|
|
->setAttributeRules(JsonApiRuleSerializer::getAttributeRules($ruleSet)) |
|
152
|
|
|
->setToOneIndexes(JsonApiRuleSerializer::getToOneRules($ruleSet)) |
|
153
|
|
|
->setToManyIndexes(JsonApiRuleSerializer::getToManyRules($ruleSet)) |
|
154
|
|
|
->disableIgnoreUnknowns(); |
|
155
|
|
|
|
|
156
|
|
|
parent::__construct(); |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
/** |
|
160
|
|
|
* @inheritdoc |
|
161
|
|
|
*/ |
|
162
|
|
|
public function assert($jsonData): JsonApiValidatorInterface |
|
163
|
|
|
{ |
|
164
|
|
|
if ($this->validate($jsonData) === false) { |
|
165
|
|
|
throw new JsonApiException($this->getJsonApiErrorCollection(), $this->getErrorStatus()); |
|
166
|
|
|
} |
|
167
|
|
|
|
|
168
|
|
|
return $this; |
|
|
|
|
|
|
169
|
|
|
} |
|
170
|
|
|
|
|
171
|
|
|
/** |
|
172
|
|
|
* @inheritdoc |
|
173
|
|
|
*/ |
|
174
|
|
|
public function validate($input): bool |
|
175
|
|
|
{ |
|
176
|
|
|
$this->reInitAggregatorsIfNeeded(); |
|
177
|
|
|
|
|
178
|
|
|
$this |
|
179
|
|
|
->validateType($input) |
|
180
|
|
|
->validateId($input) |
|
181
|
|
|
->validateAttributes($input) |
|
182
|
|
|
->validateRelationships($input); |
|
183
|
|
|
|
|
184
|
|
|
$hasNoErrors = $this->getJsonApiErrorCollection()->count() <= 0; |
|
185
|
|
|
|
|
186
|
|
|
return $hasNoErrors; |
|
187
|
|
|
} |
|
188
|
|
|
|
|
189
|
|
|
/** |
|
190
|
|
|
* @inheritdoc |
|
191
|
|
|
*/ |
|
192
|
|
|
public function getJsonApiErrors(): array |
|
193
|
|
|
{ |
|
194
|
|
|
return $this->getJsonApiErrorCollection()->getArrayCopy(); |
|
195
|
|
|
} |
|
196
|
|
|
|
|
197
|
|
|
/** |
|
198
|
|
|
* @inheritdoc |
|
199
|
|
|
*/ |
|
200
|
|
|
public function getJsonApiCaptures(): array |
|
201
|
|
|
{ |
|
202
|
|
|
return $this->getCaptures()->get(); |
|
203
|
|
|
} |
|
204
|
|
|
|
|
205
|
|
|
/** |
|
206
|
|
|
* @return BaseValidator |
|
207
|
|
|
*/ |
|
208
|
|
|
protected function resetAggregators(): BaseValidator |
|
209
|
|
|
{ |
|
210
|
|
|
$self = parent::resetAggregators(); |
|
211
|
|
|
|
|
212
|
|
|
$this->jsonApiErrors = $this->createJsonApiErrors(); |
|
213
|
|
|
$this->contextStorage = $this->createContextStorage(); |
|
214
|
|
|
|
|
215
|
|
|
return $self; |
|
216
|
|
|
} |
|
217
|
|
|
|
|
218
|
|
|
/** |
|
219
|
|
|
* @param array $jsonData |
|
220
|
|
|
* |
|
221
|
|
|
* @return self |
|
222
|
|
|
* |
|
223
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
|
224
|
|
|
* @SuppressWarnings(PHPMD.ElseExpression) |
|
225
|
|
|
*/ |
|
226
|
|
|
private function validateType(array $jsonData): self |
|
227
|
|
|
{ |
|
228
|
|
|
// execute start(s) |
|
229
|
|
|
$starts = JsonApiRuleSerializer::getRuleStartIndexes($this->getTypeRule()); |
|
230
|
|
|
$this->executeStarts($starts); |
|
231
|
|
|
|
|
232
|
|
|
if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true && |
|
233
|
|
|
array_key_exists(DI::KEYWORD_TYPE, $data = $jsonData[DI::KEYWORD_DATA]) === true |
|
234
|
|
|
) { |
|
235
|
|
|
// execute main validation block(s) |
|
236
|
|
|
$index = JsonApiRuleSerializer::getRuleIndex($this->getTypeRule()); |
|
237
|
|
|
$this->executeBlock($data[DI::KEYWORD_TYPE], $index); |
|
238
|
|
|
} else { |
|
239
|
|
|
$title = $this->formatMessage(ErrorCodes::INVALID_VALUE); |
|
240
|
|
|
$details = $this->formatMessage(ErrorCodes::TYPE_MISSING); |
|
241
|
|
|
$this->getJsonApiErrorCollection()->addDataTypeError($title, $details, $this->getErrorStatus()); |
|
242
|
|
|
} |
|
243
|
|
|
|
|
244
|
|
|
// execute end(s) |
|
245
|
|
|
$ends = JsonApiRuleSerializer::getRuleEndIndexes($this->getTypeRule()); |
|
246
|
|
|
$this->executeEnds($ends); |
|
247
|
|
|
|
|
248
|
|
View Code Duplication |
if (count($this->getErrors()) > 0) { |
|
|
|
|
|
|
249
|
|
|
$title = $this->formatMessage(ErrorCodes::INVALID_VALUE); |
|
250
|
|
|
foreach ($this->getErrors()->get() as $error) { |
|
251
|
|
|
$this->getJsonApiErrorCollection() |
|
252
|
|
|
->addDataTypeError($title, $this->getMessage($error), $this->getErrorStatus()); |
|
253
|
|
|
} |
|
254
|
|
|
$this->getErrors()->clear(); |
|
255
|
|
|
} |
|
256
|
|
|
|
|
257
|
|
|
return $this; |
|
258
|
|
|
} |
|
259
|
|
|
|
|
260
|
|
|
/** |
|
261
|
|
|
* @param array $jsonData |
|
262
|
|
|
* |
|
263
|
|
|
* @return self |
|
264
|
|
|
* |
|
265
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
|
266
|
|
|
*/ |
|
267
|
|
|
private function validateId(array $jsonData): self |
|
268
|
|
|
{ |
|
269
|
|
|
// execute start(s) |
|
270
|
|
|
$starts = JsonApiRuleSerializer::getRuleStartIndexes($this->getIdRule()); |
|
271
|
|
|
$this->executeStarts($starts); |
|
272
|
|
|
|
|
273
|
|
|
// execute main validation block(s) |
|
274
|
|
|
if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true && |
|
275
|
|
|
array_key_exists(DI::KEYWORD_ID, $data = $jsonData[DI::KEYWORD_DATA]) === true |
|
276
|
|
|
) { |
|
277
|
|
|
$index = JsonApiRuleSerializer::getRuleIndex($this->getIdRule()); |
|
278
|
|
|
$this->executeBlock($data[DI::KEYWORD_ID], $index); |
|
279
|
|
|
} |
|
280
|
|
|
|
|
281
|
|
|
// execute end(s) |
|
282
|
|
|
$ends = JsonApiRuleSerializer::getRuleEndIndexes($this->getIdRule()); |
|
283
|
|
|
$this->executeEnds($ends); |
|
284
|
|
|
|
|
285
|
|
View Code Duplication |
if (count($this->getErrors()) > 0) { |
|
|
|
|
|
|
286
|
|
|
$title = $this->formatMessage(ErrorCodes::INVALID_VALUE); |
|
287
|
|
|
foreach ($this->getErrors()->get() as $error) { |
|
288
|
|
|
$this->getJsonApiErrorCollection() |
|
289
|
|
|
->addDataIdError($title, $this->getMessage($error), $this->getErrorStatus()); |
|
290
|
|
|
} |
|
291
|
|
|
$this->getErrors()->clear(); |
|
292
|
|
|
} |
|
293
|
|
|
|
|
294
|
|
|
return $this; |
|
295
|
|
|
} |
|
296
|
|
|
|
|
297
|
|
|
/** |
|
298
|
|
|
* @param array $jsonData |
|
299
|
|
|
* |
|
300
|
|
|
* @return self |
|
301
|
|
|
* |
|
302
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
|
303
|
|
|
* @SuppressWarnings(PHPMD.ElseExpression) |
|
304
|
|
|
*/ |
|
305
|
|
|
private function validateAttributes(array $jsonData): self |
|
306
|
|
|
{ |
|
307
|
|
|
// execute start(s) |
|
308
|
|
|
$starts = JsonApiRuleSerializer::getRulesStartIndexes($this->getAttributeRules()); |
|
309
|
|
|
$this->executeStarts($starts); |
|
310
|
|
|
|
|
311
|
|
|
if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true && |
|
312
|
|
|
array_key_exists(DI::KEYWORD_ATTRIBUTES, $data = $jsonData[DI::KEYWORD_DATA]) === true |
|
313
|
|
|
) { |
|
314
|
|
|
if (is_array($attributes = $data[DI::KEYWORD_ATTRIBUTES]) === false) { |
|
315
|
|
|
$title = $this->formatMessage(ErrorCodes::INVALID_VALUE); |
|
316
|
|
|
$details = $this->formatMessage(ErrorCodes::INVALID_ATTRIBUTES); |
|
317
|
|
|
$this->getJsonApiErrorCollection()->addAttributesError($title, $details, $this->getErrorStatus()); |
|
318
|
|
|
} else { |
|
319
|
|
|
// execute main validation block(s) |
|
320
|
|
|
foreach ($attributes as $name => $value) { |
|
321
|
|
|
if ($this->hasAttributeIndex($name) === true) { |
|
322
|
|
|
$this->executeBlock($value, $this->getAttributeIndex($name)); |
|
323
|
|
View Code Duplication |
} elseif ($this->isIgnoreUnknowns() === false) { |
|
|
|
|
|
|
324
|
|
|
$title = $this->formatMessage(ErrorCodes::INVALID_VALUE); |
|
325
|
|
|
$details = $this->formatMessage(ErrorCodes::UNKNOWN_ATTRIBUTE); |
|
326
|
|
|
$status = $this->getErrorStatus(); |
|
327
|
|
|
$this->getJsonApiErrorCollection()->addDataAttributeError($name, $title, $details, $status); |
|
328
|
|
|
} |
|
329
|
|
|
} |
|
330
|
|
|
} |
|
331
|
|
|
} |
|
332
|
|
|
|
|
333
|
|
|
// execute end(s) |
|
334
|
|
|
$ends = JsonApiRuleSerializer::getRulesEndIndexes($this->getAttributeRules()); |
|
335
|
|
|
$this->executeEnds($ends); |
|
336
|
|
|
|
|
337
|
|
|
if (count($this->getErrors()) > 0) { |
|
338
|
|
|
foreach ($this->getErrors()->get() as $error) { |
|
339
|
|
|
$this->getJsonApiErrorCollection()->addValidationAttributeError($error); |
|
340
|
|
|
} |
|
341
|
|
|
$this->getErrors()->clear(); |
|
342
|
|
|
} |
|
343
|
|
|
|
|
344
|
|
|
return $this; |
|
345
|
|
|
} |
|
346
|
|
|
|
|
347
|
|
|
/** |
|
348
|
|
|
* @param array $jsonData |
|
349
|
|
|
* |
|
350
|
|
|
* @return self |
|
351
|
|
|
* |
|
352
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
|
353
|
|
|
* @SuppressWarnings(PHPMD.ElseExpression) |
|
354
|
|
|
*/ |
|
355
|
|
|
private function validateRelationships(array $jsonData): self |
|
356
|
|
|
{ |
|
357
|
|
|
// execute start(s) |
|
358
|
|
|
$starts = array_merge( |
|
359
|
|
|
JsonApiRuleSerializer::getRulesStartIndexes($this->getToOneRules()), |
|
360
|
|
|
JsonApiRuleSerializer::getRulesStartIndexes($this->getToManyRules()) |
|
361
|
|
|
); |
|
362
|
|
|
$this->executeStarts($starts); |
|
363
|
|
|
|
|
364
|
|
|
if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true && |
|
365
|
|
|
array_key_exists(DI::KEYWORD_RELATIONSHIPS, $data = $jsonData[DI::KEYWORD_DATA]) === true |
|
366
|
|
|
) { |
|
367
|
|
|
if (is_array($relationships = $data[DI::KEYWORD_RELATIONSHIPS]) === false) { |
|
368
|
|
|
$title = $this->formatMessage(ErrorCodes::INVALID_VALUE); |
|
369
|
|
|
$details = $this->formatMessage(ErrorCodes::INVALID_RELATIONSHIP_TYPE); |
|
370
|
|
|
$this->getJsonApiErrorCollection()->addRelationshipsError($title, $details, $this->getErrorStatus()); |
|
371
|
|
|
} else { |
|
372
|
|
|
// ok we got to something that could be null or a valid relationship |
|
373
|
|
|
$toOneIndexes = JsonApiRuleSerializer::getRulesIndexes($this->getToOneRules()); |
|
374
|
|
|
$toManyIndexes = JsonApiRuleSerializer::getRulesIndexes($this->getToManyRules()); |
|
375
|
|
|
|
|
376
|
|
|
foreach ($relationships as $name => $relationship) { |
|
377
|
|
|
if (array_key_exists($name, $toOneIndexes) === true) { |
|
378
|
|
|
// it might be to1 relationship |
|
379
|
|
|
$this->validateAsToOneRelationship($toOneIndexes[$name], $name, $relationship); |
|
380
|
|
|
} elseif (array_key_exists($name, $toManyIndexes) === true) { |
|
381
|
|
|
// it might be toMany relationship |
|
382
|
|
|
$this->validateAsToManyRelationship($toManyIndexes[$name], $name, $relationship); |
|
383
|
|
View Code Duplication |
} else { |
|
|
|
|
|
|
384
|
|
|
// unknown relationship |
|
385
|
|
|
$title = $this->formatMessage(ErrorCodes::INVALID_VALUE); |
|
386
|
|
|
$details = $this->formatMessage(ErrorCodes::UNKNOWN_RELATIONSHIP); |
|
387
|
|
|
$status = $this->getErrorStatus(); |
|
388
|
|
|
$this->getJsonApiErrorCollection()->addRelationshipError($name, $title, $details, $status); |
|
389
|
|
|
} |
|
390
|
|
|
} |
|
391
|
|
|
} |
|
392
|
|
|
} |
|
393
|
|
|
|
|
394
|
|
|
// execute end(s) |
|
395
|
|
|
$ends = array_merge( |
|
396
|
|
|
JsonApiRuleSerializer::getRulesEndIndexes($this->getToOneRules()), |
|
397
|
|
|
JsonApiRuleSerializer::getRulesEndIndexes($this->getToManyRules()) |
|
398
|
|
|
); |
|
399
|
|
|
$this->executeEnds($ends); |
|
400
|
|
|
|
|
401
|
|
|
if (count($this->getErrors()) > 0) { |
|
402
|
|
|
foreach ($this->getErrors()->get() as $error) { |
|
403
|
|
|
$this->getJsonApiErrorCollection()->addValidationRelationshipError($error); |
|
404
|
|
|
} |
|
405
|
|
|
$this->getErrors()->clear(); |
|
406
|
|
|
} |
|
407
|
|
|
|
|
408
|
|
|
return $this; |
|
409
|
|
|
} |
|
410
|
|
|
|
|
411
|
|
|
/** |
|
412
|
|
|
* @param int $index |
|
413
|
|
|
* @param string $name |
|
414
|
|
|
* @param mixed $mightBeRelationship |
|
415
|
|
|
* |
|
416
|
|
|
* @return void |
|
417
|
|
|
* |
|
418
|
|
|
* @SuppressWarnings(PHPMD.ElseExpression) |
|
419
|
|
|
*/ |
|
420
|
|
|
private function validateAsToOneRelationship(int $index, string $name, $mightBeRelationship): void |
|
421
|
|
|
{ |
|
422
|
|
|
if (is_array($mightBeRelationship) === true && |
|
423
|
|
|
array_key_exists(DI::KEYWORD_DATA, $mightBeRelationship) === true && |
|
424
|
|
|
($parsed = $this->parseSingleRelationship($mightBeRelationship[DI::KEYWORD_DATA])) !== false |
|
425
|
|
|
) { |
|
426
|
|
|
// All right we got something. Now pass it to a validation rule. |
|
427
|
|
|
$this->executeBlock($parsed, $index); |
|
428
|
|
|
} else { |
|
429
|
|
|
$title = $this->formatMessage(ErrorCodes::INVALID_VALUE); |
|
430
|
|
|
$details = $this->formatMessage(ErrorCodes::INVALID_RELATIONSHIP); |
|
431
|
|
|
$this->getJsonApiErrorCollection()->addRelationshipError($name, $title, $details, $this->getErrorStatus()); |
|
432
|
|
|
} |
|
433
|
|
|
} |
|
434
|
|
|
|
|
435
|
|
|
/** |
|
436
|
|
|
* @param int $index |
|
437
|
|
|
* @param string $name |
|
438
|
|
|
* @param mixed $mightBeRelationship |
|
439
|
|
|
* |
|
440
|
|
|
* @return void |
|
441
|
|
|
* |
|
442
|
|
|
* @SuppressWarnings(PHPMD.ElseExpression) |
|
443
|
|
|
*/ |
|
444
|
|
|
private function validateAsToManyRelationship(int $index, string $name, $mightBeRelationship): void |
|
445
|
|
|
{ |
|
446
|
|
|
$isParsed = true; |
|
447
|
|
|
$collectedPairs = []; |
|
448
|
|
|
if (is_array($mightBeRelationship) === true && |
|
449
|
|
|
array_key_exists(DI::KEYWORD_DATA, $mightBeRelationship) === true && |
|
450
|
|
|
is_array($data = $mightBeRelationship[DI::KEYWORD_DATA]) === true |
|
451
|
|
|
) { |
|
452
|
|
|
foreach ($data as $mightTypeAndId) { |
|
453
|
|
|
// we accept only pairs of type and id (no `null`s are accepted). |
|
454
|
|
|
if (is_array($parsed = $this->parseSingleRelationship($mightTypeAndId)) === true) { |
|
455
|
|
|
$collectedPairs[] = $parsed; |
|
456
|
|
|
} else { |
|
457
|
|
|
$isParsed = false; |
|
458
|
|
|
break; |
|
459
|
|
|
} |
|
460
|
|
|
} |
|
461
|
|
|
} else { |
|
462
|
|
|
$isParsed = false; |
|
463
|
|
|
} |
|
464
|
|
|
|
|
465
|
|
View Code Duplication |
if ($isParsed === true) { |
|
|
|
|
|
|
466
|
|
|
// All right we got something. Now pass it to a validation rule. |
|
467
|
|
|
$this->executeBlock($collectedPairs, $index); |
|
468
|
|
|
} else { |
|
469
|
|
|
$title = $this->formatMessage(ErrorCodes::INVALID_VALUE); |
|
470
|
|
|
$details = $this->formatMessage(ErrorCodes::INVALID_RELATIONSHIP); |
|
471
|
|
|
$this->getJsonApiErrorCollection()->addRelationshipError($name, $title, $details, $this->getErrorStatus()); |
|
472
|
|
|
} |
|
473
|
|
|
} |
|
474
|
|
|
|
|
475
|
|
|
/** |
|
476
|
|
|
* @param mixed $data |
|
477
|
|
|
* |
|
478
|
|
|
* @return array|null|false Either `array` ($type => $id), or `null`, or `false` on error. |
|
479
|
|
|
* |
|
480
|
|
|
* @SuppressWarnings(PHPMD.ElseExpression) |
|
481
|
|
|
*/ |
|
482
|
|
|
private function parseSingleRelationship($data) |
|
483
|
|
|
{ |
|
484
|
|
|
if ($data === null) { |
|
485
|
|
|
$result = null; |
|
486
|
|
|
} elseif (is_array($data) === true && |
|
487
|
|
|
array_key_exists(DI::KEYWORD_TYPE, $data) === true && |
|
488
|
|
|
array_key_exists(DI::KEYWORD_ID, $data) === true && |
|
489
|
|
|
is_scalar($type = $data[DI::KEYWORD_TYPE]) === true && |
|
490
|
|
|
is_scalar($index = $data[DI::KEYWORD_ID]) === true |
|
491
|
|
|
) { |
|
492
|
|
|
$result = [$type => $index]; |
|
493
|
|
|
} else { |
|
494
|
|
|
$result = false; |
|
495
|
|
|
} |
|
496
|
|
|
|
|
497
|
|
|
return $result; |
|
498
|
|
|
} |
|
499
|
|
|
|
|
500
|
|
|
/** |
|
501
|
|
|
* Re-initializes internal aggregators for captures, errors, etc. |
|
502
|
|
|
*/ |
|
503
|
|
|
private function reInitAggregatorsIfNeeded(): void |
|
504
|
|
|
{ |
|
505
|
|
|
$this->areAggregatorsDirty() === false ?: $this->resetAggregators(); |
|
506
|
|
|
} |
|
507
|
|
|
|
|
508
|
|
|
/** |
|
509
|
|
|
* @param mixed $input |
|
510
|
|
|
* @param int $index |
|
511
|
|
|
* |
|
512
|
|
|
* @return void |
|
513
|
|
|
* |
|
514
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
|
515
|
|
|
*/ |
|
516
|
|
|
private function executeBlock($input, int $index): void |
|
517
|
|
|
{ |
|
518
|
|
|
BlockInterpreter::executeBlock( |
|
519
|
|
|
$input, |
|
520
|
|
|
$index, |
|
521
|
|
|
$this->getBlocks(), |
|
522
|
|
|
$this->getContextStorage(), |
|
523
|
|
|
$this->getCaptures(), |
|
524
|
|
|
$this->getErrors() |
|
525
|
|
|
); |
|
526
|
|
|
} |
|
527
|
|
|
|
|
528
|
|
|
/** |
|
529
|
|
|
* @param array $indexes |
|
530
|
|
|
* |
|
531
|
|
|
* @return void |
|
532
|
|
|
* |
|
533
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
|
534
|
|
|
*/ |
|
535
|
|
|
private function executeStarts(array $indexes): void |
|
536
|
|
|
{ |
|
537
|
|
|
BlockInterpreter::executeStarts($indexes, $this->getBlocks(), $this->getContextStorage(), $this->getErrors()); |
|
538
|
|
|
} |
|
539
|
|
|
|
|
540
|
|
|
/** |
|
541
|
|
|
* @param array $indexes |
|
542
|
|
|
* |
|
543
|
|
|
* @return void |
|
544
|
|
|
* |
|
545
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
|
546
|
|
|
*/ |
|
547
|
|
|
private function executeEnds(array $indexes): void |
|
548
|
|
|
{ |
|
549
|
|
|
BlockInterpreter::executeEnds($indexes, $this->getBlocks(), $this->getContextStorage(), $this->getErrors()); |
|
550
|
|
|
} |
|
551
|
|
|
|
|
552
|
|
|
/** |
|
553
|
|
|
* @param ErrorInterface $error |
|
554
|
|
|
* |
|
555
|
|
|
* @return string |
|
556
|
|
|
*/ |
|
557
|
|
View Code Duplication |
private function getMessage(ErrorInterface $error): string |
|
|
|
|
|
|
558
|
|
|
{ |
|
559
|
|
|
$context = $error->getMessageContext(); |
|
560
|
|
|
$args = $context === null ? [] : $context; |
|
561
|
|
|
$message = $this->formatMessage($error->getMessageCode(), $args); |
|
562
|
|
|
|
|
563
|
|
|
return $message; |
|
564
|
|
|
} |
|
565
|
|
|
|
|
566
|
|
|
/** |
|
567
|
|
|
* @return array |
|
568
|
|
|
*/ |
|
569
|
|
|
protected function getIdRule(): array |
|
570
|
|
|
{ |
|
571
|
|
|
return $this->idRule; |
|
572
|
|
|
} |
|
573
|
|
|
|
|
574
|
|
|
/** |
|
575
|
|
|
* @return array |
|
576
|
|
|
*/ |
|
577
|
|
|
protected function getTypeRule(): array |
|
578
|
|
|
{ |
|
579
|
|
|
return $this->typeRule; |
|
580
|
|
|
} |
|
581
|
|
|
|
|
582
|
|
|
/** |
|
583
|
|
|
* @return ContextStorageInterface |
|
584
|
|
|
*/ |
|
585
|
|
|
protected function getContextStorage(): ContextStorageInterface |
|
586
|
|
|
{ |
|
587
|
|
|
return $this->contextStorage; |
|
588
|
|
|
} |
|
589
|
|
|
|
|
590
|
|
|
/** |
|
591
|
|
|
* @return ContextStorageInterface |
|
592
|
|
|
*/ |
|
593
|
|
|
protected function createContextStorage(): ContextStorageInterface |
|
594
|
|
|
{ |
|
595
|
|
|
return new ContextStorage($this->getContainer(), $this->getBlocks()); |
|
596
|
|
|
} |
|
597
|
|
|
|
|
598
|
|
|
/** |
|
599
|
|
|
* @return JsonApiErrorCollection |
|
600
|
|
|
*/ |
|
601
|
|
|
protected function getJsonApiErrorCollection(): JsonApiErrorCollection |
|
602
|
|
|
{ |
|
603
|
|
|
return $this->jsonApiErrors; |
|
604
|
|
|
} |
|
605
|
|
|
|
|
606
|
|
|
/** |
|
607
|
|
|
* @return JsonApiErrorCollection |
|
608
|
|
|
*/ |
|
609
|
|
|
protected function createJsonApiErrors(): JsonApiErrorCollection |
|
610
|
|
|
{ |
|
611
|
|
|
return new JsonApiErrorCollection($this->getContainer(), $this->getErrorStatus()); |
|
612
|
|
|
} |
|
613
|
|
|
|
|
614
|
|
|
/** |
|
615
|
|
|
* @return ContainerInterface |
|
616
|
|
|
*/ |
|
617
|
|
|
protected function getContainer(): ContainerInterface |
|
618
|
|
|
{ |
|
619
|
|
|
return $this->container; |
|
620
|
|
|
} |
|
621
|
|
|
|
|
622
|
|
|
/** |
|
623
|
|
|
* @return int |
|
624
|
|
|
*/ |
|
625
|
|
|
protected function getErrorStatus(): int |
|
626
|
|
|
{ |
|
627
|
|
|
return $this->errorStatus; |
|
628
|
|
|
} |
|
629
|
|
|
|
|
630
|
|
|
/** |
|
631
|
|
|
* @return bool |
|
632
|
|
|
*/ |
|
633
|
|
|
protected function isIgnoreUnknowns(): bool |
|
634
|
|
|
{ |
|
635
|
|
|
return $this->isIgnoreUnknowns; |
|
636
|
|
|
} |
|
637
|
|
|
|
|
638
|
|
|
/** |
|
639
|
|
|
* @return Validator |
|
640
|
|
|
*/ |
|
641
|
|
|
protected function enableIgnoreUnknowns(): self |
|
642
|
|
|
{ |
|
643
|
|
|
$this->isIgnoreUnknowns = true; |
|
644
|
|
|
|
|
645
|
|
|
return $this; |
|
646
|
|
|
} |
|
647
|
|
|
|
|
648
|
|
|
/** |
|
649
|
|
|
* @return Validator |
|
650
|
|
|
*/ |
|
651
|
|
|
protected function disableIgnoreUnknowns(): self |
|
652
|
|
|
{ |
|
653
|
|
|
$this->isIgnoreUnknowns = false; |
|
654
|
|
|
|
|
655
|
|
|
return $this; |
|
656
|
|
|
} |
|
657
|
|
|
|
|
658
|
|
|
/** |
|
659
|
|
|
* @param array $rules |
|
660
|
|
|
* |
|
661
|
|
|
* @return self |
|
662
|
|
|
*/ |
|
663
|
|
|
private function setAttributeRules(array $rules): self |
|
664
|
|
|
{ |
|
665
|
|
|
assert($this->debugCheckIndexesExist($rules)); |
|
666
|
|
|
|
|
667
|
|
|
$this->attributeRules = $rules; |
|
668
|
|
|
|
|
669
|
|
|
return $this; |
|
670
|
|
|
} |
|
671
|
|
|
|
|
672
|
|
|
/** |
|
673
|
|
|
* @param array $rules |
|
674
|
|
|
* |
|
675
|
|
|
* @return self |
|
676
|
|
|
*/ |
|
677
|
|
|
private function setToOneIndexes(array $rules): self |
|
678
|
|
|
{ |
|
679
|
|
|
assert($this->debugCheckIndexesExist($rules)); |
|
680
|
|
|
|
|
681
|
|
|
$this->toOneRules = $rules; |
|
682
|
|
|
|
|
683
|
|
|
return $this; |
|
684
|
|
|
} |
|
685
|
|
|
|
|
686
|
|
|
/** |
|
687
|
|
|
* @param array $rules |
|
688
|
|
|
* |
|
689
|
|
|
* @return self |
|
690
|
|
|
*/ |
|
691
|
|
|
private function setToManyIndexes(array $rules): self |
|
692
|
|
|
{ |
|
693
|
|
|
assert($this->debugCheckIndexesExist($rules)); |
|
694
|
|
|
|
|
695
|
|
|
$this->toManyRules = $rules; |
|
696
|
|
|
|
|
697
|
|
|
return $this; |
|
698
|
|
|
} |
|
699
|
|
|
|
|
700
|
|
|
/** |
|
701
|
|
|
* @return int[] |
|
702
|
|
|
*/ |
|
703
|
|
|
protected function getAttributeRules(): array |
|
704
|
|
|
{ |
|
705
|
|
|
return $this->attributeRules; |
|
706
|
|
|
} |
|
707
|
|
|
|
|
708
|
|
|
/** |
|
709
|
|
|
* @return int[] |
|
710
|
|
|
*/ |
|
711
|
|
|
protected function getToOneRules(): array |
|
712
|
|
|
{ |
|
713
|
|
|
return $this->toOneRules; |
|
714
|
|
|
} |
|
715
|
|
|
|
|
716
|
|
|
/** |
|
717
|
|
|
* @return int[] |
|
718
|
|
|
*/ |
|
719
|
|
|
protected function getToManyRules(): array |
|
720
|
|
|
{ |
|
721
|
|
|
return $this->toManyRules; |
|
722
|
|
|
} |
|
723
|
|
|
|
|
724
|
|
|
/** |
|
725
|
|
|
* @return array |
|
726
|
|
|
*/ |
|
727
|
|
|
private function getBlocks(): array |
|
728
|
|
|
{ |
|
729
|
|
|
return $this->blocks; |
|
730
|
|
|
} |
|
731
|
|
|
|
|
732
|
|
|
/** |
|
733
|
|
|
* @return FormatterInterface |
|
734
|
|
|
*/ |
|
735
|
|
View Code Duplication |
protected function getMessageFormatter(): FormatterInterface |
|
|
|
|
|
|
736
|
|
|
{ |
|
737
|
|
|
if ($this->messageFormatter === null) { |
|
738
|
|
|
/** @var FormatterFactoryInterface $factory */ |
|
739
|
|
|
$factory = $this->getContainer()->get(FormatterFactoryInterface::class); |
|
740
|
|
|
$this->messageFormatter = $factory->createFormatter(static::RESOURCES_NAMESPACE); |
|
741
|
|
|
} |
|
742
|
|
|
|
|
743
|
|
|
return $this->messageFormatter; |
|
744
|
|
|
} |
|
745
|
|
|
|
|
746
|
|
|
/** |
|
747
|
|
|
* @param string $name |
|
748
|
|
|
* |
|
749
|
|
|
* @return int |
|
750
|
|
|
* |
|
751
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
|
752
|
|
|
*/ |
|
753
|
|
|
private function getAttributeIndex(string $name): int |
|
754
|
|
|
{ |
|
755
|
|
|
$indexes = JsonApiRuleSerializer::getRulesIndexes($this->getAttributeRules()); |
|
756
|
|
|
$index = $indexes[$name]; |
|
757
|
|
|
|
|
758
|
|
|
return $index; |
|
759
|
|
|
} |
|
760
|
|
|
|
|
761
|
|
|
/** |
|
762
|
|
|
* @param string $name |
|
763
|
|
|
* |
|
764
|
|
|
* @return bool |
|
765
|
|
|
* |
|
766
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
|
767
|
|
|
*/ |
|
768
|
|
|
private function hasAttributeIndex(string $name): bool |
|
769
|
|
|
{ |
|
770
|
|
|
$indexes = JsonApiRuleSerializer::getRulesIndexes($this->getAttributeRules()); |
|
771
|
|
|
$hasAttribute = array_key_exists($name, $indexes); |
|
772
|
|
|
|
|
773
|
|
|
return $hasAttribute; |
|
774
|
|
|
} |
|
775
|
|
|
|
|
776
|
|
|
/** |
|
777
|
|
|
* @param int $messageId |
|
778
|
|
|
* @param array $args |
|
779
|
|
|
* |
|
780
|
|
|
* @return string |
|
781
|
|
|
*/ |
|
782
|
|
|
private function formatMessage(int $messageId, array $args = []): string |
|
783
|
|
|
{ |
|
784
|
|
|
$message = $this->getMessageFormatter()->formatMessage($messageId, $args); |
|
785
|
|
|
|
|
786
|
|
|
return $message; |
|
787
|
|
|
} |
|
788
|
|
|
|
|
789
|
|
|
/** |
|
790
|
|
|
* @param array $rules |
|
791
|
|
|
* |
|
792
|
|
|
* @return bool |
|
793
|
|
|
* |
|
794
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
|
795
|
|
|
*/ |
|
796
|
|
|
private function debugCheckIndexesExist(array $rules): bool |
|
797
|
|
|
{ |
|
798
|
|
|
$allOk = true; |
|
799
|
|
|
|
|
800
|
|
|
$indexes = array_merge( |
|
801
|
|
|
JsonApiRuleSerializer::getRulesIndexes($rules), |
|
802
|
|
|
JsonApiRuleSerializer::getRulesStartIndexes($rules), |
|
803
|
|
|
JsonApiRuleSerializer::getRulesEndIndexes($rules) |
|
804
|
|
|
); |
|
805
|
|
|
|
|
806
|
|
|
foreach ($indexes as $index) { |
|
807
|
|
|
$allOk = $allOk && is_int($index) && JsonApiRuleSerializer::isRuleExist($index, $this->getBlocks()); |
|
808
|
|
|
} |
|
809
|
|
|
|
|
810
|
|
|
return $allOk; |
|
811
|
|
|
} |
|
812
|
|
|
} |
|
813
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_functionexpects aPostobject, and outputs the author of the post. The base classPostreturns a simple string and outputting a simple string will work just fine. However, the child classBlogPostwhich is a sub-type ofPostinstead decided to return anobject, and is therefore violating the SOLID principles. If aBlogPostwere passed tomy_function, PHP would not complain, but ultimately fail when executing thestrtouppercall in its body.