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 | } |
Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not
necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once.
Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement
always has an else as part of its logical flow even if you didn't write one.
| 18 | private readonly ShapeDefinition $shape, |
| 19 | private readonly array $data, |
| 20 | ) { |
| 21 | $this->errors = new ErrorBag(); |
| 22 | } |
| 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) { |
| 252 | if ($read->issue !== null) { |
| 253 | $this->errors->add(self::path($field, $listIndex), $read->issue); |
| 254 | $this->blockRules($field, $listIndex); |
| 255 | } |
| 256 | } |
| 256 | } |
| 557 | private function blockRules(string $field, string|int|null $listIndex): void |
| 558 | { |
| 559 | $this->ruleBlockedPaths[self::pathKey(self::path($field, $listIndex))] = true; |
| 560 | } |
| 151 | private function extractValues(array $validatedValues): array |
| 152 | { |
| 153 | if ($this->shape->list) { |
| 154 | $values = []; |
| 155 | |
| 156 | foreach ($validatedValues as $item) { |
| 156 | foreach ($validatedValues as $item) { |
| 156 | foreach ($validatedValues as $item) { |
| 157 | /** @var array<string, Value> $item */ |
| 158 | $values[] = $this->getValues($item); |
| 156 | foreach ($validatedValues as $item) { |
| 157 | /** @var array<string, Value> $item */ |
| 158 | $values[] = $this->getValues($item); |
| 159 | } |
| 160 | |
| 161 | return $values; |
| 165 | return $this->getValues($validatedValues); |
| 166 | } |
| 459 | array $values, |
| 460 | array $data, |
| 461 | string|int|null $listIndex = null, |
| 462 | ): array { |
| 463 | foreach ($this->shape->fields as $field => $definition) { |
| 463 | foreach ($this->shape->fields as $field => $definition) { |
| 463 | foreach ($this->shape->fields as $field => $definition) { |
| 464 | if (array_key_exists($field, $values) || array_key_exists($field, $data)) { |
| 464 | if (array_key_exists($field, $values) || array_key_exists($field, $data)) { |
| 464 | if (array_key_exists($field, $values) || array_key_exists($field, $data)) { |
| 465 | continue; |
| 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; |
| 475 | continue; |
| 478 | if ($definition->isOptional()) { |
| 479 | continue; |
| 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( |
| 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 | } |
| 133 | private function finalizeItem(array $values): array |
| 134 | { |
| 135 | $itemValues = $this->getValues($values); |
| 136 | |
| 137 | foreach ($this->shape->fields as $field => $definition) { |
| 137 | foreach ($this->shape->fields as $field => $definition) { |
| 137 | foreach ($this->shape->fields as $field => $definition) { |
| 138 | if (!array_key_exists($field, $values)) { |
| 139 | continue; |
| 137 | foreach ($this->shape->fields as $field => $definition) { |
| 138 | if (!array_key_exists($field, $values)) { |
| 139 | continue; |
| 140 | } |
| 141 | |
| 142 | $value = $values[$field]; |
| 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 | } |
| 112 | private function finalizeValues(array $validatedValues): array |
| 113 | { |
| 114 | if ($this->shape->list) { |
| 115 | $values = []; |
| 116 | |
| 117 | foreach ($validatedValues as $item) { |
| 117 | foreach ($validatedValues as $item) { |
| 117 | foreach ($validatedValues as $item) { |
| 118 | /** @var array<string, Value> $item */ |
| 119 | $values[] = $this->finalizeItem($item); |
| 117 | foreach ($validatedValues as $item) { |
| 118 | /** @var array<string, Value> $item */ |
| 119 | $values[] = $this->finalizeItem($item); |
| 120 | } |
| 121 | |
| 122 | return $values; |
| 126 | return $this->finalizeItem($validatedValues); |
| 127 | } |
| 348 | Contract\Coercion $coercion, |
| 349 | Field $definition, |
| 350 | Contract\Coercer $coercer, |
| 351 | ): ?Issue { |
| 352 | if ($coercion->failure === null) { |
| 353 | return null; |
| 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 | } |
| 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 | } |
| 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; |
| 394 | $args = $failure->args === [] ? $args : $failure->args; |
| 394 | $args = $failure->args === [] ? $args : $failure->args; |
| 394 | $args = $failure->args === [] ? $args : $failure->args; |
| 395 | |
| 396 | return new Issue( |
| 397 | [], |
| 398 | $failure->key !== '' ? $failure->key : $defaultKey ?? 'invalid', |
| 398 | $failure->key !== '' ? $failure->key : $defaultKey ?? 'invalid', |
| 398 | $failure->key !== '' ? $failure->key : $defaultKey ?? 'invalid', |
| 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 | } |
| 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 | } |
| 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 | } |
| 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 | } |
| 549 | private function getValues(array $values): array |
| 550 | { |
| 551 | return array_map( |
| 552 | static fn(Value $item): mixed => $item->value, |
| 553 | $values, |
| 554 | ); |
| 555 | } |
| 586 | private static function isRawEmptyValue(mixed $value): bool |
| 587 | { |
| 588 | return $value === null || $value === [] || $value === ''; |
| 588 | return $value === null || $value === [] || $value === ''; |
| 588 | return $value === null || $value === [] || $value === ''; |
| 588 | return $value === null || $value === [] || $value === ''; |
| 588 | return $value === null || $value === [] || $value === ''; |
| 589 | } |
| 595 | private static function params(array $args): array |
| 596 | { |
| 597 | $params = []; |
| 598 | |
| 599 | foreach ($args as $index => $arg) { |
| 599 | foreach ($args as $index => $arg) { |
| 599 | foreach ($args as $index => $arg) { |
| 599 | foreach ($args as $index => $arg) { |
| 600 | $params['arg' . ($index + 1)] = $arg; |
| 601 | } |
| 602 | |
| 603 | return $params; |
| 604 | } |
| 571 | private static function path(string $field, string|int|null $listIndex): array |
| 572 | { |
| 573 | if ($listIndex === null) { |
| 574 | return [$field]; |
| 577 | return [$listIndex, $field]; |
| 578 | } |
| 581 | private static function pathKey(array $path): string |
| 582 | { |
| 583 | return serialize($path); |
| 584 | } |
| 57 | private function prepareData(array $data): array |
| 58 | { |
| 59 | foreach ($this->shape->prepareCallbacks as $prepare) { |
| 59 | foreach ($this->shape->prepareCallbacks as $prepare) { |
| 59 | foreach ($this->shape->prepareCallbacks as $prepare) { |
| 60 | $data = $prepare($data); |
| 59 | foreach ($this->shape->prepareCallbacks as $prepare) { |
| 60 | $data = $prepare($data); |
| 61 | } |
| 62 | |
| 63 | return $data; |
| 64 | } |
| 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 | } |
| 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); |
| 219 | if ($definition->isOptional()) { |
| 220 | return null; |
| 223 | $this->errors->add( |
| 224 | self::path($field, $listIndex), |
| 225 | $this->formatMissingFailure($definition), |
| 226 | ); |
| 227 | |
| 228 | return null; |
| 229 | } |
| 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)); |
| 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; |
| 271 | return null; |
| 272 | } |
| 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); |
| 202 | $read = $this->readKnownValue($definition, $value); |
| 203 | $this->addReadErrors($field, $read, $listIndex); |
| 204 | |
| 205 | return $read->value; |
| 206 | } |
| 169 | private function readFromData(array $data, string|int|null $listIndex = null): array |
| 170 | { |
| 171 | $values = []; |
| 172 | |
| 173 | foreach ($data as $field => $value) { |
| 173 | foreach ($data as $field => $value) { |
| 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) |
| 176 | $value = $definition instanceof Field |
| 177 | ? $this->readFieldValue($field, $definition, $value, $data, $listIndex) |
| 178 | : $this->readExtraValue($field, $value, $listIndex); |
| 176 | $value = $definition instanceof Field |
| 177 | ? $this->readFieldValue($field, $definition, $value, $data, $listIndex) |
| 178 | : $this->readExtraValue($field, $value, $listIndex); |
| 179 | |
| 180 | if ($value !== null) { |
| 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; |
| 173 | foreach ($data as $field => $value) { |
| 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 | } |
| 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), |
| 279 | $definition->isNullable() ? null : $this->formatNullFailure($definition), |
| 279 | $definition->isNullable() ? null : $this->formatNullFailure($definition), |
| 279 | $definition->isNullable() ? null : $this->formatNullFailure($definition), |
| 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); |
| 292 | $coercer = $this->shape->coercers->get($type); |
| 293 | |
| 294 | if ($coercer === null) { |
| 295 | throw new ValueError('Wrong shape type'); |
| 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 | } |
| 492 | private function readValues(array $data): array |
| 493 | { |
| 494 | if ($this->shape->list) { |
| 495 | $values = []; |
| 496 | |
| 497 | foreach ($data as $listIndex => $item) { |
| 497 | foreach ($data as $listIndex => $item) { |
| 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; |
| 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); |
| 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; |
| 516 | $values = $this->readFromData($data); |
| 517 | |
| 518 | return $this->fillMissingFromFields($values, $data); |
| 519 | } |
| 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) { |
| 448 | foreach ($this->shape->reviewCallbacks as $review) { |
| 448 | foreach ($this->shape->reviewCallbacks as $review) { |
| 449 | $review($context); |
| 448 | foreach ($this->shape->reviewCallbacks as $review) { |
| 449 | $review($context); |
| 450 | } |
| 451 | } |
| 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 | } |
| 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), |
| 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 === [])); |
| 433 | return new ReadValue( |
| 434 | new \Celemas\Sire\Value($pristine, $pristine, self::isRawEmptyValue($pristine)), |
| 435 | nestedResult: $result, |
| 436 | ); |
| 437 | } |
| 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) { |
| 31 | foreach ($values as $listIndex => $subValues) { |
| 31 | foreach ($values as $listIndex => $subValues) { |
| 30 | if ($this->shape->list) { |
| 31 | foreach ($values as $listIndex => $subValues) { |
| 37 | $validatedValues = $this->validateItem($values); |
| 38 | } |
| 39 | |
| 40 | if (!$this->errors->hasErrors()) { |
| 40 | if (!$this->errors->hasErrors()) { |
| 41 | $validatedValues = $this->finalizeValues($validatedValues); |
| 42 | } |
| 43 | |
| 44 | $extractedValues = $this->extractValues($validatedValues); |
| 44 | $extractedValues = $this->extractValues($validatedValues); |
| 45 | |
| 46 | if (!$this->errors->hasErrors()) { |
| 47 | $this->review($extractedValues); |
| 48 | } |
| 49 | |
| 50 | return new Result( |
| 50 | return new Result( |
| 51 | $this->errors->issues(), |
| 52 | $extractedValues, |
| 53 | ); |
| 54 | } |
| 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), |
| 84 | if ($this->rulesBlocked($definition->field, $listIndex)) { |
| 85 | return; |
| 88 | if (!$rule instanceof Contract\ValidatesEmpty && $value->empty) { |
| 88 | if (!$rule instanceof Contract\ValidatesEmpty && $value->empty) { |
| 88 | if (!$rule instanceof Contract\ValidatesEmpty && $value->empty) { |
| 89 | return; |
| 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 | } |
| 109 | } |
| 525 | private function validateItem(array $values, string|int|null $listIndex = null): array |
| 526 | { |
| 527 | foreach ($this->shape->fields as $field => $definition) { |
| 527 | foreach ($this->shape->fields as $field => $definition) { |
| 527 | foreach ($this->shape->fields as $field => $definition) { |
| 528 | if (!array_key_exists($field, $values)) { |
| 529 | continue; |
| 532 | foreach ($definition->rules as $rule) { |
| 532 | foreach ($definition->rules as $rule) { |
| 532 | foreach ($definition->rules as $rule) { |
| 533 | $this->validateField( |
| 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) { |
| 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 | } |
| 552 | static fn(Value $item): mixed => $item->value, |