Search by

waffle-commons / serializer

LesliePetrimaux

Compiled, reflection-free DTO serialization and Accept-header content negotiation for the Waffle framework.

Package info

github.com/waffle-commons/serializer

pkg:composer/waffle-commons/serializer

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.1.0-beta7 2026-11-05 01:39 UTC

This package is auto-updated.

Last update: 2026-11-06 16:24:34 UTC


README

Discord PHP Version Require PHP CI codecov Latest Stable Version Latest Unstable Version Total Downloads Packagist License

Waffle Serializer Component

Release: 0.1.0-beta7 ย |ย  CHANGELOG.md Status: First released in 0.1.0-beta7 โ€” roadmap [API-02] (RFC-016). In: the DTO normalizers (compiled per DTO at build time, or interpreted in development), serializer:compile, the Accept negotiation 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 set hooks validate every value. Nothing writes a property directly.
  • Out: the readable properties (get hooks run, write-only virtual properties are skipped). In: the constructor parameters. A password parameter can come in while a passwordHash property 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, null for a nullable parameter without one, and is an error otherwise. null values 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.