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. - Type factories are strict unless coercion is enabled:
isInt()accepts integers,isFloat()accepts floats and integers (PHP's ownstrict_typesint-to-float widening; JSON cannot express a whole-number float),isArray()accepts lists,isAssociative()accepts non-list arrays (plus the ambiguous empty array), andisObject()acceptsstdClass. - Structured errors keep exact
int|stringpath segments as their source of truth and derive the dottedgetPath()view. String path filters remain convenient; segment-list filters are available when keys contain dots, are empty, or must distinguish an integer index from a numeric string key.
These are the changes that touch the public contract and are therefore best done before committing to API stability.
The unified structured error contract is in place:
- 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. - A single
ValidationException::getErrors(string|array|null $path = null)accessor returning a flat list ofValidationErrorobjects.ValidationErroris JSON-serializable, exposes bothgetPath()andgetSegments(), and accepts a segment list in its constructor for exact custom paths.
Before v1.0, perform one final naming/parameter audit of ValidationCode and the JSON error shape. After v1.0, codes and serialized field names are stable integration contracts.
Remove the deprecated aliases addValidation, instance allOf, instance anyOf, instance not, and oneOf. The combinators live on Validator::; allowed-value validation uses in(). Update call sites, tests, and migration notes.
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).
- Audit every class in the runtime namespace and mark implementation-only types
@internalor remove them before the namespace becomes stable.PipelineTypewas removed because it was unused internal metadata;MixedValidator,PipelineStep, andPipelineContextare implementation details. - Keep the v1.0 promise precise: Lemmon provides runtime validation and transformation. Because arbitrary
transform()calls can change output type and validators are mutable,validate()and the data element oftryValidate()remainmixed; static schema-output inference is not part of the v1.0 contract. - Decide whether subclassing
FieldValidatoris supported. The current extension point issatisfies()/transform(), so unsupported inheritance should be made explicit before v1.0 rather than left accidental.
- Docs refresh: ensure
README.md,docs/, andllms.txtmatch the final v1.0 surface; add migration notes for the deprecation removals. - Run a focused mutation-testing pilot over coercion, null/default/required flow, and structured path aggregation before freezing their behavior.
- Record a small performance baseline for
validate(),tryValidate(), and nested schema failures.tryValidate()currently uses exceptions internally, so failure-heavy workloads should be measured even though no public optimization is required for v1.0. - Confirm the PHP 8.3–8.5 CI matrix and lowest-supported dependency installation before tagging the release candidate.
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).
- Property-based tests for core validators (string patterns, numeric constraints).
- Performance benchmarking for hot paths (
validate,tryValidate, schema validation).
uniqueField()dedup key collides distinct resources. Uniqueness is keyed onserialize($fieldValue), but PHP serializes every resource toi:0;, so two distinct resource handles compare equal and are wrongly reported as duplicates. (Related: theserialize()try/catch added for unserializable values only takes its object-identity branch for closures/objects — the non-objectitem#indexfallback is effectively unreachable, sinceserialize()does not throw for resources.)uniqueFieldis meant for scalar fields; the fix is either a value-equality key that distinguishes non-serializable non-objects, or documenting the scalar-only intent. Pre-existing; surfaced during the structured-error work.
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.