Skip to content

Add MapperProvider::pregenerate() — native mapper pre-generation - #127

Open
janedbal wants to merge 1 commit into
masterfrom
mapper-pregeneration
Open

Add MapperProvider::pregenerate() — native mapper pre-generation#127
janedbal wants to merge 1 commit into
masterfrom
mapper-pregeneration

Conversation

@janedbal

Copy link
Copy Markdown
Member

Alternative to #126, based on the review feedback there — instead of exposing the provider/compiler graph for consumers to traverse, the library records delegated classes where the truth is decided and offers pre-generation natively.

Motivation

Consumers pre-generating mappers at deploy time (e.g. a console command compiling mappers for every API input class) so far had to discover transitively referenced classes themselves — by reflecting over provider internals (get_mangled_object_vars() over plain object), which is fragile and reported by shipmonk/dead-code-detector as an unknown read over unknown type.

Design: record at the emission point

A pre-generation dependency is "a class the generated code will call $provider->get{Input,Output}Mapper() for at runtime". That is decided during compilation, at a single funnel: AbstractDelegateMapperCompiler either emits a provider call (runtime dependency) or inlines the inner mapper as a CallbackMapper method (no standalone dependency — and its own delegates are emitted, hence recorded, within the same compilation). PhpCodeBuilder now records class names at those two emission points — ground truth by construction, nothing to keep in sync, no interface churn.

MapperProvider::pregenerate(iterable $classNames): list<class-string> then runs a compile-as-you-discover fixpoint: compile the seeds, enqueue recorded delegations, repeat until nothing new appears. Each iteration is work that had to happen anyway, discovery is a free byproduct.

Properties:

  • per-direction precision — dependencies discovered while compiling an input mapper are only pre-generated as input mappers (and vice versa), so e.g. an enum's backing mapper unused on the output side generates nothing there
  • runtime factories respected — classes covered by register{Input,Output}Factory() are skipped in that direction
  • warm-cache safe — existing files are not overwritten, but are recompiled in memory so discovery still sees their delegations
  • generated code is byte-identical to before (recording is side-band), so no snapshot changes

Consumer story

The application command reduces to seed discovery + one call:

$inputClasses = ...; // Finder over src/**/*Input.php — app-specific
$mapperProvider->pregenerate($inputClasses);

If this direction is preferred, #126 (CompositeMapperCompilerProvider) becomes unnecessary for the pre-generation use case and can be closed — a graph-traversal API would only be worth keeping for hypothetical static analyses that must not compile.

Co-Authored-By: Claude Code

Drafted by Claude

…compilation

Consumers pre-generating mappers (e.g. a console command compiling
mappers for every API input class at deploy time) so far had to
discover delegated classes themselves by reflecting over provider
internals, which is fragile and opaque to static analysis.

The ground truth of what needs pre-generation is decided during
compilation: AbstractDelegateMapperCompiler either emits a
$provider->get*Mapper(X::class) call (a runtime dependency) or inlines
the inner mapper. PhpCodeBuilder now records the class names at those
two emission points, and MapperProvider::pregenerate() runs a
compile-as-you-discover fixpoint over them: compile the seeds, enqueue
recorded delegations, repeat until nothing new appears.

Dependencies are tracked per direction, so only mappers actually
reachable at runtime are generated. Classes covered by runtime
factories registered via register{Input,Output}Factory() are skipped.
Existing mapper files are not overwritten, but are recompiled in
memory so discovery also works on a warm cache.

Co-Authored-By: Claude Code
@janedbal
janedbal requested a review from JanTvrdik July 17, 2026 10:20
@janedbal
janedbal marked this pull request as ready for review August 5, 2026 15:01
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