diff --git a/bin/create-mixin b/bin/create-mixin index 36b03c05a..dda9c1841 100755 --- a/bin/create-mixin +++ b/bin/create-mixin @@ -171,6 +171,7 @@ function overwriteFile(string $content, string $basename): void 'PropertyExists', 'PropertyOptional', 'Attributes', + 'Templated', ]; $mixins = [ @@ -178,10 +179,10 @@ function overwriteFile(string $content, string $basename): void ['Length', 'length', $numberRelatedRules, []], ['Max', 'max', $numberRelatedRules, []], ['Min', 'min', $numberRelatedRules, []], - ['Not', 'not', [], ['Not', 'NotEmpty', 'NotBlank', 'NotEmoji', 'NotUndef', 'NotOptional', 'NullOr', 'UndefOr', 'Optional', 'Attributes']], - ['NullOr', 'nullOr', [], ['Nullable', 'NullOr', 'Optional', 'NotOptional', 'NotUndef', 'UndefOr']], + ['Not', 'not', [], ['Not', 'NotEmpty', 'NotBlank', 'NotEmoji', 'NotUndef', 'NotOptional', 'NullOr', 'UndefOr', 'Optional', 'Attributes', 'Templated']], + ['NullOr', 'nullOr', [], ['Nullable', 'NullOr', 'Optional', 'NotOptional', 'NotUndef', 'UndefOr', 'Templated']], ['Property', 'property', [], $structureRelatedRules], - ['UndefOr', 'undefOr', [], ['Nullable', 'NullOr', 'NotOptional', 'NotUndef', 'Optional', 'UndefOr', 'Attributes']], + ['UndefOr', 'undefOr', [], ['Nullable', 'NullOr', 'NotOptional', 'NotUndef', 'Optional', 'UndefOr', 'Attributes', 'Templated']], ['', null, [], []], ]; diff --git a/docs/09-list-of-rules-by-category.md b/docs/09-list-of-rules-by-category.md index 0ba1fce8a..187997a49 100644 --- a/docs/09-list-of-rules-by-category.md +++ b/docs/09-list-of-rules-by-category.md @@ -61,14 +61,21 @@ - [AllOf](rules/AllOf.md) - [AnyOf](rules/AnyOf.md) +- [Circuit](rules/Circuit.md) - [NoneOf](rules/NoneOf.md) - [OneOf](rules/OneOf.md) ## Conditions +- [Circuit](rules/Circuit.md) - [Not](rules/Not.md) - [When](rules/When.md) +## Core + +- [Not](rules/Not.md) +- [Templated](rules/Templated.md) + ## Date and Time - [Date](rules/Date.md) @@ -155,12 +162,14 @@ - [NotBlank](rules/NotBlank.md) - [NotEmpty](rules/NotEmpty.md) - [NotUndef](rules/NotUndef.md) +- [Templated](rules/Templated.md) ## Nesting - [AllOf](rules/AllOf.md) - [AnyOf](rules/AnyOf.md) - [Call](rules/Call.md) +- [Circuit](rules/Circuit.md) - [Each](rules/Each.md) - [Key](rules/Key.md) - [KeySet](rules/KeySet.md) @@ -254,6 +263,7 @@ - [Property](rules/Property.md) - [PropertyExists](rules/PropertyExists.md) - [PropertyOptional](rules/PropertyOptional.md) +- [Templated](rules/Templated.md) ## Transformations @@ -309,6 +319,7 @@ - [CallableType](rules/CallableType.md) - [Callback](rules/Callback.md) - [Charset](rules/Charset.md) +- [Circuit](rules/Circuit.md) - [Cnh](rules/Cnh.md) - [Cnpj](rules/Cnpj.md) - [Consonant](rules/Consonant.md) @@ -431,6 +442,7 @@ - [SubdivisionCode](rules/SubdivisionCode.md) - [Subset](rules/Subset.md) - [SymbolicLink](rules/SymbolicLink.md) +- [Templated](rules/Templated.md) - [Time](rules/Time.md) - [Tld](rules/Tld.md) - [TrueVal](rules/TrueVal.md) diff --git a/docs/rules/Attributes.md b/docs/rules/Attributes.md index 1e31afa01..3d06987cb 100644 --- a/docs/rules/Attributes.md +++ b/docs/rules/Attributes.md @@ -83,3 +83,4 @@ See also: - [Property](Property.md) - [PropertyExists](PropertyExists.md) - [PropertyOptional](PropertyOptional.md) +- [Templated](Templated.md) diff --git a/docs/rules/Not.md b/docs/rules/Not.md index 6474e2c90..28c060ca9 100644 --- a/docs/rules/Not.md +++ b/docs/rules/Not.md @@ -28,6 +28,7 @@ Each other validation has custom messages for negated rules. ## Categorization +- Core - Conditions - Nesting @@ -41,3 +42,4 @@ Each other validation has custom messages for negated rules. See also: - [NoneOf](NoneOf.md) +- [Templated](Templated.md) diff --git a/docs/rules/Templated.md b/docs/rules/Templated.md new file mode 100644 index 000000000..8d541c50c --- /dev/null +++ b/docs/rules/Templated.md @@ -0,0 +1,49 @@ +# Templated + +- `Templated(Rule $rule, string $template)` +- `Templated(Rule $rule, string $template, array $parameters)` + +Defines a rule with a custom message template. + +```php +v::templated(v::email(), 'You must provide a valid email to signup')->assert('not an email'); +// Message: You must provide a valid email to signup + +v::templated( + v::notEmpty(), + 'The author of the page {{title}} is empty, please fill it up.', + ['title' => 'Feature Guide'] +)->assert(''); +// Message: The author of the page "Feature Guide" is empty, please fill it up. +``` + +This rule can be also useful when you're using [Attributes](Attributes.md) and want a custom template for a specific property. + +## Templates + +This rule does not have any templates, as you must define the templates yourself. + +## Template placeholders + +| Placeholder | Description | +|-------------|------------------------------------------------------------------| +| `name` | The validated input or the custom validator name (if specified). | + + +## Categorization + +- Core +- Structures +- Miscellaneous + +## Changelog + +| Version | Description | +|--------:|-------------| +| 3.0.0 | Created | + +*** +See also: + +- [Attributes](Attributes.md) +- [Not](Not.md) diff --git a/library/Mixins/Builder.php b/library/Mixins/Builder.php index 0b6cd6542..7e771aa12 100644 --- a/library/Mixins/Builder.php +++ b/library/Mixins/Builder.php @@ -355,6 +355,11 @@ public static function subset(array $superset): Chain; public static function symbolicLink(): Chain; + /** + * @param array $parameters + */ + public static function templated(Rule $rule, string $template, array $parameters = []): Chain; + public static function time(string $format = 'H:i:s'): Chain; public static function tld(): Chain; diff --git a/library/Mixins/Chain.php b/library/Mixins/Chain.php index dcac73a0d..5abd15c5a 100644 --- a/library/Mixins/Chain.php +++ b/library/Mixins/Chain.php @@ -360,6 +360,11 @@ public function subset(array $superset): Chain; public function symbolicLink(): Chain; + /** + * @param array $parameters + */ + public function templated(Rule $rule, string $template, array $parameters = []): Chain; + public function time(string $format = 'H:i:s'): Chain; public function tld(): Chain; diff --git a/library/Result.php b/library/Result.php index f2905f815..6db130a25 100644 --- a/library/Result.php +++ b/library/Result.php @@ -27,15 +27,13 @@ final class Result public readonly ?string $name; - public readonly string $template; - /** @param array $parameters */ public function __construct( public readonly bool $isValid, public readonly mixed $input, public readonly Rule $rule, public readonly array $parameters = [], - string $template = Rule::TEMPLATE_STANDARD, + public readonly string $template = Rule::TEMPLATE_STANDARD, public readonly Mode $mode = Mode::DEFAULT, ?string $name = null, ?string $id = null, @@ -44,7 +42,6 @@ public function __construct( Result ...$children, ) { $this->name = $rule->getName() ?? $name; - $this->template = $rule->getTemplate() ?? $template; $this->id = $id ?? lcfirst(substr((string) strrchr($rule::class, '\\'), 1)); $this->children = $children; } @@ -97,6 +94,12 @@ public function withTemplate(string $template): self return $this->clone(template: $template); } + /** @param array $parameters */ + public function withExtraParameters(array $parameters): self + { + return $this->clone(parameters: $parameters + $this->parameters); + } + public function withId(string $id): self { if ($this->unchangeableId) { @@ -190,11 +193,13 @@ public function allowsAdjacent(): bool } /** + * @param array $parameters * @param array|null $children */ private function clone( ?bool $isValid = null, mixed $input = null, + ?array $parameters = null, ?string $template = null, ?Mode $mode = null, ?string $name = null, @@ -207,7 +212,7 @@ private function clone( $isValid ?? $this->isValid, $input ?? $this->input, $this->rule, - $this->parameters, + $parameters ?? $this->parameters, $template ?? $this->template, $mode ?? $this->mode, $name ?? $this->name, diff --git a/library/Rule.php b/library/Rule.php index 5bf6ad998..e4bbb92da 100644 --- a/library/Rule.php +++ b/library/Rule.php @@ -18,8 +18,4 @@ public function evaluate(mixed $input): Result; public function getName(): ?string; public function setName(string $name): static; - - public function getTemplate(): ?string; - - public function setTemplate(string $template): static; } diff --git a/library/Rules/Core/Binder.php b/library/Rules/Core/Binder.php index 686365457..849565987 100644 --- a/library/Rules/Core/Binder.php +++ b/library/Rules/Core/Binder.php @@ -26,10 +26,6 @@ public function evaluate(mixed $input): Result $this->bound->setName($this->source->getName()); } - if ($this->source->getTemplate() !== null && $this->bound->getTemplate() === null) { - $this->bound->setTemplate($this->source->getTemplate()); - } - return $this->bound->evaluate($input); } } diff --git a/library/Rules/Core/Composite.php b/library/Rules/Core/Composite.php index 0d67f8781..6097b35b7 100644 --- a/library/Rules/Core/Composite.php +++ b/library/Rules/Core/Composite.php @@ -23,8 +23,6 @@ abstract class Composite implements Rule private ?string $name = null; - private ?string $template = null; - public function __construct(Rule $rule1, Rule $rule2, Rule ...$rules) { $this->rules = array_merge([$rule1, $rule2], $rules); @@ -55,16 +53,4 @@ public function getName(): ?string { return $this->name; } - - public function setTemplate(string $template): static - { - $this->template = $template; - - return $this; - } - - public function getTemplate(): ?string - { - return $this->template; - } } diff --git a/library/Rules/Core/Reducer.php b/library/Rules/Core/Reducer.php index e6c58b7c6..e1a1ef7b5 100644 --- a/library/Rules/Core/Reducer.php +++ b/library/Rules/Core/Reducer.php @@ -11,6 +11,7 @@ use Respect\Validation\Rule; use Respect\Validation\Rules\AllOf; +use Respect\Validation\Rules\Templated; final class Reducer extends Wrapper { @@ -18,4 +19,13 @@ public function __construct(Rule $rule1, Rule ...$rules) { parent::__construct($rules === [] ? $rule1 : new AllOf($rule1, ...$rules)); } + + public function withTemplate(?string $template): self + { + if ($template === null) { + return $this; + } + + return new self(new Templated($this->rule, $template)); + } } diff --git a/library/Rules/Core/Standard.php b/library/Rules/Core/Standard.php index a20be9c03..8b0c6cb43 100644 --- a/library/Rules/Core/Standard.php +++ b/library/Rules/Core/Standard.php @@ -18,8 +18,6 @@ abstract class Standard implements Rule private ?string $name = null; - private ?string $template = null; - public function getName(): ?string { return $this->name; @@ -31,16 +29,4 @@ public function setName(string $name): static return $this; } - - public function getTemplate(): ?string - { - return $this->template; - } - - public function setTemplate(string $template): static - { - $this->template = $template; - - return $this; - } } diff --git a/library/Rules/Core/Wrapper.php b/library/Rules/Core/Wrapper.php index 0d511e18a..bacea25d5 100644 --- a/library/Rules/Core/Wrapper.php +++ b/library/Rules/Core/Wrapper.php @@ -39,18 +39,6 @@ public function setName(string $name): static return $this; } - public function getTemplate(): ?string - { - return $this->rule->getTemplate(); - } - - public function setTemplate(string $template): static - { - $this->rule->setTemplate($template); - - return $this; - } - public function getRule(): Rule { return $this->rule; diff --git a/library/Rules/Templated.php b/library/Rules/Templated.php new file mode 100644 index 000000000..27bb9d085 --- /dev/null +++ b/library/Rules/Templated.php @@ -0,0 +1,38 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +namespace Respect\Validation\Rules; + +use Attribute; +use Respect\Validation\Result; +use Respect\Validation\Rule; +use Respect\Validation\Rules\Core\Wrapper; + +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +final class Templated extends Wrapper +{ + /** @param array $parameters */ + public function __construct( + Rule $rule, + private readonly string $template, + private readonly array $parameters = [] + ) { + parent::__construct($rule); + } + + public function evaluate(mixed $input): Result + { + $result = $this->rule->evaluate($input); + if ($result->hasCustomTemplate()) { + return $result; + } + + return $result->withTemplate($this->template)->withExtraParameters($this->parameters); + } +} diff --git a/library/Rules/When.php b/library/Rules/When.php index 558bb6987..271d1fd6c 100644 --- a/library/Rules/When.php +++ b/library/Rules/When.php @@ -26,8 +26,7 @@ public function __construct( ?Rule $else = null ) { if ($else === null) { - $else = new AlwaysInvalid(); - $else->setTemplate(AlwaysInvalid::TEMPLATE_SIMPLE); + $else = new Templated(new AlwaysInvalid(), AlwaysInvalid::TEMPLATE_SIMPLE); } $this->else = $else; diff --git a/library/Transformers/DeprecatedKeyValue.php b/library/Transformers/DeprecatedKeyValue.php index 2a7dd9b2a..acca88599 100644 --- a/library/Transformers/DeprecatedKeyValue.php +++ b/library/Transformers/DeprecatedKeyValue.php @@ -13,10 +13,10 @@ use Respect\Validation\Rules\Key; use Respect\Validation\Rules\KeyExists; use Respect\Validation\Rules\Lazy; +use Respect\Validation\Rules\Templated; use Respect\Validation\Validator; use Throwable; -use function sprintf; use function trigger_error; use const E_USER_DEPRECATED; @@ -50,15 +50,11 @@ static function ($input) use ($comparedKey, $ruleName, $baseKey) { try { return new Key($comparedKey, Validator::__callStatic($ruleName, [$input[$baseKey]])); } catch (Throwable) { - $rule = new AlwaysInvalid(); - $rule->setName($comparedKey); - $rule->setTemplate(sprintf( - '%s must be valid to validate %s', - $baseKey, - $comparedKey, - )); - - return $rule; + return new Templated( + new AlwaysInvalid(), + '{{baseKey|raw}} must be valid to validate {{comparedKey|raw}}', + ['comparedKey' => $comparedKey, 'baseKey' => $baseKey] + ); } } ), diff --git a/library/Validator.php b/library/Validator.php index 06ba7146c..71f540fdc 100644 --- a/library/Validator.php +++ b/library/Validator.php @@ -57,7 +57,7 @@ public static function create(Rule ...$rules): self public function evaluate(mixed $input): Result { - return (new Binder($this, new Reducer(...$this->rules)))->evaluate($input); + return (new Binder($this, (new Reducer(...$this->rules))->withTemplate($this->template)))->evaluate($input); } public function isValid(mixed $input): bool diff --git a/tests/feature/Issues/Issue1477Test.php b/tests/feature/Issues/Issue1477Test.php index 8a226aa63..fb85610a1 100644 --- a/tests/feature/Issues/Issue1477Test.php +++ b/tests/feature/Issues/Issue1477Test.php @@ -13,12 +13,15 @@ function (): void { v::key( 'Address', - (new class extends Simple { - protected function isValid(mixed $input): bool - { - return false; - } - })->setTemplate('{{name}} is not good!'), + v::templated( + new class extends Simple { + protected function isValid(mixed $input): bool + { + return false; + } + }, + '{{name}} is not good!', + ), )->assert(['Address' => 'cvejvn']); }, 'Address is not good!', diff --git a/tests/feature/Rules/TemplatedTest.php b/tests/feature/Rules/TemplatedTest.php new file mode 100644 index 000000000..b720b3e9e --- /dev/null +++ b/tests/feature/Rules/TemplatedTest.php @@ -0,0 +1,52 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +test('Default', expectAll( + fn() => v::templated(v::stringType(), 'Template in "Templated"')->assert(12), + 'Template in "Templated"', + '- Template in "Templated"', + ['stringType' => 'Template in "Templated"'], +)); + +test('With parameters', expectAll( + fn() => v::templated(v::stringType(), 'Template in {{source}}', ['source' => 'Templated'])->assert(12), + 'Template in "Templated"', + '- Template in "Templated"', + ['stringType' => 'Template in "Templated"'], +)); + +test('Inverted', expectAll( + fn() => v::not(v::templated(v::intType(), 'Template in "Templated"'))->assert(12), + 'Template in "Templated"', + '- Template in "Templated"', + ['notIntType' => 'Template in "Templated"'], +)); + +test('Template in Validator', expectAll( + fn() => v::templated(v::stringType(), 'Template in "Templated"') + ->setTemplate('Template in "Validator"') + ->assert(12), + 'Template in "Templated"', + '- Template in "Templated"', + ['stringType' => 'Template in "Templated"'], +)); + +test('Template passed to Validator::assert()', expectAll( + fn() => v::templated(v::stringType(), 'Template in "Templated"')->assert(10, 'Template in "Validator::assert"'), + 'Template in "Templated"', + '- Template in "Templated"', + ['stringType' => 'Template in "Templated"'], +)); + +test('With bound', expectAll( + fn() => v::templated(v::attributes(), 'Template in "Templated"')->assert(null), + 'Template in "Templated"', + '- Template in "Templated"', + ['attributes' => 'Template in "Templated"'], +)); diff --git a/tests/unit/Rules/Core/BinderTest.php b/tests/unit/Rules/Core/BinderTest.php index 134e37091..25553c1a0 100644 --- a/tests/unit/Rules/Core/BinderTest.php +++ b/tests/unit/Rules/Core/BinderTest.php @@ -12,11 +12,8 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use Respect\Validation\Rule; use Respect\Validation\Rules\AlwaysInvalid; -use function uniqid; - #[CoversClass(Binder::class)] final class BinderTest extends TestCase { @@ -65,50 +62,4 @@ public function shouldNotBindNameToBoundRuleWhenSourceHasNoName(): void self::assertNull($bound->getName()); self::assertNull($result->name); } - - #[Test] - public function shouldBindTemplateToBoundRule(): void - { - $sourceTemplate = uniqid(); - - $source = new AlwaysInvalid(); - $source->setTemplate($sourceTemplate); - - $bound = new AlwaysInvalid(); - $binder = new Binder($source, $bound); - $result = $binder->evaluate(null); - - self::assertSame($sourceTemplate, $bound->getTemplate()); - self::assertSame($sourceTemplate, $result->template); - } - - #[Test] - public function shouldNotBindTemplateToBoundRuleWhenItAlreadyHasSomeTemplate(): void - { - $source = new AlwaysInvalid(); - $source->setTemplate('source template'); - - $boundTemplate = 'bound name'; - - $bound = new AlwaysInvalid(); - $bound->setTemplate($boundTemplate); - - $binder = new Binder($source, $bound); - $result = $binder->evaluate(null); - - self::assertSame($boundTemplate, $bound->getTemplate()); - self::assertSame($boundTemplate, $result->template); - } - - #[Test] - public function shouldNotBindTemplateToBoundRuleWhenSourceHasNoTemplate(): void - { - $bound = new AlwaysInvalid(); - - $binder = new Binder(new AlwaysInvalid(), $bound); - $result = $binder->evaluate(null); - - self::assertNull($bound->getTemplate()); - self::assertSame(Rule::TEMPLATE_STANDARD, $result->template); - } } diff --git a/tests/unit/Rules/Core/CompositeTest.php b/tests/unit/Rules/Core/CompositeTest.php index 9198b7dff..59638b0c4 100644 --- a/tests/unit/Rules/Core/CompositeTest.php +++ b/tests/unit/Rules/Core/CompositeTest.php @@ -31,17 +31,6 @@ public function itShouldReturnItsChildren(): void self::assertEquals($expected, $actual); } - #[Test] - public function itShouldDefineAndRetrieveTemplate(): void - { - $template = 'This is a template'; - - $sut = new ConcreteComposite(Stub::daze(), Stub::daze()); - $sut->setTemplate($template); - - self::assertEquals($template, $sut->getTemplate()); - } - #[Test] public function itShouldUpdateTheNameOfTheChildWhenUpdatingItsName(): void { diff --git a/tests/unit/Rules/Core/ReducerTest.php b/tests/unit/Rules/Core/ReducerTest.php index 1cacc7b61..cec94f8d5 100644 --- a/tests/unit/Rules/Core/ReducerTest.php +++ b/tests/unit/Rules/Core/ReducerTest.php @@ -41,4 +41,33 @@ public function shouldWrapWhenThereAreMultipleRules(): void self::assertEquals(new AllOf($rule1, $rule2, $rule3), $result->rule); } + + #[Test] + public function shouldCreateWithTemplate(): void + { + $rule = Stub::any(1); + + $template = 'This is my template'; + + $reducer = new Reducer($rule); + $withTemplated = $reducer->withTemplate($template); + + $result = $withTemplated->evaluate(null); + + self::assertSame($rule, $result->rule); + self::assertSame($template, $result->template); + } + + #[Test] + public function shouldReturnSelfWhenTryingToCreatedWithNullTemplate(): void + { + $rule = Stub::any(1); + + $template = null; + + $reducer = new Reducer($rule); + $withTemplated = $reducer->withTemplate($template); + + self::assertSame($reducer, $withTemplated); + } } diff --git a/tests/unit/Rules/Core/StandardTest.php b/tests/unit/Rules/Core/StandardTest.php index f348a384e..c81e0cdcd 100644 --- a/tests/unit/Rules/Core/StandardTest.php +++ b/tests/unit/Rules/Core/StandardTest.php @@ -35,21 +35,4 @@ public function itShouldBeAbleToSetName(): void self::assertEquals('foo', $rule->getName()); } - - #[Test] - public function itShouldNotHaveAnyTemplateByDefault(): void - { - $rule = new ConcreteStandard(); - - self::assertNull($rule->getTemplate()); - } - - #[Test] - public function itShouldBeAbleToSetTemplate(): void - { - $rule = new ConcreteStandard(); - $rule->setTemplate('foo'); - - self::assertEquals('foo', $rule->getTemplate()); - } } diff --git a/tests/unit/Rules/Core/WrapperTest.php b/tests/unit/Rules/Core/WrapperTest.php index 69ea5b993..8c410ce6a 100644 --- a/tests/unit/Rules/Core/WrapperTest.php +++ b/tests/unit/Rules/Core/WrapperTest.php @@ -45,18 +45,4 @@ public function shouldPassNameOnToWrapped(): void self::assertSame($name, $rule->getName()); self::assertSame($name, $sut->getName()); } - - #[Test] - public function shouldPassTemplateOnToWrapped(): void - { - $template = 'Whatever'; - - $rule = Stub::pass(1); - - $sut = new ConcreteWrapper($rule); - $sut->setTemplate($template); - - self::assertSame($template, $rule->getTemplate()); - self::assertSame($template, $sut->getTemplate()); - } }