Skip to content

Commit 9bfb023

Browse files
committed
updated DOCS
1 parent ec7884a commit 9bfb023

3 files changed

Lines changed: 386 additions & 127 deletions

File tree

‎AGENTS.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,16 @@ composer phpstan
6060
- **`assert`/`castTo` are sugar over `transform`** - one `$transforms` list running
6161
in declaration order, so `->assert()->castTo()` differs from `->castTo()->assert()`.
6262
- **`default` null is not `nullable`** (`nullable()` prepends `'null|'` to the type
63-
string); a `null` value coerces to `[]` when the default is an array. **`Structure`
64-
is required-by-default and casts to `object`, so `default()` throws on it.**
63+
string); a `null` value coerces to `[]` when the default is an array and the type
64+
is not nullable. **`Structure` is required-by-default and casts to `object`, so
65+
`default()` throws on it.**
6566
- **`AnyOf` tries variants in order in a throwaway `Context` clone** - losing
6667
variants' side effects (including transforms) are discarded. `DynamicParameter`
6768
values get **deferred** validation (recorded in `Context::dynamics` for DI) -
6869
don't validate them eagerly.
70+
- **`Expect::type()` classifies the type expression once** (`TypeExpression::parse()`)
71+
into `StringType`/`NumberType`/`ArrayType`/`EnumType` or a plain `Type`; validation
72+
itself is still `Validators::is()`. `describe()`, `Kind` and `TypeExpression` are
73+
`@internal`, `JsonSchema::export()` is the only public way out - keep it that way.
6974
- User-facing how-to (the `Expect::` API, castTo/`Expect::from` object mapping,
7075
building complex schemas) is manual material and lives in the public web docs.

‎docs/internals.md‎

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ four phases. `Processor` has two public entry points and calls only two of them
1111
per 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
3635
returns a closure that is `true` only while no new error has been added.
3736

3837
Every element's `complete()` is a chain guarded by that closure:
@@ -64,8 +63,8 @@ deferred to `Message::toString()`. Templates use `%placeholder%` substitution:
6463
does not). A placeholder whose value is `null` vanishes together with the space
6564
before 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

8483
This is the package's sharpest trap: a piece of control state travelling through
8584
the 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
9795
and returns null on failure. They therefore execute in **declaration order** in
9896
one `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
117115
call (`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

145142
Two consequences: **order matters**, and **side effects (including transforms) of
146143
losing variants are discarded** with their dolly context. On total failure, inner
147144
errors (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
210212
make 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
216222
out 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.
229235
are, not expanded**; only the variants of a `Type` union and the items of `'int[]'`
230236
are 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()`
234243
string language** (`|`, `?`, `[]`, `name:range`, `pattern:regex`) into that array.
235244
It 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
254264
object unless the key type is `Kind::Int`, narrows `Kind::Iterable` to a JSON
255265
array, 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
259271
export the only supported way out.

0 commit comments

Comments
 (0)