From a2337b0bd930c8b847f897a445849b42d30ef5dc Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Fri, 30 Jan 2026 19:55:11 +0100 Subject: [PATCH] Rename Core\Reducer to Composite and expose it as a public validator The term "Reducer" is widely used in functional programming to describe a specific pattern: a function that takes an accumulator and a value, returning a new accumulator (e.g., array_reduce, Redux reducers). This class does not perform that operation. Instead, it consolidates multiple validators into a single one, which is the definition of composition. The new name "Composite" accurately reflects what this validator does: it composes zero or more validators into a unified validation unit. This aligns with the Composite design pattern terminology and avoids semantic confusion with functional programming concepts. The class has also been moved from the internal Core namespace to the public Validators namespace. This change acknowledges that composing validators dynamically is a legitimate use case for library consumers, not just an internal implementation detail. The library itself uses this pattern in Attributes and KeySet validators, demonstrating its practical value. The signature has been extended to accept zero or more validators: - Zero validators: Returns AlwaysValid (useful for conditional chains) - One validator: Pass-through without wrapping (avoids unnecessary nesting) - Multiple validators: Combines using AllOf (all must pass) This flexibility is particularly useful when building validation chains dynamically, where the number of validators may vary at runtime based on configuration or user input. Apart from that, we consolidaded the `LogicalComposite` as a composite that would accept at least two validators. The shifted the logic from `AllOf` to the `Composite`, and made the `LogicalComposite` extends the `Composite`, so other classes can leverage the same logic. Co-authored-by: Alexandre Gomes Gaigalas Assisted-by: Claude Code (Claude Opus 4.5) --- docs/migrating-from-v2-to-v3.md | 2 +- docs/validators.md | 6 +- docs/validators/Composite.md | 64 ++++++++++++++++++ src/Mixins/AllBuilder.php | 2 + src/Mixins/AllChain.php | 2 + src/Mixins/Builder.php | 2 + src/Mixins/Chain.php | 2 + src/Mixins/KeyBuilder.php | 2 + src/Mixins/KeyChain.php | 2 + src/Mixins/NotBuilder.php | 2 + src/Mixins/NotChain.php | 2 + src/Mixins/NullOrBuilder.php | 2 + src/Mixins/NullOrChain.php | 2 + src/Mixins/PropertyBuilder.php | 2 + src/Mixins/PropertyChain.php | 2 + src/Mixins/UndefOrBuilder.php | 2 + src/Mixins/UndefOrChain.php | 2 + src/Validators/AllOf.php | 37 +---------- src/Validators/AnyOf.php | 4 +- src/Validators/Attributes.php | 8 +-- src/Validators/Circuit.php | 4 +- src/Validators/Composite.php | 66 +++++++++++++++++++ .../{Composite.php => LogicalComposite.php} | 16 +---- src/Validators/Core/Reducer.php | 30 --------- src/Validators/KeySet.php | 3 +- src/Validators/NoneOf.php | 4 +- src/Validators/OneOf.php | 4 +- tests/src/SmokeTestProvider.php | 1 + ...osite.php => ConcreteLogicalComposite.php} | 4 +- .../ReducerTest.php => CompositeTest.php} | 26 +++++--- tests/unit/Validators/Core/CompositeTest.php | 6 +- 31 files changed, 202 insertions(+), 111 deletions(-) create mode 100644 docs/validators/Composite.md create mode 100644 src/Validators/Composite.php rename src/Validators/Core/{Composite.php => LogicalComposite.php} (51%) delete mode 100644 src/Validators/Core/Reducer.php rename tests/src/Validators/Core/{ConcreteComposite.php => ConcreteLogicalComposite.php} (75%) rename tests/unit/Validators/{Core/ReducerTest.php => CompositeTest.php} (55%) diff --git a/docs/migrating-from-v2-to-v3.md b/docs/migrating-from-v2-to-v3.md index e23ab03df..92322e649 100644 --- a/docs/migrating-from-v2-to-v3.md +++ b/docs/migrating-from-v2-to-v3.md @@ -556,7 +556,7 @@ final class Custom implements Validator Base classes available in `Respect\Validation\Validators\Core`: - `Simple` - For validators with simple boolean logic -- `Composite` - For validators that combine multiple validators +- `LogicComposite` - For validators that combine two or more validators - `Envelope` - For validators that modify how another validator works --- diff --git a/docs/validators.md b/docs/validators.md index afdbeeafe..48c50fe42 100644 --- a/docs/validators.md +++ b/docs/validators.md @@ -17,7 +17,7 @@ In this page you will find a list of validators by their category. **Comparisons**: [All][] - [Between][] - [BetweenExclusive][] - [Equals][] - [Equivalent][] - [GreaterThan][] - [GreaterThanOrEqual][] - [Identical][] - [In][] - [Length][] - [LessThan][] - [LessThanOrEqual][] - [Max][] - [Min][] -**Composite**: [AllOf][] - [AnyOf][] - [Circuit][] - [NoneOf][] - [OneOf][] +**Composite**: [AllOf][] - [AnyOf][] - [Circuit][] - [Composite][] - [NoneOf][] - [OneOf][] **Conditions**: [Circuit][] - [Not][] - [When][] @@ -41,7 +41,7 @@ In this page you will find a list of validators by their category. **Miscellaneous**: [Blank][] - [Falsy][] - [Masked][] - [Named][] - [Templated][] - [Undef][] -**Nesting**: [AllOf][] - [AnyOf][] - [Call][] - [Circuit][] - [Each][] - [Key][] - [KeySet][] - [Lazy][] - [NoneOf][] - [Not][] - [NullOr][] - [OneOf][] - [Property][] - [PropertyOptional][] - [UndefOr][] - [When][] +**Nesting**: [AllOf][] - [AnyOf][] - [Call][] - [Circuit][] - [Composite][] - [Each][] - [Key][] - [KeySet][] - [Lazy][] - [NoneOf][] - [Not][] - [NullOr][] - [OneOf][] - [Property][] - [PropertyOptional][] - [UndefOr][] - [When][] **Numbers**: [Base][] - [Decimal][] - [Digit][] - [Even][] - [Factor][] - [Finite][] - [FloatType][] - [FloatVal][] - [Infinite][] - [IntType][] - [IntVal][] - [Multiple][] - [Negative][] - [Number][] - [NumericVal][] - [Odd][] - [Positive][] - [Roman][] @@ -82,6 +82,7 @@ In this page you will find a list of validators by their category. - [Circuit][] - `v::circuit(v::intVal(), v::floatVal())->assert(15);` - [Cnh][] - `v::cnh()->assert('02650306461');` - [Cnpj][] - `v::cnpj()->assert('00394460005887');` +- [Composite][] - `v::composite()->assert('anything');` - [Consonant][] - `v::consonant()->assert('xkcd');` - [Contains][] - `v::contains('ipsum')->assert('lorem ipsum');` - [ContainsAny][] - `v::containsAny(['lorem', 'dolor'])->assert('lorem ipsum');` @@ -238,6 +239,7 @@ In this page you will find a list of validators by their category. [Circuit]: validators/Circuit.md "Validates the input against a series of validators until the first fails." [Cnh]: validators/Cnh.md "Validates a Brazilian driver's license." [Cnpj]: validators/Cnpj.md "Validates if the input is a Brazilian National Registry of Legal Entities (CNPJ) number." +[Composite]: validators/Composite.md "Consolidates zero or more validators into a single validator." [Consonant]: validators/Consonant.md "Validates if the input contains only consonants." [Contains]: validators/Contains.md "Validates if the input contains some value." [ContainsAny]: validators/ContainsAny.md "Validates if the input contains at least one of defined values" diff --git a/docs/validators/Composite.md b/docs/validators/Composite.md new file mode 100644 index 000000000..65ea661b6 --- /dev/null +++ b/docs/validators/Composite.md @@ -0,0 +1,64 @@ + + +# Composite + +- `Composite()` +- `Composite(Validator ...$validators)` + +Consolidates zero or more validators into a single validator. + +- When no validators are provided, it acts as [AlwaysValid](AlwaysValid.md). +- When a single validator is provided, it acts as a pass-through. +- When multiple validators are provided, they are combined using [AllOf](AllOf.md), meaning all validators must pass for the input to be considered valid. + +```php +v::composite()->assert('anything'); +// Validation passes successfully + +v::composite(v::intType())->assert(42); +// Validation passes successfully + +v::composite(v::intType(), v::positive(), v::lessThan(100))->assert(42); +// Validation passes successfully + +v::composite(v::intType(), v::positive())->assert(-5); +// → -5 must be a positive number +``` + +## Use cases + +This validator is useful for dynamically building validation chains or when you +have a variable number of validators that need to be applied together. + +```php +$validators = [v::stringType(), v::notSpaced()]; + +if (true) { // some condition + $validators[] = v::email(); +} + +v::composite(...$validators)->assert('respectpanda#example.com'); +// → "respectpanda#example.com" must be a valid email address +``` + +## Categorization + +- Composite +- Nesting + +## Changelog + +| Version | Description | +| ------: | :---------- | +| 3.0.0 | Created | + +## See Also + +- [AllOf](AllOf.md) +- [AlwaysValid](AlwaysValid.md) +- [AnyOf](AnyOf.md) +- [NoneOf](NoneOf.md) +- [OneOf](OneOf.md) diff --git a/src/Mixins/AllBuilder.php b/src/Mixins/AllBuilder.php index 4334c708b..1b11d2c11 100644 --- a/src/Mixins/AllBuilder.php +++ b/src/Mixins/AllBuilder.php @@ -62,6 +62,8 @@ public static function allCnh(): Chain; public static function allCnpj(): Chain; + public static function allComposite(Validator ...$validators): Chain; + public static function allConsonant(string ...$additionalChars): Chain; public static function allContains(mixed $containsValue): Chain; diff --git a/src/Mixins/AllChain.php b/src/Mixins/AllChain.php index 3e2bec307..8ce7a0a63 100644 --- a/src/Mixins/AllChain.php +++ b/src/Mixins/AllChain.php @@ -62,6 +62,8 @@ public function allCnh(): Chain; public function allCnpj(): Chain; + public function allComposite(Validator ...$validators): Chain; + public function allConsonant(string ...$additionalChars): Chain; public function allContains(mixed $containsValue): Chain; diff --git a/src/Mixins/Builder.php b/src/Mixins/Builder.php index a28983d1a..2309d58d7 100644 --- a/src/Mixins/Builder.php +++ b/src/Mixins/Builder.php @@ -67,6 +67,8 @@ public static function cnh(): Chain; public static function cnpj(): Chain; + public static function composite(Validator ...$validators): Chain; + public static function consonant(string ...$additionalChars): Chain; public static function contains(mixed $containsValue): Chain; diff --git a/src/Mixins/Chain.php b/src/Mixins/Chain.php index 1500fb49f..c3fb95353 100644 --- a/src/Mixins/Chain.php +++ b/src/Mixins/Chain.php @@ -69,6 +69,8 @@ public function cnh(): Chain; public function cnpj(): Chain; + public function composite(Validator ...$validators): Chain; + public function consonant(string ...$additionalChars): Chain; public function contains(mixed $containsValue): Chain; diff --git a/src/Mixins/KeyBuilder.php b/src/Mixins/KeyBuilder.php index 617e05081..cd9a26f93 100644 --- a/src/Mixins/KeyBuilder.php +++ b/src/Mixins/KeyBuilder.php @@ -64,6 +64,8 @@ public static function keyCnh(int|string $key): Chain; public static function keyCnpj(int|string $key): Chain; + public static function keyComposite(int|string $key, Validator ...$validators): Chain; + public static function keyConsonant(int|string $key, string ...$additionalChars): Chain; public static function keyContains(int|string $key, mixed $containsValue): Chain; diff --git a/src/Mixins/KeyChain.php b/src/Mixins/KeyChain.php index 3addc3cee..8da012e00 100644 --- a/src/Mixins/KeyChain.php +++ b/src/Mixins/KeyChain.php @@ -64,6 +64,8 @@ public function keyCnh(int|string $key): Chain; public function keyCnpj(int|string $key): Chain; + public function keyComposite(int|string $key, Validator ...$validators): Chain; + public function keyConsonant(int|string $key, string ...$additionalChars): Chain; public function keyContains(int|string $key, mixed $containsValue): Chain; diff --git a/src/Mixins/NotBuilder.php b/src/Mixins/NotBuilder.php index ed6b4a5a3..9b15af5ab 100644 --- a/src/Mixins/NotBuilder.php +++ b/src/Mixins/NotBuilder.php @@ -64,6 +64,8 @@ public static function notCnh(): Chain; public static function notCnpj(): Chain; + public static function notComposite(Validator ...$validators): Chain; + public static function notConsonant(string ...$additionalChars): Chain; public static function notContains(mixed $containsValue): Chain; diff --git a/src/Mixins/NotChain.php b/src/Mixins/NotChain.php index 2bd5b6c9f..9e510e065 100644 --- a/src/Mixins/NotChain.php +++ b/src/Mixins/NotChain.php @@ -64,6 +64,8 @@ public function notCnh(): Chain; public function notCnpj(): Chain; + public function notComposite(Validator ...$validators): Chain; + public function notConsonant(string ...$additionalChars): Chain; public function notContains(mixed $containsValue): Chain; diff --git a/src/Mixins/NullOrBuilder.php b/src/Mixins/NullOrBuilder.php index d2d6a7d15..3cf27c850 100644 --- a/src/Mixins/NullOrBuilder.php +++ b/src/Mixins/NullOrBuilder.php @@ -64,6 +64,8 @@ public static function nullOrCnh(): Chain; public static function nullOrCnpj(): Chain; + public static function nullOrComposite(Validator ...$validators): Chain; + public static function nullOrConsonant(string ...$additionalChars): Chain; public static function nullOrContains(mixed $containsValue): Chain; diff --git a/src/Mixins/NullOrChain.php b/src/Mixins/NullOrChain.php index 074794e63..cb7ceb4bc 100644 --- a/src/Mixins/NullOrChain.php +++ b/src/Mixins/NullOrChain.php @@ -64,6 +64,8 @@ public function nullOrCnh(): Chain; public function nullOrCnpj(): Chain; + public function nullOrComposite(Validator ...$validators): Chain; + public function nullOrConsonant(string ...$additionalChars): Chain; public function nullOrContains(mixed $containsValue): Chain; diff --git a/src/Mixins/PropertyBuilder.php b/src/Mixins/PropertyBuilder.php index ff7564915..516708a42 100644 --- a/src/Mixins/PropertyBuilder.php +++ b/src/Mixins/PropertyBuilder.php @@ -64,6 +64,8 @@ public static function propertyCnh(string $propertyName): Chain; public static function propertyCnpj(string $propertyName): Chain; + public static function propertyComposite(string $propertyName, Validator ...$validators): Chain; + public static function propertyConsonant(string $propertyName, string ...$additionalChars): Chain; public static function propertyContains(string $propertyName, mixed $containsValue): Chain; diff --git a/src/Mixins/PropertyChain.php b/src/Mixins/PropertyChain.php index f51c244bf..ac36cde02 100644 --- a/src/Mixins/PropertyChain.php +++ b/src/Mixins/PropertyChain.php @@ -64,6 +64,8 @@ public function propertyCnh(string $propertyName): Chain; public function propertyCnpj(string $propertyName): Chain; + public function propertyComposite(string $propertyName, Validator ...$validators): Chain; + public function propertyConsonant(string $propertyName, string ...$additionalChars): Chain; public function propertyContains(string $propertyName, mixed $containsValue): Chain; diff --git a/src/Mixins/UndefOrBuilder.php b/src/Mixins/UndefOrBuilder.php index b6b756172..8ba11993c 100644 --- a/src/Mixins/UndefOrBuilder.php +++ b/src/Mixins/UndefOrBuilder.php @@ -62,6 +62,8 @@ public static function undefOrCnh(): Chain; public static function undefOrCnpj(): Chain; + public static function undefOrComposite(Validator ...$validators): Chain; + public static function undefOrConsonant(string ...$additionalChars): Chain; public static function undefOrContains(mixed $containsValue): Chain; diff --git a/src/Mixins/UndefOrChain.php b/src/Mixins/UndefOrChain.php index c14c07029..9c1bb627e 100644 --- a/src/Mixins/UndefOrChain.php +++ b/src/Mixins/UndefOrChain.php @@ -62,6 +62,8 @@ public function undefOrCnh(): Chain; public function undefOrCnpj(): Chain; + public function undefOrComposite(Validator ...$validators): Chain; + public function undefOrConsonant(string ...$additionalChars): Chain; public function undefOrContains(mixed $containsValue): Chain; diff --git a/src/Validators/AllOf.php b/src/Validators/AllOf.php index 5ee7ca546..369239817 100644 --- a/src/Validators/AllOf.php +++ b/src/Validators/AllOf.php @@ -15,42 +15,9 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Validation\Message\Template; -use Respect\Validation\Result; -use Respect\Validation\Validator; -use Respect\Validation\Validators\Core\Composite; - -use function array_filter; -use function array_map; -use function array_reduce; -use function count; +use Respect\Validation\Validators\Core\LogicalComposite; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -#[Template( - '{{subject}} must pass the rules', - '{{subject}} must pass the rules', - self::TEMPLATE_SOME, -)] -#[Template( - '{{subject}} must pass all the rules', - '{{subject}} must pass all the rules', - self::TEMPLATE_ALL, -)] -final class AllOf extends Composite +final readonly class AllOf extends LogicalComposite { - public const string TEMPLATE_ALL = '__all__'; - public const string TEMPLATE_SOME = '__some__'; - - public function evaluate(mixed $input): Result - { - $children = array_map(static fn(Validator $validator) => $validator->evaluate($input), $this->validators); - $valid = array_reduce($children, static fn(bool $carry, Result $result) => $carry && $result->hasPassed, true); - $failed = array_filter($children, static fn(Result $result): bool => !$result->hasPassed); - $template = self::TEMPLATE_SOME; - if (count($children) === count($failed)) { - $template = self::TEMPLATE_ALL; - } - - return Result::of($valid, $input, $this, [], $template)->withChildren(...$children); - } } diff --git a/src/Validators/AnyOf.php b/src/Validators/AnyOf.php index 5518e8045..032dc47c0 100644 --- a/src/Validators/AnyOf.php +++ b/src/Validators/AnyOf.php @@ -18,7 +18,7 @@ use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; -use Respect\Validation\Validators\Core\Composite; +use Respect\Validation\Validators\Core\LogicalComposite; use function array_map; use function array_reduce; @@ -28,7 +28,7 @@ '{{subject}} must pass at least one of the rules', '{{subject}} must pass at least one of the rules', )] -final class AnyOf extends Composite +final readonly class AnyOf extends LogicalComposite { public function evaluate(mixed $input): Result { diff --git a/src/Validators/Attributes.php b/src/Validators/Attributes.php index 2dab987cc..3f6550d5d 100644 --- a/src/Validators/Attributes.php +++ b/src/Validators/Attributes.php @@ -19,7 +19,6 @@ use Respect\Validation\Id; use Respect\Validation\Result; use Respect\Validation\Validator; -use Respect\Validation\Validators\Core\Reducer; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class Attributes implements Validator @@ -34,11 +33,8 @@ public function evaluate(mixed $input): Result $reflection = new ReflectionObject($input); $validators = [...$this->getClassValidators($reflection), ...$this->getPropertyValidators($reflection)]; - if ($validators === []) { - return (new AlwaysValid())->evaluate($input)->withId($id); - } - return (new Reducer(...$validators))->evaluate($input)->withId($id); + return (new Composite(...$validators))->evaluate($input)->withId($id); } /** @return array */ @@ -72,7 +68,7 @@ private function getPropertyValidators(ReflectionObject $reflection): array $allowsNull = $property->getType()?->allowsNull() ?? false; - $childRule = new Reducer(...$propertyValidators); + $childRule = new Composite(...$propertyValidators); $validators[] = new Property($propertyName, $allowsNull ? new NullOr($childRule) : $childRule); } diff --git a/src/Validators/Circuit.php b/src/Validators/Circuit.php index 8c0dd45dd..e73e58f57 100644 --- a/src/Validators/Circuit.php +++ b/src/Validators/Circuit.php @@ -13,10 +13,10 @@ use Attribute; use Respect\Validation\Result; -use Respect\Validation\Validators\Core\Composite; +use Respect\Validation\Validators\Core\LogicalComposite; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -final class Circuit extends Composite +final readonly class Circuit extends LogicalComposite { public function evaluate(mixed $input): Result { diff --git a/src/Validators/Composite.php b/src/Validators/Composite.php new file mode 100644 index 000000000..537c39983 --- /dev/null +++ b/src/Validators/Composite.php @@ -0,0 +1,66 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Validators; + +use Attribute; +use Respect\Validation\Message\Template; +use Respect\Validation\Result; +use Respect\Validation\Validator; + +use function array_filter; +use function array_map; +use function array_reduce; +use function count; + +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +#[Template( + '{{subject}} must pass the rules', + '{{subject}} must pass the rules', + self::TEMPLATE_SOME, +)] +#[Template( + '{{subject}} must pass all the rules', + '{{subject}} must pass all the rules', + self::TEMPLATE_ALL, +)] +readonly class Composite implements Validator +{ + public const string TEMPLATE_ALL = '__all__'; + public const string TEMPLATE_SOME = '__some__'; + + /** @var array */ + protected array $validators; + + public function __construct(Validator ...$validators) + { + $this->validators = $validators; + } + + public function evaluate(mixed $input): Result + { + $children = array_map(static fn(Validator $validator) => $validator->evaluate($input), $this->validators); + $valid = array_reduce($children, static fn(bool $carry, Result $result) => $carry && $result->hasPassed, true); + $failed = array_filter($children, static fn(Result $result): bool => !$result->hasPassed); + $template = self::TEMPLATE_SOME; + + if (count($children) === count($failed)) { + $template = self::TEMPLATE_ALL; + } + + return Result::of($valid, $input, $this, [], $template)->withChildren(...$children); + } + + /** @return array */ + public function getValidators(): array + { + return $this->validators; + } +} diff --git a/src/Validators/Core/Composite.php b/src/Validators/Core/LogicalComposite.php similarity index 51% rename from src/Validators/Core/Composite.php rename to src/Validators/Core/LogicalComposite.php index 3b4289608..9805565c9 100644 --- a/src/Validators/Core/Composite.php +++ b/src/Validators/Core/LogicalComposite.php @@ -11,22 +11,12 @@ namespace Respect\Validation\Validators\Core; use Respect\Validation\Validator; +use Respect\Validation\Validators\Composite; -use function array_merge; - -abstract class Composite implements Validator +abstract readonly class LogicalComposite extends Composite implements Validator { - /** @var non-empty-array */ - protected readonly array $validators; - public function __construct(Validator $validator1, Validator $validator2, Validator ...$validators) { - $this->validators = array_merge([$validator1, $validator2], $validators); - } - - /** @return non-empty-array */ - public function getValidators(): array - { - return $this->validators; + parent::__construct($validator1, $validator2, ...$validators); } } diff --git a/src/Validators/Core/Reducer.php b/src/Validators/Core/Reducer.php deleted file mode 100644 index f61297d44..000000000 --- a/src/Validators/Core/Reducer.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Validation\Validators\Core; - -use Respect\Validation\Result; -use Respect\Validation\Validator; -use Respect\Validation\Validators\AllOf; - -final readonly class Reducer implements Validator -{ - private Validator $validator; - - public function __construct(Validator $validator1, Validator ...$validators) - { - $this->validator = $validators === [] ? $validator1 : new AllOf($validator1, ...$validators); - } - - public function evaluate(mixed $input): Result - { - return $this->validator->evaluate($input); - } -} diff --git a/src/Validators/KeySet.php b/src/Validators/KeySet.php index 2c7a8efae..3ccc52cf0 100644 --- a/src/Validators/KeySet.php +++ b/src/Validators/KeySet.php @@ -21,7 +21,6 @@ use Respect\Validation\Validator; use Respect\Validation\ValidatorBuilder; use Respect\Validation\Validators\Core\KeyRelated; -use Respect\Validation\Validators\Core\Reducer; use function array_diff; use function array_filter; @@ -84,7 +83,7 @@ public function evaluate(mixed $input): Result return $arrayResult; } - $keys = new Reducer(...array_merge($this->validators, array_map( + $keys = new Composite(...array_merge($this->validators, array_map( static fn(string|int $key) => new Not(new KeyExists($key)), array_slice(array_diff(array_keys($input), $this->allKeys), 0, self::MAX_DIFF_KEYS), ))); diff --git a/src/Validators/NoneOf.php b/src/Validators/NoneOf.php index f7fb46d5c..16efc4b2a 100644 --- a/src/Validators/NoneOf.php +++ b/src/Validators/NoneOf.php @@ -17,7 +17,7 @@ use Attribute; use Respect\Validation\Message\Template; use Respect\Validation\Result; -use Respect\Validation\Validators\Core\Composite; +use Respect\Validation\Validators\Core\LogicalComposite; use function count; @@ -32,7 +32,7 @@ '{{subject}} must pass all the rules', self::TEMPLATE_ALL, )] -final class NoneOf extends Composite +final readonly class NoneOf extends LogicalComposite { public const string TEMPLATE_ALL = '__all__'; public const string TEMPLATE_SOME = '__some__'; diff --git a/src/Validators/OneOf.php b/src/Validators/OneOf.php index b3b5f3eee..05253fc42 100644 --- a/src/Validators/OneOf.php +++ b/src/Validators/OneOf.php @@ -19,7 +19,7 @@ use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; -use Respect\Validation\Validators\Core\Composite; +use Respect\Validation\Validators\Core\LogicalComposite; use function array_filter; use function array_map; @@ -38,7 +38,7 @@ '{{subject}} must pass only one of the rules', self::TEMPLATE_MORE_THAN_ONE, )] -final class OneOf extends Composite +final readonly class OneOf extends LogicalComposite { public const string TEMPLATE_NONE = '__none__'; public const string TEMPLATE_MORE_THAN_ONE = '__more_than_one__'; diff --git a/tests/src/SmokeTestProvider.php b/tests/src/SmokeTestProvider.php index 062944249..7d4e91251 100644 --- a/tests/src/SmokeTestProvider.php +++ b/tests/src/SmokeTestProvider.php @@ -48,6 +48,7 @@ public static function provideValidatorInput(): Generator yield 'Circuit' => [new vs\Circuit(new vs\IntVal(), new vs\GreaterThan(0)), 5]; yield 'Cnh' => [new vs\Cnh(), '02650306461']; yield 'Cnpj' => [new vs\Cnpj(), '11444777000161']; + yield 'Composite' => [new vs\Composite(), 'anything']; yield 'Consonant' => [new vs\Consonant(), 'bcdf']; yield 'Contains' => [new vs\Contains('needle'), 'haystack needle']; yield 'ContainsAny' => [new vs\ContainsAny(['a', 'b']), 'abc']; diff --git a/tests/src/Validators/Core/ConcreteComposite.php b/tests/src/Validators/Core/ConcreteLogicalComposite.php similarity index 75% rename from tests/src/Validators/Core/ConcreteComposite.php rename to tests/src/Validators/Core/ConcreteLogicalComposite.php index f1e779721..e7a763ca5 100644 --- a/tests/src/Validators/Core/ConcreteComposite.php +++ b/tests/src/Validators/Core/ConcreteLogicalComposite.php @@ -11,9 +11,9 @@ namespace Respect\Validation\Test\Validators\Core; use Respect\Validation\Result; -use Respect\Validation\Validators\Core\Composite; +use Respect\Validation\Validators\Core\LogicalComposite; -final class ConcreteComposite extends Composite +final readonly class ConcreteLogicalComposite extends LogicalComposite { public function evaluate(mixed $input): Result { diff --git a/tests/unit/Validators/Core/ReducerTest.php b/tests/unit/Validators/CompositeTest.php similarity index 55% rename from tests/unit/Validators/Core/ReducerTest.php rename to tests/unit/Validators/CompositeTest.php index 773ea48da..05d7d4ad5 100644 --- a/tests/unit/Validators/Core/ReducerTest.php +++ b/tests/unit/Validators/CompositeTest.php @@ -8,24 +8,32 @@ declare(strict_types=1); -namespace Respect\Validation\Validators\Core; +namespace Respect\Validation\Validators; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Respect\Validation\Test\Validators\Stub; -use Respect\Validation\Validators\AllOf; -#[CoversClass(Reducer::class)] -final class ReducerTest extends TestCase +#[CoversClass(Composite::class)] +final class CompositeTest extends TestCase { #[Test] - public function shouldWrapTheSingleRule(): void + public function shouldWrapAlwaysValidWhenNoValidatorsAreProvided(): void + { + $composite = new Composite(); + $result = $composite->evaluate(null); + + self::assertEquals(new AlwaysValid(), $result->validator); + } + + #[Test] + public function shouldWrapTheSingleValidator(): void { $validator = Stub::any(1); - $reducer = new Reducer($validator); - $result = $reducer->evaluate(null); + $composite = new Composite($validator); + $result = $composite->evaluate(null); self::assertSame($validator, $result->validator); } @@ -37,8 +45,8 @@ public function shouldWrapWhenThereAreMultipleValidators(): void $validator2 = Stub::any(1); $validator3 = Stub::any(1); - $reducer = new Reducer($validator1, $validator2, $validator3); - $result = $reducer->evaluate(null); + $composite = new Composite($validator1, $validator2, $validator3); + $result = $composite->evaluate(null); self::assertEquals(new AllOf($validator1, $validator2, $validator3), $result->validator); } diff --git a/tests/unit/Validators/Core/CompositeTest.php b/tests/unit/Validators/Core/CompositeTest.php index a942e5e93..d0bc53cb7 100644 --- a/tests/unit/Validators/Core/CompositeTest.php +++ b/tests/unit/Validators/Core/CompositeTest.php @@ -14,18 +14,18 @@ use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use Respect\Validation\Test\TestCase; -use Respect\Validation\Test\Validators\Core\ConcreteComposite; +use Respect\Validation\Test\Validators\Core\ConcreteLogicalComposite; use Respect\Validation\Test\Validators\Stub; #[Group('core')] -#[CoversClass(Composite::class)] +#[CoversClass(LogicalComposite::class)] final class CompositeTest extends TestCase { #[Test] public function itShouldReturnItsChildren(): void { $expected = [Stub::daze(), Stub::daze(), Stub::daze()]; - $sut = new ConcreteComposite(...$expected); + $sut = new ConcreteLogicalComposite(...$expected); $actual = $sut->getValidators(); self::assertCount(3, $actual);