<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Forms on Guillaume Delré</title><link>https://guillaumedelre.github.io/tags/forms/</link><description>Recent content in Forms on Guillaume Delré</description><generator>Hugo</generator><language>en</language><lastBuildDate>Mon, 12 Jan 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/tags/forms/index.xml" rel="self" type="application/rss+xml"/><item><title>Symfony 8.0: PHP 8.4 minimum, native lazy objects, and FormFlow</title><link>https://guillaumedelre.github.io/2026/01/12/symfony-8.0-php-8.4-minimum-native-lazy-objects-and-formflow/</link><pubDate>Mon, 12 Jan 2026 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2026/01/12/symfony-8.0-php-8.4-minimum-native-lazy-objects-and-formflow/</guid><description>Part 11 of 11 in &amp;quot;Symfony Releases&amp;quot;: Symfony 8.0 requires PHP 8.4, replaces its proxy code generator with native lazy objects, and introduces FormFlow.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 8.0 shipped November 27, 2025, same day as 7.4. It requires PHP 8.4 and drops everything that was deprecated in 7.4. The two most interesting changes are what it stops doing and what it starts doing with PHP 8.4.</p>
<h2 id="native-lazy-objects">Native lazy objects</h2>
<p>Symfony&rsquo;s proxy system, used for lazy service initialization and Doctrine&rsquo;s entity proxies, has historically relied on code generation. The proxy classes were generated at cache warmup, stored as files, and loaded when needed. It worked, but it added real complexity: generated files to manage, cache to invalidate, code that looked nothing like the class it proxied.</p>
<p>PHP 8.4 added native lazy objects. Symfony 8.0 uses them. The <code>LazyGhostTrait</code> and <code>LazyProxyTrait</code> that powered the old system are removed. Proxy creation is now a runtime operation backed by the engine itself, not a code generation step.</p>
<p>For application developers the change is mostly invisible: lazy services still work. For framework and library authors, a significant surface of complexity just disappears.</p>
<h2 id="formflow">FormFlow</h2>
<p>Multi-step forms have always been a DIY exercise in Symfony. Session management, step tracking, partial validation, navigation between steps: every project rolled its own solution or pulled in a third-party bundle.</p>
<p>8.0 introduces FormFlow: a built-in mechanism for multi-step form wizards. Steps are defined as a sequence of form types, partial validation is scoped to the current step, and session management is handled automatically.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[AsFormFlow]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CheckoutFlow</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">AbstractFormFlow</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">defineSteps</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Steps</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Steps</span><span style="color:#f92672">::</span><span style="color:#a6e22e">create</span>()
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">add</span>(<span style="color:#e6db74">&#39;shipping&#39;</span>, <span style="color:#a6e22e">ShippingType</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">add</span>(<span style="color:#e6db74">&#39;payment&#39;</span>, <span style="color:#a6e22e">PaymentType</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">add</span>(<span style="color:#e6db74">&#39;review&#39;</span>, <span style="color:#a6e22e">ReviewType</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="xml-and-fluent-php-config-removed">XML and fluent PHP config removed</h2>
<p>The 7.4 deprecation of the fluent PHP configuration format becomes a hard removal in 8.0. XML configuration also exits as a first-class format. The supported formats for application configuration are now YAML and PHP arrays. The footprint shrinks, but what remains is genuinely better.</p>
<h2 id="what-else-is-gone">What else is gone</h2>
<ul>
<li>PHP 8.2 and 8.3 support (8.4 minimum)</li>
<li>The <code>ContainerAwareInterface</code> and <code>ContainerAwareTrait</code></li>
<li>Symfony&rsquo;s internal use of <code>LazyGhostTrait</code> and <code>LazyProxyTrait</code></li>
<li>HTTP method override for GET and HEAD (only POST makes sense semantically)</li>
</ul>
<p>Symfony 8.0 is a clean break, and that kind of break only becomes possible when the PHP floor rises. PHP 8.4&rsquo;s lazy objects are the clearest example: the feature now exists in the language, so the framework can just stop implementing it.</p>
<h2 id="console-becomes-more-ergonomic-for-invokable-commands">Console becomes more ergonomic for invokable commands</h2>
<p>Invokable commands get a significant upgrade. The <code>#[Input]</code> attribute turns a DTO into the command&rsquo;s argument/option bag. No more calling <code>$input-&gt;getArgument()</code> inside the handler:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[AsCommand(name: &#39;app:send-report&#39;)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SendReportCommand</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__invoke</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">#[Input] SendReportInput $input,
</span></span></span><span style="display:flex;"><span>    )<span style="color:#f92672">:</span> <span style="color:#a6e22e">int</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// $input-&gt;email, $input-&gt;dryRun, etc.
</span></span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Command</span><span style="color:#f92672">::</span><span style="color:#a6e22e">SUCCESS</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>BackedEnum</code> is supported in invokable commands, so an option declared as a <code>Status</code> enum gets validated and cast automatically. Interactive commands get <code>#[Interact]</code> and <code>#[Ask]</code> attributes to declare question prompts inline. <code>CommandTester</code> works with invokable commands without any extra wiring.</p>
<h2 id="routing-finds-its-own-controllers">Routing finds its own controllers</h2>
<p>Routes defined via <code>#[Route]</code> on controller classes are auto-registered without needing an explicit <code>resource:</code> entry in <code>config/routes.yaml</code>. The tag <code>routing.controller</code> is applied automatically. You still control which directories are scanned, but your YAML config shrinks to a pointer at a directory rather than a manual file list.</p>
<p><code>#[Route]</code> also gains a <code>_query</code> parameter for setting query parameters at generation time, and multiple environments in the <code>env</code> option.</p>
<h2 id="security-csrf-and-oidc-get-better-tooling">Security: CSRF and OIDC get better tooling</h2>
<p><code>#[IsCsrfTokenValid]</code> gets a <code>$tokenSource</code> argument so you can specify where the token comes from (header, cookie, form field) rather than relying on a fixed convention. <code>SameOriginCsrfTokenManager</code> adds <code>Sec-Fetch-Site</code> header validation, a browser-native CSRF protection mechanism that doesn&rsquo;t need token injection at all.</p>
<p>The <code>security:oidc-token:generate</code> command creates tokens for testing OIDC-protected endpoints locally. Multiple OIDC discovery endpoints are supported now, useful in multi-tenant setups where each tenant has its own identity provider.</p>
<p>Two new Twig functions: <code>access_decision()</code> and <code>access_decision_for_user()</code> expose the authorization voter result in templates without going through the security facade. <code>#[IsGranted]</code> can be subclassed for repeated authorization patterns that deserve their own named attribute.</p>
<h2 id="objectmapper-and-jsonstreamer-leave-experimental">ObjectMapper and JsonStreamer leave experimental</h2>
<p>Both components introduced in 7.x graduate to stable in 8.0. <code>ObjectMapper</code> maps between objects without hand-written transformers, using attribute-based configuration. <code>JsonStreamer</code> reads and writes large JSON without loading the full document into memory, and it now supports synthetic properties: virtual fields computed at serialization time.</p>
<p><code>JsonStreamer</code> also drops its dependency on <code>nikic/php-parser</code>. The code generation for the reader/writer now uses a simpler internal mechanism, cutting a heavy dev dependency.</p>
<h2 id="uid-defaults-to-uuidv7">Uid defaults to UUIDv7</h2>
<p><code>UuidFactory</code> now generates UUIDv7 by default instead of UUIDv4. The difference: v7 is time-ordered, so generated UUIDs sort chronologically. That matters a lot for database index performance. <code>MockUuidFactory</code> provides deterministic UUID generation in tests.</p>
<h2 id="yaml-raises-an-error-on-duplicate-keys">Yaml raises an error on duplicate keys</h2>
<p>Previously, a YAML file with two identical keys silently kept the last one. 8.0 raises a parse error. This catches real bugs: duplicate keys in <code>services.yaml</code> or <code>config/packages/*.yaml</code> are almost always copy-paste mistakes and you definitely want to know about them.</p>
<h2 id="validator-video-constraint-and-wildcard-protocols">Validator: Video constraint and wildcard protocols</h2>
<p>A <code>Video</code> constraint joins the <code>Image</code> constraint for validating uploaded video files (MIME type, duration, codec). The <code>Url</code> constraint accepts <code>protocols: ['*']</code> to allow any RFC 3986-compliant scheme, useful when storing arbitrary URLs that include <code>git+ssh://</code>, <code>file://</code>, or custom app schemes.</p>
<h2 id="messenger-sqs-native-retry-and-new-events">Messenger: SQS native retry and new events</h2>
<p>SQS transport can now use its own native retry and dead-letter queue configuration instead of Symfony&rsquo;s retry middleware. For high-volume queues on AWS, this removes a round-trip through PHP for transient failures. A <code>MessageSentToTransportsEvent</code> fires after a message is dispatched, carrying information about which transports actually received it.</p>
<p><code>messenger:consume</code> gets <code>--exclude-receivers</code> to pair with <code>--all</code>.</p>
<h2 id="mailer-microsoft-graph-transport">Mailer: Microsoft Graph transport</h2>
<p>A new transport sends mail via the Microsoft Graph API, which is what Microsoft recommends for applications on Azure Active Directory these days. The other options (SMTP relay, Exchange EWS) still work, but Graph is the right choice for new Azure deployments.</p>
<h2 id="workflow-weighted-transitions">Workflow: weighted transitions</h2>
<p>Transitions can now declare weights. When multiple transitions are enabled from the same place, the highest-weight one wins. This lets you express priority directly in the workflow definition without adding a guard that reads an external counter.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">return</span> (<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Definition</span>(<span style="color:#a6e22e">states</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;draft&#39;</span>, <span style="color:#e6db74">&#39;review&#39;</span>, <span style="color:#e6db74">&#39;published&#39;</span>]))
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">addTransition</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Transition</span>(<span style="color:#e6db74">&#39;publish&#39;</span>, <span style="color:#e6db74">&#39;review&#39;</span>, <span style="color:#e6db74">&#39;published&#39;</span>, <span style="color:#a6e22e">weight</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">10</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">addTransition</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Transition</span>(<span style="color:#e6db74">&#39;reject&#39;</span>, <span style="color:#e6db74">&#39;review&#39;</span>, <span style="color:#e6db74">&#39;draft&#39;</span>, <span style="color:#a6e22e">weight</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">1</span>));
</span></span></code></pre></div><h2 id="lock-lockkeynormalizer">Lock: LockKeyNormalizer</h2>
<p><code>LockKeyNormalizer</code> normalizes a lock key to a consistent string before hashing. Useful when the key is derived from user input or external data that may vary in whitespace or casing: the normalizer makes sure the same logical key always maps to the same lock.</p>
<h2 id="httpfoundation-query-method-and-cleaner-body-parsing">HttpFoundation: QUERY method and cleaner body parsing</h2>
<p>The IETF <code>QUERY</code> method (a safe, idempotent method with a body, unlike <code>GET</code>) is now supported throughout the stack: <code>Request</code>, HTTP cache, WebProfiler, and HttpClient. If you build search APIs that need a structured request body and also want caching, <code>QUERY</code> is the right semantic choice.</p>
<p><code>Request::createFromGlobals()</code> now parses the body of <code>PUT</code>, <code>DELETE</code>, <code>PATCH</code>, and <code>QUERY</code> requests automatically.</p>
<h2 id="config-json-schema-for-yaml-validation">Config: JSON schema for YAML validation</h2>
<p>Symfony 8.0 auto-generates a JSON Schema file for each configuration section. IDEs that support JSON Schema for YAML files (VS Code, PhpStorm) can now validate <code>config/packages/*.yaml</code> against these schemas and provide autocompletion without any plugin. The schema is generated during cache warmup and placed at <code>config/reference.php</code>.</p>
<h2 id="runtime-frankenphp-auto-detection">Runtime: FrankenPHP auto-detection</h2>
<p>The Runtime component detects FrankenPHP automatically and activates worker mode without any extra package or environment variable. If <code>$_SERVER['APP_RUNTIME']</code> is set, that runtime class takes precedence. You can also pick the error renderer based on <code>APP_RUNTIME_MODE</code>, which is useful when running the same codebase in HTTP and CLI contexts with different error presentation needs.</p>
]]></content:encoded></item></channel></rss>