Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
271 / 271
99.35% covered (success)
99.35%
152 / 153
65.52% covered (warning)
65.52%
76 / 116
96.88% covered (success)
96.88%
31 / 32
CRAP
0.00% covered (danger)
0.00%
0 / 1
ValidationRun
100.00% covered (success)
100.00%
271 / 271
99.35% covered (success)
99.35%
152 / 153
65.52% covered (warning)
65.52%
76 / 116
100.00% covered (success)
100.00%
32 / 32
373.31
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validate
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
11 / 11
25.00% covered (danger)
25.00%
4 / 16
100.00% covered (success)
100.00%
1 / 1
15.55
 prepareData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
4 / 4
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
2.15
 validateField
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
11 / 11
87.50% covered (warning)
87.50%
7 / 8
100.00% covered (success)
100.00%
1 / 1
6.07
 finalizeValues
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
6 / 6
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
 finalizeItem
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
6 / 6
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
3.14
 extractValues
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
6 / 6
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
 readFromData
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
9 / 9
50.00% covered (danger)
50.00%
3 / 6
100.00% covered (success)
100.00%
1 / 1
6.00
 readFieldValue
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 readEmptyValue
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 readDefaultValue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addReadErrors
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
5 / 5
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
3.14
 readExtraValue
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 readKnownValue
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 formatExtraFailure
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatNullFailure
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatMissingFailure
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatCoercionFailure
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 formatShapeFailure
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatFailure
100.00% covered (success)
100.00%
16 / 16
85.71% covered (warning)
85.71%
6 / 7
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
 toSubValues
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 review
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
4 / 4
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
2.15
 fillMissingFromFields
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
14 / 14
58.33% covered (warning)
58.33%
7 / 12
100.00% covered (success)
100.00%
1 / 1
10.54
 readValues
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
8 / 8
40.00% covered (danger)
40.00%
2 / 5
100.00% covered (success)
100.00%
1 / 1
7.46
 validateItem
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
9 / 9
66.67% covered (warning)
66.67%
4 / 6
100.00% covered (success)
100.00%
1 / 1
4.59
 getValues
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 blockRules
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rulesBlocked
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 path
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 pathKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isRawEmptyValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
5 / 5
25.00% covered (danger)
25.00%
1 / 4
100.00% covered (success)
100.00%
1 / 1
6.80
 params
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
4 / 4
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
2.15
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Sire;
6
7use Celemas\Sire\Contract\Value;
8use ValueError;
9
10/** @internal */
11final 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}

Branches

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.

ValidationRun->__construct
18        private readonly ShapeDefinition $shape,
19        private readonly array $data,
20    ) {
21        $this->errors = new ErrorBag();
22    }
ValidationRun->addReadErrors
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    }
ValidationRun->blockRules
557    private function blockRules(string $field, string|int|null $listIndex): void
558    {
559        $this->ruleBlockedPaths[self::pathKey(self::path($field, $listIndex))] = true;
560    }
ValidationRun->extractValues
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    }
ValidationRun->fillMissingFromFields
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    }
ValidationRun->finalizeItem
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    }
ValidationRun->finalizeValues
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    }
ValidationRun->formatCoercionFailure
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    }
ValidationRun->formatExtraFailure
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    }
ValidationRun->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;
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    }
ValidationRun->formatMissingFailure
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    }
ValidationRun->formatNullFailure
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    }
ValidationRun->formatShapeFailure
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    }
ValidationRun->getValues
549    private function getValues(array $values): array
550    {
551        return array_map(
552            static fn(Value $item): mixed => $item->value,
553            $values,
554        );
555    }
ValidationRun->isRawEmptyValue
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    }
ValidationRun->params
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    }
ValidationRun->path
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    }
ValidationRun->pathKey
581    private static function pathKey(array $path): string
582    {
583        return serialize($path);
584    }
ValidationRun->prepareData
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    }
ValidationRun->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    }
ValidationRun->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);
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    }
ValidationRun->readExtraValue
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    }
ValidationRun->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);
202        $read = $this->readKnownValue($definition, $value);
203        $this->addReadErrors($field, $read, $listIndex);
204
205        return $read->value;
206    }
ValidationRun->readFromData
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    }
ValidationRun->readKnownValue
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    }
ValidationRun->readValues
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    }
ValidationRun->review
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    }
ValidationRun->rulesBlocked
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    }
ValidationRun->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),
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    }
ValidationRun->validate
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    }
ValidationRun->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),
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    }
ValidationRun->validateItem
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    }
{closure:/workspace/celemas/sire/src/ValidationRun.php:552-552}
552            static fn(Value $item): mixed => $item->value,