<?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>Thu, 18 Jun 2026 07:12:42 +0200</pubDate>
        <lastBuildDate>Wed, 17 Jun 2026 13:47:00 +0200</lastBuildDate>
        <language>en</language>
                        <item>
            <title><![CDATA[New in Twig 4.0: Expression Parsers]]></title>
            <link>https://symfony.com/blog/new-in-twig-4-0-expression-parsers?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>
    
                    
                
            
            
    
        Contributed by
                    Fabien Potencier
                                         in
                                    #4543
                    ,…</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>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/twigphp/Twig/pull/4543">#4543</a>
                    ,                                     <a target="_blank" href="https://github.com/twigphp/Twig/pull/4550">#4550</a>
                    ,                                     <a target="_blank" href="https://github.com/twigphp/Twig/pull/4578">#4578</a>
                     and                                     <a target="_blank" href="https://github.com/twigphp/Twig/pull/4775">#4775</a>
                                                </span>
            </div>
</div>
<p>In 1973, Vaughan Pratt published "Top Down Operator Precedence", a paper
describing a parsing technique so simple it almost feels like cheating.
Half a century later, that algorithm is the new heart of Twig. If you maintain
an extension that defines operators, Twig 4.0 changes your world:
<code translate="no" class="notranslate">getOperators()</code> is gone, replaced by <code translate="no" class="notranslate">getExpressionParsers()</code>. And
even if you don't, stick around for the algorithm; it is one of my favorite
pieces of code in the whole codebase.</p>
<div class="section">
<h2 id="the-problem-a-monolith-and-a-magic-array"><a class="headerlink" href="#the-problem-a-monolith-and-a-magic-array" title="Permalink to this headline">The Problem: A Monolith and a Magic Array</a></h2>
<p>In Twig 3, an extension declared its operators through <code translate="no" class="notranslate">getOperators()</code>,
which returned two nested arrays with a very specific shape:</p>
<div translate="no" data-loc="17" 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">getOperators</span><span class="hljs-params">()</span>: <span class="hljs-title">array</span>
</span>{
    <span class="hljs-keyword">return</span> [
        <span class="hljs-comment">// unary operators</span>
        [
            <span class="hljs-string">'not'</span> =&gt; [<span class="hljs-string">'precedence'</span> =&gt; <span class="hljs-number">70</span>, <span class="hljs-string">'class'</span> =&gt; NotUnary::<span class="hljs-variable language_">class</span>],
        ],
        <span class="hljs-comment">// binary operators</span>
        [
            <span class="hljs-string">'~'</span> =&gt; [
                <span class="hljs-string">'precedence'</span> =&gt; <span class="hljs-number">27</span>,
                <span class="hljs-string">'class'</span> =&gt; ConcatBinary::<span class="hljs-variable language_">class</span>,
                <span class="hljs-string">'associativity'</span> =&gt; ExpressionParser::<span class="hljs-variable constant_">OPERATOR_LEFT</span>,
            ],
        ],
    ];
}</code></pre>
    </div>
</div>
<p>Notice what is missing: behavior. The array describes operators, but the
code that parses them lived somewhere else, in the <code translate="no" class="notranslate">Twig\ExpressionParser</code>
class: 1,033 lines handling every operator of the language. And not only
operators: filters (<code translate="no" class="notranslate">|</code>), attribute access (<code translate="no" class="notranslate">.</code>), subscripts (<code translate="no" class="notranslate">[</code>),
function calls (<code translate="no" class="notranslate">(</code>), the ternary operator, <code translate="no" class="notranslate">is</code> tests, arrow
functions... all of them were pseudo-operators hardcoded in that one
sprawling class. An extension could add an operator as data, but it could
never change how one parses. The grammar was closed.</p>
</div>
<div class="section">
<h2 id="operators-are-now-objects"><a class="headerlink" href="#operators-are-now-objects" title="Permalink to this headline">Operators Are Now Objects</a></h2>
<p>In Twig 4, <code translate="no" class="notranslate">getExpressionParsers()</code> returns a list of objects, each one
owning everything about its operator: name, precedence, associativity, and
the parsing logic itself. Here is a complete extension adding a <code translate="no" class="notranslate">repeat</code>
operator that repeats a string:</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">use</span> <span class="hljs-title">Twig</span>\<span class="hljs-title">Extension</span>\<span class="hljs-title">AbstractExtension</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Twig</span>\<span class="hljs-title">ExpressionParser</span>\<span class="hljs-title">Infix</span>\<span class="hljs-title">BinaryOperatorExpressionParser</span>;

<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StringToolsExtension</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AbstractExtension</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getExpressionParsers</span><span class="hljs-params">()</span>: <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">BinaryOperatorExpressionParser</span>(
                RepeatBinary::<span class="hljs-variable language_">class</span>, <span class="hljs-string">'repeat'</span>, <span class="hljs-number">30</span>,
            ),
        ];
    }
}</code></pre>
    </div>
</div>
<p>The node class compiles to a <code translate="no" class="notranslate">str_repeat()</code> call:</p>
<div translate="no" data-loc="15" 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">Compiler</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Twig</span>\<span class="hljs-title">Node</span>\<span class="hljs-title">Expression</span>\<span class="hljs-title">Binary</span>\<span class="hljs-title">AbstractBinary</span>;

<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RepeatBinary</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AbstractBinary</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">compile</span><span class="hljs-params">(Compiler <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>compiler</span>)</span>: <span class="hljs-title">void</span>
    </span>{
        <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>compiler</span>
            -&gt;<span class="hljs-title invoke__">raw</span>(<span class="hljs-string">'str_repeat('</span>)
            -&gt;<span class="hljs-title invoke__">subcompile</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">getNode</span>(<span class="hljs-string">'left'</span>))
            -&gt;<span class="hljs-title invoke__">raw</span>(<span class="hljs-string">', '</span>)
            -&gt;<span class="hljs-title invoke__">subcompile</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">getNode</span>(<span class="hljs-string">'right'</span>))
            -&gt;<span class="hljs-title invoke__">raw</span>(<span class="hljs-string">')'</span>);
    }
}</code></pre>
    </div>
</div>
<p>And it works like any built-in operator, precedence included:</p>
<div translate="no" data-loc="2" class="notranslate codeblock codeblock-length-sm codeblock-twig">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-template-variable">{{ <span class="hljs-string">'-='</span> repeat 5 }}</span><span class="xml">      </span><span class="hljs-comment">{# outputs -=-=-=-=-= #}</span><span class="xml">
</span><span class="hljs-template-variable">{{ <span class="hljs-string">'ab'</span> repeat 2 ~ <span class="hljs-string">'!'</span> }}</span><span class="xml"> </span><span class="hljs-comment">{# outputs abab! #}</span></code></pre>
    </div>
</div>
<p>The second line is the interesting one: <code translate="no" class="notranslate">repeat</code> has precedence 30,
concatenation has 27, so the repetition binds tighter. No special case
anywhere; the numbers decide.</p>
</div>
<div class="section">
<h2 id="one-loop-to-parse-them-all"><a class="headerlink" href="#one-loop-to-parse-them-all" title="Permalink to this headline">One Loop to Parse Them All</a></h2>
<p>Now, the part I really want to show you. Pratt's idea splits the world in
two: <em>prefix</em> parsers know how to <strong>start</strong> an expression (a literal, a
unary <code translate="no" class="notranslate">-</code>, <code translate="no" class="notranslate">not</code>, an opening parenthesis), and <em>infix</em> parsers know how
to <strong>extend</strong> one (<code translate="no" class="notranslate">+</code>, <code translate="no" class="notranslate">|</code>, <code translate="no" class="notranslate">.</code>, a function call). Each parser
carries a precedence. The whole algorithm is one method, lightly edited
here for brevity:</p>
<div translate="no" data-loc="20" 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">parseExpression</span><span class="hljs-params">(<span class="hljs-keyword">int</span> <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>precedence</span> = <span class="hljs-number">0</span>)</span>: <span class="hljs-title">AbstractExpression</span>
</span>{
    <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>token</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">getCurrentToken</span>();
    <span class="hljs-keyword">if</span> (<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>parser</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">getPrefixParser</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>token</span>)) {
        <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">getStream</span>()-&gt;<span class="hljs-title invoke__">next</span>();
        <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>expr</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>parser</span>-&gt;<span class="hljs-title invoke__">parse</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>, <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>token</span>);
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>expr</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">parseLiteral</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>token</span>);
    }

    <span class="hljs-keyword">while</span> ((<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>parser</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">getInfixParser</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">getCurrentToken</span>()))
        &amp;&amp; <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>parser</span>-&gt;<span class="hljs-title invoke__">getPrecedence</span>() &gt;= <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>precedence</span>
    ) {
        <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>token</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">getCurrentToken</span>();
        <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">getStream</span>()-&gt;<span class="hljs-title invoke__">next</span>();
        <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>expr</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>parser</span>-&gt;<span class="hljs-title invoke__">parse</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>, <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>expr</span>, <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>token</span>);
    }

    <span class="hljs-keyword">return</span> <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>expr</span>;
}</code></pre>
    </div>
</div>
<p>That's it. A binary operator's <code translate="no" class="notranslate">parse()</code> method is two lines: parse my
right side, build my node:</p>
<div translate="no" data-loc="11" 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">parse</span><span class="hljs-params">(
    Parser <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>parser</span>, AbstractExpression <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>left</span>, Token <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>token</span>,
)</span>: <span class="hljs-title">AbstractExpression</span> </span>{
    <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>right</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>parser</span>-&gt;<span class="hljs-title invoke__">parseExpression</span>(
        <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">isLeftAssociative</span>()
            ? <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">getPrecedence</span>() + <span class="hljs-number">1</span>
            : <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">getPrecedence</span>()
    );

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> (<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;nodeClass)(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>left</span>, <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>right</span>, <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>token</span>-&gt;<span class="hljs-title invoke__">getLine</span>());
}</code></pre>
    </div>
</div>
<p>Let's walk through <code translate="no" class="notranslate">1 + 2 * 3</code>. The outer call parses <code translate="no" class="notranslate">1</code>, sees
<code translate="no" class="notranslate">+</code> (precedence 30), and lets it parse the right side with a minimum
precedence of 31. That inner call parses <code translate="no" class="notranslate">2</code>, sees <code translate="no" class="notranslate">*</code> (precedence 60,
which is ≥ 31), and recurses again: <code translate="no" class="notranslate">2 * 3</code> becomes a node, the inner
call returns it, and <code translate="no" class="notranslate">+</code> builds <code translate="no" class="notranslate">1 + (2 * 3)</code>. Now flip the operands:
in <code translate="no" class="notranslate">1 * 2 + 3</code>, the inner call parses <code translate="no" class="notranslate">2</code>, sees <code translate="no" class="notranslate">+</code> (precedence 30,
which is &lt; 61), and stops right there; the outer loop picks <code translate="no" class="notranslate">+</code> up and
builds <code translate="no" class="notranslate">(1 * 2) + 3</code>. Two integer comparisons, and precedence falls out
for free.</p>
<p>Associativity is the <code translate="no" class="notranslate">+ 1</code> in the snippet above. Left-associative
operators ask for <em>strictly higher</em> precedence on their right, so <code translate="no" class="notranslate">8 - 2
- 1</code> becomes <code translate="no" class="notranslate">(8 - 2) - 1</code>. Right-associative operators ask for their
own precedence, so <code translate="no" class="notranslate">2 ** 3 ** 2</code> becomes <code translate="no" class="notranslate">2 ** (3 ** 2)</code>, which is 512.
One character of code, and that is the entire theory of associativity.</p>
</div>
<div class="section">
<h2 id="everything-is-an-expression-parser"><a class="headerlink" href="#everything-is-an-expression-parser" title="Permalink to this headline">Everything Is an Expression Parser</a></h2>
<p>The first version of this work turned operators into objects and kept the
rest of the monolith untouched. Then it clicked: operators are a subset of
expression parsers. A filter is an infix parser for <code translate="no" class="notranslate">|</code>. Attribute access
is an infix parser for <code translate="no" class="notranslate">.</code>. A function call is an infix parser for <code translate="no" class="notranslate">(</code>
applied to a name. The ternary operator, <code translate="no" class="notranslate">??</code>, <code translate="no" class="notranslate">is</code>, arrow functions:
parsers, parsers, parsers. Even the humble parenthesized group is a prefix
parser for <code translate="no" class="notranslate">(</code>.</p>
<p>Treating a function call as a binary operator raised a few eyebrows during
code review, and I understand why; no mainstream language describes <code translate="no" class="notranslate">(</code>
that way. But once every construct is a parser with a precedence, they all
play by the same rules: one registry, one precedence table, one
documentation page, and an extension API that can express <em>any</em> of them.
The <code translate="no" class="notranslate">Twig\ExpressionParser</code> monolith had nothing left to do, and Twig
4.0 deletes it.</p>
<p>One refinement came out of real-world testing: the lexer needs to know
which token strings are operators, and a parser's name is not always one
of them. When the internal <code translate="no" class="notranslate">literal</code> parser shipped, its name suddenly
clashed with a Craft CMS filter named <code translate="no" class="notranslate">literal</code>, breaking templates
using <code translate="no" class="notranslate">'foo'|literal</code>. The fix is the <code translate="no" class="notranslate">getOperatorTokens()</code> method:
each parser declares the token strings it owns, and parsers like
<code translate="no" class="notranslate">literal</code> that own none return an empty list, leaving the name free for
your filters.</p>
</div>
<div class="section">
<h2 id="the-upgrade-path"><a class="headerlink" href="#the-upgrade-path" title="Permalink to this headline">The Upgrade Path</a></h2>
<p>Twig 3.21 deprecated <code translate="no" class="notranslate">getOperators()</code>, with extensions still using it
triggering "Extension "App\Twig\StringToolsExtension" uses the old
signature for "getOperators()", please implement "getExpressionParsers()"
instead.". The new method already works on 3.21+, so you can migrate your
extensions today and they will run unmodified on both majors.</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-expression-parsers?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Fabien Potencier ]]></dc:creator>
            <pubDate>Wed, 17 Jun 2026 13:47:00 +0200</pubDate>
            <comments>https://symfony.com/blog/new-in-twig-4-0-expression-parsers?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[Symfony: The Fast Track, now in nine languages]]></title>
            <link>https://symfony.com/blog/symfony-the-fast-track-now-in-nine-languages?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>Earlier this week, I announced the Symfony 8.1 edition of The Fast Track. If
you made it to the end of that post, you read that the book was available in
five languages. That line is already out of date, and I could not be happier
about it.
The Symfony 8.1…</description>
            <content:encoded><![CDATA[
                                <p>Earlier this week, I <a href="https://symfony.com/blog/symfony-the-fast-track-now-for-symfony-8-1" class="reference external">announced the Symfony 8.1 edition</a> of <em>The Fast Track</em>. If
you made it to the end of that post, you read that the book was available in
five languages. That line is already out of date, and I could not be happier
about it.</p>
<p>The Symfony 8.1 edition is now available in nine languages: English, of course,
plus French, German, Italian, Japanese, and Russian. And three newcomers I am
especially happy to welcome for this edition: Dutch, Polish, and Ukrainian. A
book that teaches you to build a real application should be able to do it in the
language you think in.</p>
<p>Every edition is free to read online at <a href="https://symfony.com/book" class="reference external">symfony.com/book</a>, in all nine
languages. Same guestbook, same Git-commit journey, same
deploy-from-chapter-one philosophy, now in your language.</p>
<p>Translating a book this size, keeping every language in sync edition after
edition, running the whole thing as an executable test suite: that is real,
continuous work, and it has to be funded somehow. If your company builds on
Symfony and wants to give back, you can sponsor the work on the book directly:
reach out to Hadrien at <a href="mailto:events@symfony.com" class="reference internal">events@symfony.com</a>. Your
name in the credits, the book in more hands, more time funded for Symfony.
Everybody wins.</p>
<p>Bouw het. Zbuduj ją. Збудуй її. Build it. Four ways to say the same thing, and
the book now says it in all nine. Pick the language you dream in; the guestbook
is waiting.</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/symfony-the-fast-track-now-in-nine-languages?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Fabien Potencier ]]></dc:creator>
            <pubDate>Tue, 16 Jun 2026 10:49:00 +0200</pubDate>
            <comments>https://symfony.com/blog/symfony-the-fast-track-now-in-nine-languages?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[New in Symfony 8.1: Tui Component]]></title>
            <link>https://symfony.com/blog/new-in-symfony-8-1-tui-component?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>
    
                    
                
            
            
    
        Contributed by
                    Fabien Potencier
                                         in
                                    #63778…</description>
            <content:encoded><![CDATA[
                                <div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/fabpot">
                <img src="https://connect.symfony.com/profile/fabpot.picture" 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://connect.symfony.com/profile/fabpot">Fabien Potencier</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/63778">#63778</a>
                                                </span>
            </div>
</div>
<p>A new generation of terminal tools (AI coding assistants, log viewers, interactive
dashboards, file managers, etc.) share something the <a href="https://symfony.com/console" class="reference external">Console component</a>
was never meant to do: render a <strong>full-screen interface that redraws in place
and reacts to every keystroke</strong>. That's exactly what the new <strong>Tui component</strong>,
included in Symfony 8.1, was built for.</p>
<p>We already explored it in depth when we <a href="https://symfony.com/blog/introducing-the-symfony-tui-component" class="reference external">introduced the Tui component</a>, so this
post is a short tour to celebrate that it's now part of a Symfony release. In a
nutshell, Tui <strong>complements the Console component</strong> rather than replacing it:
Console stays focused on commands, arguments and output, while Tui takes over
everything related to rich terminal interaction, widgets, layouts, styling, input
handling and real-time rendering. You can even run a full Tui from inside a
console command and fall back to regular Console output when it stops.</p>
<p>Creating an application is straightforward. Instantiate <code translate="no" class="notranslate">Tui</code>, add some widgets,
react to their events and run the event loop:</p>
<div translate="no" data-loc="28" 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">Tui</span>\<span class="hljs-title">Event</span>\<span class="hljs-title">InputEvent</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Tui</span>\<span class="hljs-title">Input</span>\<span class="hljs-title">Keybindings</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Tui</span>\<span class="hljs-title">Tui</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Tui</span>\<span class="hljs-title">Widget</span>\<span class="hljs-title">InputWidget</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Tui</span>\<span class="hljs-title">Widget</span>\<span class="hljs-title">TextWidget</span>;

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>tui</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">Tui</span>();

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>input</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">InputWidget</span>();
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>input</span>-&gt;<span class="hljs-title invoke__">setPrompt</span>(<span class="hljs-string">'Name: '</span>);
<span class="hljs-comment">// widgets are event-driven: react to submit, cancel, change, selection, etc.</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>input</span>-&gt;<span class="hljs-title invoke__">onSubmit</span>(<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-params">()</span> =&gt;</span> <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>tui</span>-&gt;<span class="hljs-title invoke__">stop</span>());

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>tui</span>-&gt;<span class="hljs-title invoke__">add</span>(<span class="hljs-keyword">new</span> <span class="hljs-title invoke__">TextWidget</span>(<span class="hljs-string">'Enter your name and press Enter:'</span>));
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>tui</span>-&gt;<span class="hljs-title invoke__">add</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>input</span>);
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>tui</span>-&gt;<span class="hljs-title invoke__">setFocus</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>input</span>);

<span class="hljs-comment">// intercept raw keystrokes (here, quit on Ctrl+C)</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>keys</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">Keybindings</span>([<span class="hljs-string">'quit'</span> =&gt; [<span class="hljs-string">'ctrl+c'</span>]]);
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>tui</span>-&gt;<span class="hljs-title invoke__">addListener</span>(<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(InputEvent <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>event</span>)</span> <span class="hljs-keyword">use</span> <span class="hljs-params">(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>tui</span>, <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>keys</span>)</span> </span>{
    if (<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>keys</span>-&gt;<span class="hljs-title invoke__">matches</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>event</span>-&gt;<span class="hljs-title invoke__">getData</span>(), <span class="hljs-string">'quit'</span>)) {
        <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>tui</span>-&gt;<span class="hljs-title invoke__">stop</span>();
    }
});

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>tui</span>-&gt;<span class="hljs-title invoke__">run</span>(); <span class="hljs-comment">// blocks until stop() is called; then the terminal is restored</span>

<span class="hljs-keyword">echo</span> <span class="hljs-string">'Hello, '</span>.<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>input</span>-&gt;<span class="hljs-title invoke__">getValue</span>().<span class="hljs-string">"!\n"</span>;</code></pre>
    </div>
</div>
<p>Beyond the basics, Tui ships with a complete widget toolkit, so you never draw
the terminal by hand: single and multi-line text editors (with undo/redo, and tab
autocomplete), selectable lists, settings panels, tabs, animated spinners,
progress bars, large FIGlet ASCII-art text and even a Markdown widget that renders
syntax-highlighted code, perfect for displaying AI assistant responses.</p>
<p>Every widget is styled with a <strong>CSS-like engine</strong>. The simplest way is to apply
an immutable <code translate="no" class="notranslate">Style</code> value object directly to a widget:</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">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Tui</span>\<span class="hljs-title">Style</span>\<span class="hljs-title">Border</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Tui</span>\<span class="hljs-title">Style</span>\<span class="hljs-title">Padding</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Tui</span>\<span class="hljs-title">Style</span>\<span class="hljs-title">Style</span>;

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>panel</span>-&gt;<span class="hljs-title invoke__">setStyle</span>(<span class="hljs-keyword">new</span> <span class="hljs-title invoke__">Style</span>(
    <span class="hljs-attr">color</span>: <span class="hljs-string">'cyan'</span>,
    <span class="hljs-attr">bold</span>: <span class="hljs-keyword">true</span>,
    <span class="hljs-attr">padding</span>: Padding::<span class="hljs-title invoke__">all</span>(<span class="hljs-number">1</span>),
    <span class="hljs-attr">border</span>: Border::<span class="hljs-title invoke__">all</span>(<span class="hljs-number">1</span>, <span class="hljs-string">'rounded'</span>, <span class="hljs-string">'cyan'</span>),
));</code></pre>
    </div>
</div>
<p>For larger applications you can move those rules into stylesheets with
selectors, a cascade and <code translate="no" class="notranslate">:focus</code> states, or reach for familiar Tailwind-like
utility classes such as <code translate="no" class="notranslate">p-2</code>, <code translate="no" class="notranslate">bg-slate-800</code> and <code translate="no" class="notranslate">border-rounded</code>.</p>
<p>Tui is also fully <strong>non-blocking</strong>. It runs on PHP Fibers and the Revolt event
loop, so animations keep playing and the interface stays responsive while
background work runs. A tick callback gives you a per-frame hook to poll async
results and refresh widgets:</p>
<div translate="no" data-loc="11" 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">Tui</span>\<span class="hljs-title">Event</span>\<span class="hljs-title">TickEvent</span>;

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>tui</span>-&gt;<span class="hljs-title invoke__">onTick</span>(<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(TickEvent <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>event</span>)</span> <span class="hljs-keyword">use</span> <span class="hljs-params">(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>future</span>, <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>loader</span>, <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>tui</span>)</span> </span>{
    if (<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>future</span>-&gt;<span class="hljs-title invoke__">isComplete</span>()) {
        <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>loader</span>-&gt;<span class="hljs-title invoke__">stop</span>();

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>; <span class="hljs-comment">// idle: stop ticking and save CPU</span>
    }

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>; <span class="hljs-comment">// busy: keep ticking at high frequency</span>
});</code></pre>
    </div>
</div>
<p>That same loop, combined with <code translate="no" class="notranslate">getDeltaTime()</code> for time-based updates, is
smooth enough to power long-running operations, live dashboards and even
real-time terminal games. Head over to the <a href="https://github.com/symfony/symfony-docs/pull/22201" class="reference external" rel="external noopener noreferrer" target="_blank">Tui documentation</a> to discover all
the widgets, the full styling system and the event and focus management APIs.</p>
<hr>
<p><strong>This is the last blog post</strong> in the <a href="https://symfony.com/blog/category/living-on-the-edge/8.1" class="reference external">New in Symfony 8.1</a> series. We hope you
enjoyed reading it and that you upgrade your Symfony applications soon. Meanwhile,
work on <a href="https://symfony.com/releases/8.2" class="reference external">Symfony 8.2</a> has already begun in preparation for its release in November 2026.</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/new-in-symfony-8-1-tui-component?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Javier Eguiluz ]]></dc:creator>
            <pubDate>Tue, 16 Jun 2026 08:32:00 +0200</pubDate>
            <comments>https://symfony.com/blog/new-in-symfony-8-1-tui-component?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[Symfony: The Fast Track, now for Symfony 8.1]]></title>
            <link>https://symfony.com/blog/symfony-the-fast-track-now-for-symfony-8-1?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>In November 2019, at SymfonyCon Amsterdam, I published &quot;Symfony 5: The Fast
Track&quot;; a book that teaches Symfony the way I like to learn: by building a
real application, one Git commit at a time, from the very first composer
install to production deployments,…</description>
            <content:encoded><![CDATA[
                                <p>In November 2019, at SymfonyCon Amsterdam, I published "Symfony 5: The Fast
Track"; a book that teaches Symfony the way I like to learn: by building a
real application, one Git commit at a time, from the very first <code translate="no" class="notranslate">composer
install</code> to production deployments, performance profiling, and a release
party. Since then, the book has followed Symfony, edition after edition. The
one I'm announcing today, for Symfony 8.1, is the eighth. It is also the most
ambitious rewrite since the original.</p>
<p>The guestbook application is still the same: conferences, comments, photos,
an admin backend, workflows, async processing. What changed is <em>how</em> we build
it: Symfony UX for the frontend, Symfony AI for spam detection, the Scheduler
for recurring tasks, AssetMapper for the assets, and modern Symfony idioms
everywhere.</p>
<div class="section">
<h2 id="fighting-spam-with-ai"><a class="headerlink" href="#fighting-spam-with-ai" title="Permalink to this headline">Fighting Spam with AI</a></h2>
<p>The spam chapter used to call the Akismet API by hand; a fine excuse to learn
the HttpClient component, but let's be honest, not how you would solve this
problem in 2026. The chapter has been rewritten around Symfony AI: an <em>agent</em>
now asks a Large Language Model whether a submitted comment is spam, with a
system prompt that constrains its role and its answers.</p>
<p>The chapter uses OpenAI with <code translate="no" class="notranslate">gpt-5-mini</code>, but the code does not care about
the provider: Symfony AI abstracts OpenAI, Anthropic, Google Gemini, Mistral,
and even local models via Ollama behind the same <em>platform</em> interface; switch
providers and only the configuration changes. Nothing here requires a
frontier model either: a task as narrow as spam classification would be
perfectly served by a small or specialized model. The calls still happen "out
of band" via Messenger, because an expensive call is an expensive call,
whether it hits Akismet or a model. And the chapter teaches a design rule I
care about: when the model cannot answer, fall back to human moderation. AI
should help the admin, never block the guestbook.</p>
</div>
<div class="section">
<h2 id="testing-with-object-factories"><a class="headerlink" href="#testing-with-object-factories" title="Permalink to this headline">Testing with Object Factories</a></h2>
<p>Two changes in the tests chapter. Doctrine fixtures are gone, replaced by
Zenstruck Foundry: each test now creates exactly the data set it needs via
object factories, with realistic defaults generated by Faker. Tests become
independent from each other, and reading a test tells you everything about
the data it relies on.</p>
<p>And how do you test code that calls an LLM? You don't, at least not the
model: hitting the OpenAI API in a test suite would be slow, expensive, and
not even deterministic. The book replaces the platform with Symfony AI's
<code translate="no" class="notranslate">InMemoryPlatform</code>, wrapped in a real <code translate="no" class="notranslate">Agent</code> so that the application
logic is tested for real, including the "model is down" path.</p>
</div>
<div class="section">
<h2 id="no-more-node-js"><a class="headerlink" href="#no-more-node-js" title="Permalink to this headline">No More Node.js</a></h2>
<p>The styling chapter dropped Webpack Encore for AssetMapper. Importmaps,
browser-native modules, no <code translate="no" class="notranslate">node_modules</code>, no build step. Bootstrap is still
there but Node.js disappeared from the book's requirements entirely: the first
chapter no longer asks you to install it.</p>
</div>
<div class="section">
<h2 id="an-spa-feel-without-the-spa"><a class="headerlink" href="#an-spa-feel-without-the-spa" title="Permalink to this headline">An SPA Feel, without the SPA</a></h2>
<p>Previous editions ended with a Preact single-page application for mobile
phones. This edition replaces it with a chapter on Symfony UX: Turbo makes
navigation feel instant without full page reloads, Turbo Frames update just a
fragment of the page, and a small Stimulus controller previews the comment
photo before it is uploaded. The result feels as snappy as an SPA, written
with the Twig templates we already have. The API chapter is still there, so
when a real mobile application comes knocking, the backend is ready.</p>
</div>
<div class="section">
<h2 id="scheduling-without-cron"><a class="headerlink" href="#scheduling-without-cron" title="Permalink to this headline">Scheduling without Cron</a></h2>
<p>The old cron chapter became the Scheduler chapter. The comment cleanup task
is now declared in PHP with the Scheduler component, which means the schedule
lives in the code, ships with the application, and deploys with it. No more
editing crontabs on production machines; the chapter still explains how the
two approaches compare.</p>
</div>
<div class="section">
<h2 id="symfonycloud-from-platform-sh-to-upsun"><a class="headerlink" href="#symfonycloud-from-platform-sh-to-upsun" title="Permalink to this headline">SymfonyCloud: From Platform.sh to Upsun</a></h2>
<p>The book deploys to production from the very first chapters, and that has not
changed. What changed is the platform underneath: the deployment chapters now
target Upsun, the next generation of Platform.sh. If you read a previous
edition, the workflow will feel familiar, on purpose: it is still SymfonyCloud
from where you stand, the same <code translate="no" class="notranslate">symfony cloud:</code> commands, a Git push to
deploy, one environment per branch. Only the configuration file moved, from
<code translate="no" class="notranslate">.platform.app.yaml</code> to <code translate="no" class="notranslate">.upsun/config.yaml</code>.</p>
</div>
<div class="section">
<h2 id="modern-symfony-everywhere"><a class="headerlink" href="#modern-symfony-everywhere" title="Permalink to this headline">Modern Symfony, Everywhere</a></h2>
<p>Beyond the headline chapters, the whole book has been modernized to the
idioms of today's Symfony: the <code translate="no" class="notranslate">#[Cache]</code> attribute on controllers instead
of calls on the Response, <code translate="no" class="notranslate">#[AsEventListener]</code> instead of event
subscribers, <code translate="no" class="notranslate">#[MapQueryParameter]</code> instead of poking at the Request,
invokable console commands with services injected right in <code translate="no" class="notranslate">__invoke()</code>,
and typed Doctrine lifecycle events. The workflow chapter now dumps Mermaid
diagrams, which GitHub and GitLab render natively. Small things, but they add
up: the code you write in the book is the code you would write in a new
project today.</p>
</div>
<div class="section">
<h2 id="a-book-that-runs"><a class="headerlink" href="#a-book-that-runs" title="Permalink to this headline">A Book that Runs</a></h2>
<p>This book is executable, and that is still my favorite thing about it. Every
edition is validated by tooling that replays the whole journey exactly as a
reader would: it builds the guestbook commit by commit, runs the test suite
at every step, deploys each of the 27 deployable steps to Upsun and asserts
the live pages, and takes the 40 screenshots you see in the book from the
running application. If a chapter breaks, the build breaks.</p>
<p>Fun fact: this edition's validation run found a real bug: a twelve-year-old
limitation in Symfony's HttpCache that serves file responses with an empty
body. The bug itself is next on my list.</p>
</div>
<div class="section">
<h2 id="read-it-now"><a class="headerlink" href="#read-it-now" title="Permalink to this headline">Read It Now</a></h2>
<p>The English edition is available online, for free, at
<a href="https://symfony.com/book" class="reference external">symfony.com/book</a>. The code lives on GitHub,
with one Git tag per step; check out any tag to compare your code with mine
at that exact point of the journey. And if you would rather explore the
finished application first, the Symfony CLI sets everything up for you, web
server and containers included:</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>symfony new --version=8.1-1 --book guestbook</code></pre>
    </div>
</div>
<p>The book is also available in five languages, fully translated for this
edition: French, German, Italian, Japanese, and Russian.</p>
<p>Six years and eight editions later, I still believe the fastest way to learn
Symfony is to build something real. Build the guestbook.</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-the-fast-track-now-for-symfony-8-1?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Fabien Potencier ]]></dc:creator>
            <pubDate>Mon, 15 Jun 2026 16:41:00 +0200</pubDate>
            <comments>https://symfony.com/blog/symfony-the-fast-track-now-for-symfony-8-1?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[New in Symfony 8.1: Misc Improvements (Part 2)]]></title>
            <link>https://symfony.com/blog/new-in-symfony-8-1-misc-improvements-part-2?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>In addition to the main features announced in previous posts of this series,
Symfony 8.1 includes many smaller improvements that make day-to-day work
easier. This post highlights a second batch of them.

Build Semaphores on Any Lock Backend…</description>
            <content:encoded><![CDATA[
                                <p>In addition to the main features announced in previous posts of this series,
Symfony 8.1 includes many smaller improvements that make day-to-day work
easier. This post highlights a second batch of them.</p>
<div class="section">
<h2 id="build-semaphores-on-any-lock-backend"><a class="headerlink" href="#build-semaphores-on-any-lock-backend" title="Permalink to this headline">Build Semaphores on Any Lock Backend</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/alexander-schranz">
                <img src="https://connect.symfony.com/profile/alexander-schranz.picture" alt="Alexander Schranz">
            </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/alexander-schranz">Alexander Schranz</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/59202">#59202</a>
                                                </span>
            </div>
</div>
<p>The Semaphore component limits the number of processes that can access a shared
resource at the same time. Until now, its only built-in store was based on
Redis, which left applications using other backends without a usable option.</p>
<p>Symfony 8.1 adds a <code translate="no" class="notranslate">LockStore</code> that builds a semaphore on top of the
<a href="https://symfony.com/lock" class="reference external">Lock component</a>, so you can reuse any lock backend (database, filesystem, Redis,
etc.). A semaphore with a limit of N is implemented using N individual locks
behind the scenes:</p>
<div translate="no" data-loc="11" 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">Lock</span>\<span class="hljs-title">LockFactory</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Lock</span>\<span class="hljs-title">Store</span>\<span class="hljs-title">FlockStore</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Semaphore</span>\<span class="hljs-title">SemaphoreFactory</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Semaphore</span>\<span class="hljs-title">Store</span>\<span class="hljs-title">LockStore</span>;

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>lockFactory</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">LockFactory</span>(<span class="hljs-keyword">new</span> <span class="hljs-title invoke__">FlockStore</span>());
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>semaphoreFactory</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">SemaphoreFactory</span>(<span class="hljs-keyword">new</span> <span class="hljs-title invoke__">LockStore</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>lockFactory</span>));

<span class="hljs-comment">// allow at most 3 processes to hold this semaphore at the same time</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>semaphore</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>semaphoreFactory</span>-&gt;<span class="hljs-title invoke__">createSemaphore</span>(<span class="hljs-string">'thumbnail-generation'</span>, <span class="hljs-number">3</span>);
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>semaphore</span>-&gt;<span class="hljs-title invoke__">acquire</span>();</code></pre>
    </div>
</div>
<p>When using FrameworkBundle, point the semaphore at a configured lock with the new
<code translate="no" class="notranslate">lock://</code> DSN:</p>
<div translate="no" data-loc="8" class="notranslate codeblock codeblock-length-sm codeblock-yaml">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment"># config/packages/semaphore.yaml</span>
<span class="hljs-attr">framework:</span>
    <span class="hljs-attr">lock:</span>
        <span class="hljs-attr">default:</span> <span class="hljs-string">'flock'</span>
        <span class="hljs-attr">redis:</span> <span class="hljs-string">'%env(REDIS_DSN)%'</span>
    <span class="hljs-attr">semaphore:</span>
        <span class="hljs-attr">default:</span> <span class="hljs-string">'lock://'</span>      <span class="hljs-comment"># uses the "default" lock</span>
        <span class="hljs-attr">other:</span> <span class="hljs-string">'lock://redis'</span>   <span class="hljs-comment"># uses the "redis" lock</span></code></pre>
    </div>
</div>
</div>
<div class="section">
<h2 id="collect-extra-attributes-errors-while-deserializing"><a class="headerlink" href="#collect-extra-attributes-errors-while-deserializing" title="Permalink to this headline">Collect Extra Attributes Errors While Deserializing</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/NorthBlue333">
                <img src="https://github.com/NorthBlue333.png" alt="NorthBlue333">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/NorthBlue333">NorthBlue333</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/46654">#46654</a>
                                                </span>
            </div>
</div>
<p>When deserializing an incoming payload into an object, you can pass the
<code translate="no" class="notranslate">COLLECT_DENORMALIZATION_ERRORS</code> option to gather every type mismatch in a
single <code translate="no" class="notranslate">PartialDenormalizationException</code> instead of failing on the first one.
However, attributes that didn't exist on the target class were reported
separately, so you couldn't validate everything in one pass.</p>
<p>Symfony 8.1 adds the <code translate="no" class="notranslate">COLLECT_EXTRA_ATTRIBUTES_ERRORS</code> option, which collects
those unexpected attributes together with the type errors:</p>
<div translate="no" data-loc="21" 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">Serializer</span>\<span class="hljs-title">Exception</span>\<span class="hljs-title">PartialDenormalizationException</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Serializer</span>\<span class="hljs-title">Normalizer</span>\<span class="hljs-title">AbstractNormalizer</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Serializer</span>\<span class="hljs-title">Normalizer</span>\<span class="hljs-title">DenormalizerInterface</span>;

<span class="hljs-keyword">try</span> {
    <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>post</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>serializer</span>-&gt;<span class="hljs-title invoke__">deserialize</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>request</span>-&gt;<span class="hljs-title invoke__">getContent</span>(), BlogPost::<span class="hljs-variable language_">class</span>, <span class="hljs-string">'json'</span>, [
        DenormalizerInterface::<span class="hljs-variable constant_">COLLECT_DENORMALIZATION_ERRORS</span> =&gt; <span class="hljs-keyword">true</span>,
        DenormalizerInterface::<span class="hljs-variable constant_">COLLECT_EXTRA_ATTRIBUTES_ERRORS</span> =&gt; <span class="hljs-keyword">true</span>,
        AbstractNormalizer::<span class="hljs-variable constant_">ALLOW_EXTRA_ATTRIBUTES</span> =&gt; <span class="hljs-keyword">false</span>,
    ]);
} <span class="hljs-keyword">catch</span> (PartialDenormalizationException <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>e</span>) {
    <span class="hljs-comment">// type mismatches (e.g. a string where an int was expected)</span>
    <span class="hljs-keyword">foreach</span> (<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>e</span>-&gt;<span class="hljs-title invoke__">getNotNormalizableValueErrors</span>() <span class="hljs-keyword">as</span> <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>error</span>) {
        <span class="hljs-comment">// $error-&gt;getPath(), $error-&gt;getExpectedTypes(), ...</span>
    }

    <span class="hljs-comment">// attributes sent by the client that don't exist on BlogPost</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">null</span> !== <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>extraAttributesError</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>e</span>-&gt;<span class="hljs-title invoke__">getExtraAttributesError</span>()) {
        <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>unexpected</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>extraAttributesError</span>-&gt;<span class="hljs-title invoke__">getExtraAttributes</span>();
    }
}</code></pre>
    </div>
</div>
<p>The previous <code translate="no" class="notranslate">getErrors()</code> method is now deprecated; use
<code translate="no" class="notranslate">getNotNormalizableValueErrors()</code> instead.</p>
</div>
<div class="section">
<h2 id="use-the-has-selector-in-css-expressions"><a class="headerlink" href="#use-the-has-selector-in-css-expressions" title="Permalink to this headline">Use the :has() Selector in CSS Expressions</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/franckranaivo">
                <img src="https://connect.symfony.com/profile/franckranaivo.picture" alt="Franck RANAIVO-HARISOA">
            </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/franckranaivo">Franck RANAIVO-HARISOA</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/49388">#49388</a>
                                                </span>
            </div>
</div>
<p>The <a href="https://symfony.com/css-selector" class="reference external">CssSelector component</a> converts CSS selectors into XPath expressions, which
is what lets you query the DOM with CSS syntax in the DomCrawler (for example, in
functional tests). Symfony 8.1 adds support for the <code translate="no" class="notranslate">:has()</code> relational pseudo-class,
so you can select elements based on their descendants or siblings:</p>
<div translate="no" data-loc="7" class="notranslate codeblock codeblock-length-sm 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">CssSelector</span>\<span class="hljs-title">CssSelector</span>;

<span class="hljs-comment">// &lt;article&gt; elements that contain an &lt;h2&gt; somewhere inside them</span>
CssSelector::<span class="hljs-title invoke__">toXPath</span>(<span class="hljs-string">'article:has(h2)'</span>);

<span class="hljs-comment">// &lt;div&gt; elements with a direct child that has the "error" class</span>
CssSelector::<span class="hljs-title invoke__">toXPath</span>(<span class="hljs-string">'div:has(&gt; .error)'</span>);</code></pre>
    </div>
</div>
<p>This is the same selector now available in all major browsers, and it works
everywhere CssSelector is used, including <code translate="no" class="notranslate">Crawler::filter()</code> in your tests.</p>
</div>
<div class="section">
<h2 id="wrap-response-fragments-before-parsing-in-tests"><a class="headerlink" href="#wrap-response-fragments-before-parsing-in-tests" title="Permalink to this headline">Wrap Response Fragments Before Parsing in Tests</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/hubert_lenoir">
                <img src="https://connect.symfony.com/profile/hubert_lenoir.picture" alt="Hubert Lenoir">
            </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/hubert_lenoir">Hubert Lenoir</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/62892">#62892</a>
                                                </span>
            </div>
</div>
<p>Some endpoints return HTML fragments rather than full documents (for example, an
AJAX action that returns a few table rows). When the test client parses a bare
<code translate="no" class="notranslate">&lt;tr&gt;</code> element, the HTML5 parser rewrites or drops it because it isn't valid
outside a table, so your assertions run against mangled markup.</p>
<p>Symfony 8.1 adds the <code translate="no" class="notranslate">wrapContent()</code> method to the test client. It wraps the
response body in the structure you provide (using <code translate="no" class="notranslate">%s</code> as a placeholder)
before parsing, without changing the actual HTTP response:</p>
<div translate="no" data-loc="7" class="notranslate codeblock codeblock-length-sm codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment">// wrap fragments in a full table so &lt;tr&gt;/&lt;td&gt; elements are parsed correctly</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>client</span>-&gt;<span class="hljs-title invoke__">wrapContent</span>(<span class="hljs-string">'&lt;table&gt;%s&lt;/table&gt;'</span>);

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>crawler</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>client</span>-&gt;<span class="hljs-title invoke__">request</span>(<span class="hljs-string">'GET'</span>, <span class="hljs-string">'/comments/rows'</span>);

<span class="hljs-comment">// the rows are now available in the crawler as expected</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;<span class="hljs-title invoke__">assertCount</span>(<span class="hljs-number">3</span>, <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>crawler</span>-&gt;<span class="hljs-title invoke__">filter</span>(<span class="hljs-string">'tr'</span>));</code></pre>
    </div>
</div>
</div>
<div class="section">
<h2 id="serialize-semaphore-keys-across-processes"><a class="headerlink" href="#serialize-semaphore-keys-across-processes" title="Permalink to this headline">Serialize Semaphore Keys Across Processes</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/clegginabox">
                <img src="https://connect.symfony.com/profile/clegginabox.picture" alt="Paul Clegg">
            </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/clegginabox">Paul Clegg</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/63059">#63059</a>
                                                </span>
            </div>
</div>
<p>Sometimes you need to acquire a semaphore in one process and release it in
another, for example when a controller starts a long task and a Messenger worker
finishes it. That requires passing the semaphore <code translate="no" class="notranslate">Key</code> to the other process.</p>
<p>Symfony 8.1 makes the <code translate="no" class="notranslate">Key</code> class serializable and adds a
<code translate="no" class="notranslate">SemaphoreKeyNormalizer</code> so you can serialize keys with the Serializer
component too. When using FrameworkBundle, the normalizer is registered
automatically:</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">Semaphore</span>\<span class="hljs-title">Key</span>;

<span class="hljs-comment">// process 1: acquire the semaphore, then serialize its key</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>key</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">Key</span>(<span class="hljs-string">'report-generation'</span>, <span class="hljs-number">3</span>);
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>semaphore</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>factory</span>-&gt;<span class="hljs-title invoke__">createSemaphoreFromKey</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>key</span>, <span class="hljs-attr">autoRelease</span>: <span class="hljs-keyword">false</span>);
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>semaphore</span>-&gt;<span class="hljs-title invoke__">acquire</span>();

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>serializedKey</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>serializer</span>-&gt;<span class="hljs-title invoke__">serialize</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>key</span>, <span class="hljs-string">'json'</span>);

<span class="hljs-comment">// process 2: rebuild the semaphore from the key and release it</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>key</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>serializer</span>-&gt;<span class="hljs-title invoke__">deserialize</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>serializedKey</span>, Key::<span class="hljs-variable language_">class</span>, <span class="hljs-string">'json'</span>);
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>factory</span>-&gt;<span class="hljs-title invoke__">createSemaphoreFromKey</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>key</span>)-&gt;<span class="hljs-title invoke__">release</span>();</code></pre>
    </div>
</div>
</div>
<div class="section">
<h2 id="restrict-the-allowed-values-of-app-env"><a class="headerlink" href="#restrict-the-allowed-values-of-app-env" title="Permalink to this headline">Restrict the Allowed Values of APP_ENV</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/vpabst">
                <img src="https://connect.symfony.com/profile/vpabst.picture" alt="Vincent Pabst">
            </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/vpabst">Vincent Pabst</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/63429">#63429</a>
                                                </span>
            </div>
</div>
<p>The <code translate="no" class="notranslate">APP_ENV</code> environment variable selects which configuration your application
boots with. If it ever holds an unexpected value (a typo like <code translate="no" class="notranslate">prodution</code> or a
leftover CI/CD value), Symfony happily boots into that unknown environment, which
can silently disable protections you rely on in production.</p>
<p>Symfony 8.1 lets you declare the list of valid environments via the new <code translate="no" class="notranslate">getAllowedEnvs()</code>
method of <code translate="no" class="notranslate">MicroKernelTrait</code>. If the current environment is not in the returned
list, the kernel throws an exception immediately instead of booting:</p>
<div translate="no" data-loc="18" class="notranslate codeblock codeblock-length-md codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment">// src/Kernel.php</span>
<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Bundle</span>\<span class="hljs-title">FrameworkBundle</span>\<span class="hljs-title">Kernel</span>\<span class="hljs-title">MicroKernelTrait</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpKernel</span>\<span class="hljs-title">Kernel</span> <span class="hljs-title">as</span> <span class="hljs-title">BaseKernel</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Kernel</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BaseKernel</span>
</span>{
    <span class="hljs-keyword">use</span> <span class="hljs-title">MicroKernelTrait</span>;

    <span class="hljs-comment">/**
     * <span class="hljs-doctag">@return</span> list&lt;string&gt; An array of allowed values for APP_ENV
     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getAllowedEnvs</span><span class="hljs-params">()</span>: <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [<span class="hljs-string">'prod'</span>, <span class="hljs-string">'dev'</span>, <span class="hljs-string">'test'</span>];
    }
}</code></pre>
    </div>
</div>
<p>Booting with <code translate="no" class="notranslate">APP_ENV=staging</code> now fails fast with a clear error instead of
starting in an unintended state. When the method returns an empty array,
the previous behavior is unchanged and all environments are allowed.</p>
</div>
<div class="section">
<h2 id="more-clear-site-data-directives-on-logout"><a class="headerlink" href="#more-clear-site-data-directives-on-logout" title="Permalink to this headline">More Clear-Site-Data Directives on Logout</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/xabbuh">
                <img src="https://connect.symfony.com/profile/xabbuh.picture" alt="Christian Flothmann">
            </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/xabbuh">Christian Flothmann</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/62322">#62322</a>
                                                </span>
            </div>
</div>
<p>When a user logs out, Symfony can send the <code translate="no" class="notranslate">Clear-Site-Data</code> HTTP header to
tell the browser to wipe locally stored data. Until now, you could only clear
<code translate="no" class="notranslate">cache</code>, <code translate="no" class="notranslate">cookies</code>, <code translate="no" class="notranslate">storage</code> and <code translate="no" class="notranslate">executionContexts</code>.</p>
<p>Symfony 8.1 adds the three remaining directives defined by the specification:
<code translate="no" class="notranslate">clientHints</code>, <code translate="no" class="notranslate">prefetchCache</code> and <code translate="no" class="notranslate">prerenderCache</code>:</p>
<div translate="no" data-loc="11" class="notranslate codeblock codeblock-length-md 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-attr">firewalls:</span>
        <span class="hljs-attr">main:</span>
            <span class="hljs-attr">logout:</span>
                <span class="hljs-attr">clear_site_data:</span>
                    <span class="hljs-bullet">-</span> <span class="hljs-string">'cookies'</span>
                    <span class="hljs-bullet">-</span> <span class="hljs-string">'storage'</span>
                    <span class="hljs-bullet">-</span> <span class="hljs-string">'clientHints'</span>
                    <span class="hljs-bullet">-</span> <span class="hljs-string">'prefetchCache'</span>
                    <span class="hljs-bullet">-</span> <span class="hljs-string">'prerenderCache'</span></code></pre>
    </div>
</div>
</div>
<div class="section">
<h2 id="reproducible-builds-with-source-date-epoch"><a class="headerlink" href="#reproducible-builds-with-source-date-epoch" title="Permalink to this headline">Reproducible Builds with SOURCE_DATE_EPOCH</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/charlycoste">
                <img src="https://connect.symfony.com/profile/charlycoste.picture" alt="Charles-Edouard Coste">
            </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/charlycoste">Charles-Edouard Coste</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/62538">#62538</a>
                                                </span>
            </div>
</div>
<p>When Symfony compiles the service container, it records the build time so caches
can be invalidated. Because that value defaults to the current time, two builds
from the exact same source code produce slightly different containers, which breaks
<a href="https://reproducible-builds.org/" class="reference external" rel="external noopener noreferrer" target="_blank">reproducible builds</a>.</p>
<p>Symfony 8.1 supports the conventional <code translate="no" class="notranslate">SOURCE_DATE_EPOCH</code> environment variable
shared across the reproducible-builds ecosystem. When it's defined, Symfony uses
its Unix timestamp as the container build time:</p>
<div translate="no" data-loc="3" class="notranslate codeblock codeblock-length-sm codeblock-terminal codeblock-bash">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment"># use the date of the last commit as the build time</span>
<span class="hljs-prompt">$ </span><span class="hljs-built_in">export</span> SOURCE_DATE_EPOCH=$(git <span class="hljs-built_in">log</span> -1 --pretty=%ct)
<span class="hljs-prompt">$ </span>php bin/console cache:clear</code></pre>
    </div>
</div>
<p>If you set the <code translate="no" class="notranslate">kernel.container_build_time</code> parameter explicitly, it still
takes precedence; otherwise Symfony falls back to <code translate="no" class="notranslate">SOURCE_DATE_EPOCH</code> and then
to the current time.</p>
</div>
<div class="section">
<h2 id="describe-object-shapes-with-typeinfo"><a class="headerlink" href="#describe-object-shapes-with-typeinfo" title="Permalink to this headline">Describe Object Shapes with TypeInfo</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/bnf">
                <img src="https://github.com/bnf.png" alt="Benjamin Franzke">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/bnf">Benjamin Franzke</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/62885">#62885</a>
                                                </span>
            </div>
</div>
<p>The <a href="https://symfony.com/type-info" class="reference external">TypeInfo component</a> models PHP types as objects, allowing other components
(and your own code) to reason about them. Symfony 8.1 adds support for <em>object shapes</em>:
the exact structure of an object, described by its property names and types.</p>
<p>The <code translate="no" class="notranslate">StringTypeResolver</code> now understands the <code translate="no" class="notranslate">object{...}</code> PHPDoc syntax,
where a <code translate="no" class="notranslate">?</code> suffix marks an optional property:</p>
<div translate="no" data-loc="6" class="notranslate codeblock codeblock-length-sm 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">TypeInfo</span>\<span class="hljs-title">TypeResolver</span>\<span class="hljs-title">StringTypeResolver</span>;

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>resolver</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">StringTypeResolver</span>();

<span class="hljs-comment">// resolves to an ObjectShapeType</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>type</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>resolver</span>-&gt;<span class="hljs-title invoke__">resolve</span>(<span class="hljs-string">'object{name: string, age: int, email?: string}'</span>);</code></pre>
    </div>
</div>
<p>You can also build the same type programmatically with the new
<code translate="no" class="notranslate">Type::objectShape()</code> factory:</p>
<div translate="no" data-loc="7" class="notranslate codeblock codeblock-length-sm 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">TypeInfo</span>\<span class="hljs-title">Type</span>;

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>type</span> = Type::<span class="hljs-title invoke__">objectShape</span>([
    <span class="hljs-string">'name'</span> =&gt; Type::<span class="hljs-keyword">string</span>(),
    <span class="hljs-string">'age'</span> =&gt; Type::<span class="hljs-keyword">int</span>(),
    <span class="hljs-string">'email'</span> =&gt; [<span class="hljs-string">'type'</span> =&gt; Type::<span class="hljs-keyword">string</span>(), <span class="hljs-string">'optional'</span> =&gt; <span class="hljs-keyword">true</span>],
]);</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-1-misc-improvements-part-2?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Javier Eguiluz ]]></dc:creator>
            <pubDate>Mon, 15 Jun 2026 14:47:00 +0200</pubDate>
            <comments>https://symfony.com/blog/new-in-symfony-8-1-misc-improvements-part-2?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[New in Twig 4.0: A Stricter Sandbox]]></title>
            <link>https://symfony.com/blog/new-in-twig-4-0-a-stricter-sandbox?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>
    
                    
                
            
            
    
        Contributed by
                    Fabien Potencier
                                         in
                                    #4813
                    ,…</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>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/twigphp/twig/pull/4813">#4813</a>
                    ,                                     <a target="_blank" href="https://github.com/twigphp/twig/pull/4816">#4816</a>
                    ,                                     <a target="_blank" href="https://github.com/twigphp/twig/pull/4817">#4817</a>
                     and                                     <a target="_blank" href="https://github.com/twigphp/twig/pull/4819">#4819</a>
                                                </span>
            </div>
</div>
<p>The sandbox is the part of Twig you reach for when you let people you don't
trust write templates: a CMS where users edit their own pages, an email
builder, a notification system driven by customer-provided snippets. You hand
Twig a security policy listing the tags, filters, functions, methods, and
properties you are willing to expose, and everything else is rejected.</p>
<p>That promise only holds if the policy actually covers everything a template
can do. For a long time, it didn't. Twig 4.0 closes the gaps, and most of the
work is already available on the 3.x branch so you can adopt it today.</p>
<div class="section">
<h2 id="the-problem-a-policy-that-didn-t-mean-what-it-said"><a class="headerlink" href="#the-problem-a-policy-that-didn-t-mean-what-it-said" title="Permalink to this headline">The Problem: A Policy That Didn't Mean What It Said</a></h2>
<p>Take a policy that looks airtight. You list a handful of tags and filters, no
functions, no extra tests, and you feel safe:</p>
<div translate="no" data-loc="7" class="notranslate codeblock codeblock-length-sm codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>policy</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">SecurityPolicy</span>(
    <span class="hljs-attr">allowedTags</span>: [<span class="hljs-string">'if'</span>, <span class="hljs-string">'for'</span>],
    <span class="hljs-attr">allowedFilters</span>: [<span class="hljs-string">'escape'</span>, <span class="hljs-string">'upper'</span>],
    <span class="hljs-attr">allowedMethods</span>: [<span class="hljs-string">'Article'</span> =&gt; [<span class="hljs-string">'getTitle'</span>, <span class="hljs-string">'getBody'</span>]],
    <span class="hljs-attr">allowedProperties</span>: [],
    <span class="hljs-attr">allowedFunctions</span>: [],
);</code></pre>
    </div>
</div>
<p>Now a template author writes this:</p>
<div translate="no" data-loc="5" class="notranslate codeblock codeblock-length-sm codeblock-twig">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment">{# tests were never checked, so this ran unchecked #}</span><span class="xml">
</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">if</span></span> <span class="hljs-string">'SOME_SECRET'</span> is <span class="hljs-name">constant</span><span class="hljs-params">('SOME_SECRET')</span> %}</span><span class="xml">...</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">endif</span></span> %}</span><span class="xml">

</span><span class="hljs-comment">{# functions you never listed, allowed anyway #}</span><span class="xml">
</span><span class="hljs-template-variable">{{ <span class="hljs-name">attribute</span><span class="hljs-params">(article, 'getSecret')</span> }}</span></code></pre>
    </div>
</div>
<p>None of these are in your policy. All of them ran. The sandbox ignored tests
entirely, so any test, including <code translate="no" class="notranslate">constant</code> and any custom one, went through
unchecked; and a short list of tags and functions (<code translate="no" class="notranslate">extends</code>, <code translate="no" class="notranslate">use</code>,
<code translate="no" class="notranslate">parent</code>, <code translate="no" class="notranslate">block</code>, <code translate="no" class="notranslate">attribute</code>) was hardcoded as always allowed
regardless of what your policy said.</p>
<p>Nobody likes security tools that are lenient by default; a policy you have to
second-guess is worse than no policy, because it gives you false confidence.
Twig 4.0 makes the allow-list mean exactly what it says.</p>
</div>
<div class="section">
<h2 id="tests-are-now-part-of-the-sandbox"><a class="headerlink" href="#tests-are-now-part-of-the-sandbox" title="Permalink to this headline">Tests Are Now Part of the Sandbox</a></h2>
<p>Tests (the <code translate="no" class="notranslate">is</code> operator: <code translate="no" class="notranslate">is even</code>, <code translate="no" class="notranslate">is defined</code>, <code translate="no" class="notranslate">is constant(...)</code>)
are now a first-class allow-list, alongside tags, filters, and functions. The
<code translate="no" class="notranslate">SecurityPolicy</code> constructor gained a sixth argument for them:</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-variable"><span class="hljs-variable-other-marker">$</span>policy</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">SecurityPolicy</span>(
    <span class="hljs-attr">allowedTags</span>: [<span class="hljs-string">'if'</span>, <span class="hljs-string">'for'</span>],
    <span class="hljs-attr">allowedFilters</span>: [<span class="hljs-string">'escape'</span>, <span class="hljs-string">'upper'</span>],
    <span class="hljs-attr">allowedMethods</span>: [<span class="hljs-string">'Article'</span> =&gt; [<span class="hljs-string">'getTitle'</span>, <span class="hljs-string">'getBody'</span>]],
    <span class="hljs-attr">allowedProperties</span>: [],
    <span class="hljs-attr">allowedFunctions</span>: [],
    <span class="hljs-attr">allowedTests</span>: [<span class="hljs-string">'prime'</span>],
);</code></pre>
    </div>
</div>
<p>A custom test that is not listed is now rejected like anything else:</p>
<div translate="no" data-loc="3" class="notranslate codeblock codeblock-length-sm codeblock-twig">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">if</span></span> number is prime %}</span><span class="xml">...</span><span class="hljs-template-tag">{% <span class="hljs-name"><span class="hljs-keyword">endif</span></span> %}</span><span class="xml">
</span><span class="hljs-comment">{# Twig\Sandbox\SecurityNotAllowedTestError:
   Test "prime" is not allowed in "page" at line 1. #}</span></code></pre>
    </div>
</div>
<p>You don't need to enumerate the safe built-ins, though. The harmless ones
(<code translate="no" class="notranslate">defined</code>, <code translate="no" class="notranslate">empty</code>, <code translate="no" class="notranslate">even</code>, <code translate="no" class="notranslate">odd</code>, <code translate="no" class="notranslate">iterable</code>, <code translate="no" class="notranslate">same as</code>,
<code translate="no" class="notranslate">null</code>, <code translate="no" class="notranslate">divisible by</code> and the rest) are flagged as always allowed, so
templates relying on them keep working without a single line of configuration.
The one exception is <code translate="no" class="notranslate">constant</code>, which reaches into the PHP runtime: it must
be allow-listed explicitly, like a custom test.</p>
</div>
<div class="section">
<h2 id="no-more-silent-exceptions"><a class="headerlink" href="#no-more-silent-exceptions" title="Permalink to this headline">No More Silent Exceptions</a></h2>
<p>The tags and functions that used to be allowed behind your back, <code translate="no" class="notranslate">extends</code>,
<code translate="no" class="notranslate">use</code>, <code translate="no" class="notranslate">parent</code>, <code translate="no" class="notranslate">block</code>, <code translate="no" class="notranslate">attribute</code>, and the <code translate="no" class="notranslate">constant</code> test, are
no longer special. In 4.0 they obey the policy like everything else: list them
if your templates need them, and they are rejected if you don't.</p>
<p>This is the kind of change that needs a preview before a major release, so you
can opt into the 4.0 behavior today on 3.x with a single call:</p>
<div translate="no" data-loc="1" class="notranslate codeblock codeblock-length-sm codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>policy</span>-&gt;<span class="hljs-title invoke__">setStrict</span>(<span class="hljs-keyword">true</span>);</code></pre>
    </div>
</div>
<p>In strict mode, calling one of these without allow-listing it fails right away:</p>
<div translate="no" data-loc="3" class="notranslate codeblock codeblock-length-sm codeblock-twig">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-template-variable">{{ <span class="hljs-name">attribute</span><span class="hljs-params">(article, 'getSecret')</span> }}</span><span class="xml">
</span><span class="hljs-comment">{# Twig\Sandbox\SecurityNotAllowedFunctionError:
   Function "attribute" is not allowed in "page" at line 1. #}</span></code></pre>
    </div>
</div>
<p>Turn strict mode on in your test suite, fix what it flags, and your policy is
ready for 4.0.</p>
</div>
<div class="section">
<h2 id="marking-inherently-safe-items-as-always-allowed"><a class="headerlink" href="#marking-inherently-safe-items-as-always-allowed" title="Permalink to this headline">Marking Inherently-Safe Items as Always Allowed</a></h2>
<p>Tightening the default raises a fair question: must every application now
re-list dozens of harmless filters like <code translate="no" class="notranslate">upper</code> or <code translate="no" class="notranslate">trim</code>? No.
Twig 4.0 introduces a way for the author of a callable to declare it safe for
any sandbox, so no policy has to opt into it. Set the
<code translate="no" class="notranslate">always_allowed_in_sandbox</code> option on a filter, function, or test:</p>
<div translate="no" data-loc="3" class="notranslate codeblock codeblock-length-sm codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>twig</span>-&gt;<span class="hljs-title invoke__">addFilter</span>(<span class="hljs-keyword">new</span> <span class="hljs-title invoke__">TwigFilter</span>(<span class="hljs-string">'upper'</span>, <span class="hljs-string">'strtoupper'</span>, [
    <span class="hljs-string">'always_allowed_in_sandbox'</span> =&gt; <span class="hljs-keyword">true</span>,
]));</code></pre>
    </div>
</div>
<p>For a tag, override <code translate="no" class="notranslate">isAlwaysAllowedInSandbox()</code> on its token parser:</p>
<div translate="no" data-loc="9" class="notranslate codeblock codeblock-length-sm codeblock-php">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyTagTokenParser</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AbstractTokenParser</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isAlwaysAllowedInSandbox</span><span class="hljs-params">()</span>: <span class="hljs-title">bool</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
    }

    <span class="hljs-comment">// ...</span>
}</code></pre>
    </div>
</div>
<p>A marked item skips the sandbox check entirely, so it costs nothing at runtime
and never needs to appear in an allow-list. This is a sharp tool, so the
documentation spells out the criteria an item must meet to deserve the flag:
no new capability, no PHP runtime access, no callable arguments, no template
resolution, no output-safety bypass, deterministic output, and a few more. If
in doubt, leave the flag off and let the policy decide.</p>
</div>
<div class="section">
<h2 id="built-ins-allowed-out-of-the-box"><a class="headerlink" href="#built-ins-allowed-out-of-the-box" title="Permalink to this headline">Built-ins Allowed Out of the Box</a></h2>
<p>Twig applies that same flag to its own toolbox. A curated set of built-ins
that meet the criteria, the pure value transformations and control-flow tags
you use in every template, are always allowed in 4.0:</p>
<ul>
    <li>Tags: <code translate="no" class="notranslate">apply</code>, <code translate="no" class="notranslate">block</code>, <code translate="no" class="notranslate">do</code>, <code translate="no" class="notranslate">for</code>, <code translate="no" class="notranslate">guard</code>, <code translate="no" class="notranslate">if</code>, <code translate="no" class="notranslate">macro</code>,
<code translate="no" class="notranslate">set</code>, <code translate="no" class="notranslate">types</code>, <code translate="no" class="notranslate">with</code>.</li>
<li>Filters: <code translate="no" class="notranslate">abs</code>, <code translate="no" class="notranslate">batch</code>, <code translate="no" class="notranslate">capitalize</code>, <code translate="no" class="notranslate">convert_encoding</code>,
<code translate="no" class="notranslate">default</code>, <code translate="no" class="notranslate">e</code>, <code translate="no" class="notranslate">escape</code>, <code translate="no" class="notranslate">first</code>, <code translate="no" class="notranslate">format</code>, <code translate="no" class="notranslate">join</code>, <code translate="no" class="notranslate">keys</code>,
<code translate="no" class="notranslate">last</code>, <code translate="no" class="notranslate">length</code>, <code translate="no" class="notranslate">lower</code>, <code translate="no" class="notranslate">merge</code>, <code translate="no" class="notranslate">nl2br</code>, <code translate="no" class="notranslate">number_format</code>,
<code translate="no" class="notranslate">replace</code>, <code translate="no" class="notranslate">reverse</code>, <code translate="no" class="notranslate">round</code>, <code translate="no" class="notranslate">slice</code>, <code translate="no" class="notranslate">split</code>, <code translate="no" class="notranslate">striptags</code>,
<code translate="no" class="notranslate">title</code>, <code translate="no" class="notranslate">trim</code>, <code translate="no" class="notranslate">upper</code>, <code translate="no" class="notranslate">url_encode</code>.</li>
<li>Functions: <code translate="no" class="notranslate">cycle</code>, <code translate="no" class="notranslate">max</code>, <code translate="no" class="notranslate">min</code>.</li>
</ul>
<p>Note what is <em>not</em> on the list: <code translate="no" class="notranslate">map</code>, <code translate="no" class="notranslate">filter</code>, <code translate="no" class="notranslate">sort</code> and friends (they
take a callable you may want to forbid), <code translate="no" class="notranslate">include</code>, <code translate="no" class="notranslate">source</code> and the other
template-resolving helpers, <code translate="no" class="notranslate">random</code> and <code translate="no" class="notranslate">shuffle</code> (non-deterministic),
<code translate="no" class="notranslate">json_encode</code> and <code translate="no" class="notranslate">dump</code> (object introspection), <code translate="no" class="notranslate">constant</code> (PHP runtime
access). Those stay under your policy, where they belong.</p>
<p>When you upgrade, you can delete the safe built-ins from your allow-lists;
listing a name that is always allowed has no effect, so there is no rush.</p>
</div>
<div class="section">
<h2 id="the-upgrade-path"><a class="headerlink" href="#the-upgrade-path" title="Permalink to this headline">The Upgrade Path</a></h2>
<p>As usual, Twig 3.x triggers a deprecation for everything that will break in
4.0. All the fixes work on 3.x, so an application that runs deprecation-free,
or that already enables strict mode on its security policy, is ready for the
new sandbox.</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-a-stricter-sandbox?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Fabien Potencier ]]></dc:creator>
            <pubDate>Mon, 15 Jun 2026 09:12:00 +0200</pubDate>
            <comments>https://symfony.com/blog/new-in-twig-4-0-a-stricter-sandbox?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[A Week of Symfony #1015 (June 8–14, 2026)]]></title>
            <link>https://symfony.com/blog/a-week-of-symfony-1015-june-8-14-2026?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>This week, we celebrated the SymfonyOnline June 2026 conference. In addition, we published a new case study showcasing how Symfony helps power the rental real estate market. Lastly, we continued publishing articles about new features in Symfony and Twig.…</description>
            <content:encoded><![CDATA[
                                <p>This week, we celebrated the <a href="https://live.symfony.com/2026-online-june/">SymfonyOnline June 2026</a> conference. In addition, we published a <a href="https://symfony.com/blog/case-study-treehouse-servicing-the-rental-real-estate-market-with-symfony">new case study</a> showcasing how Symfony helps power the rental real estate market. Lastly, we continued publishing articles about <a href="https://symfony.com/blog/category/living-on-the-edge">new features in Symfony and Twig</a>.</p>

<h2>Symfony development highlights</h2>

<p>This week, 52 pull requests were merged (38 in code and 14 in docs) and 34 issues were closed (31 in code and 3 in docs). Excluding merges, 28 authors made additions and deletions. See details for <a href="https://github.com/symfony/symfony/pulse">code</a> and <a href="https://github.com/symfony/symfony-docs/pulse">docs</a>.</p>

<p><a href="https://github.com/symfony/symfony/commits/6.4">6.4 changelog</a>:</p>

<ul>
<li><a href="https://github.com/symfony/symfony/commit/a965c6e8af27ba77126c86180a133589fd7c6192">a965c6e</a>: &#91;Console&#93; render formatter tags in ChoiceQuestion default value</li>
<li><a href="https://github.com/symfony/symfony/commit/b52be91fa9b41c331373bda98c1eeaa7985a2d38">b52be91</a>: &#91;HttpKernel&#93; restore null-on-invalid for nullable #[Autowire(service:)] controller args</li>
<li><a href="https://github.com/symfony/symfony/commit/0251c5cf705b3fa943ebbb11c1a650bd658d1262">0251c5c</a>: &#91;TwigBridge&#93; reject __toString trampolines in TemplatedEmail::__unserialize()</li>
<li><a href="https://github.com/symfony/symfony/commit/6d3ba7891c7aa941734e264b61c06e7e7f52e4e3">6d3ba78</a>: &#91;Translation&#93; create Crowdin files before uploading translations</li>
<li><a href="https://github.com/symfony/symfony/commit/9bd9ea893d4a98573c0f816466ac8f6371370fc0">9bd9ea8</a>: &#91;VarExporter&#93; fix exporting objects that cannot be instantiated empty</li>
</ul>

<p><a href="https://github.com/symfony/symfony/commits/7.4">7.4 changelog</a>:</p>

<ul>
<li><a href="https://github.com/symfony/symfony/commit/24df7ce6152020a379047f28da8d2e1658454c37">24df7ce</a>: &#91;ObjectMapper&#93; fix fatal errors on unreadable source properties</li>
<li><a href="https://github.com/symfony/symfony/commit/98d93ec82e5df6cc63f6a8f184fe7f569d251eb2">98d93ec</a>: &#91;Mailer, Bridge, MicrosoftGraphApi&#93; set recipients from $envelope instead of the $email headers</li>
<li><a href="https://github.com/symfony/symfony/commit/11e983b4007b7c87aaec9a73542935689507969b">11e983b</a>: &#91;ObjectMapper&#93; make existing-object mapping behavior consistent</li>
<li><a href="https://github.com/symfony/symfony/commit/5a413a2380689622697fcd0b07796c2ef30e255e">5a413a2</a>:  migrate table definitions to DBAL's TableEditor API</li>
</ul>

<p><a href="https://github.com/symfony/symfony/commits/8.1">8.1 changelog</a>:</p>

<ul>
<li><a href="https://github.com/symfony/symfony/commit/ede23b005eef7233248eac45d0f7ce9384907b8d">ede23b0</a>: &#91;Cache, VarExporter&#93; add argument $allowNamedClosure to DeepClone to fit ext-deepclone v0.8</li>
<li><a href="https://github.com/symfony/symfony/commit/f17c28054f7835e2751f7bf02401bd2fa6ccd002">f17c280</a>: &#91;DependencyInjection&#93; leep behavior-describing tags 'proxy' and 'container.service_subscriber.locator' on decorated services</li>
<li><a href="https://github.com/symfony/symfony/commit/39ed8ba0c549d3b81a7335055d1c3b691e03e896">39ed8ba</a>: &#91;HttpKernel&#93; allow leading zeros in int request attributes</li>
<li><a href="https://github.com/symfony/symfony/commit/7e4b94670ab63783b839a0d02795d31c3a14365b">7e4b946</a>: &#91;Tui&#93; fix vertical align</li>
<li><a href="https://github.com/symfony/symfony/commit/45ecb8ba80416b806bb7f8d4bddaea0166fcab3c">45ecb8b</a>: &#91;Security&#93; make RoleHierarchy::getReachableRoleNames() return a list again</li>
<li><a href="https://github.com/symfony/symfony/commit/6d47e2289a2a6a8c4169d976462a2e5033226cf0">6d47e22</a>: &#91;FrameworkBundle&#93; avoid resolving all env vars when building the router request context</li>
<li><a href="https://github.com/symfony/symfony/commit/0c19c0f4ac6f96a9ce5b08ba45d0868eeace1eb6">0c19c0f</a>: &#91;FrameworkBundle&#93; fix custom config directory being ignored when registering bundles</li>
<li><a href="https://github.com/symfony/symfony/commit/8c968cddb82e6c40d5640e987fca98502e8550f6">8c968cd</a>: &#91;DependencyInjection&#93; fix decorating an event listener no longer replacing it</li>
<li><a href="https://github.com/symfony/symfony/commit/e0548f330c08c7b7716a4906908a3560961abd6c">e0548f3</a>: &#91;ObjectMapper&#93; fix reverse class map throwing on unreadable source</li>
</ul>

<h2>Newest issues and pull requests</h2>

<ul>
<li><a href="https://github.com/symfony/symfony/pull/64559">[Tui] Add mouse event support</a></li>
<li><a href="https://github.com/symfony/symfony/issues/64562">[Form] BcMath number support</a></li>
<li><a href="https://github.com/symfony/symfony/pull/64592">[FrameworkBundle] Add --dispatchers option to debug:event-dispatcher command</a></li>
<li><a href="https://github.com/symfony/symfony/pull/64604">[Tui] Deduplicate the terminal title escape sequence into a trait</a></li>
</ul>

<h2>Symfony Jobs</h2>

<p>These are some of the most recent Symfony job offers:</p>

<ul>
<li><strong>Backend Symfony Developer</strong> at SensioLabs Deutschland<br>
Full-time - €60,000 – €75,000 / year<br>
Full remote<br>
<a href="https://symfony.com/jobs/1b49e27">View details</a></li>
<li><strong>Lead Symfony Developer</strong> at DocuPet<br>
Full-time - CA$140,000 – CA$180,000 / year<br>
Full remote<br>
<a href="https://symfony.com/jobs/b6a97b9">View details</a></li>
<li><strong>Backend Symfony Developer</strong> at KRUU GmbH<br>
Full-time - €60,000 – €75,000 / month<br>
Remote + part-time onsite (Bad Friedrichshall, Germany)<br>
<a href="https://symfony.com/jobs/b149b01">View details</a></li>
<li><strong>DevOps for a Symfony project</strong> at Cloudpepper<br>
Full-time - $150,000 – $180,000 / year<br>
Full remote<br>
<a href="https://symfony.com/jobs/a9262d7">View details</a></li>
<li><strong>Symfony Developer</strong> at Design Force Marketing<br>
Full-time - $60,000 – $100,000 / year<br>
Grand Haven Michigan, United States<br>
<a href="https://symfony.com/jobs/5ad3b96">View details</a></li>
</ul>

<p>You can <a href="https://symfony.com/jobs">publish a Symfony job offer for free</a> on symfony.com.</p>

<h2>SymfonyCasts Updates</h2>

<p><a href="https://symfonycasts.com/">SymfonyCasts</a> is the official way to learn Symfony.
Select a track for a guided path through 100+ video tutorial courses about
Symfony, PHP and JavaScript.</p>

<p>This week, SymfonyCasts published the following updates:</p>

<ul>
<li>(Video) <a href="https://symfonycasts.com/screencast/symfony8-security/user">Symfony Security: The Basics: Creating the User Class</a></li>
<li>(Video) <a href="https://symfonycasts.com/screencast/symfony8-security/installation">Symfony Security: The Basics: Installing the Security Bundle</a></li>
</ul>

<h2>They talked about us</h2>

<ul>
<li><a href="https://nicolas-jourdan.medium.com/deep-dive-into-symfony-8-1s-console-image-input-2f2ab1be172e">Deep dive into Symfony 8.1's Console image input</a></li>
<li><a href="https://dev.to/outcomer/i-stopped-following-api-validation-best-practices-heres-why-1fmj">I Stopped Following API Validation Best Practices. Here's Why.</a></li>
<li><a href="https://medium.com/@azyouness/using-symfony-forms-as-controller-arguments-with-maprequesttoform-d15f4007925a">Using Symfony Forms as Controller Arguments with #[MapRequestToForm]</a></li>
<li><a href="https://antonio-turdo.medium.com/generating-json-schema-from-php-dtos-with-symfony-serializer-awareness-7e9c093bf64f">Generating JSON Schema from PHP DTOs with Symfony Serializer awareness</a></li>
<li><a href="https://medium.com/@sharonlelo6/how-i-built-a-user-activity-log-in-symfony-that-non-technical-users-could-actually-read-f6114bf6ee6a">How I Built a User Activity Log in Symfony That Non-Technical Users Could Actually Read</a></li>
<li><a href="https://dev.to/ashrafchitambaa/deploying-symfony-8-to-cpanel-step-by-step-guide-4k5o">Deploying Symfony 8 to cPanel Step by Step guide.</a></li>
<li><a href="https://dev.to/pentiminax/ux-datatables-in-2026-typed-columns-server-side-processing-api-platform-mercure-and-inline-3o7p">UX DataTables in 2026: typed columns, server-side processing, API Platform, Mercure and inline editing</a></li>
<li><a href="https://jolicode.com/blog/comment-utiliser-les-attributs-php-sur-un-controleur-symfony">Comment utiliser les attributs PHP sur un contrôleur Symfony ? </a></li>
</ul>

<h2>Upcoming Symfony Events</h2>

<ul>
<li><a href="https://www.meetup.com/symfony-php-meetup-barcelona-by-sensiolabs/events/313664247/">Symfony/PHP Meetup Barcelona by SensioLabs</a>: Barcelona, Spain (June 25, 2026)</li>
<li><a href="https://websummercamp.com/2026">Web Summer Camp 2026</a>: Opatija, Croatia (July 2, 2026 – July 4, 2026)</li>
</ul>

<h2>Call to Action</h2>

<ul>
<li>Follow Symfony <a href="https://x.com/symfony">on X</a>, <a href="https://mastodon.social/@symfony">on Mastodon</a>, <a href="https://bsky.app/profile/symfony.com">on Bluesky</a> and <a href="https://www.threads.net/@symfony">on Threads</a> and share this article.</li>
<li><a href="https://feeds.feedburner.com/symfony/blog">Subscribe to the Symfony blog RSS</a> and never miss a Symfony story again.</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/a-week-of-symfony-1015-june-8-14-2026?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Javier Eguiluz ]]></dc:creator>
            <pubDate>Sun, 14 Jun 2026 09:48:00 +0200</pubDate>
            <comments>https://symfony.com/blog/a-week-of-symfony-1015-june-8-14-2026?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
                        <item>
            <title><![CDATA[New in Symfony 8.1: Misc Improvements (Part 1)]]></title>
            <link>https://symfony.com/blog/new-in-symfony-8-1-misc-improvements-part-1?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
            <description>In addition to the main features announced in previous posts of this series,
Symfony 8.1 includes many smaller improvements that make day-to-day work
easier. This post highlights the first batch.

Convert Between UUIDv7 and UUIDv4…</description>
            <content:encoded><![CDATA[
                                <p>In addition to the main features announced in previous posts of this series,
Symfony 8.1 includes many smaller improvements that make day-to-day work
easier. This post highlights the first batch.</p>
<div class="section">
<h2 id="convert-between-uuidv7-and-uuidv4"><a class="headerlink" href="#convert-between-uuidv7-and-uuidv4" title="Permalink to this headline">Convert Between UUIDv7 and UUIDv4</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/63593">#63593</a>
                                                </span>
            </div>
</div>
<p>UUIDv7 identifiers are time-ordered, making them ideal as database primary keys.
However, that property also leaks record creation times when you expose those
identifiers in APIs. The new <code translate="no" class="notranslate">Uuid47Transformer</code> class lets you store UUIDv7
internally while emitting UUIDv4-looking identifiers at your application boundaries:</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">Uid</span>\<span class="hljs-title">Uuid47Transformer</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Uid</span>\<span class="hljs-title">UuidV7</span>;

<span class="hljs-comment">// the secret must be at least 16 bytes; longer secrets are hashed automatically</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>transformer</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">Uuid47Transformer</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>secret</span>);

<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>uuid</span> = <span class="hljs-keyword">new</span> <span class="hljs-title invoke__">UuidV7</span>();
<span class="hljs-comment">// returns a UuidV4 instance that hides the timestamp information</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>external</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>transformer</span>-&gt;<span class="hljs-title invoke__">encode</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>uuid</span>);

<span class="hljs-comment">// returns the original UuidV7 instance (when using the same secret)</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>original</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>transformer</span>-&gt;<span class="hljs-title invoke__">decode</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>external</span>);</code></pre>
    </div>
</div>
<p>The conversion masks the UUIDv7 timestamp with a keyed <a href="https://en.wikipedia.org/wiki/SipHash" class="reference external" rel="external noopener noreferrer" target="_blank">SipHash-2-4</a> digest,
making it reversible only with the same secret. When using FrameworkBundle, the
transformer is registered as a service automatically (using <code translate="no" class="notranslate">kernel.secret</code> as
the key), so you can inject it anywhere by type-hinting <code translate="no" class="notranslate">Uuid47Transformer</code>.</p>
</div>
<div class="section">
<h2 id="convert-scalar-types-during-denormalization"><a class="headerlink" href="#convert-scalar-types-during-denormalization" title="Permalink to this headline">Convert Scalar Types During Denormalization</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/jeroens">
                <img src="https://connect.symfony.com/profile/jeroens.picture" alt="Jeroen Spee">
            </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/jeroens">Jeroen Spee</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/52173">#52173</a>
                                                </span>
            </div>
</div>
<p>Some data formats represent all values as strings (e.g. HTTP query strings or
form data). When deserializing <code translate="no" class="notranslate">XML</code> and <code translate="no" class="notranslate">CSV</code> contents, Symfony already
casts those strings to the <code translate="no" class="notranslate">int</code>, <code translate="no" class="notranslate">float</code> or <code translate="no" class="notranslate">bool</code> types expected by the
target properties. In Symfony 8.1, you can enable this behavior for any format
via the new <code translate="no" class="notranslate">ENABLE_TYPE_CONVERSION</code> context option:</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">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Serializer</span>\<span class="hljs-title">Normalizer</span>\<span class="hljs-title">AbstractObjectNormalizer</span>;
<span class="hljs-comment">// ...</span>

<span class="hljs-comment">// all values are strings, as in an HTTP query string</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>data</span> = [<span class="hljs-string">'age'</span> =&gt; <span class="hljs-string">'39'</span>, <span class="hljs-string">'sportsperson'</span> =&gt; <span class="hljs-string">'1'</span>];

<span class="hljs-comment">// 'age' is cast to int and 'sportsperson' to bool to match the Person property types</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>person</span> = <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>serializer</span>-&gt;<span class="hljs-title invoke__">denormalize</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>data</span>, Person::<span class="hljs-variable language_">class</span>, <span class="hljs-attr">context</span>: [
    AbstractObjectNormalizer::<span class="hljs-variable constant_">ENABLE_TYPE_CONVERSION</span> =&gt; <span class="hljs-keyword">true</span>,
]);</code></pre>
    </div>
</div>
<p>Set the option to <code translate="no" class="notranslate">false</code> to disable the conversion, even for the <code translate="no" class="notranslate">xml</code> and
<code translate="no" class="notranslate">csv</code> formats. The option is also available in the serializer context builders.</p>
</div>
<div class="section">
<h2 id="configurable-default-action-in-html-sanitizer"><a class="headerlink" href="#configurable-default-action-in-html-sanitizer" title="Permalink to this headline">Configurable Default Action in HTML Sanitizer</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/neirda24">
                <img src="https://connect.symfony.com/profile/neirda24.picture" alt="Adrien Roches">
            </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/neirda24">Adrien Roches</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/57653">#57653</a>
                                                </span>
            </div>
</div>
<p>When the <a href="https://symfony.com/html-sanitizer" class="reference external">HTML sanitizer</a> finds a tag that is not part of the configuration, it
drops the tag and all its children. In Symfony 8.1, you can change this
behavior with the new <code translate="no" class="notranslate">default_action</code> option, which accepts <code translate="no" class="notranslate">drop</code> (the
current default), <code translate="no" class="notranslate">block</code> (remove the tag but keep its children) and <code translate="no" class="notranslate">allow</code>:</p>
<div translate="no" data-loc="11" class="notranslate codeblock codeblock-length-md codeblock-yaml">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment"># config/packages/html_sanitizer.yaml</span>
<span class="hljs-attr">framework:</span>
    <span class="hljs-attr">html_sanitizer:</span>
        <span class="hljs-attr">sanitizers:</span>
            <span class="hljs-attr">app.post_sanitizer:</span>
                <span class="hljs-comment"># ...</span>

                <span class="hljs-comment"># remove unconfigured tags, but keep processing their children</span>
                <span class="hljs-attr">default_action:</span> <span class="hljs-string">'block'</span>
                <span class="hljs-comment"># remove &lt;figure&gt; tags and their children entirely</span>
                <span class="hljs-attr">drop_elements:</span> <span class="hljs-string">['figure']</span></code></pre>
    </div>
</div>
</div>
<div class="section">
<h2 id="reset-the-kernel-between-frankenphp-requests"><a class="headerlink" href="#reset-the-kernel-between-frankenphp-requests" title="Permalink to this headline">Reset the Kernel Between FrankenPHP Requests</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/64055">#64055</a>
                                                </span>
            </div>
</div>
<p>In <a href="https://frankenphp.dev/docs/worker/" class="reference external" rel="external noopener noreferrer" target="_blank">FrankenPHP worker mode</a>, the same kernel instance handles every request for
the lifetime of the worker process. This is great for performance, but any state
kept by services that don't implement <code translate="no" class="notranslate">ResetInterface</code> may leak across requests.</p>
<p>Symfony 8.1 adds an opt-in feature: defining the <code translate="no" class="notranslate">FRANKENPHP_RESET_KERNEL</code>
environment variable makes the runtime clone the kernel after each request, so
the next one starts with a fresh kernel and container:</p>
<div translate="no" data-loc="2" class="notranslate codeblock codeblock-length-sm codeblock-bash">
        <div class="codeblock-scroll">
        
        <pre class="codeblock-code"><code><span class="hljs-comment"># define this env var where you configure your FrankenPHP workers</span>
FRANKENPHP_RESET_KERNEL=1</code></pre>
    </div>
</div>
<p>The default behavior doesn't change: the kernel is still reused across
requests unless you set this variable. Resetting the kernel has a measurable
throughput cost on "hello world" benchmarks, but it's still several times faster
than the classic non-worker mode and it restores full per-request isolation.</p>
</div>
<div class="section">
<h2 id="null-safe-array-access-in-expressions"><a class="headerlink" href="#null-safe-array-access-in-expressions" title="Permalink to this headline">Null-Safe Array Access in Expressions</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/cancan101">
                <img src="https://github.com/cancan101.png" alt="Alex Rothberg">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/cancan101">Alex Rothberg</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/62754">#62754</a>
                                                </span>
            </div>
</div>
<p>The ExpressionLanguage component supports the null-safe operator for property
access (<code translate="no" class="notranslate">foo?.bar</code>) and method calls (<code translate="no" class="notranslate">foo?.getBar()</code>). Symfony 8.1
completes the feature with null-safe array access using the same
<code translate="no" class="notranslate">?.[...]</code> syntax as JavaScript optional chaining:</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-comment">// before: this throws an exception when getItems() returns null</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>expressionLanguage</span>-&gt;<span class="hljs-title invoke__">evaluate</span>(<span class="hljs-string">'fruit.getItems()[0]'</span>, [<span class="hljs-string">'fruit'</span> =&gt; <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>fruit</span>]);

<span class="hljs-comment">// now: this returns null instead of throwing an exception</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>expressionLanguage</span>-&gt;<span class="hljs-title invoke__">evaluate</span>(<span class="hljs-string">'fruit.getItems()?.[0]'</span>, [<span class="hljs-string">'fruit'</span> =&gt; <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>fruit</span>]);

<span class="hljs-comment">// you can combine it with the other null-safe operators</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>expressionLanguage</span>-&gt;<span class="hljs-title invoke__">evaluate</span>(<span class="hljs-string">'order?.getItems()?.[0]?.getName()'</span>, [<span class="hljs-string">'order'</span> =&gt; <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>order</span>]);</code></pre>
    </div>
</div>
</div>
<div class="section">
<h2 id="outline-style-console-blocks"><a class="headerlink" href="#outline-style-console-blocks" title="Permalink to this headline">Outline-Style Console Blocks</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/guillaume_vdp">
                <img src="https://connect.symfony.com/profile/guillaume_vdp.picture" alt="Guillaume Van Der Putten">
            </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/guillaume_vdp">Guillaume Van Der Putten</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/63546">#63546</a>
                                                </span>
            </div>
</div>
<p>The <code translate="no" class="notranslate">success()</code>, <code translate="no" class="notranslate">error()</code>, <code translate="no" class="notranslate">warning()</code> and similar <code translate="no" class="notranslate">SymfonyStyle</code>
methods fill the entire line with a background color, which can be hard to read
on terminals with custom color schemes or high-contrast accessibility settings.
Symfony 8.1 adds outline-style alternatives that display a colored border
around the message while keeping the default text color:</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-comment">// outlined alternatives exist for all the result methods:</span>
<span class="hljs-comment">// outlineSuccess(), outlineError(), outlineWarning(), outlineNote(),</span>
<span class="hljs-comment">// outlineInfo() and outlineCaution()</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>io</span>-&gt;<span class="hljs-title invoke__">outlineSuccess</span>(<span class="hljs-string">'Operation completed successfully.'</span>);
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>io</span>-&gt;<span class="hljs-title invoke__">outlineError</span>(<span class="hljs-string">'Something went wrong.'</span>);

<span class="hljs-comment">// use outlineBlock() to customize the title and the colors</span>
<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>io</span>-&gt;<span class="hljs-title invoke__">outlineBlock</span>(<span class="hljs-string">'Deployment finished in 3.2s'</span>, <span class="hljs-string">'Deploy'</span>, <span class="hljs-string">'fg=cyan'</span>);</code></pre>
    </div>
</div>
</div>
<div class="section">
<h2 id="disable-trailing-slash-on-prefixed-root-routes"><a class="headerlink" href="#disable-trailing-slash-on-prefixed-root-routes" title="Permalink to this headline">Disable Trailing Slash on Prefixed Root Routes</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://github.com/vvaswani">
                <img src="https://github.com/vvaswani.png" alt="vvaswani">
            </a>
            </div>
    <div class="blog-post-contributor-contents">
        <span>Contributed by</span>
                    <a target="_blank" class="blog-post-contributor-name" href="https://github.com/vvaswani">vvaswani</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/63689">#63689</a>
                                                </span>
            </div>
</div>
<p>When you apply a prefix to a collection of routes defined with the PHP DSL, the
root route of the collection always gets a trailing slash (e.g. <code translate="no" class="notranslate">/categories/</code>).
In Symfony 8.1, the <code translate="no" class="notranslate">prefix()</code> method accepts a new <code translate="no" class="notranslate">trailingSlashOnRoot</code>
argument (already available in YAML/XML imports) to disable this:</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-comment">// config/routes.php</span>
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Routing</span>\<span class="hljs-title">Loader</span>\<span class="hljs-title">Configurator</span>\<span class="hljs-title">RoutingConfigurator</span>;

<span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(RoutingConfigurator <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>routes</span>)</span>: <span class="hljs-title">void</span> </span>{
    <span class="hljs-comment">// this generates /categories (instead of /categories/) and /categories/{id}</span>
    <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>routes</span>-&gt;<span class="hljs-title invoke__">collection</span>(<span class="hljs-string">'category_'</span>)
        -&gt;<span class="hljs-title invoke__">prefix</span>(<span class="hljs-string">'/categories'</span>, <span class="hljs-attr">trailingSlashOnRoot</span>: <span class="hljs-keyword">false</span>)
        -&gt;<span class="hljs-title invoke__">add</span>(<span class="hljs-string">'index'</span>, <span class="hljs-string">'/'</span>)
        -&gt;<span class="hljs-title invoke__">add</span>(<span class="hljs-string">'show'</span>, <span class="hljs-string">'/{id}'</span>);
};</code></pre>
    </div>
</div>
</div>
<div class="section">
<h2 id="get-parent-role-names"><a class="headerlink" href="#get-parent-role-names" title="Permalink to this headline">Get Parent Role Names</a></h2>
<div class="blog-post-contributor-info">
    <div class="blog-post-contributor-avatar">
                    <a target="_blank" href="https://connect.symfony.com/profile/pecapel">
                <img src="https://connect.symfony.com/profile/pecapel.picture" alt="Pierre-Emmanuel CAPEL">
            </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/pecapel">Pierre-Emmanuel CAPEL</a>
                                        <span class="blog-post-contributor-prs"> in
                                    <a target="_blank" href="https://github.com/symfony/symfony/pull/53998">#53998</a>
                                                </span>
            </div>
</div>
<p>When using <a href="https://symfony.com/doc/current/security.html#hierarchical-roles" class="reference external">hierarchical roles</a>, the <code translate="no" class="notranslate">getReachableRoleNames()</code> method returns
all the roles inherited by the given roles. Symfony 8.1 adds the inverse operation:
<code translate="no" class="notranslate">getParentRoleNames()</code> returns the roles that inherit from the given roles.
This is useful for finding which roles can access everything a given role can access.</p>
<p>Consider the following role hierarchy:</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/security.yaml</span>
<span class="hljs-attr">security:</span>
    <span class="hljs-attr">role_hierarchy:</span>
        <span class="hljs-attr">ROLE_ADMIN:</span> <span class="hljs-string">ROLE_USER</span>
        <span class="hljs-attr">ROLE_SUPER_ADMIN:</span> <span class="hljs-string">ROLE_ADMIN</span></code></pre>
    </div>
</div>
<div translate="no" data-loc="16" 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">Security</span>\<span class="hljs-title">Core</span>\<span class="hljs-title">Role</span>\<span class="hljs-title">RoleHierarchyInterface</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RoleService</span>
</span>{
    <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> RoleHierarchyInterface <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>roleHierarchy</span>,
    )</span> </span>{
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getParentRoles</span><span class="hljs-params">(<span class="hljs-keyword">array</span> <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>roles</span>)</span>: <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-comment">// for ['ROLE_USER'] this returns an array with 'ROLE_USER', 'ROLE_ADMIN'</span>
        <span class="hljs-comment">// and 'ROLE_SUPER_ADMIN', because those roles inherit ROLE_USER permissions</span>
        <span class="hljs-keyword">return</span> <span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>this</span>-&gt;roleHierarchy-&gt;<span class="hljs-title invoke__">getParentRoleNames</span>(<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>roles</span>);
    }
}</code></pre>
    </div>
</div>
</div>
<div class="section">
<h2 id="mark-classes-as-safe-for-twig-s-escaper"><a class="headerlink" href="#mark-classes-as-safe-for-twig-s-escaper" title="Permalink to this headline">Mark Classes as Safe for Twig's Escaper</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/63929">#63929</a>
                                                </span>
            </div>
</div>
<p>If a value object wraps pre-escaped or trusted HTML content, you have to apply
the <code translate="no" class="notranslate">raw</code> Twig filter every time you output it in a template. In Symfony 8.1,
you can mark the class as safe for Twig's escaper using the new
<code translate="no" class="notranslate">twig.safe_class</code> resource tag, so its output is no longer escaped:</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/services.yaml</span>
<span class="hljs-attr">services:</span>
    <span class="hljs-string">App\Twig\HtmlString:</span>
        <span class="hljs-attr">resource_tags:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">{</span> <span class="hljs-attr">name:</span> <span class="hljs-string">twig.safe_class,</span> <span class="hljs-attr">strategy:</span> <span class="hljs-string">html</span> <span class="hljs-string">}</span></code></pre>
    </div>
</div>
<p>Unlike regular service tags, resource tags are attached to the class itself, not
to a service. The <code translate="no" class="notranslate">strategy</code> option accepts a single escaping strategy or a
list of them, and you can tag the same class several times to mark it as safe
for multiple strategies.</p>
</div>
<div class="section">
<h2 id="new-containerproviderinterface-contract"><a class="headerlink" href="#new-containerproviderinterface-contract" title="Permalink to this headline">New ContainerProviderInterface Contract</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/63663">#63663</a>
                                                </span>
            </div>
</div>
<p>Symfony 8.1 adds a minimal <code translate="no" class="notranslate">ContainerProviderInterface</code> to
<code translate="no" class="notranslate">symfony/service-contracts</code>, providing a standard way for objects to expose
their service container:</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-keyword">namespace</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Contracts</span>\<span class="hljs-title">Service</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Psr</span>\<span class="hljs-title">Container</span>\<span class="hljs-title">ContainerInterface</span>;

<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ContainerProviderInterface</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getContainer</span><span class="hljs-params">()</span>: <span class="hljs-title">ContainerInterface</span></span>;
}</code></pre>
    </div>
</div>
<p>The console <code translate="no" class="notranslate">Application</code> class provided by FrameworkBundle implements it
(booting the kernel if needed). This enables decoupled use cases such as
Messenger parallel workers, which need to bootstrap the application and access
the container to resolve the message bus.</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-1-misc-improvements-part-1?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
            <dc:creator><![CDATA[ Javier Eguiluz ]]></dc:creator>
            <pubDate>Fri, 12 Jun 2026 11:31:00 +0200</pubDate>
            <comments>https://symfony.com/blog/new-in-symfony-8-1-misc-improvements-part-1?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
        </item>
            </channel>
</rss>
