Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
64 / 64
93.41% covered (success)
93.41%
85 / 91
53.25% covered (warning)
53.25%
41 / 77
88.89% covered (warning)
88.89%
24 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
Field
100.00% covered (success)
100.00%
64 / 64
93.41% covered (success)
93.41%
85 / 91
53.25% covered (warning)
53.25%
41 / 77
100.00% covered (success)
100.00%
27 / 27
283.46
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
 rules
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
4 / 4
33.33% covered (danger)
33.33%
1 / 3
100.00% covered (success)
100.00%
1 / 1
3.19
 label
100.00% covered (success)
100.00%
2 / 2
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
 prepare
100.00% covered (success)
100.00%
2 / 2
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
 finalize
100.00% covered (success)
100.00%
2 / 2
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
 empty
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
7 / 7
25.00% covered (danger)
25.00%
1 / 4
100.00% covered (success)
100.00%
1 / 1
6.80
 default
100.00% covered (success)
100.00%
5 / 5
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
 nullable
100.00% covered (success)
100.00%
2 / 2
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
 optional
100.00% covered (success)
100.00%
2 / 2
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
 strict
100.00% covered (success)
100.00%
2 / 2
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
 coerce
100.00% covered (success)
100.00%
2 / 2
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
 hasDefault
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
 defaultValue
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
 isNullable
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
 isOptional
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
 coercionMode
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
 treatsMissingAsEmpty
100.00% covered (success)
100.00%
1 / 1
75.00% covered (warning)
75.00%
3 / 4
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
1.12
 isBlank
100.00% covered (success)
100.00%
4 / 4
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
 message
100.00% covered (success)
100.00%
2 / 2
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
 messages
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
4 / 4
33.33% covered (danger)
33.33%
1 / 3
100.00% covered (success)
100.00%
1 / 1
3.19
 messageOverrides
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
 name
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
 type
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 applyPreparation
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
 applyFinalization
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
 matchesEmpty
100.00% covered (success)
100.00%
5 / 5
83.33% covered (warning)
83.33%
15 / 18
62.50% covered (warning)
62.50%
5 / 8
100.00% covered (success)
100.00%
1 / 1
9.58
 messageKey
100.00% covered (success)
100.00%
7 / 7
88.24% covered (warning)
88.24%
15 / 17
22.22% covered (danger)
22.22%
6 / 27
100.00% covered (success)
100.00%
1 / 1
22.94
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Sire;
6
7/** @api */
8final class Field
9{
10    private const int FLAG_HAS_DEFAULT = 1;
11    private const int FLAG_NULLABLE = 2;
12    private const int FLAG_OPTIONAL = 4;
13
14    private ?string $label = null;
15    /** @var list<callable> */
16    private array $preparers = [];
17    /** @var list<callable> */
18    private array $finalizers = [];
19    /** @var list<Blank> */
20    private array $empty = [Blank::Missing];
21    private int $flags = 0;
22    private mixed $default = null;
23    private ?CoercionMode $coercionMode = null;
24    /** @var array<string, string> */
25    private array $messages = [];
26    /** @var list<string> */
27    public private(set) array $rules = [];
28
29    public function __construct(
30        public readonly string $field,
31        public readonly string|Contract\Validator $type,
32    ) {}
33
34    public function rules(string ...$rules): static
35    {
36        foreach ($rules as $rule) {
37            $this->rules[] = $rule;
38        }
39
40        return $this;
41    }
42
43    public function label(string $label): static
44    {
45        $this->label = $label;
46
47        return $this;
48    }
49
50    /** @param callable $callback */
51    public function prepare(callable $callback): static
52    {
53        $this->preparers[] = $callback;
54
55        return $this;
56    }
57
58    /** @param callable(mixed, array<string, mixed>): mixed $callback */
59    public function finalize(callable $callback): static
60    {
61        $this->finalizers[] = $callback;
62
63        return $this;
64    }
65
66    public function empty(Blank|string ...$empty): static
67    {
68        $this->empty = [];
69
70        foreach ($empty as $value) {
71            $this->empty[] = $value instanceof Blank ? $value : Blank::from($value);
72        }
73
74        return $this;
75    }
76
77    public function default(mixed $value): static
78    {
79        $this->default = $value;
80        $this->flags |= self::FLAG_HAS_DEFAULT;
81
82        if ($value === null) {
83            $this->nullable();
84        }
85
86        return $this;
87    }
88
89    public function nullable(): static
90    {
91        $this->flags |= self::FLAG_NULLABLE;
92
93        return $this;
94    }
95
96    public function optional(): static
97    {
98        $this->flags |= self::FLAG_OPTIONAL;
99
100        return $this;
101    }
102
103    public function strict(): static
104    {
105        $this->coercionMode = CoercionMode::Strict;
106
107        return $this;
108    }
109
110    public function coerce(): static
111    {
112        $this->coercionMode = CoercionMode::Coerce;
113
114        return $this;
115    }
116
117    public function hasDefault(): bool
118    {
119        return ($this->flags & self::FLAG_HAS_DEFAULT) !== 0;
120    }
121
122    public function defaultValue(): mixed
123    {
124        return $this->default;
125    }
126
127    public function isNullable(): bool
128    {
129        return ($this->flags & self::FLAG_NULLABLE) !== 0;
130    }
131
132    public function isOptional(): bool
133    {
134        return ($this->flags & self::FLAG_OPTIONAL) !== 0;
135    }
136
137    /** @internal */
138    public function coercionMode(CoercionMode $default): CoercionMode
139    {
140        return $this->coercionMode ?? $default;
141    }
142
143    public function treatsMissingAsEmpty(): bool
144    {
145        return in_array(Blank::Missing, $this->empty, true);
146    }
147
148    public function isBlank(mixed $value): bool
149    {
150        foreach ($this->empty as $empty) {
151            if ($this->matchesEmpty($empty, $value)) {
152                return true;
153            }
154        }
155
156        return false;
157    }
158
159    public function message(string $key, string $message): static
160    {
161        $this->messages[$this->messageKey($key)] = $message;
162
163        return $this;
164    }
165
166    /** @param array<string, string> $messages */
167    public function messages(array $messages): static
168    {
169        foreach ($messages as $key => $message) {
170            $this->message($key, $message);
171        }
172
173        return $this;
174    }
175
176    /** @return array<string, string> */
177    public function messageOverrides(): array
178    {
179        return $this->messages;
180    }
181
182    public function name(): string
183    {
184        return $this->label ?? $this->field;
185    }
186
187    public function type(): string
188    {
189        return is_string($this->type) ? $this->type : 'shape';
190    }
191
192    /** @param array<string, mixed> $data */
193    public function applyPreparation(mixed $value, array $data): mixed
194    {
195        foreach ($this->preparers as $prepare) {
196            $value = $prepare($value, $data);
197        }
198
199        return $value;
200    }
201
202    /** @param array<string, mixed> $values */
203    public function applyFinalization(mixed $value, array $values): mixed
204    {
205        foreach ($this->finalizers as $finalize) {
206            $values[$this->field] = $value;
207            $value = $finalize($value, $values);
208        }
209
210        return $value;
211    }
212
213    private function matchesEmpty(Blank $empty, mixed $value): bool
214    {
215        return match ($empty) {
216            Blank::Missing => false,
217            Blank::Null => $value === null,
218            Blank::String => $value === '',
219            Blank::Whitespace => is_string($value) && trim($value) === '',
220            Blank::List => $value === [],
221        };
222    }
223
224    private function messageKey(string $key): string
225    {
226        if ($key === 'type') {
227            return 'type.' . $this->type();
228        }
229
230        if ($key === 'missing' || $key === 'null') {
231            return $key;
232        }
233
234        if (str_starts_with($key, 'type.') || str_starts_with($key, 'rule.')) {
235            return $key;
236        }
237
238        return 'rule.' . $key;
239    }
240}