It is my fervent wish that this file guide every AI coding agent working with code in this repository.
php artisan boost:add-skill nette/schema
Save this content to: AGENTS.md
# To My Agents!
It is my fervent wish that this file guide every AI coding agent working with code in this repository.
## Documentation
Any distilled, agent-facing documentation for this package - how it works
internally and the rationale behind key design decisions - lives in `docs/`.
Consult it before non-trivial changes; it is the source of truth from which the
public manual is distilled.
Small package, one coherent but tricky mechanism (the `Processor` pipeline over
`Elements/*`). Read `docs/internals.md` before editing it - the "phase" model is
subtler than it looks.
`docs/upgrading.md` records, per released version, every change visible to
users of the package (renames, removals, deprecations, signatures, behavior,
configuration), each with its commit hash; internal changes are not in it. It
is the place to look up the history of a public symbol. Do not write to it; it
is extended at release time from the git history.
## Project Overview
**Nette Schema** validates and normalizes data structures (config files, API
inputs) through a fluent `Expect::` builder and a `Processor`.
- **PHP Version**: 8.3 - 8.6
- **Package**: `nette/schema` (dep: `nette/utils`)
## Essential Commands
```bash
# Run all tests
vendor/bin/tester tests/Schema/ -s # or: composer tester
vendor/bin/tester tests/Schema/Expect.structure.phpt -s
# Static analysis (PHPStan level 8)
composer phpstan
```
## Conventions
- Every file starts with `declare(strict_types=1);`; **tabs**; single quotes;
`@internal` for implementation details, `@method` for `Expect`'s magic methods;
Nette Coding Standard.
- Tests are Nette Tester `.phpt` named `Expect.<feature>.phpt`; `checkValidationErrors()`
asserts the expected error messages of a failing `process()`.
## Working in this repo
- **There is no `validate()` phase.** The `Schema` interface has four operations
but `Processor` runs only two per call: `process()` = `normalize()` +
`complete()`; `processMultiple()` = `normalize()` each item, `merge()`
left-to-right, one `complete()`. **Validation happens inside `complete()`**;
`merge()` is reached only via `processMultiple`. Don't trust the old "three-phase"
description.
- **`before()` runs per dataset item; `transform()`/`assert()` run once** on the
merged result - so a `before` sees one config layer, a `transform` sees the whole.
- **Errors accumulate in `Context`, never thrown mid-validation.** Each element's
`complete()` is an `$isOk = $context->createChecker(); $isOk() && nextStep()`
short-circuit chain - thread any new validation step through the checker or it
runs on already-rejected values.
- **Merging is schema-driven** (2.0): `Schema::merge()` takes a `Context`,
strategy resolves as `mergeWith(closure)` → `MergeMode` (setter on ArrayType
and Structure) → recursion **only through item schemas**. Ambiguous merges
(colliding arrays with no schema guidance) add a `Message::CannotMerge` error,
never a silent guess. `AnyOf` picks the alternative that the upper layer and
the trially merged result both match (`Context::isPartial` = validation-only
completion, bounds unchecked) and delegates to it.
- **`PreventMerging` (`'_prevent_merging'`) was removed** (BC break) —
`Helpers::rejectPreventMerging()` reports the key as an error where a schema
processes the items (untyped data are not walked); use
`mergeMode(MergeMode::Replace)` instead.
- **Defaults are not merged into supplied arrays** (`ArrayType::$mergeDefaults
= false`; `mergeDefaults()` turns it back on, silently in 2.0 for code
supporting 1.x as well) - a partial input array stays partial.
- **`assert`/`castTo` are sugar over `transform`** - one `$transforms` list running
in declaration order, so `->assert()->castTo()` differs from `->castTo()->assert()`.
- **`default` null is not `nullable`** (`nullable()` prepends `'null|'` to the type
string); a `null` value becomes the default when the default is an array and the
type is not nullable, as an empty block in NEON is no key. **`Structure` is required-by-default and casts to `object`, so
`default()` throws on it.**
- **`AnyOf` tries variants in order in a throwaway `Context` clone** - losing
variants' side effects (including transforms) are discarded. `DynamicParameter`
values get **deferred** validation (recorded in `Context::dynamics` for DI) -
don't validate them eagerly.
- **`Expect::type()` classifies the type expression once** (`TypeExpression::parse()`)
into `StringType`/`NumberType`/`ArrayType`, an `AnyOf` for a union of different
kinds, or a plain `Type` (an `EnumType` comes only from `Expect::enum()` and
`Expect::from()`, a `DateTimeType` only from `Expect::dateTime()` and
`Expect::date()`, which unlike any other element converts its `default()`).
Since 2.0 `Type` validates by itself
(`matches()`); `Validators::is()` serves only string formats and the
deprecated legacy notation (expression ranges, `numeric` & co.), which works
with a notice until 2.1 refuses it. `describe()`, `Kind` and `TypeExpression`
are `@internal`, `JsonSchema::export()` is the only public way out - keep it
that way.
- User-facing how-to (the `Expect::` API, castTo/`Expect::from` object mapping,
building complex schemas) is manual material and lives in the public web docs.
It is my fervent wish that this file guide every AI coding agent working with code in this repository.
Any distilled, agent-facing documentation for this package - how it works
internally and the rationale behind key design decisions - lives in docs/.
Consult it before non-trivial changes; it is the source of truth from which the
public manual is distilled.
Small package, one coherent but tricky mechanism (the Processor pipeline over
Elements/*). Read docs/internals.md before editing it - the "phase" model is
subtler than it looks.
docs/upgrading.md records, per released version, every change visible to
users of the package (renames, removals, deprecations, signatures, behavior,
configuration), each with its commit hash; internal changes are not in it. It
is the place to look up the history of a public symbol. Do not write to it; it
is extended at release time from the git history.
Nette Schema validates and normalizes data structures (config files, API
inputs) through a fluent Expect:: builder and a Processor.
nette/schema (dep: nette/utils)# Run all tests
vendor/bin/tester tests/Schema/ -s # or: composer tester
vendor/bin/tester tests/Schema/Expect.structure.phpt -s
# Static analysis (PHPStan level 8)
composer phpstan
declare(strict_types=1);; tabs; single quotes;
@internal for implementation details, @method for Expect's magic methods;
Nette Coding Standard..phpt named Expect.<feature>.phpt; checkValidationErrors()
asserts the expected error messages of a failing process().validate() phase. The Schema interface has four operations
but Processor runs only two per call: process() = normalize() +
complete(); processMultiple() = normalize() each item, merge()
left-to-right, one complete(). Validation happens inside complete();
merge() is reached only via processMultiple. Don't trust the old "three-phase"
description.before() runs per dataset item; transform()/assert() run once on the
merged result - so a before sees one config layer, a transform sees the whole.Context, never thrown mid-validation. Each element's
complete() is an $isOk = $context->createChecker(); $isOk() && nextStep()
short-circuit chain - thread any new validation step through the checker or it
runs on already-rejected values.Schema::merge() takes a Context,
strategy resolves as mergeWith(closure) → MergeMode (setter on ArrayType
and Structure) → recursion only through item schemas. Ambiguous merges
(colliding arrays with no schema guidance) add a Message::CannotMerge error,
never a silent guess. AnyOf picks the alternative that the upper layer and
the trially merged result both match (Context::isPartial = validation-only
completion, bounds unchecked) and delegates to it.PreventMerging ('_prevent_merging') was removed (BC break) —
Helpers::rejectPreventMerging() reports the key as an error where a schema
processes the items (untyped data are not walked); use
mergeMode(MergeMode::Replace) instead.ArrayType::$mergeDefaults = false; mergeDefaults() turns it back on, silently in 2.0 for code
supporting 1.x as well) - a partial input array stays partial.assert/castTo are sugar over transform - one $transforms list running
in declaration order, so ->assert()->castTo() differs from ->castTo()->assert().default null is not nullable (nullable() prepends 'null|' to the type
string); a null value becomes the default when the default is an array and the
type is not nullable, as an empty block in NEON is no key. Structure is required-by-default and casts to object, so
default() throws on it.AnyOf tries variants in order in a throwaway Context clone - losing
variants' side effects (including transforms) are discarded. DynamicParameter
values get deferred validation (recorded in Context::dynamics for DI) -
don't validate them eagerly.Expect::type() classifies the type expression once (TypeExpression::parse())
into StringType/NumberType/ArrayType, an AnyOf for a union of different
kinds, or a plain Type (an EnumType comes only from Expect::enum() and
Expect::from(), a DateTimeType only from Expect::dateTime() and
Expect::date(), which unlike any other element converts its default()).
Since 2.0 Type validates by itself
(matches()); Validators::is() serves only string formats and the
deprecated legacy notation (expression ranges, numeric & co.), which works
with a notice until 2.1 refuses it. describe(), Kind and TypeExpression
are @internal, JsonSchema::export() is the only public way out - keep it
that way.Expect:: API, castTo/Expect::from object mapping,
building complex schemas) is manual material and lives in the public web docs.How can I help you explore Laravel packages today?