Skip to content

Commit

Permalink
Docs, part 15 (#496)
Browse files Browse the repository at this point in the history
* Add docs to Json rule and its handler

* Apply fixes from StyleCI

* Add docs to Required rule and handler

* Apply fixes from StyleCI

* Add placeholders info

* In actually works with arrays

* Apply fixes from StyleCI

* More tests and notes for In

* Apply fixes from StyleCI

* Use more specific error message for In rule

* Add docs to Subset and handler, various fixes

* More tests

* Fix typo in mentioning handler

Co-authored-by: Sergei Predvoditelev <sergei@predvoditelev.ru>

* Fix typo - missing dollar sign

Co-authored-by: Sergei Predvoditelev <sergei@predvoditelev.ru>

* Add descriptions to test data sets

* Add docs to Composite and handler

* Apply fixes from StyleCI

* Add docs to Each and its handler, minor docs fixes for Composite

* Apply fixes from StyleCI

* Add docs for StopOnError and handler

* Apply fixes from StyleCI

* Fix psalm

* Separate psalm annotation, fix mutants

* RGB point -> RGB color

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

* Remove ambiguous phrase

Co-authored-by: StyleCI Bot <bot@styleci.io>
Co-authored-by: Sergei Predvoditelev <sergei@predvoditelev.ru>
Co-authored-by: Alexander Makarov <sam@rmcreative.ru>
  • Loading branch information
4 people committed Dec 29, 2022
1 parent 779d44b commit 2304a66
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 88 deletions.
98 changes: 88 additions & 10 deletions src/Rule/Composite.php
Expand Up @@ -20,7 +20,53 @@
use Yiisoft\Validator\WhenInterface;

/**
* Allows to combine and validate multiple rules.
* Allows to group multiple rules for validation. It's helpful when `skipOnEmpty`, `skipOnError` or `when` options are
* the same for every rule in the set.
*
* For example, with the same `when` closure, without using composite it's specified explicitly for every rule:
*
* ```php
* $when = static function ($value, ValidationContext $context): bool {
* return $context->getDataSet()->getAttributeValue('country') === Country::USA;
* };
* $rules = [
* new Required(when: $when),
* new HasLength(min: 1, max: 50, skipOnEmpty: true, when: $when),
* ];
* ```
*
* When using composite, specifying it only once will be enough:
*
* ```php
* $rule = new Composite([
* new Required(),
* new HasLength(min: 1, max: 50, skipOnEmpty: true),
* when: static function ($value, ValidationContext $context): bool {
* return $context->getDataSet()->getAttributeValue('country') === Country::USA;
* },
* ]);
* ```
*
* Another use case is reusing this rule group across different places. It's possible by creating own extended class and
* setting the properties in the constructor:
*
* ```php
* class MyComposite extends Composite
* {
* public function __construct()
* {
* $this->rules = [
* new Required(),
* new HasLength(min: 1, max: 50, skipOnEmpty: true),
* ];
* $this->when = static function ($value, ValidationContext $context): bool {
* return $context->getDataSet()->getAttributeValue('country') === Country::USA;
* };
* }
* };
* ```
*
* @see CompositeHandler Corresponding handler performing the actual validation.
*
* @psalm-import-type WhenType from WhenInterface
*/
Expand All @@ -37,27 +83,42 @@ class Composite implements
use WhenTrait;

/**
* @var iterable<int, RuleInterface>
* @var iterable A set of normalized rules that needs to be grouped.
* @psalm-var iterable<int, RuleInterface>
*/
protected iterable $rules = [];

/**
* @var bool|callable|null
* @var bool|callable|null Whether to skip this rule group if the validated value is empty / not passed. See
* {@see SkipOnEmptyInterface}.
*/
protected $skipOnEmpty;
/**
* @var bool Whether to skip this rule group if any of the previous rules gave an error. See
* {@see SkipOnErrorInterface}.
*/
protected $skipOnEmpty = null;

protected bool $skipOnError = false;

/**
* @var Closure|null A callable to define a condition for applying this rule group. See {@see WhenInterface}.
* @psalm-var WhenType
*/
protected Closure|null $when = null;

/**
* @var RulesDumper|null A rules dumper instance used to dump grouped {@see $rules} as array. Lazily created by
* {@see getRulesDumper()} only when it's needed.
*/
private ?RulesDumper $rulesDumper = null;

/**
* @param iterable<Closure|RuleInterface> $rules
* @param iterable $rules A set of rules that needs to be grouped. They will be normalized using
* {@see RulesNormalizer}.
* @psalm-param iterable<Closure|RuleInterface> $rules
*
* @param bool|callable|null $skipOnEmpty Whether to skip this rule group if the validated value is empty / not
* passed. See {@see SkipOnEmptyInterface}.
* @param bool $skipOnError Whether to skip this rule group 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 rule group. See
* {@see WhenInterface}.
* @psalm-param WhenType $when
*/
public function __construct(
Expand Down Expand Up @@ -92,7 +153,11 @@ public function getOptions(): array
}

/**
* @return iterable<int, RuleInterface>
* Gets a set of normalized rules that needs to be grouped.
*
* @return iterable<int, RuleInterface> A set of rules.
*
* @see $rules
*/
public function getRules(): iterable
{
Expand All @@ -113,11 +178,24 @@ public function afterInitAttribute(object $object, int $target): void
}
}

/**
* Dumps grouped {@see $rules} to array.
*
* @return array The array of rules with their options.
*/
final protected function dumpRulesAsArray(): array
{
return $this->getRulesDumper()->asArray($this->getRules());
}

/**
* Returns existing rules dumper instance for dumping grouped {@see $rules} as array if it's already set. If not set
* yet, creates the new instance first.
*
* @return RulesDumper A rules dumper instance.
*
* @see $rulesDumper
*/
private function getRulesDumper(): RulesDumper
{
if ($this->rulesDumper === null) {
Expand Down
22 changes: 1 addition & 21 deletions src/Rule/CompositeHandler.php
Expand Up @@ -10,27 +10,7 @@
use Yiisoft\Validator\ValidationContext;

/**
* Can be used to group rules for validation by `skipOnEmpty`, `skipOnError` or `when`.
*
* For example, we have same when closure:
* ```
* 'name' => [
* new Required(when: fn() => $this->useName),
* new HasLength(min: 1, max: 50, skipOnEmpty: true, when: fn() => $this->useName),
* ],
* ```
* So we can configure it like this:
* ```
* 'name' => [
* new Composite(
* rules: [
* new Required(),
* new HasLength(min: 1, max: 50, skipOnEmpty: true),
* ],
* when: fn() => $this->useName,
* ),
* ],
* ```
* A handler for {@see Composite} handler. Validates group of rules.
*/
final class CompositeHandler implements RuleHandlerInterface
{
Expand Down
116 changes: 103 additions & 13 deletions src/Rule/Each.php
Expand Up @@ -8,6 +8,7 @@
use Closure;
use JetBrains\PhpStorm\ArrayShape;
use Yiisoft\Validator\AfterInitAttributeEventInterface;
use Yiisoft\Validator\DataSet\ObjectDataSet;
use Yiisoft\Validator\Helper\PropagateOptionsHelper;
use Yiisoft\Validator\Helper\RulesNormalizer;
use Yiisoft\Validator\PropagateOptionsInterface;
Expand All @@ -22,7 +23,38 @@
use Yiisoft\Validator\WhenInterface;

/**
* Validates an array by checking each of its elements against a set of rules.
* Allows to define a set of rules for validating each element of an iterable.
*
* An example for simple iterable that can be used to validate RGB color:
*
* ```php
* $rules = [
* new Count(exactly: 3), // Not required for using with `Each`.
* new Each([
* new Number(min: 0, max: 255, integerOnly: true),
* // More rules can be added here.
* ]),
* ];
* ```
*
* When paired with {@see Nested} rule, it allows validation of related data:
*
* ```php
* $coordinateRules = [new Number(min: -10, max: 10)];
* $rule = new Each([
* new Nested([
* 'coordinates.x' => $coordinateRules,
* 'coordinates.y' => $coordinateRules,
* ]),
* ]);
* ```
*
* It's also possible to use DTO objects with PHP attributes, see {@see ObjectDataSet} documentation and guide for
* details.
*
* Supports propagation of options (see {@see PropagateOptionsHelper::propagate()}).
*
* @see EachHandler Corresponding handler performing the actual validation.
*
* @psalm-import-type WhenType from WhenInterface
*/
Expand All @@ -40,28 +72,50 @@ final class Each implements
use WhenTrait;

/**
* @var iterable<RuleInterface>
* @var iterable A set of normalized rules that needs to be applied to each element of the validated iterable.
* @psalm-var iterable<RuleInterface>
*/
private iterable $rules;

/**
* @var RulesDumper|null A rules dumper instance used to dump {@see $rules} as array. Lazily created by
* {@see getRulesDumper()} only when it's needed.
*/
private ?RulesDumper $rulesDumper = null;

/**
* @param callable|iterable|RuleInterface $rules A set of rules that needs to be applied to each element of the
* validated iterable. They will be normalized using {@see RulesNormalizer}.
* @param string $incorrectInputMessage Error message used when validation fails because the validated value is not
* an iterable.
*
* You may use the following placeholders in the message:
*
* - `{attribute}`: the translated label of the attribute being validated.
* - `{type}`: the type of the value being validated.
* @param string $incorrectInputKeyMessage Error message used when validation fails because the validated iterable
* contains invalid keys. Only integer and string keys are allowed.
*
* You may use the following placeholders in the message:
*
* - `{attribute}`: the translated label of the attribute being validated.
* - `{type}`: the type of the iterable key being validated.
* @param bool|callable|null $skipOnEmpty Whether to skip this `Each` rule with all defined {@see $rules} if the
* validated value is empty / not passed. See {@see SkipOnEmptyInterface}.
* @param bool $skipOnError Whether to skip this `Each` 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 `Each` rule with all defined
* {@see $rules}. See {@see WhenInterface}.
* @psalm-param WhenType $when
*/
public function __construct(
/**
* @param callable|iterable<callable|RuleInterface>|RuleInterface $rules
*/
iterable|callable|RuleInterface $rules = [],
private string $incorrectInputMessage = 'Value must be array or iterable.',
private string $incorrectInputKeyMessage = 'Every iterable key must have an integer or a string type.',

/**
* @var bool|callable|null
*/
private $skipOnEmpty = null,
private mixed $skipOnEmpty = null,
private bool $skipOnError = false,
/**
* @var WhenType
*/
private Closure|null $when = null,
) {
$this->rules = RulesNormalizer::normalizeList($rules);
Expand All @@ -78,18 +132,36 @@ public function propagateOptions(): void
}

/**
* @return iterable<Closure|Closure[]|RuleInterface|RuleInterface[]>
* Gets a set of normalized rules that needs to be applied to each element of the validated iterable.
*
* @return iterable<Closure|Closure[]|RuleInterface|RuleInterface[]> A set of rules.
*
* @see $rules
*/
public function getRules(): iterable
{
return $this->rules;
}

/**
* Gets error message used when validation fails because the validated value is not an iterable.
*
* @return string Error message / template.
*
* @see $incorrectInputMessage
*/
public function getIncorrectInputMessage(): string
{
return $this->incorrectInputMessage;
}

/**
* Error message used when validation fails because the validated iterable contains invalid keys.
*
* @return string Error message / template.
*
* @see $incorrectInputKeyMessage
*/
public function getIncorrectInputKeyMessage(): string
{
return $this->incorrectInputKeyMessage;
Expand All @@ -115,7 +187,7 @@ public function getOptions(): array
],
'skipOnEmpty' => $this->getSkipOnEmptyOption(),
'skipOnError' => $this->skipOnError,
'rules' => $this->getRulesDumper()->asArray($this->rules),
'rules' => $this->dumpRulesAsArray(),
];
}

Expand All @@ -136,6 +208,24 @@ public function afterInitAttribute(object $object, int $target): void
}
}

/**
* Dumps defined {@see $rules} to array.
*
* @return array The array of rules with their options.
*/
private function dumpRulesAsArray(): array
{
return $this->getRulesDumper()->asArray($this->getRules());
}

/**
* Returns existing rules dumper instance for dumping defined {@see $rules} as array if it's already set. If not set
* yet, creates the new instance first.
*
* @return RulesDumper A rules dumper instance.
*
* @see $rulesDumper
*/
private function getRulesDumper(): RulesDumper
{
if ($this->rulesDumper === null) {
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/EachHandler.php
Expand Up @@ -13,7 +13,7 @@
use function is_string;

/**
* Validates an array by checking each of its elements against a set of rules.
* A handler for {@see Each} rule. Validates each element of an iterable using a set of rules.
*/
final class EachHandler implements RuleHandlerInterface
{
Expand Down

0 comments on commit 2304a66

Please sign in to comment.