waffle-commons / serializer
Compiled, reflection-free DTO serialization and Accept-header content negotiation for the Waffle framework.
Requires
- php: ^8.5
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
- psr/log: ^3.0
- waffle-commons/contracts: 0.1.0-beta7
- waffle-commons/utils: 0.1.0-beta7
Requires (Dev)
- carthage-software/mago: ^1.59
- cyclonedx/cyclonedx-php-composer: ^6.2
- igor-php/igor-php: ^0.10.1
- nyholm/psr7: ^1.8
- php-mock/php-mock-phpunit: ^2.16
- phpunit/phpunit: ^12.5
- vimeo/psalm: ^6.20
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-06 16:24:34 UTC
README
Waffle Serializer Component
Release:
0.1.0-beta7ย |ยCHANGELOG.mdStatus: First released in0.1.0-beta7โ roadmap[API-02](RFC-016). In: the DTO normalizers (compiled per DTO at build time, or interpreted in development),serializer:compile, theAcceptnegotiation middleware, and the contracts'DtoSerializerInterface, through which the framework kernel hydrates#[Dto]arguments and writes the DTOs a controller returns.data/'s Hydrator stays DB-only.
Compiled, reflection-free DTO serialization and Accept-header content negotiation for the Waffle framework.
๐งฉ What it does
It turns #[Dto] objects into JSON and JSON request bodies into #[Dto] objects, at the HTTP boundary. It is
not a general-purpose serializer: it handles exactly the DTO shapes that waffle-commons/utils'
DtoShapeReader reads, the same shapes the OpenAPI generator describes.
use Waffle\Commons\Serializer\Serializer; $serializer = new Serializer($normalizers); // a NormalizerProviderInterface, see below $order = $serializer->deserialize($body, OrderInput::class); // JSON โ DTO, through the constructor $json = $serializer->serialize($receipt); // DTO โ JSON
- Hydration always goes through the constructor, with named arguments, so
sethooks validate every value. Nothing writes a property directly. - Out: the readable properties (
gethooks run, write-only virtual properties are skipped). In: the constructor parameters. Apasswordparameter can come in while apasswordHashproperty goes out. - Types:
int,float(an integer is accepted),string,bool, backed enums (as their backing value), nested#[Dto]s and lists declared with#[ListOf], each optionally nullable. - Keys are the property and parameter names, unchanged. An unknown key is ignored. An absent key takes the
parameter's default,
nullfor a nullable parameter without one, and is an error otherwise.nullvalues are written out; a DTO without readable properties is{}; floats keep their fraction (2.0).
โ๏ธ Compiled and interpreted normalizers
| Provider | When | How |
|---|---|---|
The generated index (NormalizerCompiler) |
production | One generated final readonly class per DTO: plain property reads out, a plain constructor call in. No reflection at request time. |
Normalizer\ShapeNormalizerProvider |
development | Reads the DTO shape on demand and normalizes by reflection. No build step. |
Both run every check through Normalizer\Payload, so they accept, reject and word errors identically. The test
suite holds them to it on every case. NormalizerCompiler::compile() returns the generated files (file name โ
source), one class per file, deterministic, and written to pass Mago's linter and analyzer like hand-written code.
Build and load. bin/waffle serializer:compile (Command\CompileCommand, registered in bin/waffle next to the
console commands) scans --source (default src) for #[Dto] classes and writes their normalizers, one file per
class and each atomically, to --output (default var/cache/serializer); a file a previous run generated for a
DTO that no longer exists is removed, any other file is left alone. At boot,
Normalizer\CompiledNormalizerLoader::load() returns the compiled index when WAFFLE_AOT=1 and the files load,
backed by the interpreted provider for any DTO the build did not cover (Normalizer\FallbackNormalizerProvider);
otherwise โ the flag off, the files missing or broken โ the interpreted provider, with a log line saying why.
Every successful load also logs that the build is not checked against the current code: re-run the command after
any change to a DTO, as for container:compile.
๐ค Content negotiation
Middleware\ContentNegotiationMiddleware answers the one question a JSON-only API has to settle: does the client
accept application/json? It reads the Accept header as RFC 9110 defines it โ the most specific matching range
decides (application/json, then application/*, then */*), a weight of 0 excludes โ and refuses a request that
rules JSON out with a 406 before any controller runs. Every response that passes varies on Accept. No header
accepts anything. Mount it inside the error-handler middleware, and only in front of JSON routes: it refuses a
request that asks for another type, such as a text/event-stream subscription or an HTML page. Other
representations are post-v1.
๐ฆ Errors
| Exception | Status | When |
|---|---|---|
Exception\InvalidPayloadException |
422 | A field is missing, mistyped, not one of an enum's values, or rejected by a set hook. getField() is its path from the root: email, shipping.city, lines[2].quantity. |
Exception\MalformedPayloadException |
400 | The body is not JSON, or nests deeper than the codec's limit (64 by default). |
Exception\PayloadTooLargeException |
413 | The body is longer than the codec's limit (1 MiB by default); it is not parsed. |
Exception\NormalizerException |
500 | A class has no normalizer, or is a #[Dto] the serializer cannot represent: a wiring mistake. |
Exception\NotAcceptableException |
406 | The request's Accept header rules JSON out (ContentNegotiationMiddleware). |
The error handler renders them; a 422 shows the message and the field, as for any validation failure.
๐ฆ Installation
composer require waffle-commons/serializer
๐งญ Architectural boundary (mago guard)
Waffle\Commons\Serializer depends only on waffle-commons/contracts, waffle-commons/utils (the
DTO-shape reader it shares with the OpenAPI generator) and the PSR interfaces โ never on a sibling component's
concrete classes. An interface another component consumes lands in waffle-commons/contracts first;
DtoNormalizerInterface and NormalizerProviderInterface are this component's own seam between its runtime and
the code it generates.
๐งต Worker safety
Services are resident in FrankenPHP worker mode: no mutable state may survive a request. Anything that
holds per-request state implements ResettableInterface directly; composer igor must report 0 KO.
๐งช Testing
composer mago # formatter, linter, analyzer, guard โ clean means zero output composer tests # PHPUnit 12.5, line coverage โฅ 95 % composer igor # worker-safety audit
tests/compiled/ holds the compiler's output for the fixture matrix (golden files). The test suite compares it
byte for byte, and Mago lints and analyzes it with no test-only exception. After a deliberate change to the
compiler, regenerate it with WAFFLE_UPDATE_GOLDEN=1 composer tests and review the diff.
๐ Documentation
Full documentation tree: waffle-commons/documentation
๐ License
MIT โ see LICENSE.md.