Skip to content

Commit d1e0c8b

Browse files
committed
Update NullOr to generate results with siblings
Since I updated the validation engine[1], it became possible to create results with siblings. This commit changes the "NullOr", allowing it to create a result with a sibling when possible. That will improve the clarity of the error message. I also updated the documentation, since it was still called "Nullable" [1]: 238f2d5
1 parent 061a3c9 commit d1e0c8b

File tree

10 files changed

+185
-105
lines changed

10 files changed

+185
-105
lines changed

docs/08-list-of-rules-by-category.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@
170170
- [Lazy](rules/Lazy.md)
171171
- [NoneOf](rules/NoneOf.md)
172172
- [Not](rules/Not.md)
173-
- [Nullable](rules/Nullable.md)
173+
- [NullOr](rules/NullOr.md)
174174
- [OneOf](rules/OneOf.md)
175175
- [Property](rules/Property.md)
176176
- [PropertyOptional](rules/PropertyOptional.md)
@@ -393,8 +393,8 @@
393393
- [NotEmoji](rules/NotEmoji.md)
394394
- [NotEmpty](rules/NotEmpty.md)
395395
- [NotUndef](rules/NotUndef.md)
396+
- [NullOr](rules/NullOr.md)
396397
- [NullType](rules/NullType.md)
397-
- [Nullable](rules/Nullable.md)
398398
- [Number](rules/Number.md)
399399
- [NumericVal](rules/NumericVal.md)
400400
- [ObjectType](rules/ObjectType.md)

docs/rules/NullOr.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# NullOr
2+
3+
- `NullOr(Validatable $rule)`
4+
5+
Validates the input using a defined rule when the input is not `null`.
6+
7+
## Usage
8+
9+
```php
10+
v::nullable(v::email())->isValid(null); // true
11+
v::nullable(v::email())->isValid('[email protected]'); // true
12+
v::nullable(v::email())->isValid('not an email'); // false
13+
```
14+
15+
## Prefix
16+
17+
For convenience, you can use `nullOr` as a prefix to any rule:
18+
19+
```php
20+
v::nullOrEmail()->isValid('not an email'); // false
21+
v::nullOrBetween(1, 3)->isValid(2); // true
22+
v::nullOrBetween(1, 3)->isValid(null); // true
23+
```
24+
25+
## Templates
26+
27+
| Id | Default | Inverted |
28+
|-----------------------------|-----------------|----------------------|
29+
| `NullOr::TEMPLATE_STANDARD` | or must be null | and must not be null |
30+
31+
The templates from this rule serve as message suffixes:
32+
33+
```php
34+
v::nullOr(v::alpha())->assert('has1number');
35+
// "has1number" must contain only letters (a-z) or must be null
36+
37+
v::not(v::nullOr(v::alpha()))->assert("alpha");
38+
// "alpha" must not contain letters (a-z) and must not be null
39+
```
40+
41+
## Template placeholders
42+
43+
| Placeholder | Description |
44+
|-------------|------------------------------------------------------------------|
45+
| `name` | The validated input or the custom validator name (if specified). |
46+
47+
## Categorization
48+
49+
- Nesting
50+
51+
## Changelog
52+
53+
| Version | Description |
54+
|--------:|-----------------------|
55+
| 3.0.0 | Renamed to `NullOr` |
56+
| 2.0.0 | Created as `Nullable` |
57+
58+
***
59+
See also:
60+
61+
- [NullType](NullType.md)
62+
- [UndefOr](UndefOr.md)

docs/rules/NullType.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ See also:
3131
- [NotBlank](NotBlank.md)
3232
- [NotEmpty](NotEmpty.md)
3333
- [NotUndef](NotUndef.md)
34-
- [Nullable](Nullable.md)
34+
- [NullOr](NullOr.md)
3535
- [Number](Number.md)
3636
- [ObjectType](ObjectType.md)
3737
- [ResourceType](ResourceType.md)

docs/rules/Nullable.md

Lines changed: 0 additions & 27 deletions
This file was deleted.

docs/rules/UndefOr.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,5 @@ See also:
4141
- [NotBlank](NotBlank.md)
4242
- [NotEmpty](NotEmpty.md)
4343
- [NotUndef](NotUndef.md)
44+
- [NullOr](NullOr.md)
4445
- [NullType](NullType.md)
45-
- [Nullable](Nullable.md)

library/Result.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@ public function withNextSibling(Result $nextSibling): self
118118
return $this->clone(nextSibling: $nextSibling);
119119
}
120120

121+
public function withInvertedValidation(): self
122+
{
123+
return $this->clone(
124+
isValid: !$this->isValid,
125+
nextSibling: $this->nextSibling?->withInvertedValidation(),
126+
children: array_map(static fn (Result $child) => $child->withInvertedValidation(), $this->children),
127+
);
128+
}
129+
121130
public function withInvertedMode(): self
122131
{
123132
return $this->clone(
@@ -153,6 +162,20 @@ public function isAlwaysVisible(): bool
153162
return count($childrenAlwaysVisible) !== 1;
154163
}
155164

165+
public function isSiblingCompatible(): bool
166+
{
167+
if ($this->children === [] && !$this->hasCustomTemplate()) {
168+
return true;
169+
}
170+
171+
$siblingCompatibleChildren = array_filter(
172+
$this->children,
173+
static fn (Result $child) => $child->isSiblingCompatible()
174+
);
175+
176+
return count($siblingCompatibleChildren) === 1;
177+
}
178+
156179
/**
157180
* @param array<Result>|null $children
158181
*/

library/Rules/NullOr.php

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,36 @@
1313
use Respect\Validation\Result;
1414
use Respect\Validation\Rules\Core\Wrapper;
1515

16+
use function array_map;
17+
1618
#[Template(
17-
'The value must be null',
18-
'The value must not be null',
19-
self::TEMPLATE_STANDARD,
20-
)]
21-
#[Template(
22-
'{{name}} must be null',
23-
'{{name}} must not be null',
24-
self::TEMPLATE_NAMED,
19+
'or must be null',
20+
'and must not be null',
2521
)]
2622
final class NullOr extends Wrapper
2723
{
28-
public const TEMPLATE_NAMED = '__named__';
29-
3024
public function evaluate(mixed $input): Result
3125
{
26+
$result = $this->rule->evaluate($input);
3227
if ($input !== null) {
33-
return $this->rule->evaluate($input)->withPrefixedId('nullOr');
28+
return $this->enrichResult($result);
3429
}
3530

36-
if ($this->getName()) {
37-
return Result::passed($input, $this, [], self::TEMPLATE_NAMED);
31+
if (!$result->isValid) {
32+
return $this->enrichResult($result->withInvertedValidation());
33+
}
34+
35+
return $this->enrichResult($result);
36+
}
37+
38+
private function enrichResult(Result $result): Result
39+
{
40+
if ($result->isSiblingCompatible()) {
41+
return $result
42+
->withPrefixedId('nullOr')
43+
->withNextSibling(new Result($result->isValid, $result->input, $this));
3844
}
3945

40-
return Result::passed($input, $this);
46+
return $result->withChildren(...array_map(fn(Result $child) => $this->enrichResult($child), $result->children));
4147
}
4248
}

tests/integration/rules/nullOr.phpt

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,63 +13,80 @@ run([
1313
'Inverted nullined, not name' => [v::not(v::nullOr(v::alpha()))->setName('Not'), null],
1414
'With template' => [v::nullOr(v::alpha()), 123, 'Nine nimble numismatists near Naples'],
1515
'With array template' => [v::nullOr(v::alpha()), 123, ['nullOrAlpha' => 'Next to nifty null notations']],
16+
'Inverted nullined with template' => [
17+
v::not(v::nullOr(v::alpha())),
18+
null,
19+
['notNullOrAlpha' => 'Next to nifty null notations'],
20+
],
21+
'Not a sibling compatible rule' => [
22+
v::nullOr(v::alpha()->stringType()),
23+
1234,
24+
],
25+
'Not a sibling compatible rule with templates' => [
26+
v::nullOr(v::alpha()->stringType()),
27+
1234,
28+
[
29+
'nullOrAlpha' => 'Should be nul or alpha',
30+
'nullOrStringType' => 'Should be nul or string type',
31+
],
32+
],
1633
]);
1734
?>
1835
--EXPECT--
1936
Default
2037
⎺⎺⎺⎺⎺⎺⎺
21-
1234 must contain only letters (a-z)
22-
- 1234 must contain only letters (a-z)
38+
1234 must contain only letters (a-z) or must be null
39+
- 1234 must contain only letters (a-z) or must be null
2340
[
24-
'nullOrAlpha' => '1234 must contain only letters (a-z)',
41+
'nullOrAlpha' => '1234 must contain only letters (a-z) or must be null',
2542
]
2643

2744
Inverted wrapper
2845
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
29-
"alpha" must not contain letters (a-z)
30-
- "alpha" must not contain letters (a-z)
46+
"alpha" must not contain letters (a-z) and must not be null
47+
- "alpha" must not contain letters (a-z) and must not be null
3148
[
32-
'notNullOrAlpha' => '"alpha" must not contain letters (a-z)',
49+
'notNullOrAlpha' => '"alpha" must not contain letters (a-z) and must not be null',
3350
]
3451

3552
Inverted wrapped
3653
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
37-
"alpha" must not contain letters (a-z)
38-
- "alpha" must not contain letters (a-z)
54+
"alpha" must not contain letters (a-z) or must be null
55+
- "alpha" must not contain letters (a-z) or must be null
3956
[
40-
'nullOrNotAlpha' => '"alpha" must not contain letters (a-z)',
57+
'nullOrNotAlpha' => '"alpha" must not contain letters (a-z) or must be null',
4158
]
4259

4360
Inverted nullined
4461
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
45-
The value must not be null
46-
- The value must not be null
62+
`null` must not contain letters (a-z) and must not be null
63+
- `null` must not contain letters (a-z) and must not be null
4764
[
48-
'notNullOr' => 'The value must not be null',
65+
'notNullOrAlpha' => '`null` must not contain letters (a-z) and must not be null',
4966
]
5067

5168
Inverted nullined, wrapped name
5269
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
53-
Wrapped must not be null
54-
- Wrapped must not be null
70+
Wrapped must not contain letters (a-z) and must not be null
71+
- Wrapped must not contain letters (a-z) and must not be null
5572
[
56-
'notNullOr' => 'Wrapped must not be null',
73+
'notNullOrAlpha' => 'Wrapped must not contain letters (a-z) and must not be null',
5774
]
5875

5976
Inverted nullined, wrapper name
6077
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
61-
Wrapper must not be null
62-
- Wrapper must not be null
78+
Wrapper must not contain letters (a-z) and must not be null
79+
- Wrapper must not contain letters (a-z) and must not be null
6380
[
64-
'notNullOr' => 'Wrapper must not be null',
81+
'notNullOrAlpha' => 'Wrapper must not contain letters (a-z) and must not be null',
6582
]
6683

6784
Inverted nullined, not name
6885
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
69-
Not must not be null
70-
- Not must not be null
86+
Not must not contain letters (a-z) and must not be null
87+
- Not must not contain letters (a-z) and must not be null
7188
[
72-
'notNullOr' => 'Not must not be null',
89+
'notNullOrAlpha' => 'Not must not contain letters (a-z) and must not be null',
7390
]
7491

7592
With template
@@ -87,3 +104,35 @@ Next to nifty null notations
87104
[
88105
'nullOrAlpha' => 'Next to nifty null notations',
89106
]
107+
108+
Inverted nullined with template
109+
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
110+
Next to nifty null notations
111+
- Next to nifty null notations
112+
[
113+
'notNullOrAlpha' => 'Next to nifty null notations',
114+
]
115+
116+
Not a sibling compatible rule
117+
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
118+
1234 must contain only letters (a-z) or must be null
119+
- All of the required rules must pass for 1234
120+
- 1234 must contain only letters (a-z) or must be null
121+
- 1234 must be of type string or must be null
122+
[
123+
'__root__' => 'All of the required rules must pass for 1234',
124+
'nullOrAlpha' => '1234 must contain only letters (a-z) or must be null',
125+
'nullOrStringType' => '1234 must be of type string or must be null',
126+
]
127+
128+
Not a sibling compatible rule with templates
129+
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
130+
Should be nul or alpha
131+
- All of the required rules must pass for 1234
132+
- Should be nul or alpha
133+
- Should be nul or string type
134+
[
135+
'__root__' => 'All of the required rules must pass for 1234',
136+
'nullOrAlpha' => 'Should be nul or alpha',
137+
'nullOrStringType' => 'Should be nul or string type',
138+
]

tests/integration/transformers/prefix.phpt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ not
5757

5858
nullOr
5959
⎺⎺⎺⎺⎺⎺
60-
"string" must be of type boolean
61-
- "string" must be of type boolean
60+
"string" must be of type boolean or must be null
61+
- "string" must be of type boolean or must be null
6262
[
63-
'nullOrBoolType' => '"string" must be of type boolean',
63+
'nullOrBoolType' => '"string" must be of type boolean or must be null',
6464
]
6565

6666
property

0 commit comments

Comments
 (0)