Skip to content

Commit

Permalink
Required rule: remove SkipOnEmptyInterface and add `emptyCallback…
Browse files Browse the repository at this point in the history
…` option (#342)

* `Required`: remove `SkipOnEmptyInterface` and add `emptyCallback` option

* Apply fixes from StyleCI

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

Co-authored-by: Alexander Makarov <sam@rmcreative.ru>

* Update README.md

Co-authored-by: Alexander Makarov <sam@rmcreative.ru>

* Update README.md

Co-authored-by: Alexander Makarov <sam@rmcreative.ru>

* Fix README

Co-authored-by: StyleCI Bot <bot@styleci.io>
Co-authored-by: Alexey Rogachev <arogachev90@gmail.com>
Co-authored-by: Alexander Makarov <sam@rmcreative.ru>
  • Loading branch information
4 people committed Oct 21, 2022
1 parent 44efcfc commit 9411f2a
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 44 deletions.
18 changes: 17 additions & 1 deletion README.md
Expand Up @@ -142,7 +142,10 @@ new Number(asInteger: true, max: 100, skipOnError: true)
#### Skipping empty values

By default, missing and empty values are validated (if the value is missing, it's considered `null`). That is
undesirable if you need to allow not specifying a field. To change this behavior, use `skipOnEmpty: true`:
undesirable if you need a field to be optional. To change this behavior, use `skipOnEmpty: true`.

Note that not every rule has this option, but only the ones that implement `Yiisoft\Validator\SkipOnEmptyInterface`. For
example, `Required` rule doesn't. For more details see "Requiring values" section.

```php
use Yiisoft\Validator\Rule\Number;
Expand Down Expand Up @@ -700,6 +703,19 @@ final class Post
}
```

### Requiring values

Use `Yiisoft\Validator\Rule\Required` rule to make sure a value is present. What values are considered empty can be
customized via `$emptyCallback` option. Normalization is not performed, so only a callable or special class is needed.
For more details see "Skipping empty values" section.

```php
use Yiisoft\Validator\Rule\Required;
use Yiisoft\Validator\SkipOnEmptyCallback\SkipOnNull;

$rules = [new Required(emptyCallback: new SkipOnNull())];
```

### Conditional validation

In some cases there is a need to apply rule conditionally. It could be performed by using `when()`:
Expand Down
32 changes: 23 additions & 9 deletions src/Rule/Required.php
Expand Up @@ -6,39 +6,46 @@

use Attribute;
use Closure;
use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait;
use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait;
use Yiisoft\Validator\Rule\Trait\WhenTrait;
use Yiisoft\Validator\SerializableRuleInterface;
use Yiisoft\Validator\SkipOnEmptyInterface;
use Yiisoft\Validator\SkipOnEmptyCallback\SkipOnEmpty;
use Yiisoft\Validator\SkipOnErrorInterface;
use Yiisoft\Validator\ValidationContext;
use Yiisoft\Validator\WhenInterface;

/**
* Validates that the specified value is neither null nor empty.
*
* @psalm-type EmptyCallback = callable(mixed,bool):bool
*/
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
final class Required implements SerializableRuleInterface, SkipOnErrorInterface, WhenInterface, SkipOnEmptyInterface
final class Required implements SerializableRuleInterface, SkipOnErrorInterface, WhenInterface
{
use SkipOnEmptyTrait;
use SkipOnErrorTrait;
use WhenTrait;

/**
* @var callable
* @psalm-var EmptyCallback
*/
private $emptyCallback;

public function __construct(
private string $message = 'Value cannot be blank.',
private string $notPassedMessage = 'Value not passed.',

/**
* @var bool|callable|null
* @var callable
* @psalm-var EmptyCallback
*/
private $skipOnEmpty = null,
?callable $emptyCallback = null,
private bool $skipOnError = false,
/**
* @var Closure(mixed, ValidationContext):bool|null
* @psalm-var Closure(mixed, ValidationContext):bool|null
*/
private ?Closure $when = null,
) {
$this->emptyCallback = $emptyCallback ?? new SkipOnEmpty();
}

public function getName(): string
Expand All @@ -56,12 +63,19 @@ public function getNotPassedMessage(): string
return $this->notPassedMessage;
}

/**
* @psalm-return EmptyCallback
*/
public function getEmptyCallback(): callable
{
return $this->emptyCallback;
}

public function getOptions(): array
{
return [
'message' => $this->message,
'notPassedMessage' => $this->notPassedMessage,
'skipOnEmpty' => $this->getSkipOnEmptyOption(),
'skipOnError' => $this->skipOnError,
];
}
Expand Down
3 changes: 1 addition & 2 deletions src/Rule/RequiredHandler.php
Expand Up @@ -7,7 +7,6 @@
use Yiisoft\Validator\Exception\UnexpectedRuleException;
use Yiisoft\Validator\Result;
use Yiisoft\Validator\RuleHandlerInterface;
use Yiisoft\Validator\SkipOnEmptyCallback\SkipOnEmpty;
use Yiisoft\Validator\ValidationContext;

use function is_string;
Expand All @@ -34,7 +33,7 @@ public function validate(mixed $value, object $rule, ValidationContext $context)
$value = trim($value);
}

if (!(new SkipOnEmpty())($value, $context->isAttributeMissing())) {
if (!$rule->getEmptyCallback()($value, $context->isAttributeMissing())) {
return $result;
}

Expand Down
12 changes: 6 additions & 6 deletions tests/DataSet/ObjectDataSetTest.php
Expand Up @@ -237,12 +237,12 @@ public function dataCollectRules(): array
],
[
new class () {
#[Required(skipOnEmpty: true)]
#[Required()]
private $property1;
},
[
'property1' => [
new Required(skipOnEmpty: true),
new Required(),
],
],
],
Expand All @@ -258,20 +258,20 @@ public function dataCollectRules(): array
],
[
new class () {
#[Required(skipOnEmpty: true)]
#[Required()]
#[HasLength(max: 255, skipOnEmpty: true)]
private $property1;
#[Required(skipOnEmpty: true)]
#[Required()]
#[HasLength(max: 255, skipOnEmpty: true)]
private $property2;
},
[
'property1' => [
new Required(skipOnEmpty: true),
new Required(),
new HasLength(max: 255, skipOnEmpty: true),
],
'property2' => [
new Required(skipOnEmpty: true),
new Required(),
new HasLength(max: 255, skipOnEmpty: true),
],
],
Expand Down
32 changes: 16 additions & 16 deletions tests/Php81/DataSet/ObjectDataSetTest.php
Expand Up @@ -57,12 +57,12 @@ public function dataProvider(): array
],
[
new class () {
#[Required(skipOnEmpty: true)]
#[Required()]
private $property1;
},
[
'property1' => [
new Required(skipOnEmpty: true),
new Required(),
],
],
],
Expand All @@ -78,28 +78,28 @@ public function dataProvider(): array
],
[
new class () {
#[Required(skipOnEmpty: true)]
#[Required()]
#[HasLength(max: 255, skipOnEmpty: true)]
private $property1;
#[Required(skipOnEmpty: true)]
#[Required()]
#[HasLength(max: 255, skipOnEmpty: true)]
private $property2;
},
[
'property1' => [
new Required(skipOnEmpty: true),
new Required(),
new HasLength(max: 255, skipOnEmpty: true),
],
'property2' => [
new Required(skipOnEmpty: true),
new Required(),
new HasLength(max: 255, skipOnEmpty: true),
],
],
],
[
new class () {
#[Each([
new Required(skipOnEmpty: true),
new Required(),
new HasLength(max: 255, skipOnEmpty: true),
])]
#[HasLength(max: 255, skipOnEmpty: true)]
Expand All @@ -108,7 +108,7 @@ public function dataProvider(): array
[
'property1' => [
new Each([
new Required(skipOnEmpty: true),
new Required(),
new HasLength(max: 255, skipOnEmpty: true),
]),
new HasLength(max: 255, skipOnEmpty: true),
Expand All @@ -118,11 +118,11 @@ public function dataProvider(): array
[
new class () {
#[Nested([
new Required(skipOnEmpty: true),
new Required(),
new HasLength(max: 255, skipOnEmpty: true),
])]
#[Each([
new Required(skipOnEmpty: true),
new Required(),
new HasLength(max: 255, skipOnEmpty: true),
])]
#[HasLength(max: 255, skipOnEmpty: true)]
Expand All @@ -131,11 +131,11 @@ public function dataProvider(): array
[
'property1' => [
new Nested([
new Required(skipOnEmpty: true),
new Required(),
new HasLength(max: 255, skipOnEmpty: true),
]),
new Each([
new Required(skipOnEmpty: true),
new Required(),
new HasLength(max: 255, skipOnEmpty: true),
]),
new HasLength(max: 255, skipOnEmpty: true),
Expand All @@ -158,11 +158,11 @@ public function dataProvider(): array
[
new class () {
#[Nested([
new Required(skipOnEmpty: true),
new Required(),
new HasLength(max: 255, skipOnEmpty: true),
])]
#[Nested([
new Required(skipOnEmpty: true),
new Required(),
new HasLength(max: 255, skipOnEmpty: true),
])]
#[HasLength(max: 255, skipOnEmpty: true)]
Expand All @@ -172,11 +172,11 @@ public function dataProvider(): array
[
'property1' => [
new Nested([
new Required(skipOnEmpty: true),
new Required(),
new HasLength(max: 255, skipOnEmpty: true),
]),
new Nested([
new Required(skipOnEmpty: true),
new Required(),
new HasLength(max: 255, skipOnEmpty: true),
]),
new HasLength(max: 255, skipOnEmpty: true),
Expand Down
47 changes: 42 additions & 5 deletions tests/Rule/RequiredTest.php
Expand Up @@ -4,8 +4,11 @@

namespace Yiisoft\Validator\Tests\Rule;

use Closure;
use Yiisoft\Validator\Rule\Required;
use Yiisoft\Validator\Rule\RequiredHandler;
use Yiisoft\Validator\SkipOnEmptyCallback\SkipOnEmpty;
use Yiisoft\Validator\SkipOnEmptyCallback\SkipOnNull;
use Yiisoft\Validator\Tests\Rule\Base\DifferentRuleInHandlerTestTrait;
use Yiisoft\Validator\Tests\Rule\Base\RuleTestCase;
use Yiisoft\Validator\Tests\Rule\Base\SerializableRuleTestTrait;
Expand All @@ -15,10 +18,36 @@ final class RequiredTest extends RuleTestCase
use DifferentRuleInHandlerTestTrait;
use SerializableRuleTestTrait;

public function testGetName(): void
public function testDefaultValues(): void
{
$rule = new Required();

$this->assertInstanceOf(SkipOnEmpty::class, $rule->getEmptyCallback());
$this->assertSame(RequiredHandler::class, $rule->getHandlerClassName());
$this->assertSame('Value cannot be blank.', $rule->getMessage());
$this->assertSame('required', $rule->getName());
$this->assertSame('Value not passed.', $rule->getNotPassedMessage());
$this->assertNull($rule->getWhen());
$this->assertFalse($rule->shouldSkipOnError());
}

public function dataGetEmptyCallback(): array
{
return [
'null' => [null, SkipOnEmpty::class],
'skip on null' => [new SkipOnNull(), SkipOnNull::class],
'closure' => [static fn () => false, Closure::class],
];
}

/**
* @dataProvider dataGetEmptyCallback
*/
public function testGetEmptyCallback(?callable $callback, string $expectedCallbackClassName): void
{
$rule = new Required(emptyCallback: $callback);

$this->assertInstanceOf($expectedCallbackClassName, $rule->getEmptyCallback());
}

public function dataOptions(): array
Expand All @@ -29,7 +58,6 @@ public function dataOptions(): array
[
'message' => 'Value cannot be blank.',
'notPassedMessage' => 'Value not passed.',
'skipOnEmpty' => false,
'skipOnError' => false,
],
],
Expand All @@ -41,16 +69,25 @@ public function dataValidationPassed(): array
return [
['not empty', [new Required()]],
[['with', 'elements'], [new Required()]],
'skip on null' => [
'',
[new Required(emptyCallback: new SkipOnNull())],
],
];
}

public function dataValidationFailed(): array
{
$message = 'Value cannot be blank.';
$singleMessageCannotBeBlank = ['' => ['Value cannot be blank.']];

return [
[null, [new Required()], ['' => [$message]]],
[[], [new Required()], ['' => [$message]]],
[null, [new Required()], $singleMessageCannotBeBlank],
[[], [new Required()], $singleMessageCannotBeBlank],
'custom empty callback' => [
'42',
[new Required(emptyCallback: static fn (mixed $value): bool => $value === '42')],
$singleMessageCannotBeBlank,
],
'custom error' => [null, [new Required(message: 'Custom error')], ['' => ['Custom error']]],
];
}
Expand Down

0 comments on commit 9411f2a

Please sign in to comment.