Add EachKey validator for validating array keys#1793
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1793 +/- ##
=========================================
Coverage 99.42% 99.42%
- Complexity 1034 1042 +8
=========================================
Files 195 196 +1
Lines 2416 2444 +28
=========================================
+ Hits 2402 2430 +28
Misses 14 14 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds a new EachKey validator to make array/iterable key validation composable (mirroring how Each validates values), plus the supporting fluent-builder API surface, documentation, and tests.
Changes:
- Introduces
Respect\Validation\Validators\EachKeywith templates and short-circuit support. - Adds unit + feature coverage for validation behavior, error messaging, naming/templating, and short-circuit mode.
- Updates docs and fluent mixin interfaces/builders so
v::eachKey(...)and related chain variants are discoverable, and includes the validator in smoke tests and docs index.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/Validators/EachKeyTest.php | Unit coverage for EachKey validation + short-circuit behavior. |
| tests/feature/Validators/EachKeyTest.php | Feature coverage for rendered messages/templates, naming, and short-circuit output. |
| tests/src/SmokeTestProvider.php | Adds EachKey to the smoke-test validator inventory. |
| src/Validators/EachKey.php | Implements the new validator and its templates/short-circuit evaluation. |
| src/Mixins/UndefOrChain.php | Adds undefOrEachKey(...) to fluent chain interface. |
| src/Mixins/UndefOrBuilder.php | Adds undefOrEachKey(...) to fluent static builder interface. |
| src/Mixins/PropertyChain.php | Adds propertyEachKey(...) to fluent chain interface. |
| src/Mixins/PropertyBuilder.php | Adds propertyEachKey(...) to fluent static builder interface. |
| src/Mixins/NullOrChain.php | Adds nullOrEachKey(...) to fluent chain interface. |
| src/Mixins/NullOrBuilder.php | Adds nullOrEachKey(...) to fluent static builder interface. |
| src/Mixins/NotChain.php | Adds notEachKey(...) to fluent chain interface. |
| src/Mixins/NotBuilder.php | Adds notEachKey(...) to fluent static builder interface. |
| src/Mixins/KeyChain.php | Adds keyEachKey(...) to fluent chain interface. |
| src/Mixins/KeyBuilder.php | Adds keyEachKey(...) to fluent static builder interface. |
| src/Mixins/Chain.php | Adds eachKey(...) to the core fluent chain interface (v::...). |
| src/Mixins/Builder.php | Adds eachKey(...) to the core fluent static builder interface. |
| src/Mixins/AllChain.php | Adds allEachKey(...) to fluent chain interface. |
| src/Mixins/AllBuilder.php | Adds allEachKey(...) to fluent static builder interface. |
| docs/validators/EachKey.md | New validator documentation and examples. |
| docs/validators/Each.md | Cross-links to EachKey in “See Also”. |
| docs/validators.md | Adds EachKey to category lists and alphabetical index. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
alganet
left a comment
There was a problem hiding this comment.
Looks good, just a few nits:
- The messages are effort sensitive. If it makes it too hard to use the actual keys instead of the
.dotrepresentation we can make a compromise. - The wording (switching "in" to "of" in the main message, dropping "of" in the items) is my opinion. I suggest passing the messages through an LLM for grammar checking.
- Non-scalar keys (in advanced iterators) are niche and I understand if that complicates too much the implementation. If we decide to not do it, perhaps we should think of making the
IterableTypecheck more stricter (rawArrayTypethat would block those advanced iterators from being used or equivalent).
|
The problem is that some validators (like Using actual keys (no
|
| Scenario | Output |
|---|---|
| Standalone | The key "a" must be an integer ✅ |
Inside key(data) |
The key .data must be an integer ❌ both keys identical |
Deep key(user, key(settings)) |
The key .user.settings must be an integer ❌ same |
getFullMessage():
- Each key of `.data` must be valid
- The key `.data` must be an integer
- The key `.data` must be an integer
So using actual keys isn't viable when composed inside key(). The compromise I'd suggest is keeping the .dot representation but keeping "The key of":
"The key of" (current) — getMessage()
| Scenario | Output |
|---|---|
Inside key(data) |
The key of .data.a must be an integer |
Deep key(user, key(settings)) |
The key of .user.settings.theme must be an integer |
"The key" — getMessage()
| Scenario | Output |
|---|---|
Inside key(data) |
The key .data.a must be an integer |
Deep key(user, key(settings)) |
The key .user.settings.theme must be an integer |
"The key .data.a" reads as if .data.a is a key name — but a is the key and .data is just the parent path.
"The key of .data.a" separates "key" from the path — .data.a is the location, not the key's name. The root message already scopes it ("Each key of .data must be valid"), so the child just needs to say which location and what failed.
I think "The key of" is the best compromise here.
|
@henriquemoody what about dropping the entire prefix? ".data.a must be an integer" Just that, no "The key" nor "The key of". Edit: in retrospect, not using the word "key" might make the message opaque and misleading (user thinks it's a value or something). I'm ok with the compromise. |
|
How about something like that? |
8ef8f3a to
a9a7380
Compare
Each validates array values but there is no built-in way to validate
their keys. The only workaround is transforming the input first with
v::after('array_keys', v::each(...)), which is indirect and loses the
composability that makes the library expressive. Without EachKey,
validating something as common as array<string, int> requires an
awkward transformation instead of a readable composition.
EachKey fills that gap by validating each key in an iterable against an
inner validator, mirroring how Each validates values. This makes
array-key constraints a first-class concern: v::eachKey(v::stringType())
is immediately readable, and combining it with Each via AllOf gives a
clean expression for typed arrays like
v::allOf(v::eachKey(v::stringType()), v::each(v::intType())).
Each validates array values but there is no built-in way to validate their keys. The only workaround is transforming the input first with v::after('array_keys', v::each(...)), which is indirect and loses the composability that makes the library expressive. Without EachKey, validating something as common as array<string, int> requires an awkward transformation instead of a readable composition.
EachKey fills that gap by validating each key in an iterable against an inner validator, mirroring how Each validates values. This makes array-key constraints a first-class concern: v::eachKey(v::stringType()) is immediately readable, and combining it with Each via AllOf gives a clean expression for typed arrays like
v::allOf(v::eachKey(v::stringType()), v::each(v::intType())).