Single source of truth for planned and considered work. Completed work lives in CHANGELOG.md.
- The
pipe()/transform()model with coercion is the core value to protect. It is the library's defining feature; weigh every change first against whether it keeps that flow clean and pleasant to use, and never trade its ergonomics away for other goals. - Extensibility over reinvention — integrate external libraries via
transform()/satisfies()rather than rebuilding every helper. - Validate first, then transform.
- Mutable fluent API by design — chained calls mutate and return the same instance (like Laravel's query builder); use
clone()to fork a configured validator. This is a settled decision and is not revisited for v1.0.
- Optional by default (null allowed unless
required()); form-safe coercion (empty string → null, not0/false); pipeline order guaranteed; fail-fast per field; schema validation aggregates errors across fields.
These are the changes that touch the public contract and are therefore best done before committing to API stability.
One unified design covering three things that share the same error object. The shape of what ValidationException exposes is a public contract, so it must be settled before v1.0.
- Structured error codes (e.g.
STRING_TOO_SHORT,INVALID_EMAIL) for programmatic handling and i18n. Stable machine handles decouple error identity from human wording, so messages can be reworded freely after v1.0. - Full error paths for nested structures (
user.address.street) instead of bare leaf keys. - Message placeholders (
{value},{index},{min}, …) for custom messages and localization; pair with global/default message templates for consistent branding. - Keep the current flat
getErrors()as a backward-compatible view; addgetStructuredErrors()returning the error value objects.
Remove the deprecated instance aliases addValidation, allOf, anyOf, not; the combinators live on Validator:: (now backed by MixedValidator). Update call sites and docs.
strict()— reject undeclared keys (completes thedefault/passthrough()/strict()trio).isInstance(ClassName::class)— validate object instances (closes a type-coverage gap alongside scalars, arrays, and enums).
- Docs refresh: ensure
README.md,docs/, andllms.txtmatch the final v1.0 surface; add migration notes for the deprecation removals.
None of these touch the public contract, so they are strictly better landed after v1.0.
partial(),pick(),omit(),merge()for schema variations (PATCH requests, API versioning).forbidKeys(array $keys, ?string $message = null)— explicit key deny-listing.patternProperties(),propertyNames()— key validation.dependencies()— cross-field dependencies.- Tuple validation /
additionalItems()for arrays.
Validator::conditional($discriminator, [...])for polymorphic data (select a schema based on input).
- Mutation testing pilot (Infection + baseline config, documented local run).
- Property-based tests for core validators (string patterns, numeric constraints).
- Performance benchmarking for hot paths (
validate,tryValidate, schema validation).
Kept for context; intentionally out of the lightweight core scope. Adoption is a secondary concern, so only pursue the cheap, well-aligned ones.
- Framework middleware (e.g. a PSR-15 / Laravel / Symfony bridge) — the most reasonable of these and an adoption aid. Thin glue: wrap
validate(), mapValidationExceptionto a 422 response. Low effort, but only worthwhile after the structured error model lands (the point is machine-readable errors in the response). - Schema export to a standard format (OpenAPI / JSON Schema) for frontend/backend sync — one-directional only. Depends on built-in constraints carrying machine-readable metadata, which the error model introduces; closures from
satisfies()/transform()are not introspectable, so export covers only the declarative surface. (NofromJson()/ round-trip: arbitrary closures cannot be reconstructed, and a lossy serializer that silently drops custom rules would be a footgun.) - Database-driven validation (e.g.
lemmon/validator-doctrine) — afromDatabaseTable()generator that derives a validator from DB schema. Niche, heavy deps; far-future. (Rules that query the DB need no core support — usesatisfies().)
filled()— userequired()+notEmpty()(optionallynullifyEmpty()orpipe('trim')).optional()/nullable()— redundant; fields are optional by default.when()— use external control flow or context-awaresatisfies().- Specialized string / identifier / type-conversion helpers (trim/slugify/case, cuid2/nanoid/ulid,
toDateTime(), JSON decode) — usetransform()/satisfies()with external libraries. - Validation analytics, A/B testing, result caching — out of scope for a focused validation core.