You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fix double validation and state leaks in Attributes recursion
Recursive validation for nested objects guarded against validating a
nested object twice by checking whether the property already
carried #[Attributes] directly on the property. That guard did not
recognise the rule inside a wrapper, and the state backing the
recursion was never released, producing five defects:
- #[NullOr(new Attributes())] on a class-typed property validated
the nested object twice and reported every nested failure twice. This
was the documented way of validating a nullable nested object before
recursion existed, so upgrading silently duplicated messages.
- The same object held by two sibling properties failed as a circular
reference, because visited objects accumulated for the whole traversal
instead of the current path.
- An Attributes instance could only be used once: nothing ever cleared
the visited objects, so every evaluation after the first failed.
- A union type whose value satisfied more than one of its class members
recursed once per member, and the second one reported the object it
had just visited as a circular reference.
- A custom validator attribute with cyclic internal state made wrapped-rule
detection recurse indefinitely.
Detect the rule anywhere inside a property's attributes rather than only at
the top level, so a wrapped Attributes suppresses implicit recursion as an
explicit one does. Make the rule immutable and give each recursion level its
own instance carrying the path to the object being evaluated, so visited
objects represent the path from the root rather than every object ever seen.
Collapse union recursion into a single Given over the disjunction of its
class members. Track the validators visited while inspecting a wrapped rule,
preventing a cyclic validator graph from overflowing the stack.
The detection only runs for properties whose type can hold an object, so
attributes with large arguments, such as #[In], no longer pay for it.
Copy file name to clipboardExpand all lines: docs/validators/Attributes.md
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -91,10 +91,18 @@ When a property's type is a class (named, union, or intersection type), `Attribu
91
91
-**Untyped properties** (no type declaration, or builtin types like `string`): are never recursively validated.
92
92
-**Array properties**: `Attributes`**does not** recursively validate objects inside arrays. To validate each element, use the `#[Each]` attribute on the property (e.g., `#[Each(new Attributes())]`).
93
93
94
+
The implicit recursion is skipped whenever the property's own attributes already contain an `Attributes` rule, so the nested object is never validated twice. That holds for `#[Attributes]` written directly on the property and for an `Attributes` wrapped in another rule, like `#[NullOr(new Attributes())]`, `#[Each(new Attributes())]`, or `#[Given(new Instance(Address::class), new Attributes())]`. It holds even when the wrapper applies the rule to something other than the property itself, as in `#[Property('street', new Attributes())]`, so writing `Attributes` by hand always puts you in full control of what gets validated.
95
+
96
+
Any other attribute on the property is combined with the implicit recursion instead of replacing it, so `#[Instance(Address::class)] public Address $address` still validates the nested object's own attributes.
97
+
94
98
### Circular references
95
99
96
100
When a nested object graph contains a cycle (e.g., `$a->next = $b`, `$b->next = $a`), `Attributes` detects the revisit and fails with the `TEMPLATE_CIRCULAR_REFERENCE` template. This prevents infinite recursion and stack overflow.
97
101
102
+
Detection is per *path*: only objects between the root and the property being validated count as a revisit. The same object reachable from two sibling properties (e.g., `$order->billing` and `$order->shipping` holding one `Address`) is not a cycle, and is validated on each path. The cost of validating an object graph is therefore proportional to how many distinct paths it has, not to how many objects it has, which is worth keeping in mind for graphs where many properties point at the same deeply nested objects.
103
+
104
+
Detection covers the recursion `Attributes` performs on its own. An `Attributes` you write yourself in an attribute, such as `#[NullOr(new Attributes())]`, starts a traversal of its own and knows nothing about the objects already visited, so a cycle reached through it recurses infinitely.
105
+
98
106
Note that circular reference detection only works for direct object references. If a cycle passes through an array (e.g., `$a->items = [$b]`, `$b->parent = $a`), `Attributes` cannot track the reference and the validation will recurse infinitely, causing a stack overflow.
0 commit comments