<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Javier Eguiluz</title>
    <description>The latest articles on DEV Community by Javier Eguiluz (@javiereguiluz).</description>
    <link>https://dev.to/javiereguiluz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F149151%2F680f65e7-3fa6-45ee-a2c9-769d1f2472dc.png</url>
      <title>DEV Community: Javier Eguiluz</title>
      <link>https://dev.to/javiereguiluz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/javiereguiluz"/>
    <language>en</language>
    <item>
      <title>A Simple Pattern That Makes Sorting in PHP Faster</title>
      <dc:creator>Javier Eguiluz</dc:creator>
      <pubDate>Fri, 13 Mar 2026 07:18:02 +0000</pubDate>
      <link>https://dev.to/javiereguiluz/a-simple-pattern-that-makes-sorting-in-php-faster-4km9</link>
      <guid>https://dev.to/javiereguiluz/a-simple-pattern-that-makes-sorting-in-php-faster-4km9</guid>
      <description>&lt;p&gt;Sorting arrays in PHP using a custom sorting criterion is simple thanks to the &lt;code&gt;usort()&lt;/code&gt; function. But if you are not careful, it can be &lt;strong&gt;much slower&lt;/strong&gt; than you expect.&lt;/p&gt;

&lt;p&gt;Consider this example: you have a list of CSS rules represented as PHP classes and you need to sort them by specificity. Each rule exposes a &lt;code&gt;getSpecificity()&lt;/code&gt; method that does some regex work internally. A natural first implementation looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;usort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Rule&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Rule&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSpecificity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSpecificity&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is correct, clean, readable ... and slow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hidden performance issue&lt;/strong&gt;: a comparison sort typically performs &lt;code&gt;n log(n)&lt;/code&gt; comparisons. Each comparison requires calling your callback, which in turn computes the sort key for both elements.&lt;/p&gt;

&lt;p&gt;In this example, &lt;code&gt;getSpecificity()&lt;/code&gt; is recomputed over and over again for the same objects during the sort. For a list of 100 rules, this means hundreds of comparisons and thousands of calls to &lt;code&gt;getSpecificity()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In cases like this when the sort key is expensive to compute, there's &lt;strong&gt;a better way to do it&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three step pattern
&lt;/h2&gt;

&lt;p&gt;The optimization is to compute the expensive key &lt;strong&gt;once per element&lt;/strong&gt; following a three-step pattern: &lt;strong&gt;decorate, sort, undecorate&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Step 1: decorate (compute the expensive key once) and return [$rule, key] pairs&lt;/span&gt;
&lt;span class="nv"&gt;$decorated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Rule&lt;/span&gt; &lt;span class="nv"&gt;$rule&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$rule&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSpecificity&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
    &lt;span class="nv"&gt;$rules&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Step 2: sort using the cached key&lt;/span&gt;
&lt;span class="nb"&gt;usort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$decorated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Step 3: undecorate (extract the original values)&lt;/span&gt;
&lt;span class="nv"&gt;$rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$decorated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;getSpecificity()&lt;/code&gt; is called exactly &lt;strong&gt;once per rule&lt;/strong&gt;, during the &lt;code&gt;array_map()&lt;/code&gt; pass. The &lt;code&gt;usort()&lt;/code&gt; comparisons only deal with integers and the total number of calls to the expensive method goes from &lt;code&gt;O(n log n)&lt;/code&gt; to &lt;code&gt;O(n)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This pattern is called the &lt;a href="https://en.wikipedia.org/wiki/Schwartzian_transform" rel="noopener noreferrer"&gt;Schwartzian transform&lt;/a&gt;, named after &lt;a href="https://en.wikipedia.org/wiki/Randal_L._Schwartz" rel="noopener noreferrer"&gt;Randal L. Schwartz&lt;/a&gt;, who popularized it in Perl back in 1994. The core idea comes from a Lisp idiom known as &lt;em&gt;"decorate-sort-undecorate"&lt;/em&gt; (DSU).&lt;/p&gt;

&lt;p&gt;This concept is language-agnostic: Python's &lt;code&gt;sorted()&lt;/code&gt; has a built-in &lt;code&gt;key=&lt;/code&gt; parameter that does the same thing, Ruby has &lt;code&gt;sort_by&lt;/code&gt;, Haskell has &lt;code&gt;sortOn&lt;/code&gt;. PHP does not have a native equivalent, but there's a trick you can use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;array_multisort()&lt;/code&gt; trick
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://php.net/array_multisort" rel="noopener noreferrer"&gt;array_multisort() PHP function&lt;/a&gt; can sort several arrays at once, or a multi-dimensional array by one or more dimensions. It's not very popular (&lt;a href="https://github.com/search?q=array_multisort+language%3APHP&amp;amp;type=code" rel="noopener noreferrer"&gt;60k usages on GitHub&lt;/a&gt;) compared to other array functions (e.g. 640k &lt;code&gt;array_values()&lt;/code&gt; usages, 400k &lt;code&gt;array_pop()&lt;/code&gt; usages, etc.), but it's very useful for Schwartzian transforms.&lt;/p&gt;

&lt;p&gt;This is how it works. Consider this initial array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'category'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'priority'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Deploy'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'category'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'priority'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Write docs'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'category'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'priority'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Run tests'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'category'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'priority'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Fix bug'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need to sort it by both category (first) and priority (second), you can do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// extract the sorting keys first&lt;/span&gt;
&lt;span class="nv"&gt;$categories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$priorities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'priority'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// PHP sorts items ASC by category first and then, inside each&lt;/span&gt;
&lt;span class="c1"&gt;// category group, it sorts items DESC by priority&lt;/span&gt;
&lt;span class="nb"&gt;array_multisort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$categories&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SORT_ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$priorities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SORT_DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After sorting, the &lt;code&gt;$items&lt;/code&gt; array is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'category'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'priority'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Write docs'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'category'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'priority'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Fix bug'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'category'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'priority'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Run tests'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'category'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'priority'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Deploy'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;array_multisort()&lt;/code&gt;, the "decorate, sort, undecorate" pattern becomes as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Rule&lt;/span&gt; &lt;span class="nv"&gt;$rule&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$rule&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSpecificity&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$rules&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;array_multisort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SORT_ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$rules&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A real-world case in Symfony
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/tijsverkoyen/CssToInlineStyles" rel="noopener noreferrer"&gt;tijsverkoyen/CssToInlineStyles&lt;/a&gt; is a popular PHP library (and an optional Symfony dependency) that converts CSS stylesheets to inline styles. Internally, it sorts CSS rules by specificity before applying them to DOM nodes.&lt;/p&gt;

&lt;p&gt;In a real profiling session when running tests on a Symfony app, &lt;code&gt;sortOnSpecificity()&lt;/code&gt; appeared in the flame graph with 104,000 calls triggering 126,000 calls to an inner &lt;code&gt;compareTo()&lt;/code&gt; method. The sort was costing almost 200 ms out of a total runtime of 4 seconds, which looked excessive.&lt;/p&gt;

&lt;p&gt;The original &lt;code&gt;sortOnSpecificity()&lt;/code&gt; method used a &lt;code&gt;usort()&lt;/code&gt; callback that called &lt;code&gt;getValue()&lt;/code&gt; and &lt;code&gt;getOrder()&lt;/code&gt; on every comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// original code (simplified)&lt;/span&gt;
&lt;span class="nb"&gt;usort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Rule&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Rule&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$aSpecificity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSpecificity&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$bSpecificity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSpecificity&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$aSpecificity&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$bSpecificity&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$aSpecificity&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$bSpecificity&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The optimization was a direct application of the Schwartzian transform: compute the sortable keys once and let &lt;code&gt;array_multisort()&lt;/code&gt; handle the ordering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$specificityValues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="nv"&gt;$orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rules&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$specificityValues&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$rule&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSpecificity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$orders&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$rule&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;array_multisort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$specificityValues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SORT_ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SORT_ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$rules&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this change, &lt;code&gt;sortOnSpecificity()&lt;/code&gt; &lt;strong&gt;disappeared from the hot path&lt;/strong&gt; in the profiler and the wall time improved by roughly 200 milliseconds.&lt;/p&gt;

&lt;p&gt;You can see the full change in this pull request: &lt;a href="https://github.com/tijsverkoyen/CssToInlineStyles/pull/260" rel="noopener noreferrer"&gt;PR #260&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When to apply this technique
&lt;/h2&gt;

&lt;p&gt;The Schwartzian transform is &lt;strong&gt;recommended only when&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The sort key is expensive to compute (I/O, regex, database queries, complex calculations)&lt;/li&gt;
&lt;li&gt;You do not need &lt;a href="https://en.wikipedia.org/wiki/Memoization" rel="noopener noreferrer"&gt;memoization&lt;/a&gt; across multiple sorts. You just want each element's key computed once per sort invocation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your sort key is a simple property access like &lt;code&gt;$a-&amp;gt;name&lt;/code&gt;, the overhead of decorating and undecorating is probably &lt;strong&gt;not worth it&lt;/strong&gt;. The transform shines when the key computation has real cost.&lt;/p&gt;




&lt;p&gt;✨ If you liked this post, consider &lt;a href="https://github.com/sponsors/javiereguiluz" rel="noopener noreferrer"&gt;sponsoring me on GitHub&lt;/a&gt; 🙌&lt;/p&gt;

</description>
      <category>php</category>
      <category>performance</category>
      <category>symfony</category>
    </item>
    <item>
      <title>Claude Code for Symfony and PHP: The Setup That Actually Works</title>
      <dc:creator>Javier Eguiluz</dc:creator>
      <pubDate>Fri, 27 Feb 2026 07:25:52 +0000</pubDate>
      <link>https://dev.to/javiereguiluz/claude-code-for-symfony-and-php-the-setup-that-actually-works-1che</link>
      <guid>https://dev.to/javiereguiluz/claude-code-for-symfony-and-php-the-setup-that-actually-works-1che</guid>
      <description>&lt;p&gt;&lt;a href="https://code.claude.com/docs/en/overview" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; is a CLI tool that lets you use AI directly from your terminal to write, edit, and review code in your projects. I've been using it daily to work on PHP applications (Symfony and EasyAdmin) for a long time, and the results have been impressive. But getting there required some trial and error. In this article I'll share the setup, plugins, and workflows that made the difference.&lt;/p&gt;

&lt;p&gt;To use Claude Code you'll need an active &lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;Claude AI subscription&lt;/a&gt;. I'm using the Max subscription ($100/month) but the Pro subscription ($17/month) will work too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;First, install Claude Code and log in using your account &lt;a href="https://code.claude.com/docs/en/quickstart" rel="noopener noreferrer"&gt;as explained on Claude docs&lt;/a&gt;. Now you can use Claude Code in your terminal by running the &lt;code&gt;claude&lt;/code&gt; command in your project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;my-symfony-project/
&lt;span class="nv"&gt;$ &lt;/span&gt;claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Many folks use it that way, but I prefer to use Claude Code inside my IDE. It will still be the same rough CLI-based application, but it will be next to your code and all the clickable file paths in Claude Code output will take you quickly to the related code.&lt;/p&gt;

&lt;p&gt;My IDE is PhpStorm, so I use the official &lt;strong&gt;Claude Code Plugin&lt;/strong&gt; developed by Anthropic for JetBrains IDEs (there's also an official plugin for Visual Studio Code). You can find it in the plugin marketplace right inside PhpStorm:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzmce3m5gisqpr6b5n71u.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzmce3m5gisqpr6b5n71u.webp" alt="Installing Claude Code plugin inside PhpStorm" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I use Claude Code all the time, so I keep it open as a right sidebar so I can easily focus on code or AI as needed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F55qx8osgota6la076uvt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F55qx8osgota6la076uvt.webp" alt="Using Claude Code inside PhpStorm IDE" width="800" height="653"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One thing you need to know is that Claude Code provides different models. Some are smarter, but because of that they are also slower and more expensive to run (they'll consume your Claude Code limits sooner). You can select your model and its effort level by running the &lt;code&gt;/model&lt;/code&gt; command inside Claude Code CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLAUDE.md / AGENTS.md
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This is the most important idea of this article&lt;/strong&gt;. Imagine that you ask me to implement a Symfony feature but you don't tell me anything about your project conventions. Will I do a good job? Well, maybe I used &lt;a href="https://en.wikipedia.org/wiki/Yoda_conditions" rel="noopener noreferrer"&gt;Yoda conditions&lt;/a&gt; and you hate them, maybe I used autowiring and you prefer explicit service registration, etc. If you don't explain how you like your code, you can't expect good code in return. The same happens with AI.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;AGENTS.md&lt;/code&gt; file (named exactly like that and stored at the root of your project) describes the "dos and don'ts" when writing code for your application. Before continuing: Claude looks first for a file called &lt;code&gt;CLAUDE.md&lt;/code&gt; and all Claude docs mention that file name. However, a file called &lt;code&gt;AGENTS.md&lt;/code&gt; will work exactly the same and has an additional benefit: &lt;code&gt;CLAUDE.md&lt;/code&gt; only works with Claude but &lt;code&gt;AGENTS.md&lt;/code&gt; works with all AIs (Claude, Codex, Gemini, GitHub Copilot, etc.) I recommend you to always use &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Claude provides the &lt;code&gt;/init&lt;/code&gt; command to create a first &lt;code&gt;AGENTS.md&lt;/code&gt; in projects that have none. Some people say it doesn't create a good &lt;code&gt;AGENTS.md&lt;/code&gt; file. Whether you like the result or not, you'll have to tweak this file and keep doing that continuously. Claude Code creator himself mentioned that he and his team update the &lt;code&gt;AGENTS.md&lt;/code&gt; file of their projects all the time. &lt;strong&gt;This is key to get good results&lt;/strong&gt;. You can't get good code with a bad &lt;code&gt;AGENTS.md&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;You should avoid too vague and too specific instructions. Focus on simple and actionable instructions. Use simple language like &lt;em&gt;"do this"&lt;/em&gt; or &lt;em&gt;"don't do this"&lt;/em&gt; and don't use convoluted messages (&lt;em&gt;"Don't try to avoid not doing foo and bar"&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// WRONG: too vague
* Don't make mistakes
* Check everything well

// WRONG: too specific
* On UserMessageHandler, don't return early
* Use public constants on `app:foo-bar` command

// CORRECT: general and actionable instructions
* Use Yoda conditions: `if (null === $value)`
* Use strict comparisons only (`===`, `!==`)
* Name variables and methods in `camelCase`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition to telling how to write good code, in &lt;code&gt;AGENTS.md&lt;/code&gt; you must also explain how AI can verify that it wrote good code. Mention the tools you use and how to run them (PHPUnit, PHP-CS-Fixer, PHPStan, custom Makefile tasks, etc.)&lt;/p&gt;

&lt;p&gt;For Symfony projects, here are some examples of useful &lt;code&gt;AGENTS.md&lt;/code&gt; instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* Use constructor promotion for dependency injection
* Use PHP attributes for routing, mapping, and configuration (never YAML/XML)
* Use Messenger handlers for async operations instead of putting logic in controllers
* [...]
* Run `php bin/console lint:container` to verify the dependency injection container compiles
* Run `php vendor/bin/phpstan analyse` to check for type errors
* Run `php vendor/bin/phpunit` to execute the test suite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need inspiration, you can check &lt;a href="https://github.com/EasyCorp/EasyAdminBundle/blob/5.x/AGENTS.md" rel="noopener noreferrer"&gt;the AGENTS.md file we use in the EasyAdmin project&lt;/a&gt;. We're getting excellent results when using it, but it might not work as well in your project. So, don't copy it entirely, just get inspired and copy the ideas you like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan, Plan, Plan
&lt;/h2&gt;

&lt;p&gt;This is the second most important idea of this article. Claude Code has two main working modes: &lt;strong&gt;plan&lt;/strong&gt; or &lt;strong&gt;work&lt;/strong&gt;. If you are not aware of this, Claude Code will always be in &lt;strong&gt;work mode&lt;/strong&gt; and you'll get mediocre results in complex tasks. Instead, switch Claude Code to &lt;strong&gt;plan mode&lt;/strong&gt; and draft a plan for your bug fix or new feature.&lt;/p&gt;

&lt;p&gt;Inside Claude Code CLI (standalone or embedded in your IDE) press &lt;code&gt;[Shift]&lt;/code&gt; + &lt;code&gt;[TAB]&lt;/code&gt; to cycle through the modes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdw4vjj0n079zj9cqeefh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdw4vjj0n079zj9cqeefh.gif" alt="Switching Claude Code CLI modes" width="643" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Stop when you see &lt;code&gt;⏸ plan mode on&lt;/code&gt;. Now, describe the feature you want to implement or give information about the bug you are trying to fix. Claude will start thinking about it and might ask you questions to clarify some things. I recommend selecting the most capable model of the moment (as of today, &lt;code&gt;Opus 4.6&lt;/code&gt;) and setting it at &lt;code&gt;High effort&lt;/code&gt; level. A good plan will create great code, so you really want to use the best model possible to create the plan.&lt;/p&gt;

&lt;p&gt;When Claude Code finishes the plan, it will present it to you. Review it, and if you have questions or comments about things that don't look right, tell Claude. It will make changes in the plan and present it to you again. Keep asking questions and refining things until you are satisfied.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a Good Plan
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What should you use as the prompt to get a good plan?&lt;/strong&gt; To me, the best approach is to not be too vague and not too specific either. If your prompt is very detailed and you explain how to solve the issue, you're constraining Claude Code to follow your orders instead of proposing its own solutions. Something important is that, when possible, you should mention the key files or directories to look into. This helps Claude Code immensely and produces good plans pretty quickly.&lt;/p&gt;

&lt;p&gt;For example, when we added &lt;a href="https://github.com/EasyCorp/EasyAdminBundle/releases/tag/v4.26.0" rel="noopener noreferrer"&gt;group actions in EasyAdmin 4.26&lt;/a&gt; my prompt was something like this: &lt;em&gt;"We want to add a new feature to group actions in backends. This is mostly for global actions shown at page level, but it should work for per-entity action dropdowns too. There should be two types of group actions: (1) a pure action dropdown where you click on the button and see the available actions; (2) a split-button type that provides a clickable main action and a dropdown with other actions. Use the &lt;a class="mentioned-user" href="https://dev.to/templates"&gt;@templates&lt;/a&gt;/components/Button.html.twig component for this new feature (you'll need to update it) and base the design on the Bootstrap split button component."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I didn't mention the specific classes where it should look (e.g. &lt;code&gt;ActionFactory&lt;/code&gt;) because in this case it was trivial for AI to find them. If you work on complex tasks and know that the solution should focus on certain classes, mention them explicitly in your prompt. To do so, prepend the &lt;code&gt;@&lt;/code&gt; character to the relative dir/file path (&lt;code&gt;@tests/&lt;/code&gt;, &lt;code&gt;@src/Controller/UserController.php&lt;/code&gt;, etc.) In this example, I mentioned an existing Twig component because I was certain that it would work for the new feature.&lt;/p&gt;

&lt;p&gt;In short, &lt;strong&gt;to generate a good plan&lt;/strong&gt;: don't be vague, don't do too much hand-holding, and make sure to include important clues if you know them (like files/dir to use or look into).&lt;/p&gt;

&lt;p&gt;For very complex tasks (think tens of thousands of lines of code) you can spend more than 30 minutes planning. Don't worry, a good plan is the key to getting great code. You are not wasting time, you are actually saving it.&lt;/p&gt;

&lt;p&gt;If the planning session is long, you can stop and continue later. Use the &lt;code&gt;/resume&lt;/code&gt; command to continue a past Claude Code session. If you ever lose a plan, don't worry. They are all saved in &lt;code&gt;~/.claude/plans/&lt;/code&gt;, so you can quickly find any plan you created.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context
&lt;/h3&gt;

&lt;p&gt;When you are finally happy with the plan, Claude Code will present several possible actions. My recommendation is to select the first one (&lt;em&gt;"Yes, clear context and auto-accept edits"&lt;/em&gt;). After clearing the context, Claude Code will start implementing the plan autonomously. It will create and edit files, run tests, and fix issues on its own. If you selected the "auto-accept edits" option, it won't ask for permission on each change. You can watch the progress in real time and intervene at any point if something goes off track.&lt;/p&gt;

&lt;p&gt;Think of the &lt;strong&gt;context&lt;/strong&gt; as the total capacity that AI has for any task. As you plan things, ask questions, or answer Claude Code questions, the context fills up. As of today, Claude provides a context of 1 million tokens (a unit of measure used by AIs which roughly corresponds to three-quarters of a word).&lt;/p&gt;

&lt;p&gt;If the context fills up, Claude Code tries to compact the contents, but ultimately results degrade significantly. So, if you planned a complex task and filled up more than 60% of the context, the implementation will probably suffer. That's why it's recommended to always clear the context after the plan, so the implementation work has the full context available.&lt;/p&gt;

&lt;p&gt;Whenever you start a new task, run the &lt;code&gt;/clear&lt;/code&gt; command inside Claude Code CLI to empty the context and start from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  When Things Go Wrong
&lt;/h3&gt;

&lt;p&gt;Even with a good plan, Claude Code will sometimes produce bad results. Here's how I handle that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small mistakes&lt;/strong&gt; (wrong variable name, missing type hint): just tell Claude Code what's wrong in the chat and it will fix it immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything is off&lt;/strong&gt;: if the implementation went in the wrong direction, don't try to fix it incrementally. Run &lt;code&gt;/clear&lt;/code&gt;, go back to plan mode, and refine the plan with what you learned from the failed attempt. This is faster than trying to patch bad code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight is that &lt;strong&gt;fixing a bad plan is always better than fixing bad code&lt;/strong&gt;. If results are consistently bad, the problem is almost always in the plan or in your &lt;code&gt;AGENTS.md&lt;/code&gt; file, not in Claude Code itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugins, Skills, Commands, Agents, Hooks
&lt;/h2&gt;

&lt;p&gt;Claude Code has many different ways to add capabilities to the tool, such as &lt;a href="https://code.claude.com/docs/en/plugins" rel="noopener noreferrer"&gt;plugins&lt;/a&gt;, &lt;a href="https://code.claude.com/docs/en/sub-agents" rel="noopener noreferrer"&gt;agents&lt;/a&gt;, &lt;a href="https://code.claude.com/docs/en/skills" rel="noopener noreferrer"&gt;skills&lt;/a&gt;, and &lt;a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener noreferrer"&gt;hooks&lt;/a&gt;. However, in my opinion, this is where &lt;strong&gt;most folks make a mistake&lt;/strong&gt;. They install too many plugins, skills, agents, and hooks, and they create too many custom ones. Even Claude Code's author mentioned that he doesn't use many of these, so my recommendation is to keep it minimal.&lt;/p&gt;

&lt;p&gt;Before installing a plugin, you must register the marketplace where the plugin is distributed from. The official Anthropic marketplace (&lt;code&gt;claude-plugins-official&lt;/code&gt;) is enabled by default. It's confusing, but there's another plugin marketplace maintained by Anthropic (&lt;a href="https://github.com/anthropics/claude-plugins-official" rel="noopener noreferrer"&gt;anthropics/claude-plugins-official&lt;/a&gt;) that has interesting plugins. I recommend adding that second marketplace as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the &lt;code&gt;claude&lt;/code&gt; command anywhere to start Claude Code CLI&lt;/li&gt;
&lt;li&gt;Run the &lt;code&gt;/plugins&lt;/code&gt; command inside Claude Code to open the plugin manager&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;[←]&lt;/code&gt; &lt;code&gt;[→]&lt;/code&gt; arrows on your keyboard to select the &lt;code&gt;Marketplaces&lt;/code&gt; tab&lt;/li&gt;
&lt;li&gt;Make sure &lt;code&gt;+ Add Marketplace&lt;/code&gt; is selected and press &lt;code&gt;&amp;lt;Enter&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;anthropics/claude-plugins-official&lt;/code&gt; and press &lt;code&gt;&amp;lt;Enter&amp;gt;&lt;/code&gt; to add it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After adding the second official Anthropic repository, use the keyboard arrows to select each marketplace and click on &lt;code&gt;Browse plugins&lt;/code&gt; to see the list of available plugins and install them. This is the &lt;strong&gt;list of plugins I recommend&lt;/strong&gt; installing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;php-lsp&lt;/code&gt;: PHP language server (Intelephense) for code intelligence. &lt;strong&gt;Strongly recommended&lt;/strong&gt;. Without it, Claude Code has no understanding of your project's type system: it will hallucinate method names, miss constructor parameters, and use classes that don't exist. With it, Claude Code can navigate your codebase the same way your IDE does&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context7&lt;/code&gt;: &lt;strong&gt;strongly recommended&lt;/strong&gt;; allows the AI to read up-to-date documentation for popular projects (including Symfony, Doctrine, Twig, and API Platform) without making web searches or browsing their websites&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;code-review&lt;/code&gt;: reviews pull requests using multiple specialized agents&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;code-simplifier&lt;/code&gt;: simplifies and refines code for clarity, consistency, and maintainability&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;security-guidance&lt;/code&gt;: a hook that warns about potential security issues when editing files, including command injection and XSS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;playwright&lt;/code&gt;: a browser automation and end-to-end testing tool. It allows Claude Code to use a browser to debug issues in your Symfony applications (e.g. rendering issues in Twig templates or JavaScript problems in Stimulus controllers)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's all. I get excellent results using only these plugins, so I recommend not spending too much time testing and configuring plugins.&lt;/p&gt;

&lt;p&gt;About &lt;strong&gt;skills&lt;/strong&gt;, &lt;strong&gt;commands&lt;/strong&gt; and &lt;strong&gt;hooks&lt;/strong&gt;, my recommendation is the same. When you start with Claude Code, don't worry much about them. As you learn to use it, add some custom skills, commands or hooks if you think they'll help you. For example, I have the following custom command (stored in &lt;code&gt;~/.claude/commands/fix.md&lt;/code&gt;) that fixes issues from GitHub repos (I only have to run this inside Claude Code: &lt;code&gt;/fix {the issue number}&lt;/code&gt;) because this is something I have to do frequently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;allowed-tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bash(gh:*)&lt;/span&gt;
&lt;span class="na"&gt;argument-hint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;issue-number&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Inspect a GitHub issue and plan/fix it&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## Task&lt;/span&gt;

Analyze GitHub issue #$1 from this repository and fix it.

&lt;span class="gu"&gt;## Context&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Repository: !&lt;span class="sb"&gt;`gh repo view --json nameWithOwner -q .nameWithOwner 2&amp;gt;/dev/null || echo "unknown"`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Current branch: !&lt;span class="sb"&gt;`git branch --show-current`&lt;/span&gt;

&lt;span class="gu"&gt;## Steps&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Fetch the issue: &lt;span class="sb"&gt;`gh issue view $1`&lt;/span&gt;
&lt;span class="p"&gt;
2.&lt;/span&gt; Analyze complexity:
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Trivial/Small**&lt;/span&gt; (typos, config tweaks, one-file fixes with clear solution): implement directly
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Medium/Large**&lt;/span&gt; (multiple files, architectural decisions, unclear scope): present a plan first
&lt;span class="p"&gt;
3.&lt;/span&gt; For direct implementation: make the fix, then summarize what was done.
&lt;span class="p"&gt;
4.&lt;/span&gt; For plans: detail the files to modify, proposed approach, and potential risks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sub-Agents
&lt;/h2&gt;

&lt;p&gt;This is an important concept but, as Claude Code is getting smarter every day, nowadays it's not that important to manage this manually. For certain tasks, Claude Code launches copies of itself called sub-agents automatically to work on different sub-tasks in parallel. In addition to the performance gain, the main benefit of sub-agents is that each of them can use their own context to complete their job, without filling up the context of the main task.&lt;/p&gt;

&lt;p&gt;If you are working on complex tasks, Claude Code might decide to launch sub-agents on its own. But you can also force this by mentioning explicitly in your prompt to &lt;em&gt;"use sub-agents to implement all tasks in parallel"&lt;/em&gt;. Claude Code recently launched &lt;a href="https://code.claude.com/docs/en/agent-teams" rel="noopener noreferrer"&gt;teams&lt;/a&gt; as an advanced version of sub-agents. It's still experimental, but it allows you to coordinate multiple Claude Code instances working together as a team, with shared tasks, inter-agent messaging, and centralized management.&lt;/p&gt;

&lt;p&gt;For Symfony projects, sub-agents work great when you need to implement related but independent pieces: for example, creating a Messenger handler, its corresponding message class, and the functional test, all at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Global Settings
&lt;/h2&gt;

&lt;p&gt;If you followed all the above recommendations, you're probably getting good results with Claude Code in your Symfony applications. However, Claude Code is probably annoying you a bit. That's because it asks for permission to do anything: Can I read this file? Can I list the contents of this directory? Can I check the syntax of this file?&lt;/p&gt;

&lt;p&gt;Some people add the &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; option when running the &lt;code&gt;claude&lt;/code&gt; command. &lt;strong&gt;I don't recommend doing this&lt;/strong&gt;. As its name shows, this is too dangerous in practice. My recommendation is to solve this with &lt;strong&gt;global permissions&lt;/strong&gt;. You can use the &lt;code&gt;~/.claude/settings.json&lt;/code&gt; file to configure settings applied to all Claude Code sessions. For example, this is my configuration that allows all safe commands, bans some dangerous commands, and prevents accessing certain files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://json.schemastore.org/claude-code-settings.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(cat:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(curl:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(diff:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(echo:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(find:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(gh:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(grep:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(head:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(ls:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(pwd)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(tail:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(tree:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(wc:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(which:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(php-cs-fixer:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(php:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(phpstan:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(phpunit:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(symfony:*)"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(rm:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env.*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(./config/secrets/.*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(./secrets/**)"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"defaultMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acceptEdits"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enabledPlugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"php-lsp@claude-plugins-official"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code-simplifier@claude-plugins-official"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"context7@claude-plugins-official"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code-review@claude-plugins-official"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"security-guidance@claude-plugins-official"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"playwright@claude-plugins-official"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alwaysThinkingEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this global configuration file, you can also configure &lt;strong&gt;hooks&lt;/strong&gt;. For example, I use hooks to automatically lint and format files whenever Claude Code creates or edits them. This is especially useful in Symfony projects where you want to enforce coding standards on PHP files and validate YAML configuration files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://json.schemastore.org/claude-code-settings.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"defaultMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acceptEdits"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write|Edit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"case &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$CLAUDE_FILE_PATH&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; in *.php) php-cs-fixer fix --quiet &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$CLAUDE_FILE_PATH&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; 2&amp;gt;/dev/null || true ;; esac"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"case &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$CLAUDE_FILE_PATH&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; in *.yaml|*.yml) php bin/console lint:yaml &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$CLAUDE_FILE_PATH&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ;; esac"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"case &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$CLAUDE_FILE_PATH&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; in *.json) python3 -m json.tool &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$CLAUDE_FILE_PATH&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt; /dev/null ;; esac"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;If you take away three things from this article, make it these: &lt;strong&gt;(1)&lt;/strong&gt; write a good &lt;code&gt;AGENTS.md&lt;/code&gt; file and keep refining it, &lt;strong&gt;(2)&lt;/strong&gt; always plan before you code, and &lt;strong&gt;(3)&lt;/strong&gt; don't overload Claude Code with plugins. That combination alone will get you better results than any amount of tweaking.&lt;/p&gt;

&lt;p&gt;As you become a better Claude Code user, read &lt;a href="https://code.claude.com/docs" rel="noopener noreferrer"&gt;Claude Code Docs&lt;/a&gt; to keep improving.&lt;/p&gt;




&lt;p&gt;✨ If you liked this post, consider &lt;a href="https://github.com/sponsors/javiereguiluz" rel="noopener noreferrer"&gt;sponsoring me on GitHub&lt;/a&gt; 🙌&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>symfony</category>
      <category>php</category>
    </item>
    <item>
      <title>EasyAdmin 5.0: A New Foundation for Your Admin Backends</title>
      <dc:creator>Javier Eguiluz</dc:creator>
      <pubDate>Thu, 26 Feb 2026 07:19:34 +0000</pubDate>
      <link>https://dev.to/javiereguiluz/easyadmin-50-a-new-foundation-for-your-admin-backends-gpi</link>
      <guid>https://dev.to/javiereguiluz/easyadmin-50-a-new-foundation-for-your-admin-backends-gpi</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/EasyCorp/EasyAdminBundle/releases/tag/v5.0.0" rel="noopener noreferrer"&gt;EasyAdmin 5.0.0&lt;/a&gt; has just been released as the new &lt;strong&gt;stable version&lt;/strong&gt; of EasyAdmin.&lt;/p&gt;

&lt;p&gt;EasyAdmin follows a development philosophy &lt;a href="https://symfony.com/blog/preparing-for-symfony-7-4-and-symfony-8-0" rel="noopener noreferrer"&gt;similar to Symfony&lt;/a&gt;. That means EasyAdmin 5.x includes &lt;strong&gt;exactly the same features&lt;/strong&gt; as 4.x. The difference is that 5.x removes everything that was deprecated during the 4.x cycle, providing a cleaner and more future-proof foundation.&lt;/p&gt;

&lt;p&gt;EasyAdmin 5.0 raises the minimum Symfony and PHP requirements while still offering broad compatibility. It supports &lt;strong&gt;Symfony 6.4&lt;/strong&gt;, all &lt;strong&gt;7.x&lt;/strong&gt; and &lt;strong&gt;8.x&lt;/strong&gt; versions, works with Doctrine &lt;strong&gt;ORM 2.x&lt;/strong&gt; and higher, and requires &lt;strong&gt;PHP 8.2&lt;/strong&gt; or later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed in 5.0
&lt;/h2&gt;

&lt;p&gt;All features introduced throughout the 4.x cycle are now the only supported approach in 5.0. Here is a summary of the most relevant changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pretty URLs
&lt;/h3&gt;

&lt;p&gt;In 4.x, you could still use the legacy URL format that this bundle used since day one (e.g. &lt;code&gt;https://example.com/admin?crudAction=edit&amp;amp;crudControllerFqcn=App%5CController%5CAdmin%5cPostCrudController&amp;amp;entityId=3874&lt;/code&gt;).&lt;br&gt;
In 5.0, only the pretty URLs are supported (e.g. &lt;code&gt;https://example.com/admin/post/3874/edit&lt;/code&gt;), which we introduced in &lt;a href="https://github.com/EasyCorp/EasyAdminBundle/releases/tag/v4.14.0" rel="noopener noreferrer"&gt;version 4.14&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You do not need to configure anything to use them. They are enabled by default thanks to &lt;a href="https://github.com/symfony/recipes/tree/main/easycorp/easyadmin-bundle/4.14" rel="noopener noreferrer"&gt;this Symfony Flex recipe&lt;/a&gt; which loads a &lt;a href="https://symfony.com/doc/current/routing/custom_route_loader.html" rel="noopener noreferrer"&gt;custom Symfony route loader&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Pretty URLs have been tested in production for over a year. During that time, we fixed edge cases related to menus, custom actions, and more. They are stable and ready for all applications.&lt;/p&gt;
&lt;h3&gt;
  
  
  Dashboard Definition via Attribute
&lt;/h3&gt;

&lt;p&gt;Instead of relying on Symfony's &lt;code&gt;#[Route]&lt;/code&gt; attribute, you now must apply the &lt;code&gt;#[AdminDashboard]&lt;/code&gt; attribute to your dashboard class, added in &lt;a href="https://github.com/EasyCorp/EasyAdminBundle/releases/tag/v4.24.0" rel="noopener noreferrer"&gt;version 4.24&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BEFORE (4.x)&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Routing\Attribute\Route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DashboardController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractDashboardController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;#[Route('/admin', name: 'admin')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// AFTER (5.x)&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[AdminDashboard(routePath: '/admin', routeName: 'admin')]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DashboardController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractDashboardController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new attribute supports all route options provided by Symfony and adds its own features. For example, you can customize the format of admin routes and restrict which CRUD controllers are available on each dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embedding Symfony Controllers in Your Backend
&lt;/h3&gt;

&lt;p&gt;With the new &lt;code&gt;#[AdminRoute]&lt;/code&gt; attribute added in &lt;a href="https://github.com/EasyCorp/EasyAdminBundle/releases/tag/v4.25.0" rel="noopener noreferrer"&gt;version 4.25&lt;/a&gt; you can integrate any Symfony controller action into your admin backends. This attribute automatically generates the routes required to render your action inside one or more dashboards, reusing the same layout, menus, and visual design as built-in EasyAdmin actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Action Improvements
&lt;/h3&gt;

&lt;p&gt;During the 4.x cycle, we shipped several improvements to actions: grouping them in dropdowns and split buttons (&lt;a href="https://github.com/EasyCorp/EasyAdminBundle/releases/tag/v4.26.0" rel="noopener noreferrer"&gt;version 4.26&lt;/a&gt;), showing a custom confirmation message before any action (&lt;a href="https://github.com/EasyCorp/EasyAdminBundle/releases/tag/v4.28.0" rel="noopener noreferrer"&gt;version 4.28&lt;/a&gt;), and configuring a default action triggered when clicking any row on CRUD index pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Autocomplete Customization
&lt;/h3&gt;

&lt;p&gt;In &lt;a href="https://github.com/EasyCorp/EasyAdminBundle/releases/tag/v4.28.0" rel="noopener noreferrer"&gt;version 4.28&lt;/a&gt;, we added full control over how autocomplete entries are rendered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// use callbacks for simple customizations&lt;/span&gt;
&lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nc"&gt;AssociationField&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'post.category'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setSortProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;autocomplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Category&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s %s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getIcon&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

&lt;span class="c1"&gt;// use Twig template fragments for complex customizations with HTML elements&lt;/span&gt;
&lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nc"&gt;AssociationField&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'post.author'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setSortProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'fullName'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;autocomplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'admin/post/_author_autocomplete.html.twig'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;renderAsHtml&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Custom Icon Sets
&lt;/h3&gt;

&lt;p&gt;EasyAdmin still uses FontAwesome icons by default, but since &lt;a href="https://github.com/EasyCorp/EasyAdminBundle/releases/tag/v4.16.0" rel="noopener noreferrer"&gt;version 4.16&lt;/a&gt; you can use any icon set, such as Tabler or Material Icons.&lt;/p&gt;

&lt;h3&gt;
  
  
  New Twig Components
&lt;/h3&gt;

&lt;p&gt;During the 4.x cycle, we introduced Twig Components for common UI elements. In EasyAdmin 5.x, the long-term goal is to build the entire UI on top of reusable Twig Components, making it easier to compose fully custom admin pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading to 5.x
&lt;/h2&gt;

&lt;p&gt;We strongly recommend &lt;strong&gt;upgrading as soon as possible&lt;/strong&gt;. EasyAdmin 4.x will not receive new features, and future improvements will target 5.x exclusively.&lt;/p&gt;

&lt;p&gt;First, ensure your application has no remaining deprecations from 4.x. You can check this in the Symfony profiler or by running your test suite with deprecations treated as errors. Review the &lt;a href="https://github.com/EasyCorp/EasyAdminBundle/blob/5.x/UPGRADE.md" rel="noopener noreferrer"&gt;EasyAdmin 4.x to 5.x Upgrade Guide&lt;/a&gt; for detailed instructions.&lt;/p&gt;

&lt;p&gt;Once your project is free of deprecations, update the constraint in your &lt;code&gt;composer.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;composer require easycorp/easyadmin-bundle:^5.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is all. Since 5.0 is functionally identical to the latest 4.x release, except for removed deprecated code, no additional changes should be required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying EasyAdmin for the First Time
&lt;/h2&gt;

&lt;p&gt;If you are new to EasyAdmin, getting started takes only a few commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;composer require easycorp/easyadmin-bundle

&lt;span class="nv"&gt;$ &lt;/span&gt;php bin/console make:admin:dashboard
&lt;span class="nv"&gt;$ &lt;/span&gt;php bin/console make:admin:crud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a working admin backend with a dashboard and your first CRUD controller. From there, explore the &lt;a href="https://symfony.com/bundles/EasyAdminBundle/current/index.html" rel="noopener noreferrer"&gt;EasyAdmin documentation&lt;/a&gt; to customize fields, actions, filters, and more.&lt;/p&gt;

&lt;p&gt;You can also review the &lt;a href="https://github.com/EasyCorp/easyadmin-demo" rel="noopener noreferrer"&gt;EasyAdmin Demo project&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;✨ If you enjoyed these features and want to see more like it, consider&lt;br&gt;
&lt;a href="https://github.com/sponsors/javiereguiluz" rel="noopener noreferrer"&gt;sponsoring the EasyAdmin project&lt;/a&gt; 🙌💡&lt;/p&gt;

</description>
      <category>easyadmin</category>
      <category>symfony</category>
      <category>php</category>
    </item>
    <item>
      <title>New in EasyAdmin 4.28: New Filters, Better Autocomplete, CSP, and Smarter Actions</title>
      <dc:creator>Javier Eguiluz</dc:creator>
      <pubDate>Thu, 29 Jan 2026 07:16:18 +0000</pubDate>
      <link>https://dev.to/javiereguiluz/new-in-easyadmin-428-new-filters-better-autocomplete-csp-and-smarter-actions-44l2</link>
      <guid>https://dev.to/javiereguiluz/new-in-easyadmin-428-new-filters-better-autocomplete-csp-and-smarter-actions-44l2</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/EasyCorp/EasyAdminBundle/releases/tag/v4.28.0" rel="noopener noreferrer"&gt;EasyAdmin 4.28&lt;/a&gt; is out, and it's packed with features to improve your backends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clickable Rows in the Index Page
&lt;/h2&gt;

&lt;p&gt;Here's something users have requested for years: clicking anywhere on a row in the index page now navigates to that entity. By default, it goes to the edit page (or falls back to detail if edit isn't available).&lt;/p&gt;

&lt;p&gt;But you have full control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;configureCrud&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Crud&lt;/span&gt; &lt;span class="nv"&gt;$crud&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Crud&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$crud&lt;/span&gt;
        &lt;span class="c1"&gt;// navigate to detail instead of edit&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setDefaultRowAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Action&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DETAIL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// use a custom action&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setDefaultRowAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'review'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// define a fallback chain (first available action wins)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setDefaultRowAction&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nc"&gt;Action&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DETAIL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'preview'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Action&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;EDIT&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="c1"&gt;// disable row clicks entirely&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setDefaultRowAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The behavior is smart: if the configured action isn't available for a specific entity (disabled, no permission, or condition not met), that row simply won't be clickable. Clicks on checkboxes, buttons, links, or other interactive elements within the row work as expected: they don't trigger navigation.&lt;/p&gt;

&lt;p&gt;You can set a global default in your dashboard and override it per CRUD controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// in your Dashboard: applies to all CRUD controllers&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;configureCrud&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Crud&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Crud&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setDefaultRowAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Action&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DETAIL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// in a specific CRUD controller: overrides the dashboard default&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;configureCrud&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Crud&lt;/span&gt; &lt;span class="nv"&gt;$crud&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Crud&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$crud&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setDefaultRowAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Action&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;EDIT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://symfony.com/bundles/EasyAdminBundle/current/crud.html#default-row-action" rel="noopener noreferrer"&gt;Read the docs about default row actions&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Content Security Policy (CSP) Support
&lt;/h2&gt;

&lt;p&gt;If you've ever tried to use EasyAdmin with strict CSP headers (especially with &lt;a href="https://github.com/nelmio/NelmioSecurityBundle" rel="noopener noreferrer"&gt;NelmioSecurityBundle&lt;/a&gt;), you know the frustration: script violations everywhere.&lt;/p&gt;

&lt;p&gt;Not anymore. EasyAdmin 4.28 automatically injects nonce attributes when the &lt;code&gt;csp_nonce()&lt;/code&gt; Twig function is available (i.e. when NelmioSecurityBundle is installed in your application). Inline &lt;code&gt;onclick&lt;/code&gt; handlers have been replaced with data attributes processed by JavaScript. Zero configuration required, it just works.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://github.com/oxess" rel="noopener noreferrer"&gt;Mikołaj Jeziorny&lt;/a&gt; for contributing this feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five New Filters
&lt;/h2&gt;

&lt;p&gt;Filtering by country, currency, language, locale, or timezone used to require creating custom filters. Now they're built-in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$filters&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CountryFilter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'country'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;includeOnly&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'US'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'CA'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'MX'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CurrencyFilter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'currency'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;preferredChoices&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'EUR'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TimezoneFilter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'timezone'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;forCountryCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'US'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All filters display localized names using Symfony's Intl component. They support multi-select, preferred choices at the top of the list, and many other options. Read the docs about &lt;a href="https://symfony.com/bundles/EasyAdminBundle/current/filters.html" rel="noopener noreferrer"&gt;the new filters&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirmation for Any Action
&lt;/h2&gt;

&lt;p&gt;The delete action has always shown a confirmation dialog. Now you can add the same protection to any action with a single method call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Action&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'archive'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Archive'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;askConfirmation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This displays a generic confirmation modal. But you can customize the message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Action&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'publish'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Publish'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;askConfirmation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Are you sure you want to publish this article?'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Need dynamic content? Use placeholders that get replaced automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;askConfirmation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'Delete %entity_name% #%entity_id%? This cannot be undone.'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also customize the confirmation button label (instead of the default "Confirm"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;askConfirmation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'Publish this article to production?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Yes, publish it'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both parameters support &lt;code&gt;TranslatableInterface&lt;/code&gt; objects for full i18n support. And yes, you can now disable confirmation on the delete action if you really want to (though we don't recommend it).&lt;/p&gt;

&lt;h2&gt;
  
  
  Preferred Choices
&lt;/h2&gt;

&lt;p&gt;When you have dozens of options but users pick the same three 90% of the time, put them at the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;ChoiceField&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setChoices&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="cm"&gt;/* many choices */&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setPreferredChoices&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'published'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The preferred choices appear first, visually separated from the rest.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://symfony.com/bundles/EasyAdminBundle/current/fields/ChoiceField.html#setpreferredchoices" rel="noopener noreferrer"&gt;Read the docs about preferred choices&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Autocomplete Customization
&lt;/h2&gt;

&lt;p&gt;Association fields with many options use autocomplete by default. Until now, the dropdown displayed whatever &lt;code&gt;__toString()&lt;/code&gt; returned. Now you can customize how entries are formatted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using a callback&lt;/strong&gt; for simple formatting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;AssociationField&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;autocomplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s (%s)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getFullName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getEmail&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Using a Twig template&lt;/strong&gt; for rich HTML formatting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;AssociationField&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'product'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;autocomplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'admin/fields/product/autocomplete.html.twig'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;renderAsHtml&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The template receives the entity as the &lt;code&gt;entity&lt;/code&gt; variable. Set &lt;code&gt;renderAsHtml: true&lt;/code&gt; only when you trust the content (it disables XSS escaping):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="c"&gt;{# templates/admin/fields/product/autocomplete.html.twig #}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"product-option"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;entity.name&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-muted"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;(&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;entity.sku&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;)&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;entity.stock&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;10&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"badge badge-danger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Low Stock&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://symfony.com/bundles/EasyAdminBundle/current/fields/AssociationField.html#customizing-autocomplete-display" rel="noopener noreferrer"&gt;Read the docs about autocomplete() customization&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;✨ If you enjoyed these features and want to see more like it, consider&lt;br&gt;
&lt;a href="https://github.com/sponsors/javiereguiluz" rel="noopener noreferrer"&gt;sponsoring the EasyAdmin project&lt;/a&gt; 🙌💡&lt;/p&gt;

</description>
      <category>easyadmin</category>
      <category>symfony</category>
      <category>php</category>
      <category>csp</category>
    </item>
    <item>
      <title>New in EasyAdmin: Custom Admin Routes</title>
      <dc:creator>Javier Eguiluz</dc:creator>
      <pubDate>Mon, 22 Sep 2025 10:02:21 +0000</pubDate>
      <link>https://dev.to/javiereguiluz/new-in-easyadmin-custom-admin-routes-1176</link>
      <guid>https://dev.to/javiereguiluz/new-in-easyadmin-custom-admin-routes-1176</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/EasyCorp/EasyAdminBundle" rel="noopener noreferrer"&gt;EasyAdmin&lt;/a&gt; lets you create CRUD-based backends with minimal setup: define your entities, add CRUD controllers, get a complete admin interface.&lt;/p&gt;

&lt;p&gt;You can extend these CRUD controllers with &lt;a href="https://symfony.com/bundles/EasyAdminBundle/4.x/actions.html#adding-custom-actions" rel="noopener noreferrer"&gt;custom actions&lt;/a&gt; (e.g. &lt;code&gt;approve&lt;/code&gt; action for users, &lt;code&gt;duplicate&lt;/code&gt; action for products, etc.) These custom actions must live inside the CRUD controller, which isn't always practical.&lt;/p&gt;

&lt;p&gt;Also, your application has features that don't fit the CRUD pattern and require their own controllers with custom logic. You could always embed external controllers in EasyAdmin, but the integration was clunky. The external controllers were integrated via their Symfony routes from your main menu like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nc"&gt;MenuItem&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;linkToRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Business Stats'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fa fa-chart-bar'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'business_stats_index'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'param1'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'value1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'param2'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'value2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the generated URLs revealed the awkward integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/admin?routeName=business_stats_index&amp;amp;routeParams%5Bparam1%5D=value1&amp;amp;routeParams%5Bparam2%5D=value2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The &lt;code&gt;AdminRoute&lt;/code&gt; Attribute
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/EasyCorp/EasyAdminBundle/releases/tag/v4.25.0" rel="noopener noreferrer"&gt;EasyAdmin 4.25.0&lt;/a&gt; introduces the &lt;code&gt;#[AdminRoute]&lt;/code&gt; attribute for &lt;strong&gt;seamless Symfony controller integration&lt;/strong&gt;. Take the following Symfony controller to calculate some business stats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Stats\BusinessStatsCalculator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Bundle\FrameworkBundle\Controller\AbstractController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StatsController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;BusinessStatsCalculator&lt;/span&gt; &lt;span class="nv"&gt;$statsCalculator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;statsCalculator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a pure Symfony controller. It doesn't use any EasyAdmin code or feature. Now, add the &lt;code&gt;#[AdminRoute]&lt;/code&gt; attribute to your controller action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminRoute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StatsController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;

    &lt;span class="na"&gt;#[AdminRoute('/stats', name: 'stats')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;That's it&lt;/strong&gt;. The &lt;code&gt;#[AdminRoute]&lt;/code&gt; configuration in controllers is appended to the route path/name of the dashboard(s). If your Symfony application defines an &lt;a href="https://symfony.com/bundles/EasyAdminBundle/4.x/dashboards.html" rel="noopener noreferrer"&gt;admin dashboard&lt;/a&gt; with &lt;code&gt;/admin&lt;/code&gt; route path and &lt;code&gt;admin&lt;/code&gt; route name, this code will add an admin route called &lt;code&gt;admin_stats&lt;/code&gt; with path &lt;code&gt;/admin/stats&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Browse to that URL and your custom logic is rendered inside the backend with the same layout and navigation as your CRUD pages. Use the new route to link to the controller from your dashboard menu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;configureMenuItems&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;iterable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nc"&gt;MenuItem&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;linkToRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Statistics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fa fa-chart-bar'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'admin_stats'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click on that menu item and you'll see that the URL is now just &lt;code&gt;/admin/stats&lt;/code&gt;, without the previous awkward query parameters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Composing Routes
&lt;/h2&gt;

&lt;p&gt;Similar to Symfony's &lt;code&gt;#[Route]&lt;/code&gt; attribute, &lt;code&gt;#[AdminRoute]&lt;/code&gt; supports class-level configuration for common prefixes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminRoute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="na"&gt;#[AdminRoute('/reports', name: 'reports')]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReportController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;#[AdminRoute('/sales', name: 'sales')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="na"&gt;#[AdminRoute('/inventory', name: 'inventory')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="na"&gt;#[AdminRoute('/customers/{id}', name: 'customer')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;customerReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Customer&lt;/span&gt; &lt;span class="nv"&gt;$customer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates three admin routes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;admin_reports_sales&lt;/code&gt; at &lt;code&gt;/admin/reports/sales&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;admin_reports_inventory&lt;/code&gt; at &lt;code&gt;/admin/reports/inventory&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;admin_reports_customer&lt;/code&gt; at &lt;code&gt;/admin/reports/customers/{id}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The routing system concatenates the dashboard prefix, the class-level route, and the action-level route to build the final path and name.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;#[AdminRoute]&lt;/code&gt; attribute can also &lt;a href="https://symfony.com/bundles/EasyAdminBundle/4.x/crud.html#crud-routes" rel="noopener noreferrer"&gt;customize the default CRUD route names and paths&lt;/a&gt;, giving you complete control over your admin URL structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Dashboard Control
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;#[AdminRoute]&lt;/code&gt; creates one admin route per dashboard. If your application has multiple dashboards, you can control where routes are registered with &lt;code&gt;allowedDashboards&lt;/code&gt; (route is created only in these dashboards) and &lt;code&gt;deniedDashboards&lt;/code&gt; (route is created in all dashboards except these):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;AdminRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'/financial-reports'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'financial'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;allowedDashboards&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AdminDashboardController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FinancialReportController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;AdminRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'/user-reports'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;deniedDashboards&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;GuestDashboardController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserReportController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;#[AdminRoute]&lt;/code&gt; attribute solves a common need: keeping your business logic in dedicated controllers while providing a unified admin experience. No more awkward URL parameters. Your controllers stay pure Symfony code, but render like native EasyAdmin pages.&lt;/p&gt;




&lt;p&gt;✨ If you enjoyed this feature and want to see more like it, consider&lt;br&gt;
&lt;a href="https://github.com/sponsors/javiereguiluz" rel="noopener noreferrer"&gt;sponsoring the EasyAdmin project&lt;/a&gt; 🙌💡&lt;/p&gt;

</description>
      <category>easyadmin</category>
      <category>symfony</category>
      <category>php</category>
    </item>
    <item>
      <title>Reusing SVG Icons for Faster Pages</title>
      <dc:creator>Javier Eguiluz</dc:creator>
      <pubDate>Mon, 30 Jun 2025 07:54:57 +0000</pubDate>
      <link>https://dev.to/javiereguiluz/reusing-svg-icons-for-faster-pages-62o</link>
      <guid>https://dev.to/javiereguiluz/reusing-svg-icons-for-faster-pages-62o</guid>
      <description>&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/SVG" rel="noopener noreferrer"&gt;SVG&lt;/a&gt; is an XML-based vector graphics format that supports interactivity and animation. The &lt;a href="https://www.w3.org/TR/SVG11/" rel="noopener noreferrer"&gt;SVG 1.1 specification&lt;/a&gt; spans more than 800 pages, so there's a lot hidden inside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A little-known SVG feature&lt;/strong&gt; can make your site &lt;strong&gt;faster&lt;/strong&gt; when you repeat the same SVG images, such as icons.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Too Many SVG Images
&lt;/h2&gt;

&lt;p&gt;We recently redesigned the &lt;a href="https://symfony.com/packages" rel="noopener noreferrer"&gt;Symfony Packages&lt;/a&gt; page that lists every Symfony package (currently &lt;strong&gt;266&lt;/strong&gt; and counting) and lets you filter them with a bit of JavaScript.&lt;/p&gt;

&lt;p&gt;The page itself is simple, but there is a problem: &lt;strong&gt;it is massive&lt;/strong&gt;. With so many packages, even small elements add up quickly. For example, each package shows five SVG icons. That makes a total of 266 × 5 = 1,330 SVG images, just for icons.&lt;/p&gt;

&lt;p&gt;This is not a problem for the total page size. Thanks to HTTP compression, this 1.2MB HTML page only transfers 84KB to clients. However, adding thousands of images slows down parsing and rendering, and it also hurts the JavaScript-based sorting performance.&lt;/p&gt;

&lt;p&gt;We could render the SVG icons with &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements or as CSS backgrounds, but that approach doesn't let us change the icon's color, a must-have feature in our case. We could also load the list dynamically with JavaScript, but that only defers the rendering performance issue without actually solving it.&lt;/p&gt;

&lt;p&gt;Fortunately, SVG defines a feature that allows you to &lt;strong&gt;reuse an image as many times as you want within the same HTML page&lt;/strong&gt;. Our solution displays 1,330 SVG icons while only including 5 SVG definitions in the HTML source.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: SVG &lt;code&gt;&amp;lt;use&amp;gt;&lt;/code&gt; Element
&lt;/h2&gt;

&lt;p&gt;Take this GitHub SVG icon from the &lt;a href="https://simpleicons.org" rel="noopener noreferrer"&gt;Simple Icons collection&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1em"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"1em"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, add an &lt;code&gt;id&lt;/code&gt; attribute to the &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt; element inside the &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt;. Let's call it &lt;code&gt;icon-github&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1em"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"1em"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"icon-github"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip&lt;/strong&gt;&lt;br&gt;
For complex SVG images with multiple elements, wrap everything in a &lt;code&gt;&amp;lt;g&amp;gt;&lt;/code&gt; element and add the &lt;code&gt;id&lt;/code&gt; attribute to that grouping element.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now you can reuse this image unlimited times as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;use&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#icon-github"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/use&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/use" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;use&amp;gt;&lt;/code&gt; SVG element&lt;/a&gt; takes nodes from within an SVG document and duplicates them elsewhere. It clones the referenced nodes and places them where the &lt;code&gt;&amp;lt;use&amp;gt;&lt;/code&gt; element appears, without creating a shadow DOM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's it&lt;/strong&gt;. You can reuse the same SVG image unlimited times on the same page without including its contents repeatedly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Legacy SVG Attributes
&lt;/h3&gt;

&lt;p&gt;If the &lt;code&gt;href&lt;/code&gt; attribute in &lt;code&gt;&amp;lt;use&amp;gt;&lt;/code&gt; doesn't work in older browsers, fall back to the legacy &lt;code&gt;xlink:href&lt;/code&gt; attribute with the same value. To support both modern and legacy browsers, include both attributes (browsers prioritize &lt;code&gt;href&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt; &lt;span class="na"&gt;aria-labelledby=&lt;/span&gt;&lt;span class="s"&gt;"github-icon-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"github-icon-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Source code&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;use&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#icon-github"&lt;/span&gt; &lt;span class="na"&gt;xlink:href=&lt;/span&gt;&lt;span class="s"&gt;"#icon-github"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/use&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Styling of Reused SVG Images
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;use&amp;gt;&lt;/code&gt; element clones the referenced content. CSS properties like &lt;code&gt;fill&lt;/code&gt; and &lt;code&gt;stroke&lt;/code&gt; can cascade from the &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; instance, but only if they are not already defined on the source graphics.&lt;/p&gt;

&lt;p&gt;If the cloned contents are &lt;code&gt;&amp;lt;path d="..." fill="currentColor"&amp;gt;&lt;/code&gt;, you can override the color with &lt;code&gt;&amp;lt;svg fill="red"&amp;gt;&lt;/code&gt;. However, if the cloned contents are &lt;code&gt;&amp;lt;path d="..." fill="black"&amp;gt;&lt;/code&gt;, you can't override the color because the &lt;code&gt;fill="black"&lt;/code&gt; on the source path takes precedence.&lt;/p&gt;

&lt;p&gt;That's why it's &lt;strong&gt;recommended&lt;/strong&gt; to remove the &lt;code&gt;fill&lt;/code&gt; and &lt;code&gt;stroke&lt;/code&gt; attributes from the source SVG paths or set them to &lt;code&gt;currentColor&lt;/code&gt; so the cloned icons can be styled later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating SVG Sprites
&lt;/h2&gt;

&lt;p&gt;In our web page, we did the following: render icon definitions once in a hidden container, then reference them throughout the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- the browser won't display these icons --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display: none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;svg&amp;gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"icon-github"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;svg&amp;gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"icon-download"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;svg&amp;gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"icon-calendar"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;package&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;packages&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
    &lt;span class="c"&gt;{# ... #}&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;use&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#icon-github"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/use&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
        Code
    &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;{# ... #}&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endfor&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Accessibility tip&lt;/strong&gt;&lt;br&gt;
If an icon appears next to descriptive text (like "Code" in the example above), mark it as decorative with &lt;code&gt;aria-hidden="true"&lt;/code&gt; so screen readers can ignore it. If the icon stands alone (e.g., a button without text), make it accessible with a &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; element and &lt;code&gt;aria-labelledby&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While this approach works well, SVG provides a better way of doing it thanks to the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/defs" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;defs&amp;gt;&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/symbol" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;symbol&amp;gt;&lt;/code&gt;&lt;/a&gt; elements. The &lt;code&gt;&amp;lt;defs&amp;gt;&lt;/code&gt; element is a container for graphical objects that you want to define once and use later.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;symbol&amp;gt;&lt;/code&gt; element is not rendered directly; it defines graphical template objects that are rendered only when referenced by a &lt;code&gt;&amp;lt;use&amp;gt;&lt;/code&gt; element. It also supports its own &lt;code&gt;viewBox&lt;/code&gt; and &lt;code&gt;preserveAspectRatio&lt;/code&gt; attributes, making it a self-contained, reusable component.&lt;/p&gt;

&lt;p&gt;This collection of definitions inside a single SVG is commonly called an &lt;strong&gt;SVG sprite&lt;/strong&gt;: a single file that includes multiple reusable graphics.&lt;/p&gt;

&lt;p&gt;You can replace the previous &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;-based container with an inline sprite like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display: none;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;defs&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;symbol&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"icon-github"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M12 .297c-6.63 0-12 5.373-12 12c0 5.303..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/symbol&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;symbol&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"icon-download"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/symbol&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;symbol&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"icon-calendar"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/symbol&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/defs&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's &lt;strong&gt;recommended&lt;/strong&gt; to add the &lt;code&gt;viewBox&lt;/code&gt; attribute to each &lt;code&gt;&amp;lt;symbol&amp;gt;&lt;/code&gt; so you don't have to repeat it later. Instead, the icon instances only need &lt;code&gt;height&lt;/code&gt; and &lt;code&gt;width&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;use&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#icon-github"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/use&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SVG sprites can also be defined as external files and reference them on any page via its public URL and &lt;code&gt;id&lt;/code&gt; value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;use&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/path/to/svg-icons-sprite.svg#icon-github"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/use&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you reuse the same icons across many pages, put the SVG sprite in its own file so browsers can cache it and boost performance even further.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Define each icon once and reuse it with &lt;code&gt;&amp;lt;use&amp;gt;&lt;/code&gt; so your DOM stays light and renders faster.&lt;/li&gt;
&lt;li&gt;Bundle multiple icons into a &lt;code&gt;&amp;lt;symbol&amp;gt;&lt;/code&gt;-based sprite, which can be inlined or loaded from an external, cacheable file.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;✨ If you enjoyed this or any of my other articles and want to support my work, consider &lt;a href="https://github.com/sponsors/javiereguiluz" rel="noopener noreferrer"&gt;sponsoring me on GitHub&lt;/a&gt; 🙌&lt;/p&gt;




</description>
      <category>svg</category>
      <category>performance</category>
      <category>design</category>
      <category>web</category>
    </item>
    <item>
      <title>How to Make ChatGPT Codex Work with PHP and Symfony</title>
      <dc:creator>Javier Eguiluz</dc:creator>
      <pubDate>Thu, 05 Jun 2025 20:51:49 +0000</pubDate>
      <link>https://dev.to/javiereguiluz/how-to-make-chatgpt-codex-work-with-php-and-symfony-4lj8</link>
      <guid>https://dev.to/javiereguiluz/how-to-make-chatgpt-codex-work-with-php-and-symfony-4lj8</guid>
      <description>&lt;p&gt;&lt;a href="https://openai.com/codex/" rel="noopener noreferrer"&gt;Codex&lt;/a&gt; is a cloud-based coding agent created by OpenAI. It can run multiple tasks in parallel to fix bugs, implement features, suggest improvements, and more. It can even create pull requests on platforms like GitHub.&lt;/p&gt;

&lt;p&gt;Although Codex already works well with PHP, providing solid code suggestions and improvements, its most powerful feature is that it can &lt;strong&gt;run your code&lt;/strong&gt; (e.g., executing your test suite) to verify that its proposed changes actually work. This is a game-changer for languages like Python or JavaScript, where code execution is supported out of the box.&lt;/p&gt;

&lt;p&gt;PHP, however, is not natively supported for code execution. This article explains how to make Codex work effectively with PHP and Symfony applications, including how to configure it to run your test suite and validate changes automatically.&lt;/p&gt;

&lt;p&gt;As of this writing, Codex is available to Plus ($20/month) and Pro ($200/month) ChatGPT users, but not to free-tier users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Environment
&lt;/h2&gt;

&lt;p&gt;Visit &lt;a href="https://chatgpt.com/codex" rel="noopener noreferrer"&gt;chatgpt.com/codex&lt;/a&gt; and follow the on-screen steps to connect Codex to your GitHub repositories. You'll see which repositories you can import directly and which ones require admin approval if you only have limited access.&lt;/p&gt;

&lt;p&gt;Before Codex can work with a repository, you need to create an &lt;strong&gt;environment&lt;/strong&gt; for it. An environment defines how Codex interacts with your code.&lt;/p&gt;

&lt;p&gt;Go to &lt;a href="https://chatgpt.com/codex/settings/environments" rel="noopener noreferrer"&gt;chatgpt.com/codex/settings/environments&lt;/a&gt; and click the &lt;code&gt;Create environment&lt;/code&gt; button. Each environment is tied to a single GitHub repository, so you'll need to create one environment per repo.&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;Basic&lt;/strong&gt; section, choose the GitHub organization and repository of your PHP/Symfony project:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fglc9fj83cgk437r0yj2q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fglc9fj83cgk437r0yj2q.png" alt="Basic configuration of Codex environments" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;Code execution&lt;/strong&gt; section, configure the following options:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp8rmzsvgxalp02dfazul.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp8rmzsvgxalp02dfazul.png" alt="Code Execution configuration of Codex environments" width="800" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Container image&lt;/strong&gt;: Codex tasks run your code inside a Docker container. Currently, only the &lt;code&gt;universal&lt;/code&gt; image provided by Codex is available. Support for custom images is coming soon. Check the &lt;a href="https://github.com/openai/codex-universal/blob/main/Dockerfile" rel="noopener noreferrer"&gt;list of default installed packages&lt;/a&gt; in the &lt;code&gt;universal&lt;/code&gt; image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables&lt;/strong&gt; and &lt;strong&gt;Secrets&lt;/strong&gt;: self-explanatory; covered later in this article.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setup script&lt;/strong&gt;: this is &lt;em&gt;the&lt;/em&gt; critical part for PHP/Symfony projects. It's used to install PHP, Composer, and other tools not included in the default image. A full example is provided below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent internet access&lt;/strong&gt;: disabled by default. Enable it only if you absolutely need it (e.g., to download dependencies). Even OpenAI recommends keeping it off. Instead, use the setup script to download everything you need, as explained below.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setup Script
&lt;/h2&gt;

&lt;p&gt;The setup script runs at the beginning of every task, after the repository is cloned. It's your chance to prepare the environment for PHP/Symfony development and testing. Internet access is always enabled during this step, so you can install dependencies here even if it's disabled later.&lt;/p&gt;

&lt;p&gt;Here's a full example I use for Symfony apps. Once you've saved your environment, click &lt;code&gt;Connect interactive terminal&lt;/code&gt; to test and debug the script in the actual container Codex will use for tasks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# this makes the script strict:&lt;/span&gt;
&lt;span class="c"&gt;#   -e: exit on any error&lt;/span&gt;
&lt;span class="c"&gt;#   -u: error on undefined variables&lt;/span&gt;
&lt;span class="c"&gt;#   -o pipefail: exit if any command in a pipeline fails&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="c"&gt;# Adds a package repository that provides the latest PHP versions&lt;/span&gt;
&lt;span class="c"&gt;# (see https://deb.sury.org/ for details)&lt;/span&gt;
add-apt-repository &lt;span class="nt"&gt;-y&lt;/span&gt; ppa:ondrej/php
&lt;span class="c"&gt;# Updates package lists to get the latest information about available packages&lt;/span&gt;
apt-get update

&lt;span class="c"&gt;# Install PHP 8.4 and extensions commonly needed for Symfony applications&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    php8.4 &lt;span class="se"&gt;\&lt;/span&gt;
    php8.4-cli &lt;span class="se"&gt;\&lt;/span&gt;
    php8.4-mbstring &lt;span class="se"&gt;\&lt;/span&gt;
    php8.4-xml &lt;span class="se"&gt;\&lt;/span&gt;
    php8.4-intl &lt;span class="se"&gt;\&lt;/span&gt;
    php8.4-gd &lt;span class="se"&gt;\&lt;/span&gt;
    php8.4-zip &lt;span class="se"&gt;\&lt;/span&gt;
    php8.4-curl &lt;span class="se"&gt;\&lt;/span&gt;
    php8.4-pgsql &lt;span class="c"&gt;# this is for PostgreSQL; change this if you use MySQL&lt;/span&gt;

&lt;span class="c"&gt;# This makes PHP 8.4 available through the global 'php' binary,&lt;/span&gt;
&lt;span class="c"&gt;# which is expected by many commands&lt;/span&gt;
update-alternatives &lt;span class="nt"&gt;--install&lt;/span&gt; /usr/bin/php php /usr/bin/php8.4 84
update-alternatives &lt;span class="nt"&gt;--set&lt;/span&gt; php /usr/bin/php8.4

&lt;span class="c"&gt;# Install Composer as a global 'composer' binary following the safest practices&lt;/span&gt;
&lt;span class="nv"&gt;EXPECTED_CHECKSUM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://composer.github.io/installer.sig&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://getcomposer.org/installer &lt;span class="nt"&gt;-o&lt;/span&gt; composer-setup.php
&lt;span class="nv"&gt;ACTUAL_CHECKSUM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sha384sum &lt;/span&gt;composer-setup.php | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; 1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXPECTED_CHECKSUM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACTUAL_CHECKSUM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'ERROR: Invalid composer installer checksum.'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;rm &lt;/span&gt;composer-setup.php
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi
&lt;/span&gt;php composer-setup.php &lt;span class="nt"&gt;--install-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/bin &lt;span class="nt"&gt;--filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;composer &lt;span class="nt"&gt;--quiet&lt;/span&gt;
&lt;span class="nb"&gt;rm &lt;/span&gt;composer-setup.php

&lt;span class="c"&gt;# Install the PHP dependencies of your project;&lt;/span&gt;
&lt;span class="c"&gt;# private packages require some more work explained later in this article&lt;/span&gt;
composer &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Show installed versions to check if everything worked&lt;/span&gt;
php &lt;span class="nt"&gt;-v&lt;/span&gt;
composer &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Private Composer Packages
&lt;/h2&gt;

&lt;p&gt;If your project depends on private Composer packages (e.g., from GitHub), you'll need to authenticate Composer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(1)&lt;/strong&gt; Create a &lt;strong&gt;personal access token&lt;/strong&gt;: either a &lt;a href="https://github.com/settings/tokens" rel="noopener noreferrer"&gt;classic token&lt;/a&gt; or a &lt;a href="https://github.com/settings/personal-access-tokens" rel="noopener noreferrer"&gt;fine-grained token&lt;/a&gt;. Name it something like "OpenAI Codex" and give it read access to the private repositories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(2)&lt;/strong&gt; Add the token to Codex: go to your Codex environment settings, scroll to &lt;strong&gt;Secrets&lt;/strong&gt;, and click &lt;code&gt;Add +&lt;/code&gt;. Name the secret &lt;code&gt;COMPOSER_AUTH&lt;/code&gt;.  The value must be a JSON-encoded element with the token that you created earlier. The format is the same as used in the file &lt;code&gt;~/.composer/auth.json&lt;/code&gt; but you must put everything in a single line. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"github-oauth"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"github.com"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ghp_fs2dWqUilbS0NBeJaWBw3QgIucTO5RyrXV1e"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optionally, you can add a check in your setup script to make sure this secret is set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="c"&gt;# the script will fail if this env var / secret is not set&lt;/span&gt;
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;COMPOSER_AUTH&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using Databases
&lt;/h2&gt;

&lt;p&gt;If your test suite requires a database (e.g., MySQL or PostgreSQL), you'll need to install and configure it in the setup script. Here's how to do it:&lt;/p&gt;

&lt;p&gt;First, define the following environment variables: &lt;code&gt;POSTGRES_USER&lt;/code&gt;, &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;, and &lt;code&gt;POSTGRES_DB&lt;/code&gt;. These can use dummy values for local test databases. Make sure they match the values used in the &lt;code&gt;DATABASE_DSN&lt;/code&gt; option of your Symfony application.&lt;/p&gt;

&lt;p&gt;Then add these to the top of your script to verify they're defined:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="c"&gt;# make sure the required env vars are defined&lt;/span&gt;
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;COMPOSER_AUTH&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, install PostgreSQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Update package lists&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;

&lt;span class="c"&gt;# Install PostgreSQL&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; postgresql-16

&lt;span class="c"&gt;# Install PHP and common extensions&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start PostgreSQL and create the user/database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install Composer&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;

&lt;span class="c"&gt;# Start PostgreSQL&lt;/span&gt;
pg_dropcluster &lt;span class="nt"&gt;--stop&lt;/span&gt; 16 main &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;pg_createcluster &lt;span class="nt"&gt;--start&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres 16 main

&lt;span class="c"&gt;# Create the PostgreSQL user and database&lt;/span&gt;
su postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"createuser -s &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
su postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"psql -c &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ALTER USER &lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; WITH PASSWORD '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;';&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
su postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"createdb -O &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# (Optional) Verify that the user can log in&lt;/span&gt;
&lt;span class="c"&gt;# psql -U $POSTGRES_USER -d $POSTGRES_DB&lt;/span&gt;

&lt;span class="c"&gt;# Show installed versions&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
psql &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  General Settings
&lt;/h2&gt;

&lt;p&gt;Before running your first task, visit &lt;a href="https://chatgpt.com/codex/settings/general" rel="noopener noreferrer"&gt;chatgpt.com/codex/settings/general&lt;/a&gt; to configure global settings used across all projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom instructions&lt;/strong&gt;: used to customize the behavior of Codex; e.g., &lt;em&gt;"Always use English for code, comments, commits, and branch names"&lt;/em&gt;, &lt;em&gt;"Use concise branch names prefixed with feat- or bug-"&lt;/em&gt;, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branch format&lt;/strong&gt;: defaults to &lt;code&gt;codex/{feature}&lt;/code&gt; but you can change it to &lt;code&gt;{feature}&lt;/code&gt; or anything else.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Providing Metadata to Agents
&lt;/h2&gt;

&lt;p&gt;Although optional, adding metadata files can significantly improve how agents like Codex understand and navigate your codebase. These plain-text files describe how&lt;br&gt;
to run tests, what conventions to follow, and more. With good metadata, you'll get more accurate results and fewer mistakes.&lt;/p&gt;

&lt;p&gt;There's no universal standard yet, so you'll need one file per agent. In my projects, I use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;project-root&amp;gt;/AGENTS.md&lt;/code&gt; for ChatGPT Codex&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;project-root&amp;gt;/CLAUDE.md&lt;/code&gt; for Claude AI&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;project-root&amp;gt;/.github/copilot-instructions.md&lt;/code&gt; for GitHub Copilot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three files usually share the same content. You can create one file and use symlinks for the others. Claude also supports importing files using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;See @AGENTS.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see a real-world example of &lt;a href="https://github.com/EasyCorp/EasyAdminBundle/blob/4.x/AGENTS.md" rel="noopener noreferrer"&gt;the AGENTS.md file I use in one of my projects&lt;/a&gt;. As you run tasks, update this file continuously with new instructions to prevent Codex from making the same mistakes again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and Privacy Recommendations
&lt;/h2&gt;

&lt;p&gt;In addition to disabling &lt;strong&gt;Agent internet access&lt;/strong&gt;, you should review the privacy settings at &lt;a href="https://chatgpt.com/codex/settings/data" rel="noopener noreferrer"&gt;chatgpt.com/codex/settings/data&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model improvement&lt;/strong&gt;: enabled by default; allows OpenAI to use your code to improve their models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Include environments&lt;/strong&gt;: lets OpenAI learn from your Codex environment settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Out of the box, ChatGPT Codex doesn't support executing PHP code, which limits its potential. But with the right setup, you can unlock full task automation in Symfony and PHP projects.&lt;/p&gt;

&lt;p&gt;Once configured, Codex becomes more than a code reviewer: it becomes a real coding assistant that understands your stack and validates its own changes.&lt;/p&gt;

&lt;p&gt;Try it out and leave your comments if you have any question so I can update this article.&lt;/p&gt;




&lt;p&gt;✨ If you enjoyed this or any of my other articles and want to support my work, consider &lt;a href="https://github.com/sponsors/javiereguiluz" rel="noopener noreferrer"&gt;sponsoring me on GitHub&lt;/a&gt; 🙌&lt;/p&gt;




</description>
      <category>ai</category>
      <category>codex</category>
      <category>php</category>
      <category>symfony</category>
    </item>
    <item>
      <title>Is Your CSS Logical?</title>
      <dc:creator>Javier Eguiluz</dc:creator>
      <pubDate>Tue, 26 Nov 2024 07:54:56 +0000</pubDate>
      <link>https://dev.to/javiereguiluz/is-your-css-logical-33oa</link>
      <guid>https://dev.to/javiereguiluz/is-your-css-logical-33oa</guid>
      <description>&lt;p&gt;Take a look at this CSS snippet. What's wrong with it?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="no"&gt;red&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80ch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on your website audience, there are either &lt;strong&gt;zero or three errors&lt;/strong&gt;. Before explaining which errors, let's set some context.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Matter of Perspective
&lt;/h2&gt;

&lt;p&gt;When you step onto a boat, you won't hear terms like "left" or "right". That's because the left/right sides of a boat depend on the observer's perspective. Instead, they use &lt;a href="https://en.wikipedia.org/wiki/Port_and_starboard" rel="noopener noreferrer"&gt;"port" and "starboard"&lt;/a&gt;, unambigous terms that always refer to the same sides of the boat, regardless of your position or the speaker's:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fybstxj8jji81astxap3o.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fybstxj8jji81astxap3o.jpg" alt="The names of the main ship parts" width="800" height="827"&gt;&lt;/a&gt;&lt;br&gt;
&lt;small&gt;Image created by Pearson Scott Foresman and released into the public domain. &lt;a href="https://commons.wikimedia.org/wiki/File:Aft_(PSF).jpg" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;The same principle applies to &lt;a href="https://en.wikipedia.org/wiki/Anatomical_terms_of_location" rel="noopener noreferrer"&gt;anatomical terms of location&lt;/a&gt;, which allow doctors and veterinarians to describe the location of body parts unambiguously, regardless of the relative position of the patient or doctor.&lt;/p&gt;
&lt;h2&gt;
  
  
  CSS Internationalization
&lt;/h2&gt;

&lt;p&gt;If your web applications are used globally, you must design them to adapt to various linguistic needs. For example, languages like English and Spanish are written from left-to-right (LTR); Arabic and Hebrew are written from right-to-left (RTL); Mongolian and traditional Japanese are written from top to bottom.&lt;/p&gt;

&lt;p&gt;So, when you use a CSS declaration like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;margin-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do you mean that &lt;strong&gt;(1)&lt;/strong&gt; you want to add space to the physical left of the paragraph, or &lt;strong&gt;(2)&lt;/strong&gt; you want to add some space before the content starts? For a fully internationalized UI, the correct answer is always &lt;strong&gt;(2)&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Logical Solution
&lt;/h2&gt;

&lt;p&gt;You could create separate stylesheets for LTR and RTL languages and load them conditionally. There are even tools like the &lt;a href="https://www.npmjs.com/package/@automattic/webpack-rtl-plugin" rel="noopener noreferrer"&gt;webpack-rtl plugin&lt;/a&gt; that can generate automatically a RTL stylesheet based on the original LTR stylesheet.&lt;/p&gt;

&lt;p&gt;However, the best solution would be to apply CSS styles conditionally like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;if&lt;/span&gt; &lt;span class="err"&gt;writing&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="py"&gt;left-to-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;margin-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="err"&gt;elseif&lt;/span&gt; &lt;span class="err"&gt;writing&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="py"&gt;right-to-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;margin-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="err"&gt;elseif&lt;/span&gt; &lt;span class="err"&gt;writing&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="py"&gt;top-to-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="err"&gt;endif&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can do this in CSS but with a much simpler syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;margin-inline-start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;margin-inline-start&lt;/code&gt; property is a &lt;strong&gt;logical CSS property&lt;/strong&gt; which dynamically adjusts based on the user's writing direction. Logical properties work similarly to the port/starboard analogy; they describe layout in a way that is unambiguous across different writing systems.&lt;/p&gt;

&lt;p&gt;Logical properties define &lt;strong&gt;layout directions&lt;/strong&gt; using these two terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;inline&lt;/code&gt;: parallel to the flow of text within a line.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;block&lt;/code&gt;: perpendicular to the flow of text within a line.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This illustration shows the logical positions compared to the physical locations for all the writing modes supported by CSS:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8s02v2isu41emfzuis7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8s02v2isu41emfzuis7.jpg" alt="CSS logical properties and writing modes" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using logical properties, the initial example shown at the beginning of this article can be rewritten like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;border-block-start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="no"&gt;red&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;margin-inline-start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80ch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Updating an existing CSS stylesheet to use logical properties might seem daunting at first. However, most of the work involves simply replacing  &lt;code&gt;left&lt;/code&gt; with &lt;code&gt;inline-start&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt; with &lt;code&gt;inline-end&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt; with &lt;code&gt;block-start&lt;/code&gt;, and &lt;code&gt;bottom&lt;/code&gt; with &lt;code&gt;block-end&lt;/code&gt;. Some properties require different renaming; for example, &lt;code&gt;border-bottom-left-radius&lt;/code&gt; becomes &lt;code&gt;border-end-start-radius&lt;/code&gt;, &lt;code&gt;height&lt;/code&gt; becomes &lt;code&gt;block-size&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;The effort is well worth it, as this is a &lt;strong&gt;future-proof solution&lt;/strong&gt; that ensures your website is accessible to everyone. For instance, the &lt;a href="https://github.com/EasyCorp/EasyAdminBundle" rel="noopener noreferrer"&gt;EasyAdmin&lt;/a&gt; project, which I'm involved in, has already &lt;a href="https://github.com/EasyCorp/EasyAdminBundle/pull/6525/files" rel="noopener noreferrer"&gt;updated its stylesheets&lt;/a&gt; to use logical properties.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logical Properties Reference
&lt;/h2&gt;

&lt;p&gt;Here's a reference table of all the logical properties to help you update your own projects:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Physical Property&lt;/th&gt;
&lt;th&gt;Logical Property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-bottom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-block-end&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-bottom-color&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-block-end-color&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-bottom-left-radius&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-end-start-radius&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-bottom-right-radius&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-end-end-radius&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-bottom-style&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-block-end-style&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-bottom-width&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-block-end-width&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-left&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-inline-start&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-left-color&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-inline-start-color&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-left-style&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-inline-start-style&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-left-width&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-inline-start-width&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-right&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-inline-end&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-right-color&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-inline-end-color&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-right-style&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-inline-end-style&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-right-width&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-inline-end-width&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-top&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-block-start&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-top-color&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-block-start-color&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-top-left-radius&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-start-start-radius&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-top-right-radius&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-start-end-radius&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-top-style&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-block-start-style&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-top-width&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-block-start-width&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bottom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inset-block-end&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container-intrinsic-height&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contain-intrinsic-block-size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container-intrinsic-width&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contain-intrinsic-inline-size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;height&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;block-size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;left&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inset-inline-start&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;margin-bottom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;margin-block-end&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;margin-left&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;margin-inline-start&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;margin-right&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;margin-inline-end&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;margin-top&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;margin-block-start&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max-height&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max-block-size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max-width&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max-inline-size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;min-height&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;min-block-size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;min-width&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;min-inline-size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;overscroll-behavior-x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;overscroll-behavior-inline&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;overscroll-behavior-y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;overscroll-behavior-block&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;overflow-x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;overflow-inline&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;overflow-y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;overflow-block&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;padding-bottom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;padding-block-end&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;padding-left&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;padding-inline-start&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;padding-right&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;padding-inline-end&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;padding-top&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;padding-block-start&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;right&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inset-inline-end&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;top&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inset-block-start&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;width&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inline-size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Learn More
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_logical_properties_and_values" rel="noopener noreferrer"&gt;MDN: CSS logical properties and values&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;✨ If you enjoyed this or my other articles and want to support my work, consider &lt;a href="https://github.com/sponsors/javiereguiluz" rel="noopener noreferrer"&gt;sponsoring me on GitHub&lt;/a&gt; 🙌&lt;/p&gt;




</description>
      <category>css</category>
      <category>design</category>
      <category>i18n</category>
      <category>l10n</category>
    </item>
    <item>
      <title>New in EasyAdmin: Pretty URLs</title>
      <dc:creator>Javier Eguiluz</dc:creator>
      <pubDate>Sun, 03 Nov 2024 12:01:19 +0000</pubDate>
      <link>https://dev.to/javiereguiluz/new-in-easyadmin-pretty-urls-2knk</link>
      <guid>https://dev.to/javiereguiluz/new-in-easyadmin-pretty-urls-2knk</guid>
      <description>&lt;p&gt;The most requested feature for EasyAdmin has just arrived in &lt;a href="https://github.com/EasyCorp/EasyAdminBundle/releases/tag/v4.14.0" rel="noopener noreferrer"&gt;version 4.14.0&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since day one, EasyAdmin has used query string parameters to pass the necessary&lt;br&gt;
information for rendering backend pages. This approach created URLs like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/admin?crudAction=edit&amp;amp;crudControllerFqcn=App%5CController%5CAdmin%5CPostCrudController&amp;amp;entityId=3874
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this URL format is ugly, it's functional. Some browsers, like Safari, even&lt;br&gt;
hide most of the URL by default, showing only the hostname, so the impact on&lt;br&gt;
end-users is minimal.&lt;/p&gt;

&lt;p&gt;However, starting today, EasyAdmin generates cleaner URLs that look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/admin/post/3874/edit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition to being the most requested feature, we implemented this change for&lt;br&gt;
two main reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The old, ugly URL leaks internal details about the application, like the
namespace of the CRUD controller. This is typically fine, as most backends
aren't sharing URLs with external users, but it's not ideal.&lt;/li&gt;
&lt;li&gt;Building these ugly URLs with the &lt;code&gt;AdminUrlGenerator&lt;/code&gt; class is cumbersome. Now,
you can use route names like  &lt;code&gt;admin_post_edit&lt;/code&gt; or &lt;code&gt;admin_user_index&lt;/code&gt; and rely
on Symfony's built-in features to generate the URLs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How to Enable Pretty URLs?
&lt;/h2&gt;

&lt;p&gt;This feature has been designed to work seamlessly with current applications. If&lt;br&gt;
you don't make any changes, the application will still use ugly URLs, and&lt;br&gt;
everything will continue to work as before.&lt;/p&gt;

&lt;p&gt;If you'd like to start using pretty URLs now, create the following configuration&lt;br&gt;
file in your application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes/easyadmin.yaml&lt;/span&gt;
&lt;span class="na"&gt;easyadmin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;easyadmin.routes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration enables a &lt;a href="https://symfony.com/doc/current/routing/custom_route_loader.html" rel="noopener noreferrer"&gt;custom Symfony route loader&lt;/a&gt;, a class that&lt;br&gt;
automatically generates routes in your application. Clear the cache (by running&lt;br&gt;
&lt;code&gt;php bin/console cache:clear&lt;/code&gt; or &lt;code&gt;rm -fr var/cache/*&lt;/code&gt;) and the application will&lt;br&gt;
begin using pretty URLs. Run the following command to see all the newly generated&lt;br&gt;
routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;php bin/console debug:router
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can now also update your code to stop building backend URLs with the&lt;br&gt;
&lt;code&gt;AdminUrlGenerator&lt;/code&gt; utility and use the new route names directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before:&lt;/span&gt;
&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;adminUrlGenerator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;// this is only needed if you have ore than 1 dashboard&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setDashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SomeDashboardController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductCrudController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'detail'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setEntityId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;321&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generateUrl&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// After:&lt;/span&gt;
&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'admin_product_detail'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'entityId'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;321&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated routes are composed of three parts: dashboard + crud + action. Each&lt;br&gt;
of these can be customized using the new PHP attributes &lt;code&gt;#[AdminDashboard]&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;#[AdminCrud]&lt;/code&gt;, and &lt;code&gt;#[AdminAction]&lt;/code&gt;. &lt;a href="https://symfony.com/bundles/EasyAdminBundle/current/crud.html#crud-routes" rel="noopener noreferrer"&gt;Read the docs&lt;/a&gt; to learn more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pretty URLs are still optional&lt;/strong&gt; in EasyAdmin 4.x versions, but ugly URLs are now&lt;br&gt;
deprecated. &lt;strong&gt;Starting in EasyAdmin 5.x&lt;/strong&gt; (planned for release soon), &lt;strong&gt;pretty URLs&lt;br&gt;
will be required.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;✨ If you enjoyed this feature and want to see more like it, consider&lt;br&gt;
&lt;a href="https://github.com/sponsors/javiereguiluz" rel="noopener noreferrer"&gt;sponsoring the EasyAdmin project&lt;/a&gt; 🙌💡&lt;/p&gt;

</description>
      <category>easyadmin</category>
      <category>symfony</category>
      <category>php</category>
    </item>
    <item>
      <title>Generating deterministic UUIDs from arbitrary strings with Symfony</title>
      <dc:creator>Javier Eguiluz</dc:creator>
      <pubDate>Wed, 26 Jun 2024 09:26:00 +0000</pubDate>
      <link>https://dev.to/javiereguiluz/generating-deterministic-uuids-from-arbitrary-strings-with-symfony-4ac6</link>
      <guid>https://dev.to/javiereguiluz/generating-deterministic-uuids-from-arbitrary-strings-with-symfony-4ac6</guid>
      <description>&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Universally_unique_identifier" rel="noopener noreferrer"&gt;UUIDs&lt;/a&gt; are 128-bit numbers used to identify items uniquely. You've probably seen or used UUIDs in your applications before. They are usually represented as an hexadecimal value with the format 8-4-4-4-12 (e.g. &lt;code&gt;6ba7b814-9dad-11d1-80b4-00c04fd430c8&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Most developers use random UUIDs, which don't contain any information about where or when they were generated. These are technically called UUIDv4 and they are one of the eight different types of UUIDs available.&lt;/p&gt;

&lt;p&gt;Symfony provides the &lt;a href="https://symfony.com/uid" rel="noopener noreferrer"&gt;UID component&lt;/a&gt; to generate UUIDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Uid\Uuid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// $uuid is an instance of Symfony\Component\Uid\UuidV4&lt;/span&gt;
&lt;span class="c1"&gt;// and its value is a random UUID&lt;/span&gt;
&lt;span class="nv"&gt;$uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Uuid&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;v4&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this article, we'll focus on UUIDv5, which generates UUIDs based on a name and a namespace. Before diving into it, let's explain the problem they solve.&lt;/p&gt;

&lt;p&gt;Imagine that you work on an e-commerce application and need to include the product ID in some URLs (e.g. &lt;code&gt;/show/{productId}/{productSlug}&lt;/code&gt;). The product IDs are unique within your application, but it's internal information and you are not comfortable sharing it publicly.&lt;/p&gt;

&lt;p&gt;If the product IDs are numbers, then you can use tools like &lt;a href="https://sqids.org/php" rel="noopener noreferrer"&gt;Sqids&lt;/a&gt; (formerly known as Hashids) to generate deterministic (and reversible) unique IDs from a given number(s). However, this problem can also be solved with UUIDs.&lt;/p&gt;

&lt;p&gt;UUIDv5 generate UUIDs whose contents are based on a given &lt;code&gt;name&lt;/code&gt; (any arbitrary string) and a &lt;code&gt;namespace&lt;/code&gt;. The &lt;code&gt;namespace&lt;/code&gt; is used to ensure that all the names that belong to it are unique within that namespace. The spec defines a few standard namespaces (for generating UUIDs based on URLs, DNS entries, etc.) but you can use any other UUID (e.g. a random UUID) as the namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Uid\Uuid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// this value was generated randomly using Uuid::v4()&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;string&lt;/span&gt; &lt;span class="no"&gt;UUID_NAMESPACE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'8be5ecb9-1eba-4927-b4d9-73eaa98f8b65'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// ...&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;setUuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Product&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$namespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Uuid&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UUID_NAMESPACE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setUuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Uuid&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;v5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getProductId&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="c1"&gt;// e.g. if the productId is 'acme-1234',&lt;/span&gt;
        &lt;span class="c1"&gt;// the generated UUID is 'eacc432f-a7c1-5750-8f9f-9d69cb287987'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;UUIDv5 roughly performs &lt;code&gt;sha1($namespace.$name)&lt;/code&gt; when generating values. This way, the generated UUIDs are not reversible (or guessable by external actors) and are deterministic (they always generate the same UUID for a given string).&lt;/p&gt;

&lt;p&gt;If you want to make the public IDs shorter, use any of the methods provided by Symfony to &lt;a href="https://symfony.com/doc/current/components/uid.html#converting-uuids" rel="noopener noreferrer"&gt;convert UUIDs&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$namespaceAsString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'8be5ecb9-1eba-4927-b4d9-73eaa98f8b65'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$namespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Uuid&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$namespaceAsString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'acme-1234'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Uuid&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;v5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// (string) $uuid = 'eacc432f-a7c1-5750-8f9f-9d69cb287987'&lt;/span&gt;

&lt;span class="nv"&gt;$shortUuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$uuid&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toBase58&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// $shortUuid = 'VzeJE1ydqWXpJJMwnavj3t'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UUID spec defines many other types of UUIDs which fit different scenarios. There's even a UUIDv3 which is the same as UUIDv5 but uses &lt;code&gt;md5&lt;/code&gt; hashes instead of &lt;code&gt;sha1&lt;/code&gt;. That's why UUIDv5 is preferred over UUIDv3.&lt;/p&gt;

&lt;p&gt;Check out the &lt;a href="https://symfony.com/doc/current/components/uid.html#generating-uuids" rel="noopener noreferrer"&gt;Symfony docs about the different types of UUIDs&lt;/a&gt; to know more about them.&lt;/p&gt;

</description>
      <category>symfony</category>
      <category>php</category>
      <category>uid</category>
      <category>uuid</category>
    </item>
  </channel>
</rss>
