-
-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
361 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Yiisoft\Validator\Rule; | ||
|
||
use Attribute; | ||
use Closure; | ||
use JetBrains\PhpStorm\ArrayShape; | ||
use Yiisoft\Validator\AfterInitAttributeEventInterface; | ||
use Yiisoft\Validator\Helper\RulesNormalizer; | ||
use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait; | ||
use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait; | ||
use Yiisoft\Validator\Rule\Trait\WhenTrait; | ||
use Yiisoft\Validator\Helper\RulesDumper; | ||
use Yiisoft\Validator\RuleWithOptionsInterface; | ||
use Yiisoft\Validator\SkipOnEmptyInterface; | ||
use Yiisoft\Validator\SkipOnErrorInterface; | ||
use Yiisoft\Validator\ValidatorInterface; | ||
use Yiisoft\Validator\WhenInterface; | ||
|
||
/** | ||
* Applies to a set of rules, runs validation for each one of them in the order they are defined and stops at the rule | ||
* where validation passed. The opposite to {@see StopOnError} | ||
* | ||
* An example of usage: | ||
* | ||
* ```php | ||
* $rule = new Any([ | ||
* new IntegerType(), // Let's say the validation passed here. | ||
* new FloatType(), // Then this rule will be skipped. | ||
* ]); | ||
* ``` | ||
* | ||
* When using with other rules, conditional validation options, such as {@see Any::$skipOnError} will be applied | ||
* to the whole group of {@see Any::$rules}. | ||
* | ||
* @see StopOnErrorHandler Corresponding handler performing the actual validation. | ||
* | ||
* @psalm-import-type SkipOnEmptyValue from SkipOnEmptyInterface | ||
* @psalm-import-type WhenType from WhenInterface | ||
* @psalm-import-type NormalizedRulesList from RulesNormalizer | ||
* @psalm-import-type RawRulesList from ValidatorInterface | ||
*/ | ||
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] | ||
final class Any implements | ||
RuleWithOptionsInterface, | ||
SkipOnEmptyInterface, | ||
SkipOnErrorInterface, | ||
WhenInterface, | ||
AfterInitAttributeEventInterface | ||
{ | ||
use SkipOnEmptyTrait; | ||
use SkipOnErrorTrait; | ||
use WhenTrait; | ||
|
||
/** | ||
* @var iterable A set of normalized rules that needs to be run. | ||
* | ||
* @psalm-var NormalizedRulesList | ||
*/ | ||
private iterable $rules = []; | ||
|
||
/** | ||
* @param string $message Error message used when validation fails because none of the inner {@see $rules} has | ||
* passed the validation. | ||
* | ||
* @param iterable $rules A set of rules for running the validation. They will be normalized during initialization | ||
* using {@see RulesNormalizer}. | ||
* | ||
* @psalm-param RawRulesList $rules | ||
* | ||
* @param bool|callable|null $skipOnEmpty Whether to skip this `Any` rule with all defined {@see $rules} if the | ||
* validated value is empty / not passed. See {@see SkipOnEmptyInterface}. | ||
* @param bool $skipOnError Whether to skip this `Any` rule with all defined {@see $rules} if any of the previous | ||
* rules gave an error. See {@see SkipOnErrorInterface}. | ||
* @param Closure|null $when A callable to define a condition for applying this `Any` rule with all defined | ||
* {@see $rules}. See {@see WhenInterface}. | ||
* | ||
* @psalm-param SkipOnEmptyValue $skipOnEmpty | ||
* @psalm-param WhenType $when | ||
*/ | ||
public function __construct( | ||
iterable $rules, | ||
private string $message = 'At least one of the inner rules must pass the validation.', | ||
private mixed $skipOnEmpty = null, | ||
private bool $skipOnError = false, | ||
private Closure|null $when = null, | ||
) { | ||
$this->rules = RulesNormalizer::normalizeList($rules); | ||
} | ||
|
||
public function getName(): string | ||
{ | ||
return 'any'; | ||
} | ||
|
||
/** | ||
* Gets a set of rules for running the validation. | ||
* | ||
* @return iterable A set of rules. | ||
* | ||
* @psalm-return NormalizedRulesList | ||
*/ | ||
public function getRules(): iterable | ||
{ | ||
return $this->rules; | ||
} | ||
|
||
/** | ||
* Gets error message used when validation fails because none of the inner {@see $rules} has passed the validation. | ||
* | ||
* @return string Error message / template. | ||
* | ||
* @see $message | ||
*/ | ||
public function getMessage(): string | ||
{ | ||
return $this->message; | ||
} | ||
|
||
#[ArrayShape([ | ||
'message' => 'array', | ||
'skipOnEmpty' => 'bool', | ||
'skipOnError' => 'bool', | ||
'rules' => 'array|null', | ||
])] | ||
public function getOptions(): array | ||
{ | ||
return [ | ||
'message' => [ | ||
'template' => $this->message, | ||
'parameters' => [], | ||
], | ||
'skipOnEmpty' => $this->getSkipOnEmptyOption(), | ||
'skipOnError' => $this->skipOnError, | ||
'rules' => $this->dumpRulesAsArray(), | ||
]; | ||
} | ||
|
||
public function getHandler(): string | ||
{ | ||
return AnyHandler::class; | ||
} | ||
|
||
public function afterInitAttribute(object $object): void | ||
{ | ||
foreach ($this->rules as $rule) { | ||
if ($rule instanceof AfterInitAttributeEventInterface) { | ||
$rule->afterInitAttribute($object); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Dumps defined {@see $rules} to array. | ||
* | ||
* @return array The array of rules with their options. | ||
*/ | ||
private function dumpRulesAsArray(): array | ||
{ | ||
return RulesDumper::asArray($this->getRules()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Yiisoft\Validator\Rule; | ||
|
||
use Yiisoft\Validator\Exception\UnexpectedRuleException; | ||
use Yiisoft\Validator\Result; | ||
use Yiisoft\Validator\RuleHandlerInterface; | ||
use Yiisoft\Validator\ValidationContext; | ||
|
||
/** | ||
* A handler for {@see Any} rule. Validates a set of rules consecutively and stops at the rule where validation | ||
* has passed. | ||
*/ | ||
final class AnyHandler implements RuleHandlerInterface | ||
{ | ||
public function validate(mixed $value, object $rule, ValidationContext $context): Result | ||
{ | ||
if (!$rule instanceof Any) { | ||
throw new UnexpectedRuleException(Any::class, $rule); | ||
} | ||
|
||
foreach ($rule->getRules() as $relatedRule) { | ||
$result = $context->validate($value, $relatedRule); | ||
if ($result->isValid()) { | ||
return $result; | ||
} | ||
} | ||
|
||
return (new Result())->addError($rule->getMessage(), []); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Yiisoft\Validator\Tests\Rule; | ||
|
||
use Yiisoft\Validator\Rule\Any; | ||
use Yiisoft\Validator\Rule\AnyHandler; | ||
use Yiisoft\Validator\Rule\Type\FloatType; | ||
use Yiisoft\Validator\Rule\Type\IntegerType; | ||
use Yiisoft\Validator\Tests\Rule\Base\DifferentRuleInHandlerTestTrait; | ||
use Yiisoft\Validator\Tests\Rule\Base\RuleTestCase; | ||
use Yiisoft\Validator\Tests\Rule\Base\RuleWithOptionsTestTrait; | ||
use Yiisoft\Validator\Tests\Rule\Base\SkipOnErrorTestTrait; | ||
use Yiisoft\Validator\Tests\Rule\Base\WhenTestTrait; | ||
|
||
final class AnyTest extends RuleTestCase | ||
{ | ||
use DifferentRuleInHandlerTestTrait; | ||
use RuleWithOptionsTestTrait; | ||
use SkipOnErrorTestTrait; | ||
use WhenTestTrait; | ||
|
||
public function testGetName(): void | ||
{ | ||
$rule = new Any([new IntegerType(), new FloatType()]); | ||
$this->assertSame('any', $rule->getName()); | ||
} | ||
|
||
public function dataOptions(): array | ||
{ | ||
return [ | ||
'default' => [ | ||
new Any([new IntegerType()]), | ||
[ | ||
'message' => [ | ||
'template' => 'At least one of the inner rules must pass the validation.', | ||
'parameters' => [], | ||
], | ||
'skipOnEmpty' => false, | ||
'skipOnError' => false, | ||
'rules' => [ | ||
[ | ||
'integerType', | ||
'message' => [ | ||
'template' => 'Value must be an integer.', | ||
'parameters' => [], | ||
], | ||
'skipOnEmpty' => false, | ||
'skipOnError' => false, | ||
], | ||
], | ||
], | ||
], | ||
'custom' => [ | ||
new Any( | ||
[new IntegerType(), new FloatType()], | ||
message: 'Custom message.', | ||
skipOnEmpty: true, | ||
skipOnError: true, | ||
), | ||
[ | ||
'message' => [ | ||
'template' => 'Custom message.', | ||
'parameters' => [], | ||
], | ||
'skipOnEmpty' => true, | ||
'skipOnError' => true, | ||
'rules' => [ | ||
[ | ||
'integerType', | ||
'message' => [ | ||
'template' => 'Value must be an integer.', | ||
'parameters' => [], | ||
], | ||
'skipOnEmpty' => false, | ||
'skipOnError' => false, | ||
], | ||
[ | ||
'floatType', | ||
'message' => [ | ||
'template' => 'Value must be a float.', | ||
'parameters' => [], | ||
], | ||
'skipOnEmpty' => false, | ||
'skipOnError' => false, | ||
], | ||
], | ||
], | ||
], | ||
]; | ||
} | ||
|
||
public function dataValidationPassed(): array | ||
{ | ||
return [ | ||
'right away' => [1, new Any([new IntegerType(), new FloatType()])], | ||
'later' => [1.5, new Any([new IntegerType(), new FloatType()])], | ||
]; | ||
} | ||
|
||
public function dataValidationFailed(): array | ||
{ | ||
$message = 'At least one of the inner rules must pass the validation.'; | ||
|
||
return [ | ||
'none' => ['1', new Any([new IntegerType(), new FloatType()]), ['' => [$message]]], | ||
]; | ||
} | ||
|
||
public function testSkipOnError(): void | ||
{ | ||
$this->testSkipOnErrorInternal( | ||
new Any([new IntegerType(), new FloatType()]), | ||
new Any([new IntegerType(), new FloatType()], skipOnError: true), | ||
); | ||
} | ||
|
||
public function testWhen(): void | ||
{ | ||
$when = static fn (mixed $value): bool => $value !== null; | ||
$this->testWhenInternal( | ||
new Any([new IntegerType(), new FloatType()]), | ||
new Any([new IntegerType(), new FloatType()], when: $when), | ||
); | ||
} | ||
|
||
protected function getDifferentRuleInHandlerItems(): array | ||
{ | ||
return [Any::class, AnyHandler::class]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.