Skip to content

Inline schema transforms into generated fast-path code - #115

Merged
dereuromark merged 3 commits into
masterfrom
feature/inline-transform-calls
Jul 27, 2026
Merged

Inline schema transforms into generated fast-path code#115
dereuromark merged 3 commits into
masterfrom
feature/inline-transform-calls

Conversation

@dereuromark

Copy link
Copy Markdown
Contributor

Fields declaring transformFrom / transformTo went through $this->transformValue($callable, $value) inside the generated fast-path methods. Per transformed field, on every hydration and every serialization, that costs a method call, a null check, an is_callable() string lookup and a dynamic invocation.

Measured in this repo (PHP 8.5 CLI, JIT off, 300k calls): 143.7ms through transformValue() versus 35.2ms for the equivalent direct call, so about 4x the cost. The transform name is known while generating, so there is no reason to resolve it at runtime.

TransformCompiler now compiles the transform into the generated code:

// before
$value = $this->transformValue('App\Transform\Email::normalize', $value);

// after
$value = \App\Transform\Email::normalize($value);

End to end on a single-field transform DTO that is roughly 1.9x on hydration and 1.7x on serialization. Wider DTOs with several transformed fields gain proportionally more.

When inlining is skipped

Correctness first: any site that cannot be inlined safely keeps the existing runtime dispatch. The conditions are all of:

  • the callable is a plain identifier, namespaced function or static method, matched against a strict anchored pattern. The string comes from the user's schema and ends up in generated PHP, so anything else is never interpolated.
  • it resolves at generation time. A typo like App\Transform\Email::normalizeTypo stays on the runtime path so it still surfaces as InvalidArgumentException rather than a raw Error from generated code.
  • the generated file declares strict types. An inlined call takes its argument coercion from the calling file, whereas transformValue() was always called from Dto.php, which is strict. Without this gate, scalar type hints on user transforms would silently start coercing instead of raising TypeError.
  • null-guarded sites only inline for side-effect-free expressions, so a value is never evaluated twice. The nullable toArray() expressions and the lazy ?? chains therefore stay on the runtime path.

Notes

  • transformValue() is untouched and still used by the non-fast paths and by every fallback above.
  • TwigRenderer::transformExpr() reads strictTypes from the render context, so the templates do not have to thread the flag through 20 call sites.
  • Coverage: unit tests for the compiler (including injection-shaped and unresolvable callables), plus template-level tests that render element/optimizations and assert the inlined form appears with strictTypes on and does not without it.

Fields with transformFrom/transformTo went through
`$this->transformValue($callable, $value)` in the generated fast-path
methods. Per transformed field, per hydration and per serialization, that
costs a method call, a null check, an `is_callable()` string lookup and a
dynamic invocation. Measured on this repo: 300k calls take 143.7ms that
way versus 35.2ms as a direct call, a factor of 4.

The transform name is known while generating, so `TransformCompiler` now
emits the direct call instead. End to end on a single-field transform DTO
this is roughly 1.9x on hydration and 1.7x on serialization.

Inlining only happens when all of the following hold, otherwise the
generated code keeps the existing runtime dispatch:

- the callable is a plain identifier, namespaced function or static method
  (strict pattern, since the schema string ends up in generated PHP)
- it resolves at generation time, so a typo still surfaces as
  InvalidArgumentException rather than a raw Error
- the generated file declares strict types, since an inlined call takes its
  argument coercion from the calling file rather than from Dto.php
- null-guarded sites only inline for side-effect-free expressions, so the
  value is never evaluated twice
@dereuromark dereuromark added the enhancement New feature or request label Jul 24, 2026
Copilot AI review requested due to automatic review settings July 24, 2026 17:07
@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.25%. Comparing base (de2306c) to head (8086701).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master     #115      +/-   ##
============================================
+ Coverage     83.07%   83.25%   +0.17%     
- Complexity     1555     1581      +26     
============================================
  Files            45       46       +1     
  Lines          3835     3881      +46     
============================================
+ Hits           3186     3231      +45     
- Misses          649      650       +1     

☔ View full report in Codecov by Harness.
📢 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes generated DTO fast-path hydration/serialization by compiling schema transformFrom / transformTo callables into direct PHP calls when it’s safe (notably when the generated file declares strict_types=1), keeping the existing runtime dispatch as a fallback for unsafe/unresolvable cases.

Changes:

  • Add TransformCompiler and a transformExpr() Twig function to emit either an inlined call (e.g., \Cls::method($x)) or the existing $this->transformValue(...) fallback.
  • Update the optimizations template and DTO template wiring to pass strictTypes into the render context for safe inlining decisions.
  • Add unit/template tests and update performance documentation to describe the strictTypes gating and fallback behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/TestDto/TransformDto.php Updates generated test DTO fixture to include fast-path methods using direct transform calls.
tests/Generator/TwigRendererTest.php Adds template-level assertions for inlined vs fallback transform rendering based on strictTypes and callable safety.
tests/Generator/TransformCompilerTest.php Adds unit coverage for transform compilation decisions (inline vs fallback) including injection-shaped and unresolvable callables.
templates/element/optimizations.twig Replaces runtime transformValue() calls with transformExpr() so transforms can inline where safe.
templates/dto.twig Threads strictTypes into the optimizations include context (with only) so transformExpr() can make the correct decision.
src/Generator/TwigRenderer.php Registers the new Twig function and exposes strictTypes in global render vars from config.
src/Generator/TransformCompiler.php Implements callable validation + safe inlining logic, with fallback to existing runtime dispatch.
docs/guide/performance.md Documents the strictTypes requirement and performance rationale for transform inlining.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Generator/TransformCompiler.php Outdated
…able

Avoids a needless is_callable() autoload for expressions that will fall
back to runtime dispatch anyway.
Copilot AI review requested due to automatic review settings July 27, 2026 13:40
@dereuromark
dereuromark merged commit a81ce61 into master Jul 27, 2026
13 checks passed
@dereuromark
dereuromark deleted the feature/inline-transform-calls branch July 27, 2026 13:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants