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(validator): isolate shared validator state across clones and schema reuse
- Clone schema field validators at construction time so shared instances do not leak mutations into schemas
- Make coerceAll() recursive through nested schemas and array item validators; each field is cloned so shared definitions are not mutated
- Clone FieldValidator operands in satisfies(), satisfiesAll(), satisfiesAny(), satisfiesNone(), and contains() at capture time; rebuild on __clone() to isolate pipeline state
- Reset transient currentType before and after each tryValidate() run so transform/pipe validators remain repeatable across calls and clones
- Track bindable flag per pipeline step so __clone() skips Closure::bindTo() on static closures (e.g. from in()), avoiding PHP warnings
- Add defaultUsing(callable) for fresh mutable defaults per validation run; default() returns objects by handle
- Simplify FloatValidator coercion (remove redundant ternary)
Copy file name to clipboardExpand all lines: CHANGELOG.md
+7Lines changed: 7 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,9 +7,16 @@ All notable changes to this project will be documented in this file.
7
7
### Fixed
8
8
9
9
-`BoolValidator::coerce()` no longer string-casts non-scalar values (arrays triggered `E_WARNING`, objects without `__toString` threw `Error`); non-scalars are passed through to type validation, which raises `ValidationException` as for other invalid types
10
+
-`coerceAll()` no longer calls `coerce()` on schema field validators in place (which leaked coercion to every other schema reusing the same `FieldValidator` instance); `coerceAll()` now replaces each field with a clone that has coercion enabled
11
+
-`coerceAll()` is now recursive: nested `AssociativeValidator`, `ObjectValidator`, and `ArrayValidator` (with item validators) all propagate coercion to their children; previously only top-level schema fields were coerced
12
+
-`FieldValidator::__clone()` skips `Closure::bindTo()` for static closures (for example from `in()`), avoiding PHP warnings when cloning such validators; pipeline steps now carry a `bindable` flag set at insertion time so cloning no longer needs `ReflectionFunction`
13
+
-`satisfies()`, `satisfiesAll()`, `satisfiesAny()`, `satisfiesNone()`, and `ArrayValidator::contains()` now clone any `FieldValidator` operand at capture time and rebuild those operands when the outer validator is cloned, so later mutations or clone-local pipeline state do not leak across validator instances
14
+
-`FieldValidator::tryValidate()` now resets transient type-tracking before and after every validation run, so validators that use `transform()`/`pipe()` remain repeatable across multiple calls and clones do not inherit stale runtime state
15
+
-`default()` no longer deep-copies object values; object defaults are returned as-is (shared by handle). Use `defaultUsing()` when each validation run needs a fresh object instance
10
16
11
17
### Added
12
18
19
+
-`defaultUsing(callable $factory)` on `FieldValidator`: invokes the factory whenever validation resolves to null, providing a fresh default per validation run; intended for mutable domain objects or other defaults that should not be shared across validations or clones
13
20
-`passthrough()` on `AssociativeValidator` and `ObjectValidator`: copy undeclared keys or public properties from the input to the validated output without validating them; declared schema fields are still validated; values already written from the schema (including `outputKey` targets) are not overwritten by passthrough
Sets a default value when validation passes but the value is null.
709
709
710
+
Scalars and flat arrays (no nested objects) are safe because PHP copies them by value. Object defaults and arrays containing objects are returned as-is, sharing the same object handles across runs; use `defaultUsing()` when the default contains any mutable objects.
Invokes the factory whenever validation resolves to null, so each validation run receives a fresh default value.
725
+
726
+
```php
727
+
$validator = Validator::isObject()->defaultUsing(
728
+
static fn() => (object) ['active' => true],
729
+
);
730
+
731
+
$result = $validator->validate(null); // Returns a new stdClass each time
732
+
```
733
+
734
+
Use this whenever the default contains mutable objects -- whether a plain object or an array with nested objects. Each call to `validate()` will invoke the factory and receive a fresh instance.
735
+
736
+
---
737
+
720
738
### `outputKey(string $key): self`
721
739
722
740
**Schema fields only.** Emits the validated value under a different key than the input field. Only applies when the validator is used inside `Validator::isAssociative()` or `Validator::isObject()`.
> **Scope:**`coerceAll()` propagates through schema fields, nested schemas, and array item validators. It does not reach into assertion operands -- validators passed to `satisfies()`, `satisfiesAll()`, `satisfiesAny()`, `satisfiesNone()`, or `contains()`. Call `coerce()` on those validators individually when needed.
Copy file name to clipboardExpand all lines: llms.txt
+3-1Lines changed: 3 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -11,6 +11,7 @@
11
11
- `pipe(callable)`: Skips `null` values (type-preserving, expects specific type).
12
12
- `transform(callable, bool $skipNull = true)`: Skips `null` by default (most type conversions don't need null). Set `$skipNull = false` to process null values.
13
13
- **Pipeline Order:** Pipeline steps execute in the exact order defined. `default()` and `required()` are flags evaluated after the pipeline regardless of position: `default()` is the last-resort fallback for null, `required()` enforces presence after default has had its chance.
14
+
- **Mutable Defaults:** `default()` stores the value as-is; scalars and flat arrays are copied by value in PHP, but objects (and arrays containing objects) share handles across runs. Use `defaultUsing()` when the default contains any mutable objects.
14
15
- **Transformation Types:**
15
16
- `pipe(callable)`: Intended for sanitization. Preserves type context and applies type-coercion to result.
16
17
- `transform(callable)`: Intended for type conversion. Updates type context; no coercion.
@@ -54,6 +55,7 @@ _Available on all validators_
54
55
```php
55
56
required(?string $message = null): static
56
57
default(mixed $value): static
58
+
defaultUsing(callable(): mixed $factory): static
57
59
coerce(): static
58
60
outputKey(string $key): static (schema fields only: output under different key, e.g. input service_id -> output service)
59
61
nullifyEmpty(): static (transforms '' or [] to null)
coerceAll(): static // Recursively enables coercion on schema fields
152
+
coerceAll(): static // Recursively enables coercion on schema fields, nested schemas, and array item validators (each validator is cloned so shared instances are not mutated); does not propagate into assertion operands (satisfies, contains, etc.) -- call coerce() on those individually
151
153
passthrough(): static // Copy undeclared keys/properties from input to output without validating them; schema fields still validated; does not overwrite keys already set from the schema (including outputKey targets)
0 commit comments