Skip to content

Add TransformerRegistry for global type-based casters and serializers#110

Merged
dereuromark merged 2 commits into
masterfrom
feature/global-casters
Apr 11, 2026
Merged

Add TransformerRegistry for global type-based casters and serializers#110
dereuromark merged 2 commits into
masterfrom
feature/global-casters

Conversation

@dereuromark
Copy link
Copy Markdown
Contributor

Summary

Introduces PhpCollective\Dto\Transformer\TransformerRegistry so that casters (array → object) and serializers (object → array/scalar) can be registered once per type and apply automatically to every DTO field whose declared type matches.

Today, transforming a common value object like DateTimeImmutable, Money, or a framework-specific DateTime wrapper requires repeating factory="..." / serialize="..." on every field of that type across every DTO schema. This PR adds a global registry so the mapping is declared once during bootstrap.

use PhpCollective\Dto\Transformer\TransformerRegistry;

TransformerRegistry::addCaster(
    \DateTimeImmutable::class,
    fn (string $value): \DateTimeImmutable => new \DateTimeImmutable($value),
);

TransformerRegistry::addSerializer(
    \DateTimeInterface::class,
    fn (\DateTimeInterface $value): string => $value->format(DATE_ATOM),
);

Every DTO field declared as \DateTimeImmutable (and every output of a \DateTimeInterface) is now transformed automatically.

Behaviour

Precedence — fromArray:

  1. DTO / collection handling
  2. serialize: 'FromArrayToArray' / 'array' per field
  3. Per-field factory (wins over registry)
  4. Enum handling
  5. TransformerRegistry::findCaster() ← new
  6. Default constructor fallback (new $type($value))

Precedence — toArray:

  1. Nested DTO / collection toArray()
  2. Per-field serialize (wins over registry)
  3. Unit enum handling
  4. TransformerRegistry::findSerializer() ← new
  5. Value passed through as-is

Explicit schema configuration always wins — the registry only fills in the createWithConstructor fallback on input and the pass-through branch on output.

Inheritance matching: exact class-name match first, then the first registered parent class or interface that is_a/instanceof matches. Register the most specific type you care about; interfaces act as a convenient catch-all (e.g. register once for DateTimeInterface to cover both DateTime and DateTimeImmutable).

Fast-path interaction: when any entry is registered, generated DTOs bypass the HAS_FAST_PATH optimisation (setFromArrayFast / toArrayFast) and fall back to the reflective path, guaranteeing the registry is consulted. The check is a cheap $casters !== [] || $serializers !== []. No entries registered → zero overhead, fast path is unchanged.

API

Method Description
addCaster(string \$type, callable \$caster) Register a caster for a class/interface.
addSerializer(string \$type, callable \$serializer) Register a serializer for a class/interface.
removeCaster(string \$type) Remove a caster.
removeSerializer(string \$type) Remove a serializer.
hasCaster(string \$type): bool Exact-match check.
hasSerializer(string \$type): bool Exact-match check.
findCaster(string \$type): ?callable Lookup with inheritance fallback.
findSerializer(object \$value): ?callable Lookup for an instance.
hasAny(): bool Whether any entry is registered (used by the fast-path guard).
clear() Remove all entries (intended for tests).

Changes

  • src/Transformer/TransformerRegistry.php — new class.
  • src/Dto/Dto.php — hook caster into setFromArray before the createWithConstructor fallback, hook serializer into _toArrayInternal as an else-branch after the per-field serialize and enum checks, and gate the HAS_FAST_PATH shortcut on !TransformerRegistry::hasAny().
  • tests/Dto/DtoTest.php — 7 new tests covering caster hit, serializer hit, factory precedence, inheritance matching (negative + positive), remove/clear, and leading-backslash normalisation. tearDown() also clears the registry.
  • docs/guide/custom-casters.md — new "Global Type-Based Transformers" section with precedence rules, inheritance behaviour, CakePHP/Laravel bootstrap examples, testing guidance, and API reference.
  • docs/guide/runtime-api.md — added a TransformerRegistry section and updated the "Resetting Global Runtime State" snippet.

Full DtoTest suite: 137/137 passing, CS clean. PHPStan errors are unchanged (pre-existing JsonSchema optional dependency), unrelated to this PR.

Introduces PhpCollective\Dto\Transformer\TransformerRegistry so that casters
(array -> object) and serializers (object -> array/scalar) can be registered
once per type and apply automatically to every DTO field whose declared type
matches, instead of repeating per-field factory / serialize metadata across
many fields.

Lookups match the exact type first, then walk parent classes and interfaces.
Per-field factory and serialize always win over the registry. When any entry
is registered, generated DTOs fall back from the optimized fast path to the
reflective path so the registry is always consulted.

Includes tests covering caster hit, serializer hit, factory precedence,
inheritance matching, removal/clear, and backslash normalization, plus an
updated Custom Casters guide and runtime API reference.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 11, 2026

Codecov Report

❌ Patch coverage is 92.85714% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.80%. Comparing base (3eab698) to head (20aace6).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/Transformer/TransformerRegistry.php 91.11% 4 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #110      +/-   ##
============================================
+ Coverage     82.66%   82.80%   +0.13%     
- Complexity     1500     1530      +30     
============================================
  Files            42       43       +1     
  Lines          3703     3756      +53     
============================================
+ Hits           3061     3110      +49     
- Misses          642      646       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dereuromark dereuromark merged commit dc2a3eb into master Apr 11, 2026
12 checks passed
@dereuromark dereuromark deleted the feature/global-casters branch April 11, 2026 00:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant