Skip to content

Commit eb459ad

Browse files
committed
Update UndefOr to generate results with siblings
Since I updated the validation engine[1], it became possible to create results with siblings. This commit changes the "UndefOr", allowing it to create a result with a sibling when possible. That will improve the clarity of the error message. [1]: 238f2d5
1 parent d1e0c8b commit eb459ad

File tree

6 files changed

+131
-85
lines changed

6 files changed

+131
-85
lines changed

docs/rules/UndefOr.md

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
- `UndefOr(Validatable $rule)`
44

5-
Validates if the given input is undefined or not.
5+
Validates the input using a defined rule when the input is not `null` or an empty string (`''`).
66

7-
By _undefined_ we consider `null` or an empty string (`''`), which implies that the input is not set. This is particularly useful when validating form fields
7+
This rule can be particularly useful when validating form fields.
8+
9+
## Usage
810

911
```php
1012
v::undefOr(v::alpha())->isValid(''); // true
@@ -14,7 +16,7 @@ v::undefOr(v::alpha())->isValid('username'); // true
1416
v::undefOr(v::alpha())->isValid('has1number'); // false
1517
```
1618

17-
## Note
19+
## Prefix
1820

1921
For convenience, you can use the `undefOr` as a prefix to any rule:
2022

@@ -23,16 +25,38 @@ v::undefOrEmail()->isValid('not an email'); // false
2325
v::undefOrBetween(1, 3)->isValid(2); // true
2426
```
2527

28+
## Templates
29+
30+
| Id | Default | Inverted |
31+
|-----------------------------|----------------------|---------------------------|
32+
| `NullOr::TEMPLATE_STANDARD` | or must be undefined | and must not be undefined |
33+
34+
The templates from this rule serve as message suffixes:
35+
36+
```php
37+
v::undefOr(v::alpha())->assert('has1number');
38+
// "has1number" must contain only letters (a-z) or must be undefined
39+
40+
v::not(v::undefOr(v::alpha()))->assert("alpha");
41+
// "alpha" must not contain letters (a-z) and must not be undefined
42+
```
43+
44+
## Template placeholders
45+
46+
| Placeholder | Description |
47+
|-------------|------------------------------------------------------------------|
48+
| `name` | The validated input or the custom validator name (if specified). |
49+
2650
## Categorization
2751

2852
- Nesting
2953

3054
## Changelog
3155

32-
| Version | Description |
33-
|--------:|--------------------------------------|
34-
| 3.0.0 | Renamed from "Optional" to "UndefOr" |
35-
| 1.0.0 | Created |
56+
| Version | Description |
57+
|--------:|-----------------------|
58+
| 3.0.0 | Renamed to `UndefOr` |
59+
| 1.0.0 | Created as `Optional` |
3660

3761
***
3862
See also:

library/Rules/UndefOr.php

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

17+
use function array_map;
18+
1719
#[Template(
18-
'The value must be undefined',
19-
'The value must not be undefined',
20-
self::TEMPLATE_STANDARD,
21-
)]
22-
#[Template(
23-
'{{name}} must be undefined',
24-
'{{name}} must not be undefined',
25-
self::TEMPLATE_NAMED,
20+
'or must be undefined',
21+
'and must not be undefined',
2622
)]
2723
final class UndefOr extends Wrapper
2824
{
2925
use CanValidateUndefined;
3026

31-
public const TEMPLATE_NAMED = '__named__';
32-
3327
public function evaluate(mixed $input): Result
3428
{
29+
$result = $this->rule->evaluate($input);
3530
if (!$this->isUndefined($input)) {
36-
return $this->rule->evaluate($input)->withPrefixedId('undefOr');
31+
return $this->enrichResult($result);
3732
}
3833

39-
if ($this->getName()) {
40-
return Result::passed($input, $this, [], self::TEMPLATE_NAMED);
34+
if (!$result->isValid) {
35+
return $this->enrichResult($result->withInvertedValidation());
36+
}
37+
38+
return $this->enrichResult($result);
39+
}
40+
41+
private function enrichResult(Result $result): Result
42+
{
43+
if ($result->isSiblingCompatible()) {
44+
return $result
45+
->withPrefixedId('undefOr')
46+
->withNextSibling(new Result($result->isValid, $result->input, $this));
4147
}
4248

43-
return Result::passed($input, $this);
49+
return $result->withChildren(...array_map(fn(Result $child) => $this->enrichResult($child), $result->children));
4450
}
4551
}

tests/integration/rules/undefOr.phpt

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,63 +13,80 @@ run([
1313
'Inverted undefined, not name' => [v::not(v::undefOr(v::alpha()))->setName('Not'), null],
1414
'With template' => [v::undefOr(v::alpha()), 123, 'Underneath the undulating umbrella'],
1515
'With array template' => [v::undefOr(v::alpha()), 123, ['undefOrAlpha' => 'Undefined number of unique unicorns']],
16+
'Inverted undefined with template' => [
17+
v::not(v::undefOr(v::alpha())),
18+
'',
19+
['notUndefOrAlpha' => 'Should not be undefined or alpha'],
20+
],
21+
'Not a sibling compatible rule' => [
22+
v::undefOr(v::alpha()->stringType()),
23+
1234,
24+
],
25+
'Not a sibling compatible rule with templates' => [
26+
v::undefOr(v::alpha()->stringType()),
27+
1234,
28+
[
29+
'undefOrAlpha' => 'Should be nul or alpha',
30+
'undefOrStringType' => '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 undefined
39+
- 1234 must contain only letters (a-z) or must be undefined
2340
[
24-
'undefOrAlpha' => '1234 must contain only letters (a-z)',
41+
'undefOrAlpha' => '1234 must contain only letters (a-z) or must be undefined',
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 undefined
47+
- "alpha" must not contain letters (a-z) and must not be undefined
3148
[
32-
'notUndefOrAlpha' => '"alpha" must not contain letters (a-z)',
49+
'notUndefOrAlpha' => '"alpha" must not contain letters (a-z) and must not be undefined',
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 undefined
55+
- "alpha" must not contain letters (a-z) or must be undefined
3956
[
40-
'undefOrNotAlpha' => '"alpha" must not contain letters (a-z)',
57+
'undefOrNotAlpha' => '"alpha" must not contain letters (a-z) or must be undefined',
4158
]
4259

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

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

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

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

7592
With template
@@ -87,3 +104,35 @@ Undefined number of unique unicorns
87104
[
88105
'undefOrAlpha' => 'Undefined number of unique unicorns',
89106
]
107+
108+
Inverted undefined with template
109+
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
110+
Should not be undefined or alpha
111+
- Should not be undefined or alpha
112+
[
113+
'notUndefOrAlpha' => 'Should not be undefined or alpha',
114+
]
115+
116+
Not a sibling compatible rule
117+
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
118+
1234 must contain only letters (a-z) or must be undefined
119+
- All of the required rules must pass for 1234
120+
- 1234 must contain only letters (a-z) or must be undefined
121+
- 1234 must be of type string or must be undefined
122+
[
123+
'__root__' => 'All of the required rules must pass for 1234',
124+
'undefOrAlpha' => '1234 must contain only letters (a-z) or must be undefined',
125+
'undefOrStringType' => '1234 must be of type string or must be undefined',
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+
'undefOrAlpha' => 'Should be nul or alpha',
137+
'undefOrStringType' => 'Should be nul or string type',
138+
]

tests/integration/transformers/aliases.phpt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ run([
1010
--EXPECT--
1111
optional
1212
⎺⎺⎺⎺⎺⎺⎺⎺
13-
`[]` must be a scalar value
14-
- `[]` must be a scalar value
13+
`[]` must be a scalar value or must be undefined
14+
- `[]` must be a scalar value or must be undefined
1515
[
16-
'undefOrScalarVal' => '`[]` must be a scalar value',
16+
'undefOrScalarVal' => '`[]` must be a scalar value or must be undefined',
1717
]

tests/integration/transformers/prefix.phpt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ foo must be between 1 and 3
7373

7474
undefOr
7575
⎺⎺⎺⎺⎺⎺⎺
76-
"string" must be a URL
77-
- "string" must be a URL
76+
"string" must be a URL or must be undefined
77+
- "string" must be a URL or must be undefined
7878
[
79-
'undefOrUrl' => '"string" must be a URL',
79+
'undefOrUrl' => '"string" must be a URL or must be undefined',
8080
]

tests/unit/Rules/UndefOrTest.php

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
use PHPUnit\Framework\Attributes\CoversClass;
1313
use PHPUnit\Framework\Attributes\Group;
14-
use PHPUnit\Framework\Attributes\Test;
1514
use Respect\Validation\Test\Rules\Stub;
1615
use Respect\Validation\Test\RuleTestCase;
1716
use stdClass;
@@ -20,45 +19,13 @@
2019
#[CoversClass(UndefOr::class)]
2120
final class UndefOrTest extends RuleTestCase
2221
{
23-
#[Test]
24-
public function itShouldUseStandardTemplateWhenItHasNameWhenInputIsOptional(): void
25-
{
26-
$rule = new UndefOr(Stub::pass(1));
27-
28-
$result = $rule->evaluate('');
29-
30-
self::assertSame($rule, $result->rule);
31-
self::assertSame(UndefOr::TEMPLATE_STANDARD, $result->template);
32-
}
33-
34-
#[Test]
35-
public function itShouldUseNamedTemplateWhenItHasNameWhenInputIsOptional(): void
36-
{
37-
$rule = new UndefOr(Stub::pass(1));
38-
$rule->setName('foo');
39-
40-
$result = $rule->evaluate('');
41-
42-
self::assertSame($rule, $result->rule);
43-
self::assertSame(UndefOr::TEMPLATE_NAMED, $result->template);
44-
}
45-
46-
#[Test]
47-
public function itShouldUseWrappedRuleToEvaluateWhenNotUndef(): void
48-
{
49-
$input = new stdClass();
50-
51-
$wrapped = Stub::pass(2);
52-
$rule = new UndefOr($wrapped);
53-
54-
self::assertEquals($wrapped->evaluate($input)->withPrefixedId('undefOr'), $rule->evaluate($input));
55-
}
56-
5722
/** @return iterable<string, array{UndefOr, mixed}> */
5823
public static function providerForValidInput(): iterable
5924
{
60-
yield 'null' => [new UndefOr(Stub::daze()), null];
61-
yield 'empty string' => [new UndefOr(Stub::daze()), ''];
25+
yield 'null' => [new UndefOr(Stub::pass(1)), null];
26+
yield 'empty string' => [new UndefOr(Stub::pass(1)), ''];
27+
yield 'null with failing rule' => [new UndefOr(Stub::fail(1)), null];
28+
yield 'empty string with failing rule' => [new UndefOr(Stub::fail(1)), ''];
6229
yield 'not optional' => [new UndefOr(Stub::pass(1)), 42];
6330
}
6431

0 commit comments

Comments
 (0)