Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
271 / 271 |
|
99.35% |
152 / 153 |
|
65.52% |
76 / 116 |
|
96.88% |
31 / 32 |
CRAP | |
0.00% |
0 / 1 |
| ValidationRun | |
100.00% |
271 / 271 |
|
99.35% |
152 / 153 |
|
65.52% |
76 / 116 |
|
100.00% |
32 / 32 |
373.31 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| validate | |
100.00% |
16 / 16 |
|
100.00% |
11 / 11 |
|
25.00% |
4 / 16 |
|
100.00% |
1 / 1 |
15.55 | |||
| prepareData | |
100.00% |
3 / 3 |
|
100.00% |
4 / 4 |
|
66.67% |
2 / 3 |
|
100.00% |
1 / 1 |
2.15 | |||
| validateField | |
100.00% |
27 / 27 |
|
100.00% |
11 / 11 |
|
87.50% |
7 / 8 |
|
100.00% |
1 / 1 |
6.07 | |||
| finalizeValues | |
100.00% |
6 / 6 |
|
100.00% |
6 / 6 |
|
50.00% |
2 / 4 |
|
100.00% |
1 / 1 |
4.12 | |||
| finalizeItem | |
100.00% |
8 / 8 |
|
100.00% |
6 / 6 |
|
75.00% |
3 / 4 |
|
100.00% |
1 / 1 |
3.14 | |||
| extractValues | |
100.00% |
6 / 6 |
|
100.00% |
6 / 6 |
|
50.00% |
2 / 4 |
|
100.00% |
1 / 1 |
4.12 | |||
| readFromData | |
100.00% |
10 / 10 |
|
100.00% |
9 / 9 |
|
50.00% |
3 / 6 |
|
100.00% |
1 / 1 |
6.00 | |||
| readFieldValue | |
100.00% |
6 / 6 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| readEmptyValue | |
100.00% |
9 / 9 |
|
100.00% |
5 / 5 |
|
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| readDefaultValue | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addReadErrors | |
100.00% |
6 / 6 |
|
100.00% |
5 / 5 |
|
75.00% |
3 / 4 |
|
100.00% |
1 / 1 |
3.14 | |||
| readExtraValue | |
100.00% |
8 / 8 |
|
100.00% |
5 / 5 |
|
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| readKnownValue | |
100.00% |
20 / 20 |
|
100.00% |
10 / 10 |
|
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
5 | |||
| formatExtraFailure | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| formatNullFailure | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| formatMissingFailure | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| formatCoercionFailure | |
100.00% |
11 / 11 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| formatShapeFailure | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| formatFailure | |
100.00% |
16 / 16 |
|
85.71% |
6 / 7 |
|
50.00% |
2 / 4 |
|
100.00% |
1 / 1 |
4.12 | |||
| toSubValues | |
100.00% |
13 / 13 |
|
100.00% |
5 / 5 |
|
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| review | |
100.00% |
7 / 7 |
|
100.00% |
4 / 4 |
|
66.67% |
2 / 3 |
|
100.00% |
1 / 1 |
2.15 | |||
| fillMissingFromFields | |
100.00% |
15 / 15 |
|
100.00% |
14 / 14 |
|
58.33% |
7 / 12 |
|
100.00% |
1 / 1 |
10.54 | |||
| readValues | |
100.00% |
15 / 15 |
|
100.00% |
8 / 8 |
|
40.00% |
2 / 5 |
|
100.00% |
1 / 1 |
7.46 | |||
| validateItem | |
100.00% |
11 / 11 |
|
100.00% |
9 / 9 |
|
66.67% |
4 / 6 |
|
100.00% |
1 / 1 |
4.59 | |||
| getValues | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| blockRules | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| rulesBlocked | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| path | |
100.00% |
3 / 3 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| pathKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| isRawEmptyValue | |
100.00% |
1 / 1 |
|
100.00% |
5 / 5 |
|
25.00% |
1 / 4 |
|
100.00% |
1 / 1 |
6.80 | |||
| params | |
100.00% |
4 / 4 |
|
100.00% |
4 / 4 |
|
66.67% |
2 / 3 |
|
100.00% |
1 / 1 |
2.15 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celemas\Sire; |
| 6 | |
| 7 | use Celemas\Sire\Contract\Value; |
| 8 | use ValueError; |
| 9 | |
| 10 | /** @internal */ |
| 11 | final class ValidationRun |
| 12 | { |
| 13 | private ErrorBag $errors; |
| 14 | /** @var array<string, true> */ |
| 15 | private array $ruleBlockedPaths = []; |
| 16 | |
| 17 | public function __construct( |
| 18 | private readonly ShapeDefinition $shape, |
| 19 | private readonly array $data, |
| 20 | ) { |
| 21 | $this->errors = new ErrorBag(); |
| 22 | } |
| 23 | |
| 24 | public function validate(): Result |
| 25 | { |
| 26 | $data = $this->prepareData($this->data); |
| 27 | $values = $this->readValues($data); |
| 28 | $validatedValues = []; |
| 29 | |
| 30 | if ($this->shape->list) { |
| 31 | foreach ($values as $listIndex => $subValues) { |
| 32 | /** @var array<string, Value> $subValues */ |
| 33 | $validatedValues[] = $this->validateItem($subValues, $listIndex); |
| 34 | } |
| 35 | } else { |
| 36 | /** @var array<string, Value> $values */ |
| 37 | $validatedValues = $this->validateItem($values); |
| 38 | } |
| 39 | |
| 40 | if (!$this->errors->hasErrors()) { |
| 41 | $validatedValues = $this->finalizeValues($validatedValues); |
| 42 | } |
| 43 | |
| 44 | $extractedValues = $this->extractValues($validatedValues); |
| 45 | |
| 46 | if (!$this->errors->hasErrors()) { |
| 47 | $this->review($extractedValues); |
| 48 | } |
| 49 | |
| 50 | return new Result( |
| 51 | $this->errors->issues(), |
| 52 | $extractedValues, |
| 53 | ); |
| 54 | } |
| 55 | |
| 56 | /** @return array<array-key, mixed> */ |
| 57 | private function prepareData(array $data): array |
| 58 | { |
| 59 | foreach ($this->shape->prepareCallbacks as $prepare) { |
| 60 | $data = $prepare($data); |
| 61 | } |
| 62 | |
| 63 | return $data; |
| 64 | } |
| 65 | |
| 66 | private function validateField( |
| 67 | Field $definition, |
| 68 | Value $value, |
| 69 | string $ruleDefinition, |
| 70 | string|int|null $listIndex, |
| 71 | ): void { |
| 72 | $parsedRule = $this->shape->ruleParser->parse($ruleDefinition); |
| 73 | $ruleName = $parsedRule['name']; |
| 74 | $ruleArgs = $parsedRule['args']; |
| 75 | |
| 76 | $rule = $this->shape->rules->get($ruleName); |
| 77 | |
| 78 | if ($rule === null) { |
| 79 | throw new ValueError( |
| 80 | sprintf('Unknown rule "%s" in field "%s"', $ruleName, $definition->field), |
| 81 | ); |
| 82 | } |
| 83 | |
| 84 | if ($this->rulesBlocked($definition->field, $listIndex)) { |
| 85 | return; |
| 86 | } |
| 87 | |
| 88 | if (!$rule instanceof Contract\ValidatesEmpty && $value->empty) { |
| 89 | return; |
| 90 | } |
| 91 | |
| 92 | $validation = $rule->validate($value, ...$ruleArgs); |
| 93 | |
| 94 | if ($validation->failure !== null) { |
| 95 | $this->errors->add( |
| 96 | self::path($definition->field, $listIndex), |
| 97 | $this->formatFailure( |
| 98 | $validation->failure, |
| 99 | $definition->name(), |
| 100 | $definition->field, |
| 101 | $value->pristine, |
| 102 | 'rule.' . $ruleName, |
| 103 | $rule->message, |
| 104 | $ruleArgs, |
| 105 | $definition->messageOverrides(), |
| 106 | ), |
| 107 | ); |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | /** @param array<string, Value>|list<array<string, Value>> $validatedValues */ |
| 112 | private function finalizeValues(array $validatedValues): array |
| 113 | { |
| 114 | if ($this->shape->list) { |
| 115 | $values = []; |
| 116 | |
| 117 | foreach ($validatedValues as $item) { |
| 118 | /** @var array<string, Value> $item */ |
| 119 | $values[] = $this->finalizeItem($item); |
| 120 | } |
| 121 | |
| 122 | return $values; |
| 123 | } |
| 124 | |
| 125 | /** @var array<string, Value> $validatedValues */ |
| 126 | return $this->finalizeItem($validatedValues); |
| 127 | } |
| 128 | |
| 129 | /** |
| 130 | * @param array<string, Value> $values |
| 131 | * @return array<string, Value> |
| 132 | */ |
| 133 | private function finalizeItem(array $values): array |
| 134 | { |
| 135 | $itemValues = $this->getValues($values); |
| 136 | |
| 137 | foreach ($this->shape->fields as $field => $definition) { |
| 138 | if (!array_key_exists($field, $values)) { |
| 139 | continue; |
| 140 | } |
| 141 | |
| 142 | $value = $values[$field]; |
| 143 | $finalValue = $definition->applyFinalization($value->value, $itemValues); |
| 144 | $values[$field] = new \Celemas\Sire\Value($finalValue, $value->pristine, $value->empty); |
| 145 | } |
| 146 | |
| 147 | return $values; |
| 148 | } |
| 149 | |
| 150 | /** @param array<string, Value>|list<array<string, Value>> $validatedValues */ |
| 151 | private function extractValues(array $validatedValues): array |
| 152 | { |
| 153 | if ($this->shape->list) { |
| 154 | $values = []; |
| 155 | |
| 156 | foreach ($validatedValues as $item) { |
| 157 | /** @var array<string, Value> $item */ |
| 158 | $values[] = $this->getValues($item); |
| 159 | } |
| 160 | |
| 161 | return $values; |
| 162 | } |
| 163 | |
| 164 | /** @var array<string, Value> $validatedValues */ |
| 165 | return $this->getValues($validatedValues); |
| 166 | } |
| 167 | |
| 168 | /** @return array<string, Value> */ |
| 169 | private function readFromData(array $data, string|int|null $listIndex = null): array |
| 170 | { |
| 171 | $values = []; |
| 172 | |
| 173 | foreach ($data as $field => $value) { |
| 174 | $field = (string) $field; |
| 175 | $definition = $this->shape->fields[$field] ?? null; |
| 176 | $value = $definition instanceof Field |
| 177 | ? $this->readFieldValue($field, $definition, $value, $data, $listIndex) |
| 178 | : $this->readExtraValue($field, $value, $listIndex); |
| 179 | |
| 180 | if ($value !== null) { |
| 181 | $values[$field] = $value; |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | return $values; |
| 186 | } |
| 187 | |
| 188 | /** @param array<string, mixed> $data */ |
| 189 | private function readFieldValue( |
| 190 | string $field, |
| 191 | Field $definition, |
| 192 | mixed $value, |
| 193 | array $data, |
| 194 | string|int|null $listIndex, |
| 195 | ): ?Value { |
| 196 | $value = $definition->applyPreparation($value, $data); |
| 197 | |
| 198 | if ($definition->isBlank($value)) { |
| 199 | return $this->readEmptyValue($field, $definition, $data, $listIndex); |
| 200 | } |
| 201 | |
| 202 | $read = $this->readKnownValue($definition, $value); |
| 203 | $this->addReadErrors($field, $read, $listIndex); |
| 204 | |
| 205 | return $read->value; |
| 206 | } |
| 207 | |
| 208 | /** @param array<string, mixed> $data */ |
| 209 | private function readEmptyValue( |
| 210 | string $field, |
| 211 | Field $definition, |
| 212 | array $data, |
| 213 | string|int|null $listIndex, |
| 214 | ): ?Value { |
| 215 | if ($definition->hasDefault()) { |
| 216 | return $this->readDefaultValue($field, $definition, $data, $listIndex); |
| 217 | } |
| 218 | |
| 219 | if ($definition->isOptional()) { |
| 220 | return null; |
| 221 | } |
| 222 | |
| 223 | $this->errors->add( |
| 224 | self::path($field, $listIndex), |
| 225 | $this->formatMissingFailure($definition), |
| 226 | ); |
| 227 | |
| 228 | return null; |
| 229 | } |
| 230 | |
| 231 | /** @param array<string, mixed> $data */ |
| 232 | private function readDefaultValue( |
| 233 | string $field, |
| 234 | Field $definition, |
| 235 | array $data, |
| 236 | string|int|null $listIndex, |
| 237 | ): Value { |
| 238 | $value = $definition->applyPreparation($definition->defaultValue(), $data); |
| 239 | $read = $this->readKnownValue($definition, $value); |
| 240 | $this->addReadErrors($field, $read, $listIndex); |
| 241 | |
| 242 | return $read->value; |
| 243 | } |
| 244 | |
| 245 | private function addReadErrors(string $field, ReadValue $read, string|int|null $listIndex): void |
| 246 | { |
| 247 | if ($read->nestedResult !== null) { |
| 248 | $this->errors->addNested(self::path($field, $listIndex), $read->nestedResult); |
| 249 | $this->blockRules($field, $listIndex); |
| 250 | } |
| 251 | |
| 252 | if ($read->issue !== null) { |
| 253 | $this->errors->add(self::path($field, $listIndex), $read->issue); |
| 254 | $this->blockRules($field, $listIndex); |
| 255 | } |
| 256 | } |
| 257 | |
| 258 | private function readExtraValue(string $field, mixed $value, string|int|null $listIndex): ?Value |
| 259 | { |
| 260 | if ($this->shape->extra === Extra::Allow) { |
| 261 | return new \Celemas\Sire\Value($value, $value, self::isRawEmptyValue($value)); |
| 262 | } |
| 263 | |
| 264 | if ($this->shape->extra === Extra::Forbid) { |
| 265 | $this->errors->add( |
| 266 | self::path($field, $listIndex), |
| 267 | $this->formatExtraFailure($field, $value), |
| 268 | ); |
| 269 | } |
| 270 | |
| 271 | return null; |
| 272 | } |
| 273 | |
| 274 | private function readKnownValue(Field $definition, mixed $value): ReadValue |
| 275 | { |
| 276 | if ($value === null) { |
| 277 | return new ReadValue( |
| 278 | new \Celemas\Sire\Value(null, null, true), |
| 279 | $definition->isNullable() ? null : $this->formatNullFailure($definition), |
| 280 | ); |
| 281 | } |
| 282 | |
| 283 | $type = $definition->type(); |
| 284 | |
| 285 | if ($type === 'shape') { |
| 286 | $shape = $definition->type; |
| 287 | assert($shape instanceof Contract\Validator, 'Expected shape field type to be a shape instance'); |
| 288 | |
| 289 | return $this->toSubValues($value, $definition, $shape); |
| 290 | } |
| 291 | |
| 292 | $coercer = $this->shape->coercers->get($type); |
| 293 | |
| 294 | if ($coercer === null) { |
| 295 | throw new ValueError('Wrong shape type'); |
| 296 | } |
| 297 | |
| 298 | $coercion = $coercer->coerce( |
| 299 | $value, |
| 300 | $definition->coercionMode($this->shape->coercionMode), |
| 301 | ); |
| 302 | |
| 303 | return new ReadValue( |
| 304 | new \Celemas\Sire\Value($coercion->value, $coercion->pristine, $coercion->empty), |
| 305 | $this->formatCoercionFailure($coercion, $definition, $coercer), |
| 306 | ); |
| 307 | } |
| 308 | |
| 309 | private function formatExtraFailure(string $field, mixed $value): Issue |
| 310 | { |
| 311 | return $this->formatFailure( |
| 312 | Failure::invalid(), |
| 313 | $field, |
| 314 | $field, |
| 315 | $value, |
| 316 | 'extra', |
| 317 | 'Field "{field}" is not allowed', |
| 318 | ); |
| 319 | } |
| 320 | |
| 321 | private function formatNullFailure(Field $definition): Issue |
| 322 | { |
| 323 | return $this->formatFailure( |
| 324 | Failure::key('null'), |
| 325 | $definition->name(), |
| 326 | $definition->field, |
| 327 | null, |
| 328 | 'null', |
| 329 | '{label} must not be null', |
| 330 | messages: $definition->messageOverrides(), |
| 331 | ); |
| 332 | } |
| 333 | |
| 334 | private function formatMissingFailure(Field $definition): Issue |
| 335 | { |
| 336 | return $this->formatFailure( |
| 337 | Failure::key('missing'), |
| 338 | $definition->name(), |
| 339 | $definition->field, |
| 340 | null, |
| 341 | 'missing', |
| 342 | '{label} is required', |
| 343 | messages: $definition->messageOverrides(), |
| 344 | ); |
| 345 | } |
| 346 | |
| 347 | private function formatCoercionFailure( |
| 348 | Contract\Coercion $coercion, |
| 349 | Field $definition, |
| 350 | Contract\Coercer $coercer, |
| 351 | ): ?Issue { |
| 352 | if ($coercion->failure === null) { |
| 353 | return null; |
| 354 | } |
| 355 | |
| 356 | return $this->formatFailure( |
| 357 | $coercion->failure, |
| 358 | $definition->name(), |
| 359 | $definition->field, |
| 360 | $coercion->pristine, |
| 361 | 'type.' . $definition->type(), |
| 362 | $coercer->message, |
| 363 | messages: $definition->messageOverrides(), |
| 364 | ); |
| 365 | } |
| 366 | |
| 367 | private function formatShapeFailure(Field $definition, mixed $value): Issue |
| 368 | { |
| 369 | return $this->formatFailure( |
| 370 | Failure::invalid(), |
| 371 | $definition->name(), |
| 372 | $definition->field, |
| 373 | $value, |
| 374 | 'type.shape', |
| 375 | '{label} must be an array', |
| 376 | messages: $definition->messageOverrides(), |
| 377 | ); |
| 378 | } |
| 379 | |
| 380 | /** |
| 381 | * @param list<mixed> $args |
| 382 | * @param array<string, string> $messages |
| 383 | */ |
| 384 | private function formatFailure( |
| 385 | Failure $failure, |
| 386 | string $label, |
| 387 | string $field, |
| 388 | mixed $pristine, |
| 389 | ?string $defaultKey, |
| 390 | string $fallback, |
| 391 | array $args = [], |
| 392 | array $messages = [], |
| 393 | ): Issue { |
| 394 | $args = $failure->args === [] ? $args : $failure->args; |
| 395 | |
| 396 | return new Issue( |
| 397 | [], |
| 398 | $failure->key !== '' ? $failure->key : $defaultKey ?? 'invalid', |
| 399 | $this->shape->messageFormatter->format( |
| 400 | $failure, |
| 401 | $label, |
| 402 | $field, |
| 403 | $pristine, |
| 404 | $defaultKey, |
| 405 | $fallback, |
| 406 | $args, |
| 407 | $messages, |
| 408 | ), |
| 409 | self::params($args), |
| 410 | ); |
| 411 | } |
| 412 | |
| 413 | private function toSubValues( |
| 414 | mixed $pristine, |
| 415 | Field $definition, |
| 416 | Contract\Validator $shape, |
| 417 | ): ReadValue { |
| 418 | if (!is_array($pristine)) { |
| 419 | return new ReadValue( |
| 420 | new \Celemas\Sire\Value($pristine, $pristine, self::isRawEmptyValue($pristine)), |
| 421 | $this->formatShapeFailure($definition, $pristine), |
| 422 | ); |
| 423 | } |
| 424 | |
| 425 | $result = $shape->validate($pristine); |
| 426 | |
| 427 | if ($result->valid()) { |
| 428 | $values = $result->values(); |
| 429 | |
| 430 | return new ReadValue(new \Celemas\Sire\Value($values, $pristine, $values === [])); |
| 431 | } |
| 432 | |
| 433 | return new ReadValue( |
| 434 | new \Celemas\Sire\Value($pristine, $pristine, self::isRawEmptyValue($pristine)), |
| 435 | nestedResult: $result, |
| 436 | ); |
| 437 | } |
| 438 | |
| 439 | /** @param array<string, mixed>|list<array<string, mixed>> $values */ |
| 440 | private function review(array $values): void |
| 441 | { |
| 442 | $context = new Review( |
| 443 | $this->errors, |
| 444 | $values, |
| 445 | $this->shape->list, |
| 446 | ); |
| 447 | |
| 448 | foreach ($this->shape->reviewCallbacks as $review) { |
| 449 | $review($context); |
| 450 | } |
| 451 | } |
| 452 | |
| 453 | /** |
| 454 | * @param array<string, Value> $values |
| 455 | * @param array<string, mixed> $data |
| 456 | * @return array<string, Value> |
| 457 | */ |
| 458 | private function fillMissingFromFields( |
| 459 | array $values, |
| 460 | array $data, |
| 461 | string|int|null $listIndex = null, |
| 462 | ): array { |
| 463 | foreach ($this->shape->fields as $field => $definition) { |
| 464 | if (array_key_exists($field, $values) || array_key_exists($field, $data)) { |
| 465 | continue; |
| 466 | } |
| 467 | |
| 468 | if ($definition->treatsMissingAsEmpty()) { |
| 469 | $value = $this->readEmptyValue($field, $definition, $data, $listIndex); |
| 470 | |
| 471 | if ($value !== null) { |
| 472 | $values[$field] = $value; |
| 473 | } |
| 474 | |
| 475 | continue; |
| 476 | } |
| 477 | |
| 478 | if ($definition->isOptional()) { |
| 479 | continue; |
| 480 | } |
| 481 | |
| 482 | $this->errors->add( |
| 483 | self::path($field, $listIndex), |
| 484 | $this->formatMissingFailure($definition), |
| 485 | ); |
| 486 | } |
| 487 | |
| 488 | return $values; |
| 489 | } |
| 490 | |
| 491 | /** @return array<string, Value>|list<array<string, Value>> */ |
| 492 | private function readValues(array $data): array |
| 493 | { |
| 494 | if ($this->shape->list) { |
| 495 | $values = []; |
| 496 | |
| 497 | foreach ($data as $listIndex => $item) { |
| 498 | if (!is_array($item)) { |
| 499 | $this->errors->add( |
| 500 | [$listIndex], |
| 501 | new Issue([], 'type.shape', 'Item must be an array'), |
| 502 | ); |
| 503 | $values[] = []; |
| 504 | |
| 505 | continue; |
| 506 | } |
| 507 | |
| 508 | /** @var array<string, mixed> $item */ |
| 509 | $subValues = $this->readFromData($item, $listIndex); |
| 510 | $values[] = $this->fillMissingFromFields($subValues, $item, $listIndex); |
| 511 | } |
| 512 | |
| 513 | return $values; |
| 514 | } |
| 515 | |
| 516 | $values = $this->readFromData($data); |
| 517 | |
| 518 | return $this->fillMissingFromFields($values, $data); |
| 519 | } |
| 520 | |
| 521 | /** |
| 522 | * @param array<string, Value> $values |
| 523 | * @return array<string, Value> |
| 524 | */ |
| 525 | private function validateItem(array $values, string|int|null $listIndex = null): array |
| 526 | { |
| 527 | foreach ($this->shape->fields as $field => $definition) { |
| 528 | if (!array_key_exists($field, $values)) { |
| 529 | continue; |
| 530 | } |
| 531 | |
| 532 | foreach ($definition->rules as $rule) { |
| 533 | $this->validateField( |
| 534 | $definition, |
| 535 | $values[$field], |
| 536 | $rule, |
| 537 | $listIndex, |
| 538 | ); |
| 539 | } |
| 540 | } |
| 541 | |
| 542 | return $values; |
| 543 | } |
| 544 | |
| 545 | /** |
| 546 | * @param array<string, Value> $values |
| 547 | * @return array<string, mixed> |
| 548 | */ |
| 549 | private function getValues(array $values): array |
| 550 | { |
| 551 | return array_map( |
| 552 | static fn(Value $item): mixed => $item->value, |
| 553 | $values, |
| 554 | ); |
| 555 | } |
| 556 | |
| 557 | private function blockRules(string $field, string|int|null $listIndex): void |
| 558 | { |
| 559 | $this->ruleBlockedPaths[self::pathKey(self::path($field, $listIndex))] = true; |
| 560 | } |
| 561 | |
| 562 | private function rulesBlocked(string $field, string|int|null $listIndex): bool |
| 563 | { |
| 564 | return array_key_exists( |
| 565 | self::pathKey(self::path($field, $listIndex)), |
| 566 | $this->ruleBlockedPaths, |
| 567 | ); |
| 568 | } |
| 569 | |
| 570 | /** @return list<string|int> */ |
| 571 | private static function path(string $field, string|int|null $listIndex): array |
| 572 | { |
| 573 | if ($listIndex === null) { |
| 574 | return [$field]; |
| 575 | } |
| 576 | |
| 577 | return [$listIndex, $field]; |
| 578 | } |
| 579 | |
| 580 | /** @param list<string|int> $path */ |
| 581 | private static function pathKey(array $path): string |
| 582 | { |
| 583 | return serialize($path); |
| 584 | } |
| 585 | |
| 586 | private static function isRawEmptyValue(mixed $value): bool |
| 587 | { |
| 588 | return $value === null || $value === [] || $value === ''; |
| 589 | } |
| 590 | |
| 591 | /** |
| 592 | * @param list<mixed> $args |
| 593 | * @return array<string, mixed> |
| 594 | */ |
| 595 | private static function params(array $args): array |
| 596 | { |
| 597 | $params = []; |
| 598 | |
| 599 | foreach ($args as $index => $arg) { |
| 600 | $params['arg' . ($index + 1)] = $arg; |
| 601 | } |
| 602 | |
| 603 | return $params; |
| 604 | } |
| 605 | } |