@@ -11,7 +11,7 @@ four phases. `Processor` has two public entry points and calls only two of them
1111per run:
1212
1313- ` process() ` → ` normalize() ` then ` complete() ` (each followed by
14- ` throwsErrors ()` ).
14+ ` throwErrors ()` ).
1515- ` processMultiple() ` → ` normalize() ` each dataset item, ` merge() ` them
1616 left-to-right, then a single ` complete() ` .
1717
@@ -29,10 +29,9 @@ run once** on the already-merged result (inside the single `complete`). So a
2929
3030## Error accumulation is the control-flow spine
3131
32- Errors are ** collected in ` Context ` , never thrown mid-validation** (` Context ` ,
33- ` Processor::throwsErrors ` — yes, with the typo — fires only between phases).
34- The mechanism that makes this
35- work is ` Context::createChecker() ` : it snapshots the current error count and
32+ Errors are ** collected in ` Context ` , never thrown mid-validation**
33+ (` Processor::throwErrors ` fires only between phases). The mechanism that makes
34+ this work is ` Context::createChecker() ` : it snapshots the current error count and
3635returns a closure that is ` true ` only while no new error has been added.
3736
3837Every element's ` complete() ` is a chain guarded by that closure:
@@ -64,8 +63,8 @@ deferred to `Message::toString()`. Templates use `%placeholder%` substitution:
6463does not). A placeholder whose value is ` null ` vanishes together with the space
6564before it — that is how ` %path% ` disappears at the root. Codes are the
6665` Message::* ` string constants, whose docblocks list the expected variables; a
67- placeholder with no matching variable triggers an undefined-array-key warning
68- in ` toString() ` , so keep template and variables in sync .
66+ placeholder with no matching variable is left in the text verbatim ( ` %foo% ` ),
67+ so a template/variables mismatch shows up only in the rendered message .
6968
7069## ` PreventMerging ` : in-band metadata, handled in many places
7170
@@ -83,16 +82,15 @@ it** — and they do so in subtly different ways:
8382
8483This is the package's sharpest trap: a piece of control state travelling through
8584the payload, replicated across five sites. Any new ` Schema ` element must reproduce
86- the strip-and-honor dance or merging silently misbehaves. (There is a standing
87- idea to replace it with a declarative ` MergeMode::Replace ` ; DI carries its own
88- parallel ` PREVENT_MERGING ` constant. See ` docs/local/ideas/odstranit-prevent-merging.md ` .)
85+ the strip-and-honor dance or merging silently misbehaves. DI carries its own
86+ parallel ` PREVENT_MERGING ` constant, so the key name is a cross-package contract.
8987
9088## One transform pipeline; ` assert ` /` castTo ` are sugar over ` transform `
9189
9290` before() ` , ` transform() ` , ` assert() ` , and ` castTo() ` are ** not** independent
93- stages. ` before ` runs in ` normalize ` (pre-validation) and has a ** single slot ** :
94- a second ` before() ` call silently replaces the first. Everything else
95- appends to a single ` $transforms ` list (` Base ` ): ` castTo ` is
91+ stages. ` before ` runs in ` normalize ` (pre-validation); repeated calls chain in
92+ declaration order, each handler receiving the previous result ( ` Base::$before ` ).
93+ Everything else appends to a single ` $transforms ` list (` Base ` ): ` castTo ` is
9694` transform(getCastStrategy(...)) ` , ` assert ` is a ` transform ` that reports an error
9795and returns null on failure. They therefore execute in ** declaration order** in
9896one ` doTransform ` pass, after type/range/pattern validation. Reordering
@@ -104,20 +102,21 @@ one `doTransform` pass, after type/range/pattern validation. Reordering
104102 ` nullable() ` was called — which works by prepending ` 'null|' ` to the type
105103 string (` Type::nullable ` ), not by a flag. ` dynamic() ` similarly prepends
106104 ` DynamicParameter::class . '|' ` .
107- - ** null-to-empty-array coercion:** ` complete() ` turns a ` null ` value into ` [] `
108- whenever the default is an array, with the comment "NEON cannot distinguish null
109- from an empty array". The check is ** unconditional — it fires even after
110- ` nullable() ` ** , so a nullable array-typed item never yields ` null ` , and a NEON
111- key written bare ( ` key: ` ) validates as an empty array .
105+ - ** null-to-empty-array coercion:** ` Type:: complete()` turns a ` null ` value into
106+ ` [] ` when the default is an array ** and the type does not accept null** ("NEON
107+ cannot distinguish null from an empty array"), so a NEON key written bare
108+ ( ` key: ` ) validates as an empty array, while after ` nullable() ` the ` null ` is
109+ kept. ` Structure::complete() ` coerces unconditionally .
112110
113- ## Keys validate like values — and collapse on failure
111+ ## Keys validate like values
114112
115113` arrayOf(value, key) ` runs the key schema through the same
116114` normalize ` /` complete ` cycle as values, with ` Context::isKey ` set around the
117115call (` Type::normalize ` , ` Type::validateItems ` ); ` Message::toString ` renders
118- such errors as "key of item". The trap: a key that fails ` complete() ` comes
119- back as ` null ` and lands in ` $res[$key ?? ''] ` , so ** all invalid keys silently
120- collapse into a single ` '' ` entry** , later ones overwriting earlier ones.
116+ such errors as "key of item". An item whose key fails ` complete() ` is ** dropped
117+ from the result** (` validateItems ` keeps it only while the key's own checker
118+ stays clean); its value is still completed first, so errors in both key and
119+ value are reported.
121120
122121## Structure specifics
123122
@@ -126,27 +125,29 @@ collapse into a single `''` entry**, later ones overwriting earlier ones.
126125 ` stdClass ` and ` default() ` ** throws** — it cannot have one.
127126- ** A missing required structure still fills nested defaults.**
128127 ` completeDefault ` completes ` [] ` through the normal path (recursively producing
129- every child's default). That path includes ` doDeprecation ` , so a deprecated
130- structure emits its warning even when merely absent from the input .
128+ every child's default), with ` deprecated ` suspended for the call , so an absent
129+ deprecated structure does not warn; a present one does .
131130- ** ` skipDefaults ` has two independent switches** — the ` Processor `
132131 (` Context::skipDefaults ` ) and the ` Structure ` — and ` validateItems ` fills in a
133132 default only when ** neither** asks to skip it.
134133
135134## AnyOf: first clean variant wins, in a throwaway context
136135
137136` findAlternative ` tries each variant ** in order** . Schema variants are run
138- against a ** fresh throwaway ` Context ` (` $dolly ` )** that copies only ` path ` ; the
139- first variant that completes with ** no errors** wins, and only then are its
140- ` warnings ` merged back into the real context. The dolly does ** not** inherit
141- ` skipDefaults ` /` isKey ` , and even the winning variant's ` dynamics ` are ** not**
142- merged back — a dynamic parameter nested inside an ` anyOf ` variant silently loses
143- its deferred validation. Scalar variants are matched with strict ` === ` .
137+ against a ** fresh throwaway ` Context ` (` $dolly ` )** that copies ` path ` ,
138+ ` skipDefaults ` and ` isKey ` ; the first variant that completes with ** no errors**
139+ wins, and only then are its ` warnings ` and ` dynamics ` merged back into the real
140+ context. Scalar variants are matched with strict ` === ` .
144141
145142Two consequences: ** order matters** , and ** side effects (including transforms) of
146143losing variants are discarded** with their dolly context. On total failure, inner
147144errors (different path) are surfaced if any exist; otherwise a single aggregated
148145"expects to be A|B|C" error is produced.
149146
147+ ` AnyOf::dynamic() ` is a ** flag, not a variant** : a ` DynamicParameter ` value skips
148+ ` findAlternative ` entirely and goes straight to the transforms. Nothing is
149+ recorded in ` dynamics ` , there is no single expected type to defer.
150+
150151` completeDefault ` has one extra fork: when the default is itself a ` Schema `
151152(` firstIsDefault() ` with a schema variant), it delegates to that schema's
152153` completeDefault ` .
@@ -169,12 +170,13 @@ by validating dynamics eagerly.
169170 (` $index = $this->otherItems === null ? null : 0 ` ); ` Type::merge ` and
170171 ` Helpers::merge ` always append numeric-keyed items.
171172- ** ` castTo ` forks by target** (` Helpers::getCastStrategy ` ): builtin →
172- ` settype ` ; class ** with** constructor → named args from the array/stdClass
173+ ` settype ` ; backed enum → ` from() ` , a wrong value reports ` TypeMismatch `
174+ listing the cases (a pure enum throws ` InvalidStateException ` when the schema
175+ is built); class ** with** constructor → named args from the array/stdClass
173176 (a scalar is passed as a single argument); anything else → property assignment
174- via ` Arrays::toObject((array) $value, new $type) ` . There is ** no enum branch** :
175- an enum has no constructor, falls into the ` new $type ` path and dies with a
176- PHP ` Error ` . This fork is the mechanism behind both ` castTo(Class::class) `
177- and Structure's object output.
177+ via ` Arrays::toObject((array) $value, new $type) ` . A failing instantiation is
178+ rethrown as ` InvalidStateException ` naming the target class. This fork is the
179+ mechanism behind both ` castTo(Class::class) ` and Structure's object output.
178180- ** ` min ` /` max ` mean different things by type** (` validateRange ` ): item count for
179181 arrays, character length (` unicode ` type) or byte length (otherwise) for
180182 strings, the value itself for numbers.
@@ -208,9 +210,13 @@ plain `Type`. **String formats are the Validators pseudo-types** (`email`, `url`
208210` identifier ` , ` digit ` , ...): ` Expect::type('url') ` is a ` StringType ` validated by
209211` Validators::isUrl() ` exactly as before. The subclasses deprecate the methods that
210212make no sense for their kind (` NumberType::pattern() ` , ` ArrayType::pattern() ` ,
211- ` items() ` outside arrays) with an ` E_USER_DEPRECATED ` notice; on a plain ` Type ` the
212- same methods stay silent, a union may well hold a string. The fluent setters in
213- ` Base ` and ` Type ` return ` static ` for the same reason.
213+ ` items() ` outside arrays) with an ` E_USER_DEPRECATED ` notice. On a plain ` Type `
214+ they stay silent when the expression is a single kind (` 'bool' ` , a class); when it
215+ is a union (` 'int|string' ` , ` 'scalar' ` ), ` min() ` , ` max() ` , ` pattern() ` and ` items() `
216+ raise a deprecation too (` Type::unionDeprecated ` ): a union takes no options of its
217+ own, they belong to the variants of ` anyOf() ` . A union of one kind (` 'int|float' ` )
218+ gets the kind's subclass and stays silent. The fluent setters in ` Base ` and ` Type `
219+ return ` static ` so the subclass survives chaining.
214220
215221` EnumType ` (` Expect::enum() ` , and what ` Expect::type() ` and ` Expect::from() ` hand
216222out for a backed enum class) is the one subclass that changes behavior, and only
@@ -229,12 +235,16 @@ a value, not as an object to recurse into.
229235are, not expanded** ; only the variants of a ` Type ` union and the items of ` 'int[]' `
230236are arrays, because there is no element behind them. The array is exactly what
231237` JsonSchema::export() ` needs and nothing more; there is no descriptor class.
238+ ` AnyOf::describe() ` sorts its set: scalars into ` values ` , schemas into
239+ ` variants ` , ` null ` into the ` nullable ` flag; the kind is ` Union ` if any schema
240+ variant exists, ` Enum ` if only scalars do.
232241
233242** ` TypeExpression::parse() ` is the single translator of the ` Expect::type() `
234243string language** (` | ` , ` ? ` , ` [] ` , ` name:range ` , ` pattern:regex ` ) into that array.
235244It follows what ` Validators::is() ` accepts, not what the author probably meant:
236245` 'int[]' ` is ` Kind::Iterable ` of ints (any iterable, keys unchecked),
237- ` 'number' ` /` 'scalar' ` expand to unions, ` '?x' ` /` 'null|x' ` set ` nullable ` ,
246+ ` 'number' ` is ` Kind::Number ` (a kind of its own, not a union), ` 'scalar' ` expands
247+ to a union of bool, number and string, ` '?x' ` /` 'null|x' ` set ` nullable ` ,
238248` DynamicParameter ` sets ` dynamic ` , the string pseudo-types of Validators (` email ` ,
239249` url ` , ` identifier ` , ` digit ` , ...) are ` Kind::String ` with the name under ` format ` ,
240250** an unknown name is a class name** (` Kind::Instance ` , decided by exclusion, never
@@ -253,7 +263,9 @@ transforms no), passes on only the string formats JSON Schema knows (`email`,
253263` ^(?:…)$ ` , maps ` Kind::Array ` to a JSON
254264object unless the key type is ` Kind::Int ` , narrows ` Kind::Iterable ` to a JSON
255265array, and ** throws ` NotSupportedException ` for ` Instance ` , ` Object ` , ` Callable `
256- and ` Other ` ** rather than emitting a schema the ` Processor ` would then reject.
266+ and ` Other ` ** rather than emitting a schema the ` Processor ` would then reject. An
267+ ` Expect::array() ` without an item or key type throws too: nothing says whether
268+ it is a JSON array or object.
257269` JsonSchema::export() ` is public API; the vocabulary it is built on — ` describe() ` ,
258270` Kind ` , ` TypeExpression ` — stays ` @internal ` and can still change, so keep the
259271export the only supported way out.
0 commit comments