Skip to content

Commit

Permalink
Fix #186: Add options' progagation for Nested rule (#317)
Browse files Browse the repository at this point in the history
  • Loading branch information
arogachev committed Sep 21, 2022
1 parent f0b6560 commit d1524d1
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 21 deletions.
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -209,9 +209,9 @@ For multiple rules this can be also set more conveniently at validator level:
use Yiisoft\Validator\SimpleRuleHandlerContainer;
use Yiisoft\Validator\Validator;

$validator = new Validator(new SimpleRuleHandlerContainer(), skipOnEmpty: true);
$validator = new Validator(new SimpleRuleHandlerContainer($translator), skipOnEmpty: true);
$validator = new Validator(
new SimpleRuleHandlerContainer(),
new SimpleRuleHandlerContainer($translator),
skipOnEmptyCallback: static function (mixed $value): bool {
return $value === 0;
}
Expand Down
11 changes: 8 additions & 3 deletions src/BeforeValidationInterface.php
Expand Up @@ -7,16 +7,21 @@
use Closure;

/**
* `BeforeValidationInterface` is the interface implemented by rules that need to execute checks before the validation.
* `BeforeValidationInterface` is an interface implemented by rules that need to execute checks before the validation.
*/
interface BeforeValidationInterface
{
public function skipOnError(bool $value): static;

public function shouldSkipOnError(): bool;

/**
* @psalm-param Closure(mixed, ValidationContext):bool|null $value
*/
public function when(?Closure $value): static;

/**
* @psalm-return Closure(mixed, ValidationContext):bool|null
*
* @return Closure|null
*/
public function getWhen(): ?Closure;
}
14 changes: 14 additions & 0 deletions src/PropagateOptionsInterface.php
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Validator;

/**
* An interface implemented by rules that can propagate their common options (such as `skipOnEmpty`, `skipOnError`,
* `when`) to child rules as an alternative way of specifying them explicitly in every child rule.
*/
interface PropagateOptionsInterface
{
public function propagateOptions(): void;
}
29 changes: 26 additions & 3 deletions src/Rule/Each.php
Expand Up @@ -8,6 +8,7 @@
use Closure;
use JetBrains\PhpStorm\ArrayShape;
use Yiisoft\Validator\BeforeValidationInterface;
use Yiisoft\Validator\PropagateOptionsInterface;
use Yiisoft\Validator\Rule\Trait\BeforeValidationTrait;
use Yiisoft\Validator\Rule\Trait\RuleNameTrait;
use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait;
Expand All @@ -20,15 +21,19 @@
* Validates an array by checking each of its elements against a set of rules.
*/
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
final class Each implements SerializableRuleInterface, BeforeValidationInterface, SkipOnEmptyInterface
final class Each implements
SerializableRuleInterface,
BeforeValidationInterface,
SkipOnEmptyInterface,
PropagateOptionsInterface
{
use BeforeValidationTrait;
use RuleNameTrait;
use SkipOnEmptyTrait;

public function __construct(
/**
* @var iterable<RuleInterface>
* @var iterable<BeforeValidationInterface|RuleInterface|SkipOnEmptyInterface>
*/
private iterable $rules = [],
private string $incorrectInputMessage = 'Value must be array or iterable.',
Expand All @@ -46,8 +51,26 @@ public function __construct(
) {
}

public function propagateOptions(): void
{
$rules = [];
foreach ($this->rules as $rule) {
$rule = $rule->skipOnEmpty($this->skipOnEmpty);
$rule = $rule->skipOnError($this->skipOnError);
$rule = $rule->when($this->when);

$rules[] = $rule;

if ($rule instanceof PropagateOptionsInterface) {
$rule->propagateOptions();
}
}

$this->rules = $rules;
}

/**
* @return iterable<\Closure|\Closure[]|RuleInterface|RuleInterface[]>
* @return iterable<BeforeValidationInterface|BeforeValidationInterface[]|\Closure|\Closure[]|RuleInterface|RuleInterface[]|SkipOnEmptyInterface|SkipOnEmptyInterface[]>
*/
public function getRules(): iterable
{
Expand Down
53 changes: 46 additions & 7 deletions src/Rule/Nested.php
Expand Up @@ -12,6 +12,7 @@
use Traversable;
use Yiisoft\Strings\StringHelper;
use Yiisoft\Validator\BeforeValidationInterface;
use Yiisoft\Validator\PropagateOptionsInterface;
use Yiisoft\Validator\Rule\Trait\BeforeValidationTrait;
use Yiisoft\Validator\Rule\Trait\RuleNameTrait;
use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait;
Expand All @@ -35,7 +36,11 @@
* Can be used for validation of nested structures.
*/
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
final class Nested implements SerializableRuleInterface, BeforeValidationInterface, SkipOnEmptyInterface
final class Nested implements
SerializableRuleInterface,
BeforeValidationInterface,
SkipOnEmptyInterface,
PropagateOptionsInterface
{
use BeforeValidationTrait;
use RuleNameTrait;
Expand Down Expand Up @@ -80,6 +85,7 @@ public function __construct(
private bool $requirePropertyPath = false,
private string $noPropertyPathMessage = 'Property path "{path}" is not found.',
private bool $normalizeRules = true,
private bool $propagateOptions = false,

/**
* @var bool|callable|null
Expand All @@ -92,7 +98,7 @@ public function __construct(
*/
private ?Closure $when = null,
) {
$this->rules = $this->prepareRules($rules);
$this->prepareRules($rules);
}

/**
Expand Down Expand Up @@ -127,10 +133,12 @@ public function getNoPropertyPathMessage(): string
/**
* @param class-string|iterable<Closure|Closure[]|RuleInterface|RuleInterface[]>|RulesProviderInterface|null $source
*/
private function prepareRules(iterable|object|string|null $source): ?iterable
private function prepareRules(iterable|object|string|null $source): void
{
if ($source === null) {
return null;
$this->rules = null;

return;
}

if ($source instanceof RulesProviderInterface) {
Expand All @@ -144,7 +152,15 @@ private function prepareRules(iterable|object|string|null $source): ?iterable
$rules = $rules instanceof Traversable ? iterator_to_array($rules) : $rules;
self::ensureArrayHasRules($rules);

return $this->normalizeRules ? $this->normalizeRules($rules) : $rules;
$this->rules = $rules;

if ($this->normalizeRules) {
$this->normalizeRules();
}

if ($this->propagateOptions) {
$this->propagateOptions();
}
}

private static function ensureArrayHasRules(iterable &$rules)
Expand All @@ -163,8 +179,11 @@ private static function ensureArrayHasRules(iterable &$rules)
}
}

private function normalizeRules(iterable $rules): iterable
private function normalizeRules(): void
{
/** @var iterable $rules */
$rules = $this->rules;

while (true) {
$breakWhile = true;
$rulesMap = [];
Expand Down Expand Up @@ -209,7 +228,27 @@ private function normalizeRules(iterable $rules): iterable
}
}

return $rules;
$this->rules = $rules;
}

public function propagateOptions(): void
{
$rules = [];
foreach ($this->rules as $attributeRulesIndex => $attributeRules) {
foreach ($attributeRules as $attributeRule) {
$attributeRule = $attributeRule->skipOnEmpty($this->skipOnEmpty);
$attributeRule = $attributeRule->skipOnError($this->skipOnError);
$attributeRule = $attributeRule->when($this->when);

$rules[$attributeRulesIndex][] = $attributeRule;

if ($attributeRule instanceof PropagateOptionsInterface) {
$attributeRule->propagateOptions();
}
}
}

$this->rules = $rules;
}

#[ArrayShape([
Expand Down
19 changes: 17 additions & 2 deletions src/Rule/Trait/BeforeValidationTrait.php
Expand Up @@ -9,15 +9,30 @@

trait BeforeValidationTrait
{
public function skipOnError(bool $value): static
{
$new = clone $this;
$new->skipOnError = $value;
return $new;
}

public function shouldSkipOnError(): bool
{
return $this->skipOnError;
}

/**
* @psalm-param Closure(mixed, ValidationContext):bool|null $value
*/
public function when(?Closure $value): static
{
$new = clone $this;
$new->when = $value;
return $new;
}

/**
* @psalm-return Closure(mixed, ValidationContext):bool|null
*
* @return Closure|null
*/
public function getWhen(): ?Closure
{
Expand Down
46 changes: 46 additions & 0 deletions tests/Rule/NestedHandlerTest.php
Expand Up @@ -4,6 +4,7 @@

namespace Yiisoft\Validator\Tests\Rule;

use Yiisoft\Arrays\ArrayHelper;
use Yiisoft\Validator\Error;
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Callback;
Expand Down Expand Up @@ -499,6 +500,51 @@ public function testNestedWithoutRulesWithObject(): void
], $result->getErrorMessagesIndexedByPath());
}

public function testPropagateOptions(): void
{
$rule = new Nested([
'posts' => [
new Each([new Nested([
'title' => [new HasLength(min: 3)],
'authors' => [
new Each([new Nested([
'name' => [new HasLength(min: 5)],
'age' => [
new Number(min: 18),
new Number(min: 20),
],
])]),
],
])]),
],
'meta' => [new HasLength(min: 7)],
], propagateOptions: true, skipOnEmpty: true, skipOnError: true);
$options = $rule->getOptions();
$paths = [
[],
['rules', 'posts', 0],
['rules', 'posts', 0, 'rules', 0],
['rules', 'posts', 0, 'rules', 0, 'rules', 'title', 0],
['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0],
['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0, 'rules', 0],
['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0, 'rules', 0, 'rules', 'name', 0],
['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0, 'rules', 0, 'rules', 'age', 0],
['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0, 'rules', 0, 'rules', 'age', 1],
['rules', 'meta', 0],
];
$keys = ['skipOnEmpty', 'skipOnError'];

foreach ($paths as $path) {
foreach ($keys as $key) {
$fullPath = $path;
$fullPath[] = $key;

$value = ArrayHelper::getValueByPath($options, $fullPath);
$this->assertTrue($value);
}
}
}

protected function getRuleHandler(): RuleHandlerInterface
{
return new NestedHandler($this->getTranslator());
Expand Down
6 changes: 2 additions & 4 deletions tests/Stub/TranslatorFactory.php
Expand Up @@ -14,16 +14,14 @@ final class TranslatorFactory
{
public function create(): TranslatorInterface
{
$translator = new Translator(
'en'
);

$translator = new Translator('en');
$categorySource = new CategorySource(
'validator',
new IdMessageReader(),
new SimpleMessageFormatter()
);
$translator->addCategorySource($categorySource);

return $translator->withCategory('validator');
}
}

0 comments on commit d1524d1

Please sign in to comment.