Skip to content

Commit fdabae5

Browse files
authored
Fix #132: Adjust skip on empty (#288)
1 parent cd2db12 commit fdabae5

38 files changed

Lines changed: 832 additions & 36 deletions

README.md

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ if ($result->isValid() === false) {
131131
#### Skipping validation on error
132132

133133
By default, if an error occurred during validation of an attribute, further rules for this attribute are processed.
134-
To change this behavior use `skipOnError: true` when configuring rules:
134+
To change this behavior, use `skipOnError: true` when configuring rules:
135135

136136
```php
137137
use Yiisoft\Validator\Rule\Number;
@@ -142,12 +142,80 @@ new Number(asInteger: true, max: 100, skipOnError: true)
142142
#### Skipping empty values
143143

144144
By default, empty values are validated. That is undesirable if you need to allow not specifying a field.
145-
To change this behavior use `skipOnEmpty: true`:
145+
To change this behavior, use `skipOnEmpty: true`:
146146

147147
```php
148148
use Yiisoft\Validator\Rule\Number;
149149

150-
new Number(asInteger: true, max: 100, skipOnEmpty: true)
150+
new Number(asInteger: true, max: 100, skipOnEmpty: true);
151+
```
152+
153+
What exactly to consider to be empty is vague and can vary depending on a scope of usage.
154+
155+
`skipOnEmpty` is more like a shortcut, `skipOnEmptyCallback` argument checks if a given value is empty:
156+
157+
- If `skipOnEmpty` is `false`, `Yiisoft\Validator\SkipOnEmptyCallback\SkipNone` is used automatically for
158+
`skipOnEmptyCallback` - every value is considered non-empty and validated without skipping (default).
159+
- If `skipOnEmpty` is `true`, `Yiisoft\Validator\SkipOnEmptyCallback\SkipOnEmpty` is used automatically for
160+
`skipOnEmptyCallback` - only non-empty values (not `null`, `[]`, or `''`) are validated.
161+
- If `skipOnEmptyCallback` is set, it takes precedence to determine emptiness.
162+
163+
Using first option is usually good for HTML forms. The second one is more suitable for APIs.
164+
165+
The empty values can be also limited to `null` only:
166+
167+
```php
168+
use Yiisoft\Validator\Rule\Number;
169+
use Yiisoft\Validator\SkipOnEmptyCallback\SkipOnNull;
170+
171+
new Number(asInteger: true, max: 100, skipOnEmptyCallback: new SkipOnNull());
172+
```
173+
174+
Note that in this case `skipOnEmpty` will be automatically set to `true`, so there is no need to do it manually.
175+
176+
For even more customization you can use your own class implementing `__invoke()` magic method:
177+
178+
```php
179+
use Yiisoft\Validator\Rule\Number;
180+
181+
final class SkipOnZero
182+
{
183+
public function __invoke($value): bool
184+
{
185+
return $value === 0;
186+
}
187+
}
188+
189+
new Number(asInteger: true, max: 100, skipOnEmptyCallback: new SkipOnZero());
190+
```
191+
192+
or just a callable:
193+
194+
```php
195+
use Yiisoft\Validator\Rule\Number;
196+
197+
new Number(
198+
asInteger: true,
199+
max: 100,
200+
skipOnEmptyCallback: static function (mixed $value): bool {
201+
return $value === 0;
202+
}
203+
);
204+
```
205+
206+
For multiple rules this can be also set more conveniently at validator level:
207+
208+
```php
209+
use Yiisoft\Validator\SimpleRuleHandlerContainer;
210+
use Yiisoft\Validator\Validator;
211+
212+
$validator = new Validator(new SimpleRuleHandlerContainer(), skipOnEmpty: true);
213+
$validator = new Validator(
214+
new SimpleRuleHandlerContainer(),
215+
skipOnEmptyCallback: static function (mixed $value): bool {
216+
return $value === 0;
217+
}
218+
);
151219
```
152220

153221
#### Nested and related data

src/Attribute/Embedded.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ final class Embedded extends Composite
2424
public function __construct(
2525
private string $referenceClassName,
2626
bool $skipOnEmpty = false,
27+
callable $skipOnEmptyCallback = null,
2728
bool $skipOnError = false,
2829
?Closure $when = null,
2930
) {
30-
parent::__construct([], $skipOnEmpty, $skipOnError, $when);
31+
parent::__construct([], $skipOnEmpty, $skipOnEmptyCallback, $skipOnError, $when);
3132
}
3233

3334
public function getRules(): array

src/BeforeValidationInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212
interface BeforeValidationInterface
1313
{
14-
public function shouldSkipOnEmpty(): bool;
14+
public function shouldSkipOnEmpty(mixed $validatedValue): bool;
1515

1616
public function shouldSkipOnError(): bool;
1717

src/Rule/AtLeast.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66

77
use Attribute;
88
use Closure;
9-
use Yiisoft\Validator\SerializableRuleInterface;
109
use Yiisoft\Validator\BeforeValidationInterface;
1110
use Yiisoft\Validator\Rule\Trait\BeforeValidationTrait;
1211
use Yiisoft\Validator\Rule\Trait\RuleNameTrait;
12+
use Yiisoft\Validator\SerializableRuleInterface;
1313
use Yiisoft\Validator\ValidationContext;
1414

1515
/**
@@ -36,12 +36,17 @@ public function __construct(
3636
*/
3737
private string $message = 'The model is not valid. Must have at least "{min}" filled attributes.',
3838
private bool $skipOnEmpty = false,
39+
/**
40+
* @var callable
41+
*/
42+
private $skipOnEmptyCallback = null,
3943
private bool $skipOnError = false,
4044
/**
4145
* @var Closure(mixed, ValidationContext):bool|null
4246
*/
43-
private ?Closure $when = null,
47+
private ?Closure $when = null
4448
) {
49+
$this->initSkipOnEmptyProperties($skipOnEmpty, $skipOnEmptyCallback);
4550
}
4651

4752
/**

src/Rule/Boolean.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66

77
use Attribute;
88
use Closure;
9-
use Yiisoft\Validator\SerializableRuleInterface;
109
use Yiisoft\Validator\BeforeValidationInterface;
1110
use Yiisoft\Validator\Rule\Trait\BeforeValidationTrait;
1211
use Yiisoft\Validator\Rule\Trait\RuleNameTrait;
12+
use Yiisoft\Validator\SerializableRuleInterface;
1313
use Yiisoft\Validator\ValidationContext;
1414

1515
/**
@@ -38,12 +38,17 @@ public function __construct(
3838
private bool $strict = false,
3939
private string $message = 'The value must be either "{true}" or "{false}".',
4040
private bool $skipOnEmpty = false,
41+
/**
42+
* @var callable
43+
*/
44+
private $skipOnEmptyCallback = null,
4145
private bool $skipOnError = false,
4246
/**
4347
* @var Closure(mixed, ValidationContext):bool|null
4448
*/
4549
private ?Closure $when = null,
4650
) {
51+
$this->initSkipOnEmptyProperties($skipOnEmpty, $skipOnEmptyCallback);
4752
}
4853

4954
/**

src/Rule/Callback.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
namespace Yiisoft\Validator\Rule;
66

77
use Closure;
8-
use Yiisoft\Validator\SerializableRuleInterface;
98
use Yiisoft\Validator\BeforeValidationInterface;
109
use Yiisoft\Validator\Rule\Trait\BeforeValidationTrait;
1110
use Yiisoft\Validator\Rule\Trait\RuleNameTrait;
11+
use Yiisoft\Validator\SerializableRuleInterface;
1212
use Yiisoft\Validator\ValidationContext;
1313

1414
final class Callback implements SerializableRuleInterface, BeforeValidationInterface
@@ -22,12 +22,17 @@ public function __construct(
2222
*/
2323
private $callback,
2424
private bool $skipOnEmpty = false,
25+
/**
26+
* @var callable
27+
*/
28+
private $skipOnEmptyCallback = null,
2529
private bool $skipOnError = false,
2630
/**
2731
* @var Closure(mixed, ValidationContext):bool|null
2832
*/
2933
private ?Closure $when = null,
3034
) {
35+
$this->initSkipOnEmptyProperties($skipOnEmpty, $skipOnEmptyCallback);
3136
}
3237

3338
/**

src/Rule/Compare.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
use InvalidArgumentException;
99
use RuntimeException;
1010
use Yiisoft\Validator\BeforeValidationInterface;
11-
use Yiisoft\Validator\SerializableRuleInterface;
1211
use Yiisoft\Validator\Rule\Trait\BeforeValidationTrait;
1312
use Yiisoft\Validator\Rule\Trait\RuleNameTrait;
13+
use Yiisoft\Validator\SerializableRuleInterface;
1414
use Yiisoft\Validator\ValidationContext;
1515

1616
abstract class Compare implements SerializableRuleInterface, BeforeValidationInterface
@@ -82,12 +82,18 @@ public function __construct(
8282
*/
8383
private string $operator = '==',
8484
private bool $skipOnEmpty = false,
85+
/**
86+
* @var callable
87+
*/
88+
private $skipOnEmptyCallback = null,
8589
private bool $skipOnError = false,
8690
/**
8791
* @var Closure(mixed, ValidationContext):bool|null
8892
*/
8993
private ?Closure $when = null,
9094
) {
95+
$this->initSkipOnEmptyProperties($skipOnEmpty, $skipOnEmptyCallback);
96+
9197
if (!isset($this->validOperators[$operator])) {
9298
throw new InvalidArgumentException("Operator \"$operator\" is not supported.");
9399
}

src/Rule/Composite.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
use Closure;
99
use JetBrains\PhpStorm\ArrayShape;
1010
use Yiisoft\Validator\BeforeValidationInterface;
11-
use Yiisoft\Validator\SerializableRuleInterface;
1211
use Yiisoft\Validator\Rule\Trait\BeforeValidationTrait;
1312
use Yiisoft\Validator\Rule\Trait\RuleNameTrait;
1413
use Yiisoft\Validator\RuleInterface;
14+
use Yiisoft\Validator\SerializableRuleInterface;
1515
use Yiisoft\Validator\ValidationContext;
1616

1717
/**
@@ -29,12 +29,17 @@ public function __construct(
2929
*/
3030
private iterable $rules = [],
3131
private bool $skipOnEmpty = false,
32+
/**
33+
* @var callable
34+
*/
35+
private $skipOnEmptyCallback = null,
3236
private bool $skipOnError = false,
3337
/**
3438
* @var Closure(mixed, ValidationContext):bool|null
3539
*/
3640
private ?Closure $when = null,
3741
) {
42+
$this->initSkipOnEmptyProperties($skipOnEmpty, $skipOnEmptyCallback);
3843
}
3944

4045
#[ArrayShape([

src/Rule/Count.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
use Attribute;
88
use Closure;
99
use Countable;
10-
use Yiisoft\Validator\Rule\Trait\LimitTrait;
11-
use Yiisoft\Validator\SerializableRuleInterface;
1210
use Yiisoft\Validator\BeforeValidationInterface;
1311
use Yiisoft\Validator\Rule\Trait\BeforeValidationTrait;
12+
use Yiisoft\Validator\Rule\Trait\LimitTrait;
1413
use Yiisoft\Validator\Rule\Trait\RuleNameTrait;
14+
use Yiisoft\Validator\SerializableRuleInterface;
1515
use Yiisoft\Validator\ValidationContext;
1616

1717
/**
@@ -68,12 +68,17 @@ public function __construct(
6868
string $notExactlyMessage = 'This value must contain exactly {exactly, number} {exactly, plural, one{item} ' .
6969
'other{items}}.',
7070
private bool $skipOnEmpty = false,
71+
/**
72+
* @var callable
73+
*/
74+
private $skipOnEmptyCallback = null,
7175
private bool $skipOnError = false,
7276
/**
7377
* @var Closure(mixed, ValidationContext):bool|null
7478
*/
7579
private ?Closure $when = null,
7680
) {
81+
$this->initSkipOnEmptyProperties($skipOnEmpty, $skipOnEmptyCallback);
7782
$this->initLimitProperties(
7883
$min,
7984
$max,

src/Rule/Each.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
use Attribute;
88
use Closure;
99
use JetBrains\PhpStorm\ArrayShape;
10-
use Yiisoft\Validator\SerializableRuleInterface;
1110
use Yiisoft\Validator\BeforeValidationInterface;
1211
use Yiisoft\Validator\Rule\Trait\BeforeValidationTrait;
1312
use Yiisoft\Validator\Rule\Trait\RuleNameTrait;
1413
use Yiisoft\Validator\RuleInterface;
14+
use Yiisoft\Validator\SerializableRuleInterface;
1515
use Yiisoft\Validator\ValidationContext;
1616

1717
/**
@@ -31,12 +31,17 @@ public function __construct(
3131
private string $incorrectInputMessage = 'Value must be array or iterable.',
3232
private string $message = '{error} {value} given.',
3333
private bool $skipOnEmpty = false,
34+
/**
35+
* @var callable
36+
*/
37+
private $skipOnEmptyCallback = null,
3438
private bool $skipOnError = false,
3539
/**
3640
* @var Closure(mixed, ValidationContext):bool|null
3741
*/
3842
private ?Closure $when = null,
3943
) {
44+
$this->initSkipOnEmptyProperties($skipOnEmpty, $skipOnEmptyCallback);
4045
}
4146

4247
/**

0 commit comments

Comments
 (0)