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