Skip to content

Commit ccc2ce1

Browse files
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 <alganet@gmail.com> Assisted-by: Claude Code (Claude Opus 4.5)
1 parent b352f17 commit ccc2ce1

30 files changed

+196
-108
lines changed

docs/validators.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ In this page you will find a list of validators by their category.
1717

1818
**Comparisons**: [All][] - [Between][] - [BetweenExclusive][] - [Equals][] - [Equivalent][] - [GreaterThan][] - [GreaterThanOrEqual][] - [Identical][] - [In][] - [Length][] - [LessThan][] - [LessThanOrEqual][] - [Max][] - [Min][]
1919

20-
**Composite**: [AllOf][] - [AnyOf][] - [Circuit][] - [NoneOf][] - [OneOf][]
20+
**Composite**: [AllOf][] - [AnyOf][] - [Circuit][] - [Composite][] - [NoneOf][] - [OneOf][]
2121

2222
**Conditions**: [Circuit][] - [Not][] - [When][]
2323

@@ -41,7 +41,7 @@ In this page you will find a list of validators by their category.
4141

4242
**Miscellaneous**: [Blank][] - [Falsy][] - [Masked][] - [Named][] - [Templated][] - [Undef][]
4343

44-
**Nesting**: [AllOf][] - [AnyOf][] - [Call][] - [Circuit][] - [Each][] - [Key][] - [KeySet][] - [Lazy][] - [NoneOf][] - [Not][] - [NullOr][] - [OneOf][] - [Property][] - [PropertyOptional][] - [UndefOr][] - [When][]
44+
**Nesting**: [AllOf][] - [AnyOf][] - [Call][] - [Circuit][] - [Composite][] - [Each][] - [Key][] - [KeySet][] - [Lazy][] - [NoneOf][] - [Not][] - [NullOr][] - [OneOf][] - [Property][] - [PropertyOptional][] - [UndefOr][] - [When][]
4545

4646
**Numbers**: [Base][] - [Decimal][] - [Digit][] - [Even][] - [Factor][] - [Finite][] - [FloatType][] - [FloatVal][] - [Infinite][] - [IntType][] - [IntVal][] - [Multiple][] - [Negative][] - [Number][] - [NumericVal][] - [Odd][] - [Positive][] - [Roman][]
4747

@@ -82,6 +82,7 @@ In this page you will find a list of validators by their category.
8282
- [Circuit][] - `v::circuit(v::intVal(), v::floatVal())->assert(15);`
8383
- [Cnh][] - `v::cnh()->assert('02650306461');`
8484
- [Cnpj][] - `v::cnpj()->assert('00394460005887');`
85+
- [Composite][] - `v::composite()->assert('anything');`
8586
- [Consonant][] - `v::consonant()->assert('xkcd');`
8687
- [Contains][] - `v::contains('ipsum')->assert('lorem ipsum');`
8788
- [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.
238239
[Circuit]: validators/Circuit.md "Validates the input against a series of validators until the first fails."
239240
[Cnh]: validators/Cnh.md "Validates a Brazilian driver's license."
240241
[Cnpj]: validators/Cnpj.md "Validates if the input is a Brazilian National Registry of Legal Entities (CNPJ) number."
242+
[Composite]: validators/Composite.md "Consolidates zero or more validators into a single validator."
241243
[Consonant]: validators/Consonant.md "Validates if the input contains only consonants."
242244
[Contains]: validators/Contains.md "Validates if the input contains some value."
243245
[ContainsAny]: validators/ContainsAny.md "Validates if the input contains at least one of defined values"

docs/validators/Composite.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<!--
2+
SPDX-FileCopyrightText: (c) Respect Project Contributors
3+
SPDX-License-Identifier: MIT
4+
-->
5+
6+
# Composite
7+
8+
- `Composite()`
9+
- `Composite(Validator ...$validators)`
10+
11+
Consolidates zero or more validators into a single validator.
12+
13+
- When no validators are provided, it acts as [AlwaysValid](AlwaysValid.md).
14+
- When a single validator is provided, it acts as a pass-through.
15+
- When multiple validators are provided, they are combined using [AllOf](AllOf.md), meaning all validators must pass for the input to be considered valid.
16+
17+
```php
18+
v::composite()->assert('anything');
19+
// Validation passes successfully
20+
21+
v::composite(v::intType())->assert(42);
22+
// Validation passes successfully
23+
24+
v::composite(v::intType(), v::positive(), v::lessThan(100))->assert(42);
25+
// Validation passes successfully
26+
27+
v::composite(v::intType(), v::positive())->assert(-5);
28+
// → -5 must be a positive number
29+
```
30+
31+
## Use cases
32+
33+
This validator is useful for dynamically building validation chains or when you
34+
have a variable number of validators that need to be applied together.
35+
36+
```php
37+
$validators = [v::stringType(), v::notSpaced()];
38+
39+
if (true) { // some condition
40+
$validators[] = v::email();
41+
}
42+
43+
v::composite(...$validators)->assert('respectpanda#example.com');
44+
// → "respectpanda#example.com" must be a valid email address
45+
```
46+
47+
## Categorization
48+
49+
- Composite
50+
- Nesting
51+
52+
## Changelog
53+
54+
| Version | Description |
55+
| ------: | :---------- |
56+
| 3.0.0 | Created |
57+
58+
## See Also
59+
60+
- [AllOf](AllOf.md)
61+
- [AlwaysValid](AlwaysValid.md)
62+
- [AnyOf](AnyOf.md)
63+
- [NoneOf](NoneOf.md)
64+
- [OneOf](OneOf.md)

src/Mixins/AllBuilder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public static function allCnh(): Chain;
6262

6363
public static function allCnpj(): Chain;
6464

65+
public static function allComposite(Validator ...$validators): Chain;
66+
6567
public static function allConsonant(string ...$additionalChars): Chain;
6668

6769
public static function allContains(mixed $containsValue): Chain;

src/Mixins/AllChain.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public function allCnh(): Chain;
6262

6363
public function allCnpj(): Chain;
6464

65+
public function allComposite(Validator ...$validators): Chain;
66+
6567
public function allConsonant(string ...$additionalChars): Chain;
6668

6769
public function allContains(mixed $containsValue): Chain;

src/Mixins/Builder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ public static function cnh(): Chain;
6767

6868
public static function cnpj(): Chain;
6969

70+
public static function composite(Validator ...$validators): Chain;
71+
7072
public static function consonant(string ...$additionalChars): Chain;
7173

7274
public static function contains(mixed $containsValue): Chain;

src/Mixins/Chain.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ public function cnh(): Chain;
6969

7070
public function cnpj(): Chain;
7171

72+
public function composite(Validator ...$validators): Chain;
73+
7274
public function consonant(string ...$additionalChars): Chain;
7375

7476
public function contains(mixed $containsValue): Chain;

src/Mixins/KeyBuilder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ public static function keyCnh(int|string $key): Chain;
6464

6565
public static function keyCnpj(int|string $key): Chain;
6666

67+
public static function keyComposite(int|string $key, Validator ...$validators): Chain;
68+
6769
public static function keyConsonant(int|string $key, string ...$additionalChars): Chain;
6870

6971
public static function keyContains(int|string $key, mixed $containsValue): Chain;

src/Mixins/KeyChain.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ public function keyCnh(int|string $key): Chain;
6464

6565
public function keyCnpj(int|string $key): Chain;
6666

67+
public function keyComposite(int|string $key, Validator ...$validators): Chain;
68+
6769
public function keyConsonant(int|string $key, string ...$additionalChars): Chain;
6870

6971
public function keyContains(int|string $key, mixed $containsValue): Chain;

src/Mixins/NotBuilder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ public static function notCnh(): Chain;
6464

6565
public static function notCnpj(): Chain;
6666

67+
public static function notComposite(Validator ...$validators): Chain;
68+
6769
public static function notConsonant(string ...$additionalChars): Chain;
6870

6971
public static function notContains(mixed $containsValue): Chain;

src/Mixins/NotChain.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ public function notCnh(): Chain;
6464

6565
public function notCnpj(): Chain;
6666

67+
public function notComposite(Validator ...$validators): Chain;
68+
6769
public function notConsonant(string ...$additionalChars): Chain;
6870

6971
public function notContains(mixed $containsValue): Chain;

0 commit comments

Comments
 (0)