limoncello-php-dist /
flute
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
arrayas a return type and suggests a more specific type. This type is inferred from the actual code.