<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0"
    xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:atom="http://www.w3.org/2005/Atom"
>
    <channel>
        <title>Symfony Blog</title>
        <atom:link href="https://feeds.feedburner.com/symfony/blog" rel="self" type="application/rss+xml" />
        <link>https://symfony.com/blog/</link>
        <description>Most recent posts published on the Symfony project blog</description>
        <pubDate>Sun, 27 Sep 2026 08:29:58 +0200</pubDate>
        <lastBuildDate>Fri, 25 Sep 2026 10:58:00 +0200</lastBuildDate>
        <language>en</language>
                        <item>
            <title><![CDATA[New in Twig 4.0: Composing Blocks at Runtime]]></title>
            <link>https://symfony.com/blog/new-in-twig-4-0-composing-blocks-at-runtime?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>
    
                    
                
            
            
    
        Contributed by
                    Fabien Potencier
                                

A CMS often lets each site customize how content fields are displayed. The
default theme…</description>
            <content:encoded><![CDATA[
                                <div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/fabpot">
                <img src="https://github.com/fabpot.png" alt="Fabien Potencier">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/fabpot">Fabien Potencier</a>
                                </div>
</div>
<p>A CMS often lets each site customize how content fields are displayed. The
default theme defines the field wrapper, while a site theme overrides the block
used to render an image. The wrapper calls that block, but neither theme
extends the other. Inheritance alone cannot make the wrapper see the site's
override. <code translate="no" class="notranslate">BlockChain</code> composes their blocks at runtime. It first shipped
in Twig 3.29, so you can use it today.</p>
<div class="section">
<h2 id="using-blockchain"><a class="headerlink" href="#using-blockchain" title="Permalink to this headline">Using BlockChain</a></h2>
<p>The same pattern works for a data-grid cell: the admin theme provides the
wrapper, while the application theme customizes the value:</p>
<div translate="no" data-loc="3" class="notranslate codeblock codeblock-length-sm codeblock-html+twig codeblock-twig">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment">{# admin_theme.html.twig #}</span><span class="xml">
</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">extends</span></span> <span class="hljs-string">'admin_base.html.twig'</span> %}</span><span class="xml">
</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">block</span></span> cell %}</span><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"admin"</span>&gt;</span></span><span class="hljs-template-variable">{{ <span class="hljs-name">parent</span><span class="hljs-params">()</span> }}</span><span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span></span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">endblock</span></span> %}</span></code></pre>
    </div>
</div>
<div translate="no" data-loc="2" class="notranslate codeblock codeblock-length-sm codeblock-html+twig codeblock-twig">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment">{# admin_base.html.twig #}</span><span class="xml">
</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">block</span></span> cell %}</span><span class="hljs-template-variable">{{ <span class="hljs-name">block</span><span class="hljs-params">('cell_value')</span> }}</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">endblock</span></span> %}</span></code></pre>
    </div>
</div>
<div translate="no" data-loc="2" class="notranslate codeblock codeblock-length-sm codeblock-html+twig codeblock-twig">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment">{# application_theme.html.twig #}</span><span class="xml">
</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">block</span></span> cell_value %}</span><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">strong</span>&gt;</span></span><span class="hljs-template-variable">{{ value }}</span><span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">strong</span>&gt;</span></span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">endblock</span></span> %}</span></code></pre>
    </div>
</div>
<div translate="no" data-loc="2" class="notranslate codeblock codeblock-length-sm codeblock-html+twig codeblock-twig">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment">{# base_theme.html.twig #}</span><span class="xml">
</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">block</span></span> cell_value %}</span><span class="hljs-template-variable">{{ value }}</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">endblock</span></span> %}</span></code></pre>
    </div>
</div>
<p>Create a chain in order of precedence, then render the block you need:</p>
<div translate="no" data-loc="10" class="notranslate codeblock codeblock-length-md codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-keyword">use</span> <span class="hljs-title">Twig</span>\<span class="hljs-title">BlockChain</span>;

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>defaults</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">BlockChain</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>twig</span>, [<span class="hljs-string">'base_theme.html.twig'</span>]);
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>blocks</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">BlockChain</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>twig</span>, [
    <span class="hljs-string">'admin_theme.html.twig'</span>,
    <span class="hljs-string">'application_theme.html.twig'</span>,
    <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>defaults</span>,
]);

<span class="hljs-keyword">echo</span> <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>blocks</span>-&gt;<span class="hljs-title invoke__">renderBlock</span>(<span class="hljs-string">'cell'</span>, [<span class="hljs-string">'value'</span> =&gt; <span class="hljs-string">'Pending'</span>]);</code></pre>
    </div>
</div>
<p>The result is <code translate="no" class="notranslate">&lt;td class="admin"&gt;&lt;strong&gt;Pending&lt;/strong&gt;&lt;/td&gt;</code>. Each entry
can be a template name, a loaded template or another chain. You can reuse
<code translate="no" class="notranslate">$defaults</code> in other theme stacks.</p>
</div>
<div class="section">
<h2 id="how-block-lookup-works"><a class="headerlink" href="#how-block-lookup-works" title="Permalink to this headline">How Block Lookup Works</a></h2>
<p>The first matching block wins: <code translate="no" class="notranslate">cell</code> comes from the admin theme, while
<code translate="no" class="notranslate">block('cell_value')</code> picks the application override. <code translate="no" class="notranslate">parent()</code> stays
within the defining template's inheritance hierarchy; it does not jump to
the next theme. See the <a href="https://twig.symfony.com/doc/3.x/api.html#composing-blocks" class="reference external">BlockChain documentation</a> for the full lookup rules,
context handling and other methods.</p>
<p>The same pattern is useful for form controls, CMS fields and email fragments:
your renderer picks the block name, and Twig finds the right theme.</p>
</div>
<div class="section">
<h2 id="how-it-helps-open-source-projects"><a class="headerlink" href="#how-it-helps-open-source-projects" title="Permalink to this headline">How It Helps Open-Source Projects</a></h2>
<p><strong>Symfony Forms</strong>: Symfony's form renderer has long combined blocks from
unrelated themes: a form view can have its own theme, inherit themes from its
parent view and fall back to the default layout. A <code translate="no" class="notranslate">form_row</code> block from one
theme can call <code translate="no" class="notranslate">block('form_widget')</code> and find an override in another.
Before <code translate="no" class="notranslate">BlockChain</code>, the <a href="https://github.com/symfony/symfony/blob/6.4/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php" class="reference external" rel="external noopener noreferrer" target="_blank">Twig Bridge</a> had to walk template parents, merge
block maps and pass those maps to Twig's <strong>internal</strong> rendering API.</p>
<p>The Twig Bridge now uses <code translate="no" class="notranslate">BlockChain</code> when available.</p>
<p>This integration is available in all currently supported Symfony versions.
If you upgrade to Twig 3.29, update the Twig Bridge too.</p>
<p><strong>Beyond Symfony</strong>: Other renderers implement similar block composition and
could benefit from the same Twig API:</p>
<ul>
    <li><a href="https://github.com/ibexa/core/blob/6.0/src/lib/MVC/Symfony/Templating/Twig/FieldBlockRenderer.php" class="reference external" rel="external noopener noreferrer" target="_blank">Ibexa Core</a> selects field-type blocks, such as <code translate="no" class="notranslate">ibexa_string_field</code>,
across local and configured CMS field templates. Its field renderer currently
builds the block map itself.</li>
<li><a href="https://github.com/Prezent/prezent-grid/blob/master/src/Twig/GridRenderer.php" class="reference external" rel="external noopener noreferrer" target="_blank">Prezent Grid</a> layers grid themes and resolves widget blocks for column
types. A chain could compose its themes while the grid renderer keeps its
column-type fallback logic.</li>
<li><a href="https://github.com/pawellen/listing/blob/master/Renderer/ListingRenderer.php" class="reference external" rel="external noopener noreferrer" target="_blank">Listing</a> traverses template parents and merges their blocks before
rendering listing and column blocks. A chain could take over that traversal.</li>
</ul>
<p>The need to compose blocks across independent templates is not
specific to Symfony, which is why the API landed in Twig.</p>
</div>
                <hr style="margin-bottom: 5px" />
                <div style="font-size: 90%">
                    <a href="https://symfony.com/sponsor">Sponsor</a> the Symfony project.
                </div>
            ]]></content:encoded>
            <guid isPermaLink="false">https://symfony.com/blog/new-in-twig-4-0-composing-blocks-at-runtime?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Fabien Potencier ]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 10:58:00 +0200</pubDate>
            <comments>https://symfony.com/blog/new-in-twig-4-0-composing-blocks-at-runtime?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[New in Symfony 8.2: Faster Messenger Workers]]></title>
            <link>https://symfony.com/blog/new-in-symfony-8-2-faster-messenger-workers?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>
Parallel Message Processing

    
                    
                
            
            
    
        Contributed by
                    Nicolas Grekas
                                         in
                                    #63650…</description>
            <content:encoded><![CDATA[
                                <div class="section">
<h2 id="parallel-message-processing"><a class="headerlink" href="#parallel-message-processing" title="Permalink to this headline">Parallel Message Processing</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/nicolas-grekas">
                <img src="https://connect.symfony.com/profile/nicolas-grekas.picture" alt="Nicolas Grekas">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://connect.symfony.com/profile/nicolas-grekas">Nicolas Grekas</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/63650">#63650</a>
                                                </span>
            </div>
</div>
<p>A <a href="https://symfony.com/doc/current/messenger.html" class="reference external">Symfony Messenger</a> worker handles one message at a time. While a handler
waits for an HTTP response or a database query, its worker cannot handle another
message. If you wanted to process several messages at once, you had to run several
<code translate="no" class="notranslate">messenger:consume</code> processes. In Symfony 8.2, you can use the new
<code translate="no" class="notranslate">--concurrency</code> option:</p>
<div translate="no" data-loc="5" class="notranslate codeblock codeblock-length-sm codeblock-terminal codeblock-bash">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment"># first, install this dependency once in your project</span>
<span class="hljs-prompt">$ </span>composer require amphp/parallel

<span class="hljs-comment"># handle up to 4 messages at the same time</span>
<span class="hljs-prompt">$ </span>php bin/console messenger:consume async --concurrency=4</code></pre>
    </div>
</div>
<p>When using concurrency, the worker relies on the <a href="https://github.com/amphp/parallel" class="reference external" rel="external noopener noreferrer" target="_blank">amphp/parallel</a> library to
start a pool of child processes which boot your application and handle the
messages (if the <a href="https://www.php.net/manual/en/book.parallel.php" class="reference external" rel="external noopener noreferrer" target="_blank">parallel PHP extension</a> is also installed, it uses threads
instead of processes). The worker still fetches the messages itself, so it only
needs one connection to the transport, no matter how many messages are
handled in parallel.</p>
<p>In a benchmark with messages that take 20 ms each, <code translate="no" class="notranslate">--concurrency=8</code> was
<strong>7.5 times faster</strong> than a single worker.</p>
<p><a href="https://symfony.com/doc/current/messenger.html#process-messages-by-batches" class="reference external">Batch handlers</a> are an exception. All the messages of a batch must be handled
by the same child process, so <code translate="no" class="notranslate">--concurrency</code> doesn't make them faster. To
handle batched messages in parallel, keep running several workers as before.</p>
<p>If you used a batch handler only to handle messages faster (and not because
handling them together is cheaper), consider replacing it with a regular handler
and the new <code translate="no" class="notranslate">--concurrency</code> option.</p>
</div>
<div class="section">
<h2 id="faster-amqp-transport"><a class="headerlink" href="#faster-amqp-transport" title="Permalink to this headline">Faster AMQP Transport</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/nicolas-grekas">
                <img src="https://connect.symfony.com/profile/nicolas-grekas.picture" alt="Nicolas Grekas">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://connect.symfony.com/profile/nicolas-grekas">Nicolas Grekas</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/65924">#65924</a>
                     and                                     <a target="_blank" href="https://github.com/symfony/symfony/pull/65168">#65168</a>
                                                </span>
            </div>
</div>
<p>The AMQP transport asks the broker for one message at a time, which requires a
network round trip per message. RabbitMQ considers this the least efficient way
to consume messages. Symfony 8.2 adds a <code translate="no" class="notranslate">prefetch_count</code> option which registers
a real consumer on each queue and <strong>lets the broker push messages</strong> to it in advance:</p>
<div translate="no" data-loc="2" class="notranslate codeblock codeblock-length-sm codeblock-env codeblock-bash">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment"># .env</span>
MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages?prefetch_count=20</code></pre>
    </div>
</div>
<p>In benchmarks against a local broker, this consumed messages <strong>13 to 18 times
faster</strong>. Registered consumers also appear in the RabbitMQ management UI. Set
<code translate="no" class="notranslate">prefetch_count</code> higher than the worker's fetch size, which defaults to the
<code translate="no" class="notranslate">--concurrency</code> value.</p>
<p>Prefetching is disabled by default because it changes how messages are consumed:
the broker now decides the order across queues, and any prefetched messages still
waiting to be handled are redelivered when the worker stops.</p>
<p><strong>Delayed messages</strong> also become cheaper to send. AMQP has no native delays, so
Symfony creates a delay queue for each distinct delay value. The default retry
strategy adds jitter to these delays, resulting in more than a thousand different values.
Each retry therefore usually declares and binds a new queue.</p>
<p>In Symfony 8.2, delays are rounded up to two significant digits (e.g. <code translate="no" class="notranslate">5234</code>
ms becomes <code translate="no" class="notranslate">5300</code> ms). This reduces the number of delay queues by orders of
magnitude, while ensuring that messages are never released before the requested
delay and at most 10% later. Use the new <code translate="no" class="notranslate">delay[granularity]</code> option to round
delays to a fixed number of milliseconds instead, or set it to <code translate="no" class="notranslate">1</code> to disable rounding:</p>
<div translate="no" data-loc="2" class="notranslate codeblock codeblock-length-sm codeblock-env codeblock-bash">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment"># .env</span>
MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages?delay[granularity]=1000</code></pre>
    </div>
</div>
</div>
<div class="section">
<h2 id="logging-processing-time-and-memory-usage"><a class="headerlink" href="#logging-processing-time-and-memory-usage" title="Permalink to this headline">Logging Processing Time and Memory Usage</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/mariecharles">
                <img src="https://connect.symfony.com/profile/mariecharles.picture" alt="Marie Charles">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://connect.symfony.com/profile/mariecharles">Marie Charles</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/64621">#64621</a>
                                                </span>
            </div>
</div>
<p>This feature won't make your workers faster, but it will help you find which
messages are slow. The new optional <code translate="no" class="notranslate">logging</code> middleware records processing
time and memory usage for each message. Enable it on the buses you want to
monitor:</p>
<div class="configuration-block">
    <div role="tablist" aria-label="Configuration formats" class="configuration-tabs configuration-tabs-length-2">
                    <button role="tab" type="button" data-language="yaml" aria-controls="configuration-block-tabpanel-3881ecddc9ed75eac9253ba0a8e4a19b3f45ba46" aria-selected="true" data-active="true">
                <span>YAML</span>
            </button>
                    <button role="tab" type="button" data-language="php" aria-controls="configuration-block-tabpanel-c171d21b4b635e9d566e503a82a5f9a1aab248b6" aria-selected="false" tabindex="-1">
                <span>PHP</span>
            </button>
            </div>

            <div role="tabpanel" id="configuration-block-tabpanel-3881ecddc9ed75eac9253ba0a8e4a19b3f45ba46" aria-label="YAML" class="configuration-codeblock" data-language="yaml" style="">
            <div translate="no" data-loc="7" class="notranslate codeblock codeblock-length-sm codeblock-yaml">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment"># config/packages/messenger.yaml</span>
<span class="hljs-attr">framework:</span>
    <span class="hljs-attr">messenger:</span>
        <span class="hljs-attr">buses:</span>
            <span class="hljs-attr">messenger.bus.default:</span>
                <span class="hljs-attr">middleware:</span>
                    <span class="hljs-bullet">-</span> <span class="hljs-string">logging</span></code></pre>
    </div>
</div>
        </div>
            <div role="tabpanel" id="configuration-block-tabpanel-c171d21b4b635e9d566e503a82a5f9a1aab248b6" aria-label="PHP" class="configuration-codeblock" data-language="php" style="display: none">
            <div translate="no" data-loc="14" class="notranslate codeblock codeblock-length-md codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment">// config/packages/messenger.php</span>
<span class="hljs-keyword">namespace</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">DependencyInjection</span>\<span class="hljs-title">Loader</span>\<span class="hljs-title">Configurator</span>;

<span class="hljs-keyword">return</span> App::<span class="hljs-title invoke__">config</span>([
    <span class="hljs-string">'framework'</span> =&gt; [
        <span class="hljs-string">'messenger'</span> =&gt; [
            <span class="hljs-string">'buses'</span> =&gt; [
                <span class="hljs-string">'messenger.bus.default'</span> =&gt; [
                    <span class="hljs-string">'middleware'</span> =&gt; [<span class="hljs-string">'logging'</span>],
                ],
            ],
        ],
    ],
]);</code></pre>
    </div>
</div>
        </div>
    </div>
<p>The middleware measures everything that runs after it in the stack. It logs
messages in the <code translate="no" class="notranslate">messenger</code> channel with the <code translate="no" class="notranslate">class</code>, <code translate="no" class="notranslate">duration_ms</code> and
<code translate="no" class="notranslate">memory_usage</code> (in bytes) context keys, so you can build dashboards and alerts
from them:</p>
<div translate="no" data-loc="2" class="notranslate codeblock codeblock-length-sm codeblock-text">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code>messenger.INFO: "App\Message\GenerateInvoice" message successfully handled. {"class":"App\\Message\\GenerateInvoice","duration_ms":1834,"memory_usage":12582912}
messenger.ERROR: Unable to handle "App\Message\SyncCatalog" message. {"class":"App\\Message\\SyncCatalog","duration_ms":30012,"memory_usage":524288,"exception":"..."}</code></pre>
    </div>
</div>
</div>
                <hr style="margin-bottom: 5px" />
                <div style="font-size: 90%">
                    <a href="https://symfony.com/sponsor">Sponsor</a> the Symfony project.
                </div>
            ]]></content:encoded>
            <guid isPermaLink="false">https://symfony.com/blog/new-in-symfony-8-2-faster-messenger-workers?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Javier Eguiluz ]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 09:07:00 +0200</pubDate>
            <comments>https://symfony.com/blog/new-in-symfony-8-2-faster-messenger-workers?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[SymfonyCon Warsaw 2026: PHP’s Type System Dissected]]></title>
            <link>https://symfony.com/blog/symfonycon-warsaw-2026-php-s-type-system-dissected?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>
    

Looking to supercharge your PHP and Symfony expertise? SymfonyCon Warsaw 2026 is coming to Poland on November 26-27! Join developers from across the globe for two days packed with insight, featuring 3 parallel tracks and engaging community events.…</description>
            <content:encoded><![CDATA[
                                <p><a class="block text-center" href="https://live.symfony.com/2026-warsaw-con" title="Nl Blog Banner">
    <img src="https://symfony.com/uploads/assets/blog/NL-BLOG-Banner.png" alt="Nl Blog Banner">
</a>
Looking to supercharge your PHP and Symfony expertise? <strong><a href="https://live.symfony.com/2026-warsaw-con/">SymfonyCon Warsaw 2026</a></strong> is coming to Poland on November 26-27! Join developers from across the globe for two days packed with insight, featuring 3 parallel tracks and engaging community events. Save the date and secure your tickets today!</p>

<hr />

<h3>🎤 Speaker announcement</h3>

<p>Unraveling the mechanics under the hood: <strong><a href="https://connect.symfony.com/profile/girgias">Gina Banyard</a></strong>, PHP Core developer and PHP Doc maintainer at the PHP Foundation, is joining the speaker lineup with the talk <strong><a href="https://live.symfony.com/2026-warsaw-con/schedule/php-s-type-system-dissected">"PHP’s Type System Dissected"</a></strong>. We’re excited to have this expertise on stage!</p>

<p>"PHP has a type system, and it has quietly become one of the more interesting ones among dynamic languages. It grew by accretion over two decades: scalar types, nullable types, union and intersection types, never, true, and a notion of subtyping built on Liskov's Substitution Principle.</p>

<p>This talk starts from the formal question of what a type system actually is, then uses that vocabulary to explain PHP's. Why LSP is the rule that governs subtyping, what variance means for the signatures you write every day, and where PHP's system is genuinely sound as opposed to pragmatically compromised.</p>

<p>We finish by looking forward: what PHP's type system could still gain, what each addition would cost, and what is realistically on the table. And there's a practical reason to care right now. Types are the clearest signal a codebase gives a model about what it may do, so knowing what they actually guarantee is worth more than it was two years ago."</p>

<p>Click here to see more <strong><a href="https://live.symfony.com/2026-warsaw-con/schedule">talks</a></strong>. The full schedule will be unveiled step-by-step over the coming weeks. Stay tuned!</p>

<h3>Ready to join us?</h3>

<h3>🏨 Accommodation</h3>

<p>The official conference hotel offers special preferential rates and flexible conditions for our attendees. Make sure to use our <strong><a href="https://www.hilton.com/en/book/reservation/rooms/?ctyhocn=WAWHIHI&amp;arrivalDate=2026-11-23&amp;departureDate=2026-11-29&amp;groupCode=GSYMDP&amp;room1NumAdults=1">dedicated link</a></strong> to book your room and stay right where the action happens!</p>

<h3>🥳 Save the date for the Community Party!</h3>

<p>Because SymfonyCon isn't just about code, we are happy to confirm that our traditional Community Party will take place on the evening of <strong>Thursday, November 26</strong>. Get ready for a great night of networking, fun, and connecting with peers!</p>

<h3>💻 Hackday</h3>

<p>Ready to contribute? The Hackday on <strong>Saturday, November 28</strong> (free and open to all!) will take place at "The Brain Embassy - Czackiego" thanks to the support of <a href="https://baksla.sh/">Baksla.sh</a>. Bring your laptop and your good mood!</p>

<h3>🎟️ Get your tickets</h3>

<p>Tickets are available: <a href="https://live.symfony.com/2026-warsaw-con/registration">Secure your ticket today!</a>**</p>

<hr />

<h3>Join us online!</h3>

<p>💡Follow the "conference" blog posts to not miss anything!</p>

<p>Want the latest Symfony updates? Follow us and tune in from wherever you are 🌎</p>

<p><a class="block text-center" href="https://linktr.ee/symfony">
   <img src="https://symfony.com/uploads/assets/blog/Banner-BLOG.png" alt="Banner Blog">
</a></p>

                <hr style="margin-bottom: 5px" />
                <div style="font-size: 90%">
                    <a href="https://symfony.com/sponsor">Sponsor</a> the Symfony project.
                </div>
            ]]></content:encoded>
            <guid isPermaLink="false">https://symfony.com/blog/symfonycon-warsaw-2026-php-s-type-system-dissected?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Eloïse Charrier ]]></dc:creator>
            <pubDate>Thu, 24 Sep 2026 14:30:00 +0200</pubDate>
            <comments>https://symfony.com/blog/symfonycon-warsaw-2026-php-s-type-system-dissected?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[New in Symfony 8.2: Provider-Hosted Email Templates and Tracking Control]]></title>
            <link>https://symfony.com/blog/new-in-symfony-8-2-provider-hosted-email-templates-and-tracking-control?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>
Provider-Hosted Templates

    
                    
                
            
                    
                
            
            
    
        Contributed by
                    Florent Blaison
             and                     Nicolas…</description>
            <content:encoded><![CDATA[
                                <div class="section">
<h2 id="provider-hosted-templates"><a class="headerlink" href="#provider-hosted-templates" title="Permalink to this headline">Provider-Hosted Templates</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/Orkin">
                <img src="https://connect.symfony.com/profile/Orkin.picture" alt="Florent Blaison">
            </a>
                    <a target="_blank" href="https://connect.symfony.com/profile/nicolas-grekas">
                <img src="https://connect.symfony.com/profile/nicolas-grekas.picture" alt="Nicolas Grekas">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://connect.symfony.com/profile/Orkin">Florent Blaison</a>
             and                     <a target="_blank" class="blog-post-contributor-name" href="https://connect.symfony.com/profile/nicolas-grekas">Nicolas Grekas</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/64782">#64782</a>
                     and                                     <a target="_blank" href="https://github.com/symfony/symfony/pull/65819">#65819</a>
                                                </span>
            </div>
</div>
<p>Many email providers (Brevo, Mailgun, Mailjet, etc.) include a visual
drag-and-drop builder for email templates. This lets non-technical people, such
as marketing or support teams, design emails and update their content when
needed. However, if your emails use Twig templates stored in your application,
those same people can't edit them without asking a developer.</p>
<p>Using templates stored in the provider account solves this problem, but until
now Symfony Mailer had no common way to use them. Instead, some bridges relied
on special headers (such as <code translate="no" class="notranslate">templateid</code> in Brevo or <code translate="no" class="notranslate">X-MJ-TemplateID</code> in
Mailjet).</p>
<p>Symfony 8.2 adds a new <code translate="no" class="notranslate">RemoteTemplateEmail</code> class. Instead of building the
email body yourself, you pass the template reference and the variables the
provider needs to render it:</p>
<div translate="no" data-loc="12" class="notranslate codeblock codeblock-length-md codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Mailer</span>\<span class="hljs-title">RemoteTemplateEmail</span>;

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>email</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">RemoteTemplateEmail</span>()
    -&gt;<span class="hljs-keyword">from</span>(<span class="hljs-string">'sales@example.com'</span>)
    -&gt;<span class="hljs-title invoke__">to</span>(<span class="hljs-string">'kevin@example.com'</span>)
    -&gt;<span class="hljs-title invoke__">template</span>(<span class="hljs-string">'order-confirmation'</span>, [
        <span class="hljs-string">'firstName'</span> =&gt; <span class="hljs-string">'Kevin'</span>,
        <span class="hljs-string">'orderId'</span> =&gt; <span class="hljs-number">4321</span>,
    ])
;

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>mailer</span>-&gt;<span class="hljs-title invoke__">send</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>email</span>);</code></pre>
    </div>
</div>
<p>The template reference is always a string (an ID, UUID, name, or alias,
depending on the provider), and each transport converts it to the type expected
by its API. Everything else (recipients, attachments, tags, metadata, etc.)
works just like a regular <code translate="no" class="notranslate">Email</code>. You can also set a subject to override the
one defined in the template, except with Amazon SES, Mailtrap, and Postmark,
which don't support this.</p>
<p>This feature is supported by the API transports of Amazon SES, Brevo,
MailerSend, Mailgun, Mailjet, Mailtrap, Mandrill, Postmark, Resend, and
SendGrid.</p>
</div>
<div class="section">
<h2 id="per-message-open-and-click-tracking"><a class="headerlink" href="#per-message-open-and-click-tracking" title="Permalink to this headline">Per-Message Open and Click Tracking</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/Starfox64">
                <img src="https://github.com/Starfox64.png" alt="Starfox64">
            </a>
                    <a target="_blank" href="https://connect.symfony.com/profile/nicolas-grekas">
                <img src="https://connect.symfony.com/profile/nicolas-grekas.picture" alt="Nicolas Grekas">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Starfox64">Starfox64</a>
             and                     <a target="_blank" class="blog-post-contributor-name" href="https://connect.symfony.com/profile/nicolas-grekas">Nicolas Grekas</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/65451">#65451</a>
                                                </span>
            </div>
</div>
<p>Email open and click tracking is usually a global setting in your provider
account, so it applies to all your emails. However, privacy regulations may
require asking each user for consent before tracking them. The new
<code translate="no" class="notranslate">TrackingHeader</code> lets you override the provider setting for a single email,
and each bridge maps it to the provider's native mechanism:</p>
<div translate="no" data-loc="12" class="notranslate codeblock codeblock-length-md codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Mailer</span>\<span class="hljs-title">Header</span>\<span class="hljs-title">TrackingHeader</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Mime</span>\<span class="hljs-title">Email</span>;

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>email</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">Email</span>()
    -&gt;<span class="hljs-title invoke__">to</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>user</span>-&gt;<span class="hljs-title invoke__">getEmail</span>())
    <span class="hljs-comment">// ...</span>
;

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>email</span>-&gt;<span class="hljs-title invoke__">getHeaders</span>()-&gt;<span class="hljs-title invoke__">add</span>(<span class="hljs-keyword">new</span> <span class="hljs-title invoke__">TrackingHeader</span>(
    <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>user</span>-&gt;<span class="hljs-title invoke__">hasConsentedToOpenTracking</span>(),
    <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>user</span>-&gt;<span class="hljs-title invoke__">hasConsentedToClickTracking</span>(),
));</code></pre>
    </div>
</div>
<p>Both arguments are optional. When an argument is <code translate="no" class="notranslate">null</code>, the provider setting
is used for that type of tracking.</p>
<p>If an email doesn't add this header (for example, emails sent by third-party
bundles), the provider setting applies. To be on the safe side, use the new
<code translate="no" class="notranslate">tracking</code> option to disable tracking by default for all emails. A header
added to an individual email always takes precedence over this option:</p>
<div class="configuration-block">
    <div role="tablist" aria-label="Configuration formats" class="configuration-tabs configuration-tabs-length-2">
                    <button role="tab" type="button" data-language="yaml" aria-controls="configuration-block-tabpanel-772757ad324a24d0aa8658767a3520f43cedd0c6" aria-selected="true" data-active="true">
                <span>YAML</span>
            </button>
                    <button role="tab" type="button" data-language="php" aria-controls="configuration-block-tabpanel-5145c66d0a02286566441fa3dbaa28298d96bf32" aria-selected="false" tabindex="-1">
                <span>PHP</span>
            </button>
            </div>

            <div role="tabpanel" id="configuration-block-tabpanel-772757ad324a24d0aa8658767a3520f43cedd0c6" aria-label="YAML" class="configuration-codeblock" data-language="yaml" style="">
            <div translate="no" data-loc="6" class="notranslate codeblock codeblock-length-sm codeblock-yaml">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment"># config/packages/mailer.yaml</span>
<span class="hljs-attr">framework:</span>
    <span class="hljs-attr">mailer:</span>
        <span class="hljs-attr">tracking:</span>
            <span class="hljs-attr">opens:</span> <span class="hljs-literal">false</span>
            <span class="hljs-attr">clicks:</span> <span class="hljs-literal">false</span></code></pre>
    </div>
</div>
        </div>
            <div role="tabpanel" id="configuration-block-tabpanel-5145c66d0a02286566441fa3dbaa28298d96bf32" aria-label="PHP" class="configuration-codeblock" data-language="php" style="display: none">
            <div translate="no" data-loc="8" class="notranslate codeblock codeblock-length-sm codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment">// config/packages/mailer.php</span>
<span class="hljs-keyword">return</span> App::<span class="hljs-title invoke__">config</span>([
    <span class="hljs-string">'framework'</span> =&gt; [
        <span class="hljs-string">'mailer'</span> =&gt; [
            <span class="hljs-string">'tracking'</span> =&gt; [<span class="hljs-string">'opens'</span> =&gt; <span class="hljs-keyword">false</span>, <span class="hljs-string">'clicks'</span> =&gt; <span class="hljs-keyword">false</span>],
        ],
    ],
]);</code></pre>
    </div>
</div>
        </div>
    </div>
<p>This feature is supported by AhaSend, Azure, Brevo, Infobip, Mailchimp,
MailerSend, Mailgun, Mailjet, Postmark, and SendGrid. With other transports,
it's sent as a regular <code translate="no" class="notranslate">X-Track</code> header and has no effect.</p>
<p>Read the <a href="https://symfony.com/doc/8.2/mailer.html" class="reference external">Mailer documentation</a> for all the details about these features,
including the specific behavior of each provider.</p>
</div>
                <hr style="margin-bottom: 5px" />
                <div style="font-size: 90%">
                    <a href="https://symfony.com/sponsor">Sponsor</a> the Symfony project.
                </div>
            ]]></content:encoded>
            <guid isPermaLink="false">https://symfony.com/blog/new-in-symfony-8-2-provider-hosted-email-templates-and-tracking-control?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Javier Eguiluz ]]></dc:creator>
            <pubDate>Thu, 24 Sep 2026 09:27:00 +0200</pubDate>
            <comments>https://symfony.com/blog/new-in-symfony-8-2-provider-hosted-email-templates-and-tracking-control?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[Case Study: Scanning for quality: How Yuka fuels healthy choices at global scale with Symfony]]></title>
            <link>https://symfony.com/blog/case-study-scanning-for-quality-how-yuka-fuels-healthy-choices-at-global-scale-with-symfony?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>
    


Yuka is a global leader in health and environmental transparency. Available across iOS and Android, its mobile application helps over 68 million consumers worldwide decode food and cosmetic labels instantly. With a lean technical team of just five…</description>
            <content:encoded><![CDATA[
                                <p><a class="block d-block text-center" href="https://yuka.io/" title="Logo Yuka">
    <img src="https://symfony.com/uploads/assets/blog/Logo-Yuka.png" width="400" height="130" alt="Logo Yuka">
</a></p>

<p><strong><a href="https://yuka.io/">Yuka</a></strong> is a global leader in health and environmental transparency. Available across iOS and Android, its mobile application helps over 68 million consumers worldwide decode food and cosmetic labels instantly. With a lean technical team of just five people, including two co-founders, Yuka manages a massive database of over 5 million products, handling an intense traffic load of more than 70 product scans every single second. To scale this Tech for Good powerhouse without inflating engineering overhead, Yuka relies on Symfony as its back-end foundation.</p>

<h2>The high-growth diet: 70 scans per second on a lean back-end</h2>

<p>Operating globally across 13 countries, the Yuka mobile app acts as a direct consumer client that communicates natively with a customized Symfony API. The application serves a simple color-coded rating (green to red) alongside deep-dive nutritional evaluations instantly upon a barcode scan.</p>

<p>When your application handles 70 request cycles per second, any latency in data hydration or controller execution ripples into user frustration. Symfony’s highly optimized HTTP kernel and flexible caching abstraction layers became crucial assets. During their recent modernization, the team refactored core API endpoints to cache complex database payloads. By integrating native caching mechanisms seamlessly within the business logic, Yuka successfully minimized expensive, repeated queries to their Algolia search index. This directly stabilized back-end performance across millions of unique products while significantly slashing external infrastructure costs.</p>

<p><em>"For a team of our size, falling behind on aging dependencies is a risk we couldn't take. Moving to Symfony 7 proved that you don't need a massive engineering department to run a global scale app, you just need the right framework architecture."</em> — François Martin, Co-Founder and CTO at Yuka</p>

<h2>The clean upgrade: jumping from Symfony 4 to 7</h2>

<p>As the application grew rapidly, the core architecture remained tied to Symfony 4, EasyAdmin 1, and PHP 7.4. While highly stable, this legacy stack created a compounding technical debt: the team missed out on modern PHP 8+ performance optimizations, security patches were nearing end-of-life, and recruiting new talent to an aging codebase grew increasingly complex.</p>

<p>To break this bottleneck, Yuka partnered with the framework experts at <a href="https://sensiolabs.com/">SensioLabs</a> to orchestrate a systematic major version upgrade. The application was incrementally migrated through Symfony 5 and 6, ultimately landing cleanly on Symfony 7. The database abstraction layer was simultaneously lifted from Doctrine ORM 2 to version 3, and the runtime environment transitioned to PHP 8.3.</p>

<pre><code>[Symfony 4 / PHP 7.4] ──&gt; [Symfony 5 &amp; 6] ──&gt; [Symfony 7 / PHP 8.3]
</code></pre>

<p><br>
To manage the extensive structural changes and custom back-office overrides required when moving from EasyAdmin 1 to EasyAdmin 4, the engineering team automated code compliance by introducing a modern developer toolkit:</p>

<ul>
<li><strong>Rector &amp; PHP CS Fixer:</strong> Automated abstract syntax tree (AST) refactoring to rewrite deprecated method calls and enforce styling standards.</li>
<li><strong>PHPStan:</strong> Introduced strict static analysis to capture type-hint mismatches and hidden regressions before runtime.</li>
<li><strong>Continuous Integration:</strong> Built a robust automated CI test pipeline to safeguard API endpoint contracts across language upgrades.</li>
</ul>

<p>Because Symfony maintains strict backward compatibility standards across its release cycles, this modernization path ensures Yuka can easily transition to Symfony 7.4, the Long-Term Support (LTS) release, guaranteeing operational support and security patches through the end of 2028.</p>

<h2>Giving back: Open Source fuel for the ecosystem</h2>

<p>For Yuka, building on Symfony isn't a one-way street. The process of upgrading heavily relied on a highly cooperative relationship with the open-source community. Resolving version constraints for high-throughput mobile applications frequently requires extending ecosystem packages.
Throughout the upgrade process, Yuka and the SensioLabs experts directly contributed features, compatibility patches, and bug fixes back into several vital third-party packages, including:</p>

<ul>
<li><strong><a href="https://github.com/EasyCorp/EasyAdminBundle/">EasyAdminBundle</a></strong> (Back-office administration)</li>
<li><strong><a href="https://github.com/nayzo/NzoUrlEncryptorBundle">NzoUrlEncryptorBundle</a></strong> (Secure parameter encryption)</li>
<li><strong><a href="https://github.com/beste/firebase-bundle">kreait/firebase-bundle</a></strong> (Mobile push notification infrastructure)</li>
<li><strong><a href="https://github.com/Algolia/search-bundle">Algolia/search-bundle</a></strong> (High-performance product index searching)</li>
</ul>

<p>By choosing an open-source architectural foundation, Yuka did not merely resolve its technical debt, the engineering team actively enriched the global PHP ecosystem, proving that Tech for Good companies can directly drive the open-source tools they rely on.</p>

<h2>A shared vision for a better digital and physical world</h2>

<p>Ultimately, the collaboration between Yuka and Symfony is rooted in a deep alignment of values. Yuka is fundamentally a Tech for Good company, empowering millions of citizens to make informed, healthier choices for their bodies and the planet through transparency. Symfony operates on a parallel philosophy in the digital space, championing open-source software, transparency, collective collaboration, and democratization of high-performance technology.</p>

<p>When a major consumer-advocacy app scales globally on an open framework, it proves that building a healthier world physical-wise is made entirely possible by fostering a sustainable, collaborative world digital-wise. By working together to uplift both codebase quality and ecosystem packages, Symfony and Yuka demonstrate that technology reaches its highest potential when it serves the common good.</p>

<p><em>Thanks to François Martin, Co-Founder and CTO at <a href="https://yuka.io/">Yuka</a>, and the engineering team at <a href="https://sensiolabs.com/">SensioLabs</a>.</em></p>

<hr />

<h3>Technical stack</h3>

<ul>
<li>Symfony</li>
<li>Doctrine ORM, EasyAdmin</li>
<li>Algolia, Firebase, NzoUrlEncryptorBundle</li>
<li>PHPStan, Rector, PHP CS Fixer, Automated CI</li>
<li>Native iOS &amp; Android apps</li>
</ul>

                <hr style="margin-bottom: 5px" />
                <div style="font-size: 90%">
                    <a href="https://symfony.com/sponsor">Sponsor</a> the Symfony project.
                </div>
            ]]></content:encoded>
            <guid isPermaLink="false">https://symfony.com/blog/case-study-scanning-for-quality-how-yuka-fuels-healthy-choices-at-global-scale-with-symfony?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Eloïse Charrier ]]></dc:creator>
            <pubDate>Wed, 23 Sep 2026 14:30:00 +0200</pubDate>
            <comments>https://symfony.com/blog/case-study-scanning-for-quality-how-yuka-fuels-healthy-choices-at-global-scale-with-symfony?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[New in Symfony 8.2: JSON Schema for Configuration]]></title>
            <link>https://symfony.com/blog/new-in-symfony-8-2-json-schema-for-configuration?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>Symfony 7.4 added JSON schemas for YAML files like services.yaml and
routes.yaml, so IDEs can autocomplete and validate them. The files in
config/packages/ weren&#039;t included and that&#039;s where you write or maintain most
of your YAML config.
Each bundle defines…</description>
            <content:encoded><![CDATA[
                                <p>Symfony 7.4 added <a href="https://symfony.com/blog/new-in-symfony-7-4-deprecated-xml-configuration" class="reference external">JSON schemas</a> for YAML files like <code translate="no" class="notranslate">services.yaml</code> and
<code translate="no" class="notranslate">routes.yaml</code>, so IDEs can autocomplete and validate them. The files in
<code translate="no" class="notranslate">config/packages/</code> weren't included and that's where you write or maintain most
of your YAML config.</p>
<p>Each bundle defines its own configuration tree, so the schema depends on which
bundles your application uses. Symfony 8.2 handles this by <strong>generating a JSON
Schema dynamically</strong> for your application configuration.</p>
<div class="section">
<h2 id="using-the-schema-in-your-editor"><a class="headerlink" href="#using-the-schema-in-your-editor" title="Permalink to this headline">Using the Schema in Your Editor</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/gromnan">
                <img src="https://connect.symfony.com/profile/gromnan.picture" alt="Jérôme Tamarelle">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://connect.symfony.com/profile/gromnan">Jérôme Tamarelle</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/62125">#62125</a>
                                                </span>
            </div>
</div>
<p>When the container is compiled in debug mode and the <code translate="no" class="notranslate">symfony/yaml</code> package is
installed, Symfony 8.2 writes a <code translate="no" class="notranslate">config/schema.json</code> file. It merges the
configuration trees of every registered bundle, including the <code translate="no" class="notranslate">when@&lt;env&gt;</code>
blocks. The file is regenerated each time the container is compiled, so it
always matches the bundles installed in your app.</p>
<p>This is the YAML counterpart of the <code translate="no" class="notranslate">config/reference.php</code> file that Symfony 7.4
introduced for <a href="https://symfony.com/blog/new-in-symfony-7-4-better-php-configuration" class="reference external">PHP configuration</a>. Your IDE doesn't pick up the new file
automatically, so you need to point it to <code translate="no" class="notranslate">config/schema.json</code>.</p>
<p>Any editor based on <a href="https://github.com/redhat-developer/yaml-language-server" class="reference external" rel="external noopener noreferrer" target="_blank">yaml-language-server</a> (Visual Studio Code with the YAML
extension, Neovim, Emacs, etc.) reads this comment at the top of the file:</p>
<div translate="no" data-loc="5" class="notranslate codeblock codeblock-length-sm codeblock-yaml">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment"># config/packages/framework.yaml</span>
<span class="hljs-comment"># yaml-language-server: $schema=../schema.json</span>
<span class="hljs-attr">framework:</span>
    <span class="hljs-attr">secret:</span> <span class="hljs-string">'%env(APP_SECRET)%'</span>
    <span class="hljs-comment"># ...</span></code></pre>
    </div>
</div>
<p>The path is relative to the edited file, so use <code translate="no" class="notranslate">../../schema.json</code> inside
<code translate="no" class="notranslate">config/packages/&lt;env&gt;/</code>. In Visual Studio Code you can also map the schema to
every file in <code translate="no" class="notranslate">config/packages/</code> at once in <code translate="no" class="notranslate">.vscode/settings.json</code>:</p>
<div translate="no" data-loc="5" class="notranslate codeblock codeblock-length-sm codeblock-json">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code>{
    <span class="hljs-attr">"yaml.schemas"</span>: {
        <span class="hljs-attr">"config/schema.json"</span>: <span class="hljs-string">"config/packages/**/*.yaml"</span>
    }
}</code></pre>
    </div>
</div>
<p>In PhpStorm, add the same mapping in <em>Settings &gt; Languages &amp; Frameworks &gt;
Schemas and DTDs &gt; JSON Schema Mappings</em>. Your editor can then autocomplete
option names and show their descriptions and default values. It also flags
unknown or deprecated options and values with the wrong type.</p>
</div>
<div class="section">
<h2 id="validating-yaml-files-with-lint-yaml"><a class="headerlink" href="#validating-yaml-files-with-lint-yaml" title="Permalink to this headline">Validating YAML Files with <code translate="no" class="notranslate">lint:yaml</code></a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/gromnan">
                <img src="https://connect.symfony.com/profile/gromnan.picture" alt="Jérôme Tamarelle">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://connect.symfony.com/profile/gromnan">Jérôme Tamarelle</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/65394">#65394</a>
                                                </span>
            </div>
</div>
<p>The <code translate="no" class="notranslate">lint:yaml</code> command can now check YAML files against a JSON Schema as well
as checking their syntax. You can use this in CI to catch configuration errors.
Schema validation requires installing the <a href="https://github.com/opis/json-schema" class="reference external" rel="external noopener noreferrer" target="_blank">opis/json-schema</a> library:</p>
<div translate="no" data-loc="1" class="notranslate codeblock codeblock-length-sm codeblock-terminal codeblock-bash">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-prompt">$ </span>composer require opis/json-schema</code></pre>
    </div>
</div>
<p>Then, pass the new <code translate="no" class="notranslate">--check-schema</code> option:</p>
<div translate="no" data-loc="5" class="notranslate codeblock codeblock-length-sm codeblock-terminal codeblock-bash">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment"># validate the given files against a specific schema</span>
<span class="hljs-prompt">$ </span>php bin/console lint:yaml config/packages/ --check-schema=config/schema.json

<span class="hljs-comment"># validate each file against its own schema</span>
<span class="hljs-prompt">$ </span>php bin/console lint:yaml config/ --check-schema</code></pre>
    </div>
</div>
<p>The second command reads the same <code translate="no" class="notranslate"># yaml-language-server: $schema=</code> comment
that editors use. In Symfony applications, files in the usual locations don't
even need that comment:</p>
<ul>
    <li>Files in <code translate="no" class="notranslate">config/packages/</code> use the generated <code translate="no" class="notranslate">config/schema.json</code>.</li>
<li>Routes, services, serializer and validator files (<code translate="no" class="notranslate">config/routes.yaml</code>,
<code translate="no" class="notranslate">config/services.yaml</code>, etc.) use their components' schemas.</li>
</ul>
<p>Files without a schema are reported as valid, so you can run the command on the
whole <code translate="no" class="notranslate">config/</code> directory.</p>
<div class="admonition admonition-note ">
    <p class="admonition-title">
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" width="24" height="24" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
                <span>Note</span>
    </p><p>The generated schema describes the static configuration tree, so it can't
represent some values that bundles accept via runtime normalization (for
example, options that accept a list only thanks to a <code translate="no" class="notranslate">beforeNormalization()</code>
closure). In those cases, <code translate="no" class="notranslate">lint:yaml</code> may report errors for valid
configuration.</p>
</div>
</div>
                <hr style="margin-bottom: 5px" />
                <div style="font-size: 90%">
                    <a href="https://symfony.com/sponsor">Sponsor</a> the Symfony project.
                </div>
            ]]></content:encoded>
            <guid isPermaLink="false">https://symfony.com/blog/new-in-symfony-8-2-json-schema-for-configuration?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Javier Eguiluz ]]></dc:creator>
            <pubDate>Wed, 23 Sep 2026 09:07:00 +0200</pubDate>
            <comments>https://symfony.com/blog/new-in-symfony-8-2-json-schema-for-configuration?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[SymfonyCon Warsaw 2026: Real Browsers, Real Kernel: Playwright Testing in Symfony]]></title>
            <link>https://symfony.com/blog/symfonycon-warsaw-2026-real-browsers-real-kernel-playwright-testing-in-symfony?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>
    

Connect, learn, and grow with the international Symfony ecosystem! SymfonyCon Warsaw 2026 lands in Poland on November 26-27. Expect two full days of high-value talks across 3 tracks, paired with vibrant community side-events. Don&#039;t miss out—lock in…</description>
            <content:encoded><![CDATA[
                                <p><a class="block text-center" href="https://live.symfony.com/2026-warsaw-con" title="Nl Blog Banner">
    <img src="https://symfony.com/uploads/assets/blog/NL-BLOG-Banner.png" alt="Nl Blog Banner">
</a>
Connect, learn, and grow with the international Symfony ecosystem! <strong><a href="https://live.symfony.com/2026-warsaw-con/">SymfonyCon Warsaw 2026</a></strong> lands in Poland on November 26-27. Expect two full days of high-value talks across 3 tracks, paired with vibrant community side-events. Don't miss out—lock in your tickets today!</p>

<hr />

<h3>🎤 Speaker announcement</h3>

<p>Please welcome <strong><a href="https://connect.symfony.com/profile/simonandre">Simon André</a></strong>, Lead Developer, SensioLabs. Join us to catch his presentation on <strong><a href="https://live.symfony.com/2026-warsaw-con/schedule/testing-symfony-apps-beyond-the-response">"Real Browsers, Real Kernel: Playwright Testing in Symfony"</a></strong>:</p>

<p>"A DOM emulator reproduces your HTML. It does not reproduce a browser. Meanwhile browser tests usually run against a separate server, blind to application state. PlaywrightPHP and the Playwright Symfony bundle close that gap: a real browser drives the page while the test kernel handles the request in the same PHP process, no web server involved. One PHPUnit test can drag a card in a Live Component, wait for the async update, then assert the DOM, the dispatched events, and the database rows. Three live demos cover payment flows, Live Components, and visual regression on a design system."</p>

<p>Click here to see more <strong><a href="https://live.symfony.com/2026-warsaw-con/schedule">talks</a></strong>. The full schedule will be unveiled step-by-step over the coming weeks. Stay tuned!</p>

<h3>Ready to join us?</h3>

<h3>🏨 Accommodation</h3>

<p>The official conference hotel offers special preferential rates and flexible conditions for our attendees. Make sure to use our <strong><a href="https://www.hilton.com/en/book/reservation/rooms/?ctyhocn=WAWHIHI&amp;arrivalDate=2026-11-23&amp;departureDate=2026-11-29&amp;groupCode=GSYMDP&amp;room1NumAdults=1">dedicated link</a></strong> to book your room and stay right where the action happens!</p>

<h3>🥳 Save the date for the Community Party!</h3>

<p>Because SymfonyCon isn't just about code, we are happy to confirm that our traditional Community Party will take place on the evening of <strong>Thursday, November 26</strong>. Get ready for a great night of networking, fun, and connecting with peers!</p>

<h3>💻 Hackday</h3>

<p>Ready to contribute? The Hackday on <strong>Saturday, November 28</strong> (free and open to all!) will take place at "The Brain Embassy - Czackiego" thanks to the support of <a href="https://baksla.sh/">Baksla.sh</a>. Bring your laptop and your good mood!</p>

<h3>🎟️ Get your tickets</h3>

<p>Regular tickets are available now and for <strong>tomorrow only! <a href="https://live.symfony.com/2026-warsaw-con/registration">Secure your ticket today</a></strong></p>

<hr />

<h3>Join us online!</h3>

<p>💡Follow the "conference" blog posts to not miss anything!</p>

<p>Want the latest Symfony updates? Follow us and tune in from wherever you are 🌎</p>

<p><a class="block text-center" href="https://linktr.ee/symfony">
   <img src="https://symfony.com/uploads/assets/blog/Banner-BLOG.png" alt="Banner Blog">
</a></p>

                <hr style="margin-bottom: 5px" />
                <div style="font-size: 90%">
                    <a href="https://symfony.com/sponsor">Sponsor</a> the Symfony project.
                </div>
            ]]></content:encoded>
            <guid isPermaLink="false">https://symfony.com/blog/symfonycon-warsaw-2026-real-browsers-real-kernel-playwright-testing-in-symfony?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Eloïse Charrier ]]></dc:creator>
            <pubDate>Tue, 22 Sep 2026 17:49:00 +0200</pubDate>
            <comments>https://symfony.com/blog/symfonycon-warsaw-2026-real-browsers-real-kernel-playwright-testing-in-symfony?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[New in Symfony 8.2: Role Hierarchy Wildcards and Debugging]]></title>
            <link>https://symfony.com/blog/new-in-symfony-8-2-role-hierarchy-wildcards-and-debugging?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>The role hierarchy lets you define which roles are included in other roles, so
you don&#039;t have to assign all of them to each user. In applications with many
roles, this configuration grows quickly and it&#039;s hard to tell which exact roles
belong to a given user.…</description>
            <content:encoded><![CDATA[
                                <p>The <a href="https://symfony.com/doc/current/security.html#hierarchical-roles" class="reference external">role hierarchy</a> lets you define which roles are included in other roles, so
you don't have to assign all of them to each user. In applications with many
roles, this configuration grows quickly and it's hard to tell which exact roles
belong to a given user. Symfony 8.2 makes role hierarchies <strong>shorter to write and
easier to debug</strong>.</p>
<div class="section">
<h2 id="wildcards-in-the-role-hierarchy"><a class="headerlink" href="#wildcards-in-the-role-hierarchy" title="Permalink to this headline">Wildcards in the Role Hierarchy</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/squrious">
                <img src="https://connect.symfony.com/profile/squrious.picture" alt="Nicolas Rigaud">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://connect.symfony.com/profile/squrious">Nicolas Rigaud</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/52099">#52099</a>
                                                </span>
            </div>
</div>
<p>Role hierarchies list their roles one by one. If your application defines e.g.
<code translate="no" class="notranslate">ROLE_BLOG_AUTHOR</code>, <code translate="no" class="notranslate">ROLE_BLOG_EDITOR</code> and <code translate="no" class="notranslate">ROLE_BLOG_MODERATOR</code>, and all of
them should include <code translate="no" class="notranslate">ROLE_BLOG_READER</code>, you need one entry for each of them, plus
another one whenever you add a new blog role.</p>
<p>In Symfony 8.2, the keys of the role hierarchy can include the <code translate="no" class="notranslate">*</code> wildcard to
match an entire family of roles:</p>
<div translate="no" data-loc="9" class="notranslate codeblock codeblock-length-sm codeblock-yaml">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment"># config/packages/security.yaml</span>
<span class="hljs-attr">security:</span>
    <span class="hljs-comment"># ...</span>

    <span class="hljs-attr">role_hierarchy:</span>
        <span class="hljs-attr">ROLE_*:</span>              <span class="hljs-string">ROLE_USER</span>
        <span class="hljs-attr">ROLE_*_MODERATOR:</span>    <span class="hljs-string">ROLE_MODERATOR</span>
        <span class="hljs-attr">ROLE_BLOG_*:</span>         <span class="hljs-string">ROLE_BLOG_READER</span>
        <span class="hljs-attr">ROLE_BLOG_MODERATOR:</span> <span class="hljs-string">[ROLE_BLOG_DELETE_POST,</span> <span class="hljs-string">ROLE_BLOG_LOCK_POST]</span></code></pre>
    </div>
</div>
<p>Roles don't need to be listed in the hierarchy to match a wildcard. With this
configuration, a user with <code translate="no" class="notranslate">ROLE_BLOG_COMMENTER</code> also gets <code translate="no" class="notranslate">ROLE_BLOG_READER</code>
and <code translate="no" class="notranslate">ROLE_USER</code>, even if that role doesn't appear anywhere in the configuration.</p>
<p>The <code translate="no" class="notranslate">*</code> character is only considered a wildcard when it's wrapped by underscores
(<code translate="no" class="notranslate">ROLE_*_FOO</code>) or placed after an underscore at the end of the role name (<code translate="no" class="notranslate">ROLE_BAR_*</code>).
Keys such as <code translate="no" class="notranslate">ROLE_BLOG*</code> or <code translate="no" class="notranslate">ROLE_*BLOG</code> are still regular role names.</p>
<p>Wildcards are patterns, not roles, so you can only use them in the keys of the
hierarchy. For example, <code translate="no" class="notranslate">is_granted('ROLE_BLOG_*')</code> still looks for a role with
that exact name and wildcards are never returned by the <code translate="no" class="notranslate">getReachableRoleNames()</code>
and <code translate="no" class="notranslate">getParentRoleNames()</code> methods.</p>
</div>
<div class="section">
<h2 id="the-debug-roles-command"><a class="headerlink" href="#the-debug-roles-command" title="Permalink to this headline">The <code translate="no" class="notranslate">debug:roles</code> Command</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/squrious">
                <img src="https://connect.symfony.com/profile/squrious.picture" alt="Nicolas Rigaud">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://connect.symfony.com/profile/squrious">Nicolas Rigaud</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/65345">#65345</a>
                                                </span>
            </div>
</div>
<p>Wildcards simplifies role configuration but can make debugging a bit more difficult.
If you need to know the exact roles granted to a user, Symfony 8.2 adds a <code translate="no" class="notranslate">debug:roles</code>
command that does that for you:</p>
<div translate="no" data-loc="12" class="notranslate codeblock codeblock-length-md codeblock-terminal codeblock-bash">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment"># displays the whole role hierarchy</span>
<span class="hljs-prompt">$ </span>php bin/console debug:roles

<span class="hljs-comment"># displays all the roles granted by the given role(s)</span>
<span class="hljs-prompt">$ </span>php bin/console debug:roles ROLE_BLOG_MODERATOR

 * ROLE_BLOG_MODERATOR
 * ROLE_BLOG_DELETE_POST
 * ROLE_BLOG_LOCK_POST
 * ROLE_USER
 * ROLE_MODERATOR
 * ROLE_BLOG_READER</code></pre>
    </div>
</div>
<p>Add the <code translate="no" class="notranslate">--tree</code> option to see why each role is granted, including the wildcards
that were matched:</p>
<div translate="no" data-loc="11" class="notranslate codeblock codeblock-length-md codeblock-terminal codeblock-bash">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-prompt">$ </span>php bin/console debug:roles ROLE_BLOG_MODERATOR --tree

ROLE_BLOG_MODERATOR
├── ROLE_*
│   └── ROLE_USER
├── ROLE_*_MODERATOR
│   └── ROLE_MODERATOR
├── ROLE_BLOG_*
│   └── ROLE_BLOG_READER
├── ROLE_BLOG_DELETE_POST
└── ROLE_BLOG_LOCK_POST</code></pre>
    </div>
</div>
</div>
<div class="section">
<h2 id="role-hierarchy-graph-in-the-profiler"><a class="headerlink" href="#role-hierarchy-graph-in-the-profiler" title="Permalink to this headline">Role Hierarchy Graph in the Profiler</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/damienfern">
                <img src="https://connect.symfony.com/profile/damienfern.picture" alt="Damien Fernandes">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://connect.symfony.com/profile/damienfern">Damien Fernandes</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/61786">#61786</a>
                                                </span>
            </div>
</div>
<p>Symfony 7.4 introduced the <code translate="no" class="notranslate">debug:security:role-hierarchy</code> command to dump the
role hierarchy as a <a href="https://mermaid.js.org/" class="reference external" rel="external noopener noreferrer" target="_blank">Mermaid</a> chart. In Symfony 8.2, that chart is displayed
directly in the <strong>Security panel of the profiler</strong>, so you can check the roles
available in your application without running any command:</p>
<div class="figure">
    <img alt="Symfony 8.2 profiler showing the role hierarchy graph in the Security panel" class="align-center" src="https://symfony.com/uploads/assets/blog/symfony-profiler-role-hiearchy-diagram.png">
</div>
</div>
                <hr style="margin-bottom: 5px" />
                <div style="font-size: 90%">
                    <a href="https://symfony.com/sponsor">Sponsor</a> the Symfony project.
                </div>
            ]]></content:encoded>
            <guid isPermaLink="false">https://symfony.com/blog/new-in-symfony-8-2-role-hierarchy-wildcards-and-debugging?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Javier Eguiluz ]]></dc:creator>
            <pubDate>Tue, 22 Sep 2026 09:44:00 +0200</pubDate>
            <comments>https://symfony.com/blog/new-in-symfony-8-2-role-hierarchy-wildcards-and-debugging?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[Symfony UX 3.5 released]]></title>
            <link>https://symfony.com/blog/symfony-ux-3-5-released?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>Symfony UX 3.5 is out, with close to ninety pull requests merged since 3.4,
enough to make it one of the largest releases the project has shipped. It brings
a brand new Pagination package, hands component attribute rendering over to Twig
itself, lets you…</description>
            <content:encoded><![CDATA[
                                <p>Symfony UX 3.5 is out, with close to ninety pull requests merged since 3.4,
enough to make it one of the largest releases the project has shipped. It brings
a brand new Pagination package, hands component attribute rendering over to Twig
itself, lets you document component props with the Twig 3.29 comment syntax, and
adds twenty new Shadcn recipes to the UX Toolkit.</p>
<div class="section">
<h2 id="ux-pagination-at-last"><a class="headerlink" href="#ux-pagination-at-last" title="Permalink to this headline">UX Pagination, At Last</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/smnandre">
                <img src="https://github.com/smnandre.png" alt="Simon André">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/smnandre">Simon André</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3753">#3753</a>
                                                </span>
            </div>
</div>
<p>For years, whenever Fabien Potencier teased a new Symfony component, someone
in the community guessed "Pagination". It finally exists, though in Symfony UX
rather than in Symfony itself.</p>
<p><code translate="no" class="notranslate">symfony/ux-pagination</code> paginates arrays, Doctrine ORM and DBAL queries, and
any custom data source behind a single request-aware <code translate="no" class="notranslate">PaginatorInterface</code>.
PHP owns the query, Twig renders accessible navigation, and the browser follows
ordinary links: no JavaScript, no Stimulus, no Turbo required.</p>
<div translate="no" data-loc="14" class="notranslate codeblock codeblock-length-md codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment">// src/Controller/ProductController.php</span>
<span class="hljs-meta">#[Route(<span class="hljs-string">'/products'</span>, <span class="hljs-attr">name</span>: <span class="hljs-string">'product_index'</span>)]</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__invoke</span><span class="hljs-params">(
    ProductRepository <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>repository</span>,
    PaginatorInterface <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>paginator</span>,
)</span>: <span class="hljs-title">Response</span> </span>{
    <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>products</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>paginator</span>
        -&gt;<span class="hljs-title invoke__">cursor</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>repository</span>-&gt;<span class="hljs-title invoke__">createQueryBuilder</span>(<span class="hljs-string">'product'</span>))
        -&gt;<span class="hljs-title invoke__">orderBy</span>([<span class="hljs-string">'createdAt'</span>, <span class="hljs-string">'id'</span>], <span class="hljs-string">'DESC'</span>)
        -&gt;<span class="hljs-title invoke__">perPage</span>(<span class="hljs-number">20</span>)
        -&gt;<span class="hljs-title invoke__">paginate</span>();

    <span class="hljs-keyword">return</span> <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">render</span>(<span class="hljs-string">'product/index.html.twig'</span>, [<span class="hljs-string">'products'</span> =&gt; <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>products</span>]);
}</code></pre>
    </div>
</div>
<div translate="no" data-loc="6" class="notranslate codeblock codeblock-length-sm codeblock-html+twig codeblock-twig">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment">{# templates/product/index.html.twig #}</span><span class="xml">
</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">for</span></span> product in products %}</span><span class="xml">
    <span class="hljs-tag">&lt;<span class="hljs-name">article</span>&gt;</span></span><span class="hljs-template-variable">{{ product.name }}</span><span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">article</span>&gt;</span>
</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">endfor</span></span> %}</span><span class="xml">

</span><span class="hljs-template-variable">{{ ux_pagination(products) }}</span></code></pre>
    </div>
</div>
<p>Three strategies are available: numbered pagination for exact totals,
lookahead when you only need a reliable next link and want to skip the
<code translate="no" class="notranslate">COUNT</code> query, and bidirectional cursor pagination when rows can move while
somebody is browsing.</p>
<p>The <code translate="no" class="notranslate">&lt;twig:ux:pagination&gt;</code> component ships default, Bootstrap and Tailwind
themes, translations in ten locales, JSON serialization, and an optional
LiveComponent integration through <code translate="no" class="notranslate">ComponentWithPaginationTrait</code>. The bundle
is experimental, but <a href="https://ux.symfony.com/pagination" class="reference external">its own page</a> on the UX website is already live.</p>
</div>
<div class="section">
<h2 id="component-attributes-are-now-rendered-by-twig"><a class="headerlink" href="#component-attributes-are-now-rendered-by-twig" title="Permalink to this headline">Component Attributes Are Now Rendered by Twig</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/Kocal">
                <img src="https://github.com/Kocal.png" alt="Hugo Alliaume">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Kocal">Hugo Alliaume</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3820">#3820</a>
                     and                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3756">#3756</a>
                                                </span>
            </div>
</div>
<p><code translate="no" class="notranslate">ComponentAttributes</code> used to hand-roll its own attribute rendering: it threw
on <code translate="no" class="notranslate">null</code>, dropped <code translate="no" class="notranslate">aria-*</code> set to <code translate="no" class="notranslate">false</code>, rendered a boolean <code translate="no" class="notranslate">true</code>
bare, and threw on arrays, iterables and enums. That was a partial, in places
invalid, reimplementation of what Twig has rendered natively since 3.24, when
<a href="https://twig.symfony.com/doc/3.x/functions/html_attr.html" class="reference external">html_attr()</a> landed.</p>
<p>Both <code translate="no" class="notranslate">{{ attributes }}</code> and <code translate="no" class="notranslate">attributes.render()</code> now resolve values through
that same logic, so a component and a plain Twig template render attributes
identically. Twig 3.29 is what made the reuse possible: it exposed the per-value
building block behind <code translate="no" class="notranslate">html_attr()</code>.</p>
<div translate="no" data-loc="9" class="notranslate codeblock codeblock-length-sm codeblock-html+twig codeblock-twig">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment">{# null omits the attribute instead of throwing #}</span><span class="xml">
<span class="hljs-tag">&lt;<span class="hljs-name">twig:Button</span> <span class="hljs-attr">:title</span>=<span class="hljs-string">"null"</span> /&gt;</span>          </span><span class="hljs-comment">{# &lt;button&gt; #}</span><span class="xml">

</span><span class="hljs-comment">{# aria-* booleans render "true"/"false", like in Vue and React #}</span><span class="xml">
<span class="hljs-tag">&lt;<span class="hljs-name">twig:Button</span> <span class="hljs-attr">:aria-expanded</span>=<span class="hljs-string">"false"</span> /&gt;</span> </span><span class="hljs-comment">{# &lt;button aria-expanded="false"&gt; #}</span><span class="xml">

</span><span class="hljs-comment">{# arrays, iterables and backed enums are now accepted #}</span><span class="xml">
<span class="hljs-tag">&lt;<span class="hljs-name">twig:Button</span> <span class="hljs-attr">:class</span>=<span class="hljs-string">"['btn', 'btn-lg']"</span> <span class="hljs-attr">:style</span>=<span class="hljs-string">"{ color: 'red' }"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">twig:Button</span> <span class="hljs-attr">:type</span>=<span class="hljs-string">"buttonType"</span> /&gt;</span>      </span><span class="hljs-comment">{# a BackedEnum renders its backing value #}</span></code></pre>
    </div>
</div>
<p><code translate="no" class="notranslate">ComponentAttributes#defaults()</code> also understands <code translate="no" class="notranslate">MergeableInterface</code> from
<code translate="no" class="notranslate">twig/html-extra</code>, which is how the Toolkit now merges Tailwind classes. This
needs <code translate="no" class="notranslate">twig/html-extra</code> <code translate="no" class="notranslate">^3.29</code> and <code translate="no" class="notranslate">twig/twig</code> <code translate="no" class="notranslate">^3.24</code>, and it <strong>may
break your tests</strong> if you assert on the rendered HTML of a component's
attributes: boolean, <code translate="no" class="notranslate">null</code>, <code translate="no" class="notranslate">aria-*</code> and non-scalar values render
differently now.</p>
</div>
<div class="section">
<h2 id="ux-icons-and-ux-map-follow-the-same-rules"><a class="headerlink" href="#ux-icons-and-ux-map-follow-the-same-rules" title="Permalink to this headline">UX Icons and UX Map Follow the Same Rules</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/Kocal">
                <img src="https://github.com/Kocal.png" alt="Hugo Alliaume">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Kocal">Hugo Alliaume</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3821">#3821</a>
                                                </span>
            </div>
</div>
<p><code translate="no" class="notranslate">ux_icon()</code>, <code translate="no" class="notranslate">&lt;twig:ux:icon&gt;</code>, <code translate="no" class="notranslate">ux_map()</code> and <code translate="no" class="notranslate">&lt;twig:ux:map&gt;</code> render
their attributes through the same code path, so the typed values of
<code translate="no" class="notranslate">html_attr_type()</code> and <code translate="no" class="notranslate">tailwind_classes</code> now work there too. The same
warning applies: if you assert on the <code translate="no" class="notranslate">&lt;svg&gt;</code> or map <code translate="no" class="notranslate">&lt;div&gt;</code> attributes,
regenerate those assertions.</p>
<p>The same change fixes a separate bug in UX Icons: an omitted <code translate="no" class="notranslate">aria-label</code>,
<code translate="no" class="notranslate">aria-labelledby</code> or <code translate="no" class="notranslate">title</code> used to suppress the automatic
<code translate="no" class="notranslate">aria-hidden="true"</code>, leaving the icon neither labelled nor hidden from
assistive technology.</p>
</div>
<div class="section">
<h2 id="documenting-props-and-blocks-with-twig-comments"><a class="headerlink" href="#documenting-props-and-blocks-with-twig-comments" title="Permalink to this headline">Documenting Props and Blocks with Twig Comments</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/Kocal">
                <img src="https://github.com/Kocal.png" alt="Hugo Alliaume">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Kocal">Hugo Alliaume</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3795">#3795</a>
                     and                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3796">#3796</a>
                                                </span>
            </div>
</div>
<p>Twig 3.29 introduced <a href="https://symfony.com/blog/new-in-twig-3-29-documentation-comments" class="reference external">documentation comments</a>, and TwigComponent now reads
them. Write a <code translate="no" class="notranslate">##</code> comment above a prop inside <code translate="no" class="notranslate">{% props %}</code>, or a
<code translate="no" class="notranslate">{## ... ##}</code> comment above a block, and the description is captured at
compile time.</p>
<div translate="no" data-loc="8" class="notranslate codeblock codeblock-length-sm codeblock-html+twig codeblock-twig">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-template-tag">{%- <span class="hljs-name">props</span>
    <span class="hljs-comment">## 'default'|'sm' Size variant of the card.</span>
    size = <span class="hljs-string">'default'</span>
-%}</span><span class="xml">
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> </span></span><span class="hljs-template-variable">{{ attributes }}</span><span class="xml"><span class="hljs-tag">&gt;</span>
    </span><span class="hljs-comment">{##- The card content, typically includes `Card:Header` and `Card:Footer`. -#}</span><span class="xml">
    </span><span class="hljs-template-tag">{%- <span class="hljs-name"><span class="hljs-keyword">block</span></span> content %}</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">endblock</span></span> -%}</span><span class="xml">
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span></code></pre>
    </div>
</div>
<p><code translate="no" class="notranslate">PropsNode::getPropDocumentation()</code> exposes it per prop. The UX Toolkit is
both the reason this syntax exists and its first consumer: wanting to document
the props and blocks of every kit recipe is what pushed for an official Twig
syntax instead of inventing another convention. Every kit recipe then dropped
its old <code translate="no" class="notranslate">{# @prop #}</code> and <code translate="no" class="notranslate">{# @block #}</code> docblocks for it, and the prop
tables on the <a href="https://ux.symfony.com" class="reference external">UX website</a> are generated from it.</p>
</div>
<div class="section">
<h2 id="dynamic-component-names-in-the-html-syntax"><a class="headerlink" href="#dynamic-component-names-in-the-html-syntax" title="Permalink to this headline">Dynamic Component Names in the HTML Syntax</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/seb-jean">
                <img src="https://github.com/seb-jean.png" alt="Sébastien Jean">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/seb-jean">Sébastien Jean</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3699">#3699</a>
                                                </span>
            </div>
</div>
<p><code translate="no" class="notranslate">{% component %}</code> has accepted a dynamic expression since 3.4. The HTML syntax
now does too, through <code translate="no" class="notranslate">&lt;twig:component&gt;</code> and its <code translate="no" class="notranslate">is</code> attribute:</p>
<div translate="no" data-loc="10" class="notranslate codeblock codeblock-length-md codeblock-html+twig codeblock-twig">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">twig:component</span> <span class="hljs-attr">is</span>=<span class="hljs-string">"Alert"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"success"</span> /&gt;</span>

</span><span class="hljs-comment">{# Dynamic component name #}</span><span class="xml">
<span class="hljs-tag">&lt;<span class="hljs-name">twig:component</span> <span class="hljs-attr">:is</span>=<span class="hljs-string">"componentName"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"success"</span> /&gt;</span>

</span><span class="hljs-comment">{# Dynamic in a loop #}</span><span class="xml">
</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">set</span></span> prefix = <span class="hljs-string">'DynamicNameComponent'</span> %}</span><span class="xml">
</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">for</span></span> i in 1..2 %}</span><span class="xml">
    <span class="hljs-tag">&lt;<span class="hljs-name">twig:component</span> <span class="hljs-attr">:is</span>=<span class="hljs-string">"prefix ~ i"</span> /&gt;</span>
</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">endfor</span></span> %}</span></code></pre>
    </div>
</div>
</div>
<div class="section">
<h2 id="twig-4-support"><a class="headerlink" href="#twig-4-support" title="Permalink to this headline">Twig 4 Support</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/Kocal">
                <img src="https://github.com/Kocal.png" alt="Hugo Alliaume">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Kocal">Hugo Alliaume</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3814">#3814</a>
                                                </span>
            </div>
</div>
<p>Autocomplete, Cropper.js, Dropzone, Icons, LiveComponent, Map, StimulusBundle,
Toolkit, Turbo and TwigComponent now run on Twig 4 as well as Twig 3.</p>
<p>One Twig 4 change affects Turbo templates that pass a fully qualified class
name to <code translate="no" class="notranslate">turbo_stream_from()</code> or <code translate="no" class="notranslate">&lt;twig:Turbo:Stream:From&gt;</code>. <a href="https://twig.symfony.com/doc/3.x/templates.html#literals" class="reference external">Twig 3 requires escaping backslashes in string literals</a> and silently drops the
ones you forget, so <code translate="no" class="notranslate">'App\Entity\Book'</code> reached Turbo as the unusable
topic <code translate="no" class="notranslate">AppEntityBook</code>. Twig 4 keeps an unrecognized escape sequence
intact, the way PHP does, so the class name now resolves. Templates
already using the documented <code translate="no" class="notranslate">'App<wbr></wbr>\<wbr></wbr>\Entity<wbr></wbr>\<wbr></wbr>\Book'</code> form behave the
same on both versions.</p>
</div>
<div class="section">
<h2 id="autocomplete-without-doctrine"><a class="headerlink" href="#autocomplete-without-doctrine" title="Permalink to this headline">Autocomplete Without Doctrine</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/Prometee">
                <img src="https://github.com/Prometee.png" alt="Francis Hilaire">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Prometee">Francis Hilaire</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3441">#3441</a>
                                                </span>
            </div>
</div>
<p>UX Autocomplete was tied to Doctrine ORM. Backing a field with an API, a search
engine or an in-memory list meant writing a custom endpoint. It now accepts any
data source: implement <code translate="no" class="notranslate">AutocompleterInterface</code>, tag the service with an
alias, and the new <code translate="no" class="notranslate">ux_autocomplete</code> route serves it.</p>
<div translate="no" data-loc="14" class="notranslate codeblock codeblock-length-md codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-meta">#[AutoconfigureTag(<span class="hljs-string">'ux.autocompleter'</span>, [<span class="hljs-string">'alias'</span> =&gt; <span class="hljs-string">'food'</span>])]</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FoodAutocompleter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">AutocompleterInterface</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchResults</span><span class="hljs-params">(<span class="hljs-keyword">string</span> <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>query</span>, <span class="hljs-keyword">int</span> <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>page</span>)</span>: <span class="hljs-title">AutocompleteResults</span>
    </span>{
        <span class="hljs-comment">// ... fetch from wherever your data lives</span>
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">AutocompleteResults</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>results</span>, <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>hasNextPage</span>);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isGranted</span><span class="hljs-params">(Security <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>security</span>)</span>: <span class="hljs-title">bool</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
    }
}</code></pre>
    </div>
</div>
<div translate="no" data-loc="1" class="notranslate codeblock codeblock-length-sm codeblock-twig">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-template-variable">{{ path(<span class="hljs-string">'ux_autocomplete'</span>, { alias: <span class="hljs-string">'food'</span> }) }}</span></code></pre>
    </div>
</div>
<p>The Doctrine API stays fully supported: <code translate="no" class="notranslate">EntityAutocompleterInterface</code> and the
<code translate="no" class="notranslate">ux.entity_autocompleter</code> tag work exactly as before. The
<code translate="no" class="notranslate">ux_entity_autocomplete</code> route is now an alias of <code translate="no" class="notranslate">ux_autocomplete</code>, and is
deprecated.</p>
</div>
<div class="section">
<h2 id="removing-a-component-and-downloading-a-file-from-a-liveaction"><a class="headerlink" href="#removing-a-component-and-downloading-a-file-from-a-liveaction" title="Permalink to this headline">Removing a Component, and Downloading a File, from a LiveAction</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/smnandre">
                <img src="https://github.com/smnandre.png" alt="Simon André">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/smnandre">Simon André</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3773">#3773</a>
                     and                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3761">#3761</a>
                                                </span>
            </div>
</div>
<p>A <code translate="no" class="notranslate">LiveAction</code> can now end its own component. <code translate="no" class="notranslate">LiveResponse::remove()</code>
performs one final render, so emitted events still reach other components, then
disconnects the Stimulus controller and takes the element off the page:</p>
<div translate="no" data-loc="8" class="notranslate codeblock codeblock-length-sm codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-meta">#[LiveAction]</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">dismiss</span><span class="hljs-params">(NotificationRepository <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>repository</span>)</span>: <span class="hljs-title">LiveResponse</span>
</span>{
    <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>repository</span>-&gt;<span class="hljs-title invoke__">markAsRead</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;notification);
    <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">emit</span>(<span class="hljs-string">'notificationDismissed'</span>, [<span class="hljs-string">'id'</span> =&gt; <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;notification-&gt;<span class="hljs-title invoke__">getId</span>()]);

    <span class="hljs-keyword">return</span> LiveResponse::<span class="hljs-title invoke__">remove</span>();
}</code></pre>
    </div>
</div>
<p>The element carries a <code translate="no" class="notranslate">data-live-removing</code> attribute until any animation on
it finishes, then is dropped: nothing is deleted server-side, only the
component leaves the page. <code translate="no" class="notranslate">LiveResponse::downloadUrl()</code> and
<code translate="no" class="notranslate">LiveResponse::downloadFile()</code> trigger a file download while the component
keeps its state and re-renders. Prefer <code translate="no" class="notranslate">downloadUrl()</code> whenever the file can
be served from its own route: the browser downloads it natively, so nothing
sits in memory on either side, progress is reported, and range requests and
resuming work.</p>
</div>
<div class="section">
<h2 id="dropzone-accepts-several-files"><a class="headerlink" href="#dropzone-accepts-several-files" title="Permalink to this headline">Dropzone Accepts Several Files</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/Dooij">
                <img src="https://github.com/Dooij.png" alt="Dooji">
            </a>
                    <a target="_blank" href="https://github.com/Kocal">
                <img src="https://github.com/Kocal.png" alt="Hugo Alliaume">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Dooij">Dooji</a>
             and                     <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Kocal">Hugo Alliaume</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3684">#3684</a>
                     and                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3843">#3843</a>
                                                </span>
            </div>
</div>
<p>Pass the standard <code translate="no" class="notranslate">multiple</code> option inherited from <code translate="no" class="notranslate">FileType</code>, and the
Dropzone accumulates files across successive selections instead of replacing the
previous one, previews each of them, and lets the user remove them individually
before submitting:</p>
<div translate="no" data-loc="4" class="notranslate codeblock codeblock-length-sm codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code>-&gt;<span class="hljs-title invoke__">add</span>(<span class="hljs-string">'photos'</span>, DropzoneType::<span class="hljs-variable language_">class</span>, [
    <span class="hljs-string">'multiple'</span> =&gt; <span class="hljs-keyword">true</span>,
    <span class="hljs-string">'remove_label'</span> =&gt; <span class="hljs-string">'Delete'</span>, <span class="hljs-comment">// labels the per-file remove button</span>
])</code></pre>
    </div>
</div>
<p>In <code translate="no" class="notranslate">multiple</code> mode, <code translate="no" class="notranslate">dropzone:change</code> carries a <code translate="no" class="notranslate">FileList</code> rather than a
single <code translate="no" class="notranslate">File</code>, a new <code translate="no" class="notranslate">dropzone:remove</code> event is dispatched with the removed
file, and <code translate="no" class="notranslate">dropzone:clear</code> is not dispatched since there is no clear button.
The same release fixes the drop zone appearing empty when the very same file is
dropped twice in a row, which Chrome reports without firing a <code translate="no" class="notranslate">change</code> event.</p>
</div>
<div class="section">
<h2 id="cropper-js-runs-on-intervention-image-3-and-4"><a class="headerlink" href="#cropper-js-runs-on-intervention-image-3-and-4" title="Permalink to this headline">Cropper.js Runs on Intervention Image 3 and 4</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/deluxetom">
                <img src="https://github.com/deluxetom.png" alt="Thomas Picquet">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/deluxetom">Thomas Picquet</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3679">#3679</a>
                                                </span>
            </div>
</div>
<p>Server-side cropping is powered by Intervention Image, and the package was
pinned to version 2, which triggers PHP deprecations. Versions 3 and 4 are now
supported, and version 2 keeps working as the lowest supported version.</p>
<p>The image driver is configurable too, which was not possible before:</p>
<div translate="no" data-loc="3" class="notranslate codeblock codeblock-length-sm codeblock-yaml">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment"># config/packages/cropperjs.yaml</span>
<span class="hljs-attr">cropperjs:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">gd</span> <span class="hljs-comment"># "gd" (default), "imagick" or "vips"</span></code></pre>
    </div>
</div>
<p><code translate="no" class="notranslate">gd</code> and <code translate="no" class="notranslate">imagick</code> ship with <code translate="no" class="notranslate">intervention/image</code>. <code translate="no" class="notranslate">vips</code> also needs
the <code translate="no" class="notranslate">intervention/image-driver-vips</code> package, the libvips system library and
<code translate="no" class="notranslate">ext-ffi</code>. If you need full control, register your own service implementing
<code translate="no" class="notranslate">DriverInterface</code> and point <code translate="no" class="notranslate">cropperjs.driver_service</code> at it. Both options
require <code translate="no" class="notranslate">intervention/image</code> 3 or higher.</p>
</div>
<div class="section">
<h2 id="custom-icon-finders"><a class="headerlink" href="#custom-icon-finders" title="Permalink to this headline">Custom Icon Finders</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/pierredup">
                <img src="https://github.com/pierredup.png" alt="Pierre du Plessis">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/pierredup">Pierre du Plessis</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3876">#3876</a>
                                                </span>
            </div>
</div>
<p><code translate="no" class="notranslate">ux:icons:lock</code> scans your Twig templates, which means it cannot find an icon
whose name is built at runtime. <code translate="no" class="notranslate">IconFinderInterface</code> lets you declare those
names from any source, a database, an API or a PHP enum:</p>
<div translate="no" data-loc="14" class="notranslate codeblock codeblock-length-md codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment">// src/Icons/AppIconFinder.php</span>
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppIconFinder</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">IconFinderInterface</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">icons</span><span class="hljs-params">()</span>: <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>icons</span> = [<span class="hljs-string">'tabler:mail'</span>];

        <span class="hljs-keyword">foreach</span> (Status::<span class="hljs-title invoke__">cases</span>() <span class="hljs-keyword">as</span> <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>status</span>) {
            <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>icons</span>[] = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>status</span>-&gt;<span class="hljs-title invoke__">icon</span>();
        }

        <span class="hljs-keyword">return</span> <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>icons</span>;
    }
}</code></pre>
    </div>
</div>
<p>Thanks to autoconfiguration, the finder is registered for you. Its icons are
merged with the ones found in your templates, and are used both by
<code translate="no" class="notranslate">ux:icons:lock</code> and when warming the icon cache.</p>
</div>
<div class="section">
<h2 id="ux-icons-parses-svg-with-the-php-8-4-dom-api"><a class="headerlink" href="#ux-icons-parses-svg-with-the-php-8-4-dom-api" title="Permalink to this headline">UX Icons Parses SVG with the PHP 8.4 Dom API</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/Kocal">
                <img src="https://github.com/Kocal.png" alt="Hugo Alliaume">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Kocal">Hugo Alliaume</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3771">#3771</a>
                                                </span>
            </div>
</div>
<p>SVG parsing moved from <code translate="no" class="notranslate">\DOMDocument</code> to the <code translate="no" class="notranslate">\Dom\XMLDocument</code> API
introduced in PHP 8.4. Beyond being the modern API, it fixes a real bug:
<code translate="no" class="notranslate">\DOMDocument</code> dropped the <code translate="no" class="notranslate">xmlns</code> attribute when rebuilding an icon's
attributes, so an icon rendered from a local file came out without it.</p>
<p>That is a markup change. If you assert on the output of <code translate="no" class="notranslate">ux_icon()</code> or
<code translate="no" class="notranslate">&lt;twig:ux:icon&gt;</code>, regenerate those assertions and clear your icon cache after
upgrading.</p>
</div>
<div class="section">
<h2 id="stimulushelper-is-autowirable"><a class="headerlink" href="#stimulushelper-is-autowirable" title="Permalink to this headline">StimulusHelper Is Autowirable</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/Kocal">
                <img src="https://github.com/Kocal.png" alt="Hugo Alliaume">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Kocal">Hugo Alliaume</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3849">#3849</a>
                                                </span>
            </div>
</div>
<p>The Stimulus attributes the Twig helpers build are also available from PHP,
through the <code translate="no" class="notranslate">StimulusHelper</code> service, which you can now autowire. Reach for it
when the element you want to decorate is not written in a template, a form field
for instance:</p>
<div translate="no" data-loc="14" class="notranslate codeblock codeblock-length-md codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">(<span class="hljs-keyword">private</span> StimulusHelper <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>stimulusHelper</span>)</span>
</span>{
}

<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">buildForm</span><span class="hljs-params">(FormBuilderInterface <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>builder</span>, <span class="hljs-keyword">array</span> <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>options</span>)</span>: <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>attributes</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;stimulusHelper-&gt;<span class="hljs-title invoke__">createStimulusAttributes</span>();
    <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>attributes</span>-&gt;<span class="hljs-title invoke__">addController</span>(<span class="hljs-string">'country-picker'</span>, [<span class="hljs-string">'locale'</span> =&gt; <span class="hljs-string">'fr'</span>]);
    <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>attributes</span>-&gt;<span class="hljs-title invoke__">addAction</span>(<span class="hljs-string">'country-picker'</span>, <span class="hljs-string">'refresh'</span>, <span class="hljs-string">'change'</span>);

    <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>builder</span>-&gt;<span class="hljs-title invoke__">add</span>(<span class="hljs-string">'country'</span>, CountryType::<span class="hljs-variable language_">class</span>, [
        <span class="hljs-string">'attr'</span> =&gt; <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>attributes</span>-&gt;<span class="hljs-title invoke__">toArray</span>(),
    ]);
}</code></pre>
    </div>
</div>
<p>The field then renders with <code translate="no" class="notranslate">data-controller</code>, <code translate="no" class="notranslate">data-action</code> and the value
attributes the Twig helpers would have produced.</p>
</div>
<div class="section">
<h2 id="ux-toolkit-blocks"><a class="headerlink" href="#ux-toolkit-blocks" title="Permalink to this headline">UX Toolkit: Blocks</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/seb-jean">
                <img src="https://github.com/seb-jean.png" alt="Sébastien Jean">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/seb-jean">Sébastien Jean</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3596">#3596</a>
                                                </span>
            </div>
</div>
<p>A recipe is a pack of files and dependencies for one component. A <em>block</em> is a
whole page section built from a kit's components, installed and previewed as a
unit. The Shadcn kit ships the first two, <code translate="no" class="notranslate">login-01</code> and <code translate="no" class="notranslate">login-02</code>:</p>
<div translate="no" data-loc="1" class="notranslate codeblock codeblock-length-sm codeblock-terminal codeblock-bash">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-prompt">$ </span>php bin/console ux:install login-01 --kit=shadcn</code></pre>
    </div>
</div>
<p>Installing it pulls in the recipes it is built from, here <code translate="no" class="notranslate">button</code>, <code translate="no" class="notranslate">card</code>,
<code translate="no" class="notranslate">input</code> and <code translate="no" class="notranslate">label</code>, and writes the <code translate="no" class="notranslate">LoginForm</code> component into your
templates. Dashboards, sidebars and other ready-to-use sections are where this
goes next.</p>
</div>
<div class="section">
<h2 id="ux-toolkit-twenty-new-shadcn-recipes"><a class="headerlink" href="#ux-toolkit-twenty-new-shadcn-recipes" title="Permalink to this headline">UX Toolkit: Twenty New Shadcn Recipes</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/Kocal">
                <img src="https://github.com/Kocal.png" alt="Hugo Alliaume">
            </a>
                    <a target="_blank" href="https://github.com/ker0x">
                <img src="https://github.com/ker0x.png" alt="Romain Monteil">
            </a>
                    <a target="_blank" href="https://github.com/Amoifr">
                <img src="https://github.com/Amoifr.png" alt="Pascal CESCON">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Kocal">Hugo Alliaume</a>
            ,                     <a target="_blank" class="blog-post-contributor-name" href="https://github.com/ker0x">Romain Monteil</a>
             and                     <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Amoifr">Pascal CESCON</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3869">#3869</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3870">#3870</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3855">#3855</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3884">#3884</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3883">#3883</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3806">#3806</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3232">#3232</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3804">#3804</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3483">#3483</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3871">#3871</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3486">#3486</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3889">#3889</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3479">#3479</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3801">#3801</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3487">#3487</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3872">#3872</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3480">#3480</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3805">#3805</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3469">#3469</a>
                     and                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3481">#3481</a>
                                                </span>
            </div>
</div>
<p>The Shadcn kit gained twenty recipes: <code translate="no" class="notranslate">attachment</code>, <code translate="no" class="notranslate">bubble</code>, <code translate="no" class="notranslate">calendar</code>,
<code translate="no" class="notranslate">carousel</code>, <code translate="no" class="notranslate">date-picker</code>, <code translate="no" class="notranslate">drawer</code>, <code translate="no" class="notranslate">dropdown-menu</code>, <code translate="no" class="notranslate">form</code>,
<code translate="no" class="notranslate">input-otp</code>, <code translate="no" class="notranslate">marker</code>, <code translate="no" class="notranslate">menubar</code>, <code translate="no" class="notranslate">message</code>, <code translate="no" class="notranslate">native-select</code>,
<code translate="no" class="notranslate">navigation-menu</code>, <code translate="no" class="notranslate">popover</code>, <code translate="no" class="notranslate">questionnaire</code>, <code translate="no" class="notranslate">scroll-area</code>, <code translate="no" class="notranslate">sheet</code>,
<code translate="no" class="notranslate">sidebar</code> and <code translate="no" class="notranslate">slider</code>. The <code translate="no" class="notranslate">typography</code> recipe was removed.</p>
<p>Both Tailwind kits also changed how they merge classes: component variants now
go through <code translate="no" class="notranslate">attributes.defaults({ class: '...'|tailwind_classes })</code> instead of
<code translate="no" class="notranslate">tailwind_merge</code>, so the classes you pass to a component override its own.
This requires <code translate="no" class="notranslate">symfony/ux-twig-component</code> 3.5.</p>
<p>Browse them all, with a live preview, on <a href="https://ux.symfony.com/toolkit" class="reference external">ux.symfony.com/toolkit</a>.</p>
</div>
<div class="section">
<h2 id="ux-toolkit-accessibility-documented-on-every-recipe"><a class="headerlink" href="#ux-toolkit-accessibility-documented-on-every-recipe" title="Permalink to this headline">UX Toolkit: Accessibility, Documented on Every Recipe</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/Amoifr">
                <img src="https://github.com/Amoifr.png" alt="Pascal CESCON">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Amoifr">Pascal CESCON</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3798">#3798</a>
                                                </span>
            </div>
</div>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/Kocal">
                <img src="https://github.com/Kocal.png" alt="Hugo Alliaume">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Kocal">Hugo Alliaume</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3887">#3887</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3803">#3803</a>
                     and                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3807">#3807</a>
                                                </span>
            </div>
</div>
<p>Every one of the sixty Shadcn recipes now carries an <code translate="no" class="notranslate">## Accessibility</code>
section in its <code translate="no" class="notranslate">README.md</code>, visible on the <a href="https://ux.symfony.com" class="reference external">UX website</a>. It says what the
recipe already handles, what it expects from your markup, and what you still
have to do yourself.</p>
<p>Writing them surfaced real bugs, now fixed. <code translate="no" class="notranslate">alert-dialog</code>, <code translate="no" class="notranslate">dialog</code>,
<code translate="no" class="notranslate">drawer</code> and <code translate="no" class="notranslate">sheet</code> rendered <code translate="no" class="notranslate">aria-labelledby</code> and <code translate="no" class="notranslate">aria-describedby</code>
on a roleless wrapper rather than on the <code translate="no" class="notranslate">&lt;dialog&gt;</code> element, which left the
modal without an accessible name. Right-to-left layouts were broken by physical
spacing and border utilities, now replaced with their logical counterparts. A
dialog focuses its first form field, or the <code translate="no" class="notranslate">[autofocus]</code> element, when it
opens. And <code translate="no" class="notranslate">button</code> no longer renders <code translate="no" class="notranslate">type="button"</code> when <code translate="no" class="notranslate">as</code> is not a
button.</p>
</div>
<div class="section">
<h2 id="the-standalone-assetmapper-bundle"><a class="headerlink" href="#the-standalone-assetmapper-bundle" title="Permalink to this headline">The Standalone AssetMapper Bundle</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/smnandre">
                <img src="https://github.com/smnandre.png" alt="Simon André">
            </a>
                    <a target="_blank" href="https://github.com/Kocal">
                <img src="https://github.com/Kocal.png" alt="Hugo Alliaume">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/smnandre">Simon André</a>
             and                     <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Kocal">Hugo Alliaume</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3867">#3867</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3891">#3891</a>
                     and                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3892">#3892</a>
                                                </span>
            </div>
</div>
<p>Symfony 8.2 moves the AssetMapper configuration out of FrameworkBundle and into
a standalone <a href="https://github.com/symfony/symfony/pull/65939" class="reference external" rel="external noopener noreferrer" target="_blank">AssetMapperBundle</a>. Every UX package that registers assets
detects it, so nothing changes for you whether you run Symfony 7.4, 8.0 or 8.2.</p>
</div>
<div class="section">
<h2 id="a-performance-pass"><a class="headerlink" href="#a-performance-pass" title="Permalink to this headline">A Performance Pass</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/javiereguiluz">
                <img src="https://github.com/javiereguiluz.png" alt="Javier Eguiluz">
            </a>
                    <a target="_blank" href="https://github.com/Kocal">
                <img src="https://github.com/Kocal.png" alt="Hugo Alliaume">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/javiereguiluz">Javier Eguiluz</a>
             and                     <a target="_blank" class="blog-post-contributor-name" href="https://github.com/Kocal">Hugo Alliaume</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/ux/pull/3692">#3692</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3841">#3841</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3775">#3775</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3776">#3776</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3777">#3777</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3779">#3779</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3780">#3780</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3781">#3781</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3782">#3782</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3783">#3783</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3784">#3784</a>
                    ,                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3785">#3785</a>
                     and                                     <a target="_blank" href="https://github.com/symfony/ux/pull/3786">#3786</a>
                                                </span>
            </div>
</div>
<p>A dozen pull requests went after hot paths across the monorepo, each one
measured before and after. TwigComponent absorbed most of the work, on two
fronts: its render pipeline and its pre-lexer.</p>
<p>Its render pipeline no longer rebuilds a <code translate="no" class="notranslate">ComponentMetadata</code> on every call, no
longer allocates and dispatches five events per render when nothing listens to
them, and now builds its render variables in a single pass. That pipeline was
profiled against an EasyAdmin index page, the kind of page that renders hundreds
of small components. Its pre-lexer used to copy the whole remaining template on
every call to <code translate="no" class="notranslate">consume()</code>, several times per character, which made pre-lexing
quadratic in the size of the template. It now scans the template in place
instead.</p>
<p>The rest is smaller and spread out. LiveComponent memoizes its attribute-method
lookups per class instead of rebuilding a <code translate="no" class="notranslate">ReflectionClass</code> on every render,
and skips the bracket pipeline for plain model names. UX Icons cuts allocations
in <code translate="no" class="notranslate">IconRenderer::renderIcon()</code>, and UX Map renders each icon once instead of
once per marker. In the browser, StimulusBundle stops watching the DOM once
every lazy controller is loaded and normalizes controller names faster, while
the Translator caches the regular expression compiled by <code translate="no" class="notranslate">strtr()</code>. The
Toolkit stops re-walking the filesystem on every <code translate="no" class="notranslate">Recipe::getFiles()</code>.</p>
</div>
<div class="section">
<h2 id="full-changelog"><a class="headerlink" href="#full-changelog" title="Permalink to this headline">Full Changelog</a></h2>
<ul>
    <li><a href="https://github.com/symfony/ux/pull/3890" class="reference external" rel="external noopener noreferrer" target="_blank">#3890</a> [Toolkit][Shadcn] Drop the dead height option from recipe previews (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3889" class="reference external" rel="external noopener noreferrer" target="_blank">#3889</a> [Toolkit][Shadcn] Add message recipe (@ker0x)</li>
<li><a href="https://github.com/symfony/ux/pull/3872" class="reference external" rel="external noopener noreferrer" target="_blank">#3872</a> [Toolkit][Shadcn] Add questionnaire recipe (@ker0x)</li>
<li><a href="https://github.com/symfony/ux/pull/3883" class="reference external" rel="external noopener noreferrer" target="_blank">#3883</a> [Toolkit][Shadcn] Add date-picker recipe (@ker0x)</li>
<li><a href="https://github.com/symfony/ux/pull/3888" class="reference external" rel="external noopener noreferrer" target="_blank">#3888</a> [Map] Make the Google Maps browser test resilient (@smnandre)</li>
<li><a href="https://github.com/symfony/ux/pull/3855" class="reference external" rel="external noopener noreferrer" target="_blank">#3855</a> [Toolkit][Shadcn] Add calendar recipe (@ker0x)</li>
<li><a href="https://github.com/symfony/ux/pull/3796" class="reference external" rel="external noopener noreferrer" target="_blank">#3796</a> [Toolkit] Read prop and block docs from Twig documentation comments (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3814" class="reference external" rel="external noopener noreferrer" target="_blank">#3814</a> [Autocomplete][Cropperjs][Dropzone][LiveComponent][StimulusBundle][Toolkit][TwigComponent] Add support for Twig 4.x (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3821" class="reference external" rel="external noopener noreferrer" target="_blank">#3821</a> [Icons][Map] Render attributes through twig/html-extra's html_attr() logic (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3820" class="reference external" rel="external noopener noreferrer" target="_blank">#3820</a> [TwigComponent] Render component attributes through twig/html-extra's <code translate="no" class="notranslate">html_attr()</code> (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3870" class="reference external" rel="external noopener noreferrer" target="_blank">#3870</a> [Toolkit][Shadcn] Add bubble recipe (@ker0x)</li>
<li><a href="https://github.com/symfony/ux/pull/3880" class="reference external" rel="external noopener noreferrer" target="_blank">#3880</a> [LiveComponent] Handle paths beginning with double slash in LiveUrlSubscriber (@mbuliard)</li>
<li><a href="https://github.com/symfony/ux/pull/3876" class="reference external" rel="external noopener noreferrer" target="_blank">#3876</a> [Icons] Add <code translate="no" class="notranslate">IconFinderInterface</code> to create custom finders to lock icons (@pierredup)</li>
<li><a href="https://github.com/symfony/ux/pull/3887" class="reference external" rel="external noopener noreferrer" target="_blank">#3887</a> [Toolkit][Shadcn] Document accessibility, fix <code translate="no" class="notranslate">&lt;dialog&gt;</code>-based recipies accessibility, and improve right-to-left layouts (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3871" class="reference external" rel="external noopener noreferrer" target="_blank">#3871</a> [Toolkit][Shadcn] Add marker recipe (@ker0x)</li>
<li><a href="https://github.com/symfony/ux/pull/3869" class="reference external" rel="external noopener noreferrer" target="_blank">#3869</a> [Toolkit][Shadcn] Add attachment recipe (@ker0x)</li>
<li><a href="https://github.com/symfony/ux/pull/3884" class="reference external" rel="external noopener noreferrer" target="_blank">#3884</a> [Toolkit][Shadcn] Add carousel recipe (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3878" class="reference external" rel="external noopener noreferrer" target="_blank">#3878</a> [Toolkit][Shadcn] Fix the rich colors of Sonner recipe (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3868" class="reference external" rel="external noopener noreferrer" target="_blank">#3868</a> [LiveComponent] Upgrade Idiomorph to 0.7.4 (@smnandre)</li>
<li><a href="https://github.com/symfony/ux/pull/3858" class="reference external" rel="external noopener noreferrer" target="_blank">#3858</a> [Autocomplete] Add a max_options option to control how many options the dropdown displays (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3882" class="reference external" rel="external noopener noreferrer" target="_blank">#3882</a> [Autocomplete][Chartjs][Cropperjs][Dropzone][LiveComponent][Map][Notify][React][StimulusBundle][Turbo][Vue] Update TypeScript's <code translate="no" class="notranslate">target</code> to <code translate="no" class="notranslate">es2022</code> (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3881" class="reference external" rel="external noopener noreferrer" target="_blank">#3881</a> [Autocomplete] Make search test fixtures deterministic (fix flakky) (@smnandre)</li>
<li><a href="https://github.com/symfony/ux/pull/3867" class="reference external" rel="external noopener noreferrer" target="_blank">#3867</a> [StimulusBundle] Detect the standalone AssetMapper bundle (@smnandre)</li>
<li><a href="https://github.com/symfony/ux/pull/3851" class="reference external" rel="external noopener noreferrer" target="_blank">#3851</a> [Toolkit] Fix <code translate="no" class="notranslate">ux:install</code> reporting the source path of the installed files (@ker0x)</li>
<li><a href="https://github.com/symfony/ux/pull/3860" class="reference external" rel="external noopener noreferrer" target="_blank">#3860</a> [Toolkit] Restore the "available since" note on recipe install steps (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3849" class="reference external" rel="external noopener noreferrer" target="_blank">#3849</a> [StimulusBundle] Make StimulusHelper autowirable and document PHP usage (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3847" class="reference external" rel="external noopener noreferrer" target="_blank">#3847</a> [Turbo] Fix broadcasting an entity whose identifier is made of associations (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3846" class="reference external" rel="external noopener noreferrer" target="_blank">#3846</a> [Translator] Cover the parent locale fallback in the dumper tests (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3843" class="reference external" rel="external noopener noreferrer" target="_blank">#3843</a> [Dropzone] Fix the drop zone appearing empty when the same file is dropped again (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3842" class="reference external" rel="external noopener noreferrer" target="_blank">#3842</a> [Toolkit][Flowbite] Rename the modal Stimulus controller to avoid a collision with Flowbite (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3518" class="reference external" rel="external noopener noreferrer" target="_blank">#3518</a> [TwigComponent] Fix PreLexer confusing <code translate="no" class="notranslate">{#</code> inside output expressions with comments (@Amoifr)</li>
<li><a href="https://github.com/symfony/ux/pull/3441" class="reference external" rel="external noopener noreferrer" target="_blank">#3441</a> [Autocomplete] Decouple from Doctrine ORM - support any data source (@Prometee)</li>
<li><a href="https://github.com/symfony/ux/pull/3679" class="reference external" rel="external noopener noreferrer" target="_blank">#3679</a> [Cropperjs] Upgrade to Intervention Image v4 and make the image driver configurable (@deluxetom)</li>
<li><a href="https://github.com/symfony/ux/pull/3684" class="reference external" rel="external noopener noreferrer" target="_blank">#3684</a> Allow for multiple file uploads at once (@Dooij)</li>
<li><a href="https://github.com/symfony/ux/pull/3763" class="reference external" rel="external noopener noreferrer" target="_blank">#3763</a> [Autocomplete] Translate the optgroup labels returned by the AJAX endpoint (@kira0269)</li>
<li><a href="https://github.com/symfony/ux/pull/3836" class="reference external" rel="external noopener noreferrer" target="_blank">#3836</a> [TwigComponent] Fix null-safe operator in component tag props (@alireza-aminzadeh)</li>
<li><a href="https://github.com/symfony/ux/pull/3827" class="reference external" rel="external noopener noreferrer" target="_blank">#3827</a> [TwigComponent] Treat a prop explicitly passed as <code translate="no" class="notranslate">null</code> as defined (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3823" class="reference external" rel="external noopener noreferrer" target="_blank">#3823</a> [LiveComponent] Use test stubs instead of mocks (@smnandre)</li>
<li><a href="https://github.com/symfony/ux/pull/3832" class="reference external" rel="external noopener noreferrer" target="_blank">#3832</a> Remove vulnerable extract-zip dependency (@smnandre)</li>
<li><a href="https://github.com/symfony/ux/pull/3831" class="reference external" rel="external noopener noreferrer" target="_blank">#3831</a> [CI] Pin DOCtor-RST container image (@smnandre)</li>
<li><a href="https://github.com/symfony/ux/pull/3829" class="reference external" rel="external noopener noreferrer" target="_blank">#3829</a> [React] Update E2E dependencies (@smnandre)</li>
<li><a href="https://github.com/symfony/ux/pull/3753" class="reference external" rel="external noopener noreferrer" target="_blank">#3753</a> [Pagination] Add new UX Pagination component (@smnandre)</li>
<li><a href="https://github.com/symfony/ux/pull/3773" class="reference external" rel="external noopener noreferrer" target="_blank">#3773</a> [LiveComponent] Add LiveResponse::remove() to trigger component deletion (@smnandre)</li>
<li><a href="https://github.com/symfony/ux/pull/3761" class="reference external" rel="external noopener noreferrer" target="_blank">#3761</a> [LiveComponent] Add file downloads from a LiveAction (@smnandre)</li>
<li><a href="https://github.com/symfony/ux/pull/3817" class="reference external" rel="external noopener noreferrer" target="_blank">#3817</a> [CalendarLink] Derive a stable ICS UID from the event content (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3816" class="reference external" rel="external noopener noreferrer" target="_blank">#3816</a> [CalendarLink] Make <code translate="no" class="notranslate">DTSTAMP</code> deterministic via the Clock component (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3818" class="reference external" rel="external noopener noreferrer" target="_blank">#3818</a> [CalendarLink] Anchor timed events to their time zone with TZID and VTIMEZONE (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3826" class="reference external" rel="external noopener noreferrer" target="_blank">#3826</a> [CalendarLink] Make <code translate="no" class="notranslate">IcsBuilder</code> internal (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3819" class="reference external" rel="external noopener noreferrer" target="_blank">#3819</a> [Autocomplete] Translated 'Add placeholder' to Italian (@luigif)</li>
<li><a href="https://github.com/symfony/ux/pull/3469" class="reference external" rel="external noopener noreferrer" target="_blank">#3469</a> [Toolkit][Shadcn] Add sidebar recipe (@Amoifr)</li>
<li><a href="https://github.com/symfony/ux/pull/3804" class="reference external" rel="external noopener noreferrer" target="_blank">#3804</a> [Toolkit][Shadcn] Add form recipe (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3806" class="reference external" rel="external noopener noreferrer" target="_blank">#3806</a> [Toolkit][Shadcn] Add drawer recipe (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3805" class="reference external" rel="external noopener noreferrer" target="_blank">#3805</a> [Toolkit][Shadcn] Add sheet recipe (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3807" class="reference external" rel="external noopener noreferrer" target="_blank">#3807</a> [Toolkit][Shadcn] popover: honor [autofocus] and skip hidden inputs when focusing content (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3803" class="reference external" rel="external noopener noreferrer" target="_blank">#3803</a> [Toolkit][Shadcn] Focus the first form field when a dialog opens (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3481" class="reference external" rel="external noopener noreferrer" target="_blank">#3481</a> [Toolkit][Shadcn] Add slider recipe (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3480" class="reference external" rel="external noopener noreferrer" target="_blank">#3480</a> [Toolkit][Shadcn] Add scroll-area recipe (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3802" class="reference external" rel="external noopener noreferrer" target="_blank">#3802</a> [Toolkit][Shadcn] Remove typography recipe (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3479" class="reference external" rel="external noopener noreferrer" target="_blank">#3479</a> [Toolkit][Shadcn] Add native-select recipe (@Amoifr)</li>
<li><a href="https://github.com/symfony/ux/pull/3801" class="reference external" rel="external noopener noreferrer" target="_blank">#3801</a> [Toolkit][Shadcn] Add navigation-menu recipe (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3232" class="reference external" rel="external noopener noreferrer" target="_blank">#3232</a> [Toolkit][Shadcn] Add DropdownMenu component (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3487" class="reference external" rel="external noopener noreferrer" target="_blank">#3487</a> [Toolkit][Shadcn] Add popover recipe (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3791" class="reference external" rel="external noopener noreferrer" target="_blank">#3791</a> [LiveComponent] Fix proxy turning "toJSON" and "then" protocol probes into server actions (@Amoifr)</li>
<li><a href="https://github.com/symfony/ux/pull/3798" class="reference external" rel="external noopener noreferrer" target="_blank">#3798</a> [Toolkit][Shadcn] Fix the hover-card usage example crashing on a plain HTML trigger (@Amoifr)</li>
<li><a href="https://github.com/symfony/ux/pull/3795" class="reference external" rel="external noopener noreferrer" target="_blank">#3795</a> [TwigComponent] Capture prop documentation comments (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3486" class="reference external" rel="external noopener noreferrer" target="_blank">#3486</a> [Toolkit][Shadcn] Add menubar recipe (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3483" class="reference external" rel="external noopener noreferrer" target="_blank">#3483</a> [Toolkit][Shadcn] Add input-otp recipe (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3777" class="reference external" rel="external noopener noreferrer" target="_blank">#3777</a> [LiveComponent] Cache the attribute-method lookups per component class (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3762" class="reference external" rel="external noopener noreferrer" target="_blank">#3762</a> [TwigComponent] Fix <a href="twig:blockquote" class="reference internal">twig:blockquote</a> is not a block (@smnandre)</li>
<li><a href="https://github.com/symfony/ux/pull/3779" class="reference external" rel="external noopener noreferrer" target="_blank">#3779</a> [Map] Render each UX icon once instead of once per marker (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3783" class="reference external" rel="external noopener noreferrer" target="_blank">#3783</a> [StimulusBundle] Stop watching the DOM once every lazy controller is loaded (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3776" class="reference external" rel="external noopener noreferrer" target="_blank">#3776</a> [TwigComponent] Reduce per-character work in the pre-lexer scan loops (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3784" class="reference external" rel="external noopener noreferrer" target="_blank">#3784</a> [Translator] Cache the RegExp compiled by strtr() (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3786" class="reference external" rel="external noopener noreferrer" target="_blank">#3786</a> [LiveComponent] Skip the bracket pipeline for plain model names (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3781" class="reference external" rel="external noopener noreferrer" target="_blank">#3781</a> [StimulusBundle] Speed up Stimulus name normalization (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3841" class="reference external" rel="external noopener noreferrer" target="_blank">#3841</a> [TwigComponent] Apply review remarks from #3692 (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3692" class="reference external" rel="external noopener noreferrer" target="_blank">#3692</a> [TwigComponent] Reduce per-render CPU cost of static components (@javiereguiluz)</li>
<li><a href="https://github.com/symfony/ux/pull/3775" class="reference external" rel="external noopener noreferrer" target="_blank">#3775</a> [TwigComponent] Fix quadratic scanning in TwigPreLexer::consume() (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3780" class="reference external" rel="external noopener noreferrer" target="_blank">#3780</a> [Toolkit] Stop re-walking the filesystem on every Recipe::getFiles() (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3785" class="reference external" rel="external noopener noreferrer" target="_blank">#3785</a> [TwigComponent] Build the exposed properties without a generator (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3782" class="reference external" rel="external noopener noreferrer" target="_blank">#3782</a> [Icons] Cut per-render allocations in IconRenderer::renderIcon() (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3764" class="reference external" rel="external noopener noreferrer" target="_blank">#3764</a> [TwigComponent] Fix { verbatim } pre-lexing (@smnandre)</li>
<li><a href="https://github.com/symfony/ux/pull/3771" class="reference external" rel="external noopener noreferrer" target="_blank">#3771</a> [Icons] Migrate to modern <code translate="no" class="notranslate">\Dom\XMLDocument</code> API (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3699" class="reference external" rel="external noopener noreferrer" target="_blank">#3699</a> [TwigComponent] Add dynamic component support for HTML-syntax (@seb-jean)</li>
<li><a href="https://github.com/symfony/ux/pull/3596" class="reference external" rel="external noopener noreferrer" target="_blank">#3596</a> [Toolkit] Finalize support for Blocks, add Shadcn's login-01 and login-02 blocks (@seb-jean)</li>
<li><a href="https://github.com/symfony/ux/pull/3760" class="reference external" rel="external noopener noreferrer" target="_blank">#3760</a> [Toolkit] Migrate Tailwind kits to the <code translate="no" class="notranslate">tailwind_classes</code> mergeable idiom (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3756" class="reference external" rel="external noopener noreferrer" target="_blank">#3756</a> [TwigComponent] Support <code translate="no" class="notranslate">MergeableInterface</code> in <code translate="no" class="notranslate">ComponentAttributes</code> (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3758" class="reference external" rel="external noopener noreferrer" target="_blank">#3758</a> Upgrade pnpm to 11.21.0 (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3749" class="reference external" rel="external noopener noreferrer" target="_blank">#3749</a> [Toolkit] Restore the filename above installation code blocks (<code translate="no" class="notranslate">filename</code> code options) (@Kocal)</li>
</ul>
<p>Symfony UX 3.5.1 followed a few hours later, with two fixes for the standalone
AssetMapper bundle that 3.5.0 had missed:</p>
<ul>
    <li><a href="https://github.com/symfony/ux/pull/3892" class="reference external" rel="external noopener noreferrer" target="_blank">#3892</a> [Map] Add missing support for Symfony 8.2's standalone AssetMapperBundle (@Kocal)</li>
<li><a href="https://github.com/symfony/ux/pull/3891" class="reference external" rel="external noopener noreferrer" target="_blank">#3891</a> [Pagination] Add missing support for Symfony 8.2's standalone AssetMapperBundle (@Kocal)</li>
</ul>
<p>UX Pagination is experimental, which means its API can still change before a
stable release. That is exactly when your feedback is worth the most, so try it
and open an issue if something does not fit. The same goes for the attribute
rendering change: if upgrading broke an assertion in a way this post did not
warn you about, we want to hear about it.</p>
</div>
                <hr style="margin-bottom: 5px" />
                <div style="font-size: 90%">
                    <a href="https://symfony.com/sponsor">Sponsor</a> the Symfony project.
                </div>
            ]]></content:encoded>
            <guid isPermaLink="false">https://symfony.com/blog/symfony-ux-3-5-released?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Hugo Alliaume ]]></dc:creator>
            <pubDate>Mon, 21 Sep 2026 10:30:00 +0200</pubDate>
            <comments>https://symfony.com/blog/symfony-ux-3-5-released?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
            </channel>
</rss>
