Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 2.5.2 under development

- Bug #791: Allow access to sibling properties in nested validation context (@WarLikeLaux)
Comment thread
samdark marked this conversation as resolved.
Outdated
Comment thread
samdark marked this conversation as resolved.
Outdated
- Enh #787: Explicitly import classes, functions, and constants in "use" section (@mspirkov)

## 2.5.1 December 12, 2025
Expand Down
15 changes: 14 additions & 1 deletion src/Rule/NestedHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use function is_array;
use function is_int;
use function is_object;
use function array_key_exists;
use function count;

/**
* A handler for {@see Nested} rule. Validates nested structures.
Expand Down Expand Up @@ -93,8 +95,19 @@ public function validate(mixed $value, RuleInterface $rule, ValidationContext $c
} else {
$valuePathList = StringHelper::parsePath($valuePath);
$property = end($valuePathList);

$scopeData = $data;
for ($i = 0, $limit = count($valuePathList) - 1; $i < $limit; $i++) {
$key = $valuePathList[$i];
if (!is_array($scopeData) || !array_key_exists($key, $scopeData)) {
$scopeData = [];
Comment on lines +99 to +103
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After switching to passing $scopeData into context->validate(), the earlier $validatedValue = ArrayHelper::getValueByPath($data, $valuePath); is only used for the is_int($valuePath) branch. Consider moving that getValueByPath() call into the integer-path branch (or otherwise avoiding it for string paths) to prevent doing a potentially expensive path lookup for every nested rule.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WarLikeLaux would you please check this one? Looks serious.

break;
}
$scopeData = $scopeData[$key];
}

$itemResult = $context->validate(
ArrayHelper::keyExists($data, $valuePathList) ? [$property => $validatedValue] : [],
$scopeData,
[$property => $rules],
);
}
Expand Down
84 changes: 84 additions & 0 deletions tests/Rule/Nested/NestedTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,54 @@ public function getRules(): iterable
['' => 17],
new Nested(['' => new Integer(min: 15)]),
],
'sibling access skips rule when sibling is false' => [
[
'push' => [
'isEnabled' => false,
'content' => null,
],
],
new Nested([
'push' => new Nested([
'isEnabled' => [new BooleanValue()],
'content' => [
new Required(
when: static function (mixed $value, ValidationContext $context): bool {
return (bool) $context->getDataSet()->getPropertyValue('isEnabled');
},
),
],
]),
]),
],
'sibling access with each and nested' => [
[
'tasks' => [
[
'push' => [
'isEnabled' => false,
'content' => null,
],
],
],
],
new Nested([
'tasks' => new Each(
new Nested([
'push' => new Nested([
'isEnabled' => [new BooleanValue()],
'content' => [
new Required(
when: static function (mixed $value, ValidationContext $context): bool {
return (bool) $context->getDataSet()->getPropertyValue('isEnabled');
},
),
],
]),
]),
),
]),
],
];
}

Expand Down Expand Up @@ -1284,6 +1332,29 @@ public function hasProperty(string $property): bool
'properties.abc' => ['Abc cannot be blank.'],
],
],
'sibling access in when callback' => [
[
'push' => [
'isEnabled' => true,
'content' => null,
],
],
new Nested([
'push' => new Nested([
'isEnabled' => [new BooleanValue()],
'content' => [
new Required(
when: static function (mixed $value, ValidationContext $context): bool {
return (bool) $context->getDataSet()->getPropertyValue('isEnabled');
},
),
],
]),
]),
[
'push.content' => ['Content cannot be blank.'],
],
],
'deep level of nesting with plain keys' => [
[
'level1' => [
Expand Down Expand Up @@ -1313,6 +1384,19 @@ public function hasProperty(string $property): bool
'level1.level2.level3.name' => ['Name must contain at least 5 characters.'],
],
],
'dotted path with missing intermediate key' => [
[
'level1' => [
'x' => 1,
],
],
new Nested([
'level1.level2.key' => [new Required()],
]),
[
'level1.level2.key' => ['Key not passed.'],
],
],
'error messages with properties in nested structure' => [
[
'user' => [
Expand Down
Loading