Unlocking the Mystery of Custom Twig Functions
When Drupal 8 came out, it introduced the Drupal community to the concept of separating the theming layer from the logic layer through the Twig templating language. This had many advantages. For one, instead of needing to know PHP, a themer could just know Twig – an extremely stripped-down templating engine that has a few syntax elements, minimal built-in operators, and only about 50 provided functions. Additionally, the limited list of available functions makes it much harder to shoehorn heavy logic or even more dangerous things like SQL queries directly into the theming layer.
There are, of course, trade-offs. While the list of functions Twig provides is quite robust, it didn’t take long for Drupal developers to recognize snippets of code they needed to do over and over again within their templates. As such, Drupal provided a handful of custom functions and filters specific to core functionality, like the ‘without’ filter, which “Removes child elements from a copy of the original array” – i.e. lets you print the contents of a render array without certain fields.
The contrib space provides even more options for Twig themers – modules like Twig Tweak or Twig Field Value provides additional functions and filters for things themers often need to do over and over, like render a block within a template, or grab a field’s raw value.
But sometimes you still find yourself repeating the same code over and over within your templates. Maybe it’s a bit of logic specific to the project you’re working on. Or maybe it’s a piece of code that needs logic too complex to be put in a template. Or maybe it requires context not easily accessible within the template, like a setting controlled in the Drupal admin area.
In these cases you can create your own custom Twig functions.
What you’ll need:
Creating a custom Twig filter is relatively simple. You’ll just need the following things to begin:
- A custom module you can add code to
- A service declaration .yml file with reference to the twig.extension tag
- A class that extends the
\Twig_Extension
class
If you have those things, you can create as many Twig filters or functions as you’d like from within one class file. Making the filters or functions available is just one cache clear away!
A practical example:
Note, the finished module created below is also available here.
Let’s say you want to print out a field label within a template. For example, the default Drupal 8 Article display shows the label for the Tags field:
Let’s say we also, for some reason, wanted to put the “Tags” label somewhere else on the page, like right under the node’s “Submitted” information. Out of the box, you can use Twig to get the label’s raw value if you’re extending your node.twig.html file:
<span class="cp">{{</span> <span class="nv">content.field_tags</span><span class="p">[</span><span class="s1">'#title'</span><span class="p">]</span> <span class="cp">}}</span>
That works!
Now, suppose a site builder decides they don’t want to show the label on the Tags field anymore. They go into the admin area, change the field value on the label from “Above” to “Hidden”, and reload the entity.
If you’re using the Twig Field Value module and use this syntax:
<span class="cp">{{</span> <span class="nv">content.field_tags</span><span class="o">|</span><span class="nf">field_label</span> <span class="cp">}}</span>
The same thing happens. That’s because the field_label function doesn’t take into account the settings on the admin side either.
If we want to print out a label in a custom place, but still have the label respect the admin settings, we can create our own Twig function. Building on the idea of the Twig Field Value module’s filter “field_label” filter, let’s call our custom function “field_respectful_label”.
As outlined above, first you need to create a service. The quickest way to do this is using the Drupal console “generate:service” command.
Let’s assume you’ve already got a module called “custom_twig”. The command to generate this service would look like this:
<span class="nv">$ </span>drupal generate:service // Welcome to the Drupal service generator Enter the module name <span class="o">[</span>address]: <span class="o">></span> custom_twig Enter the service name <span class="o">[</span>custom_twig.default]: <span class="o">></span> custom_twig.my_custom_twig_items Enter the Class name <span class="o">[</span>DefaultService]: <span class="o">></span> MyCustomTwigItems Create an interface <span class="o">(</span><span class="nb">yes</span>/no<span class="o">)</span> <span class="o">[</span><span class="nb">yes</span><span class="o">]</span>: <span class="o">></span> no Do you want to load services from the container? <span class="o">(</span><span class="nb">yes</span>/no<span class="o">)</span> <span class="o">[</span>no]: <span class="o">></span> no Enter the path <span class="k">for </span>the services <span class="o">[</span>/modules/custom/custom_twig/src/]: <span class="o">></span> Do you want proceed with the operation? <span class="o">(</span><span class="nb">yes</span>/no<span class="o">)</span> <span class="o">[</span><span class="nb">yes</span><span class="o">]</span>: <span class="o">></span> // cache:rebuild Rebuilding cache<span class="o">(</span>s<span class="o">)</span>, <span class="nb">wait </span>a moment please. <span class="o">[</span>OK] Done clearing cache<span class="o">(</span>s<span class="o">)</span><span class="nb">.</span>Generated or updated files 1 - modules/custom/custom_twig/custom_twig.services.yml 2 - modules/custom/custom_twig/src/MyCustomTwigItems.php
This creates two files. The custom_twig.services.yml file:
<span class="na">services</span><span class="pi">:</span> <span class="s">custom_twig.my_custom_twig_items</span><span class="pi">:</span> <span class="na">class</span><span class="pi">:</span> <span class="s">Drupal\custom_twig\MyCustomTwigItems</span> <span class="na">arguments</span><span class="pi">:</span> <span class="pi">[]</span>
And the MyCustomTwigItems php file:
<span class="nt"><</span><span class="err">?</span><span class="na">php</span><span class="na">namespace</span> <span class="na">Drupal</span><span class="err">\</span><span class="na">custom_twig</span><span class="err">;</span><span class="err">/**</span> <span class="err">*</span> <span class="na">Class</span> <span class="na">MyCustomTwigItems</span><span class="err">.</span> <span class="err">*/</span><span class="na">class</span> <span class="na">MyCustomTwigItems</span> <span class="err">{</span> <span class="err">/**</span> <span class="err">*</span> <span class="na">Constructs</span> <span class="na">a</span> <span class="na">new</span> <span class="na">MyCustomTwigItems</span> <span class="na">object</span><span class="err">.</span> <span class="err">*/</span> <span class="na">public</span> <span class="na">function</span> <span class="na">__construct</span><span class="err">()</span> <span class="err">{</span> <span class="err">}</span><span class="err">}</span>
We now need to alter these files slightly. For the services.yml file, we will need to remove the “arguments” line, and add a reference to the twig extension tag:
<span class="na">services</span><span class="pi">:</span> <span class="s">custom_twig.my_custom_twig_items</span><span class="pi">:</span> <span class="na">class</span><span class="pi">:</span> <span class="s">Drupal\custom_twig\MyCustomTwigItems</span> <span class="na">tags</span><span class="pi">:</span> <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">twig.extension</span> <span class="pi">}</span>
In the MyCustomTwigItems.php file, we can make a few changes as well. First, the MyCustomTwigItems class must extend the \Twig_Extension class. We can also get rid of the constructor, which leaves you with a very bare class declaration:
<span class="sd">/** * Class MyCustomTwigItems. */</span><span class="k">class</span> <span class="nc">MyCustomTwigItems</span> <span class="k">extends</span> <span class="nx">\Twig_Extension</span> <span class="p">{</span><span class="p">}</span>
You now have a skeleton service in place, but it’s currently not doing anything. To let Twig know about a new Twig filter, you implement the “getFilters” method on this class:
<span class="sd">/** * {@inheritdoc} */</span> <span class="k">public</span> <span class="k">function</span> <span class="nf">getFilters</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="p">[</span> <span class="k">new</span> <span class="nx">\Twig_SimpleFilter</span><span class="p">(</span><span class="s1">'field_respectful_label'</span><span class="p">,</span> <span class="p">[</span><span class="nv">$this</span><span class="p">,</span> <span class="s1">'getRespectfulFieldLabel'</span><span class="p">]),</span> <span class="p">];</span> <span class="p">}</span>
The syntax above links up the string “field_respectful_label” (which will be used in our twig templates) to the method “getRespectfulFieldLabel”, a function we’ll create within our current MyCustomTwigItems class. Note that this syntax means you can call your custom filter whatever you want, although you will want to stick to the accepted coding style for Twig functions (lowercase letters and underscores, aka snake_case) and Drupal class methods (lowerCamelCase). Also notice that here we’re extending the “getFilters()” method on the parent class. There’s also a “getFunctions()” method, which is how you’d define a function. For more on the difference between Filters and Functions in Twig, refer to the Extending Twig documentation.
Then you just write your custom method! Here’s the full code for the getRespectfulFieldLabel method, which you’d place anywhere within the MyCustomTwigItems class declaration:
<span class="sd">/** * Twig filter callback: Only return a field's label if not hidden. * * @param array $build * Render array of a field. * * @return string * The label of a field. If $build is not a render array of a field, NULL is * returned. */</span> <span class="k">public</span> <span class="k">function</span> <span class="nf">getRespectfulFieldLabel</span><span class="p">(</span><span class="k">array</span> <span class="nv">$build</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Only proceed if this is a renderable field array.</span> <span class="k">if</span> <span class="p">(</span><span class="nb">isset</span><span class="p">(</span><span class="nv">$build</span><span class="p">[</span><span class="s1">'#theme'</span><span class="p">])</span> <span class="o">&&</span> <span class="nv">$build</span><span class="p">[</span><span class="s1">'#theme'</span><span class="p">]</span> <span class="o">==</span> <span class="s1">'field'</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Find out the label value.</span> <span class="nv">$disrespectful_label</span> <span class="o">=</span> <span class="nb">isset</span><span class="p">(</span><span class="nv">$build</span><span class="p">[</span><span class="s1">'#title'</span><span class="p">])</span> <span class="o">?</span> <span class="nv">$build</span><span class="p">[</span><span class="s1">'#title'</span><span class="p">]</span> <span class="o">:</span> <span class="k">NULL</span><span class="p">;</span> <span class="c1">// Find out the visibility status of the label.</span> <span class="nv">$display_label</span> <span class="o">=</span> <span class="nb">isset</span><span class="p">(</span><span class="nv">$build</span><span class="p">[</span><span class="s1">'#label_display'</span><span class="p">])</span> <span class="o">?</span> <span class="p">(</span><span class="nv">$build</span><span class="p">[</span><span class="s1">'#label_display'</span><span class="p">]</span> <span class="o">!=</span> <span class="s1">'hidden'</span><span class="p">)</span> <span class="o">:</span> <span class="k">FALSE</span><span class="p">;</span> <span class="k">return</span> <span class="p">(</span><span class="nv">$disrespectful_label</span> <span class="o">&&</span> <span class="nv">$display_label</span><span class="p">)</span> <span class="o">?</span> <span class="nv">$disrespectful_label</span> <span class="o">:</span> <span class="k">NULL</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="k">NULL</span><span class="p">;</span> <span class="p">}</span>
If we now use our custom twig filter in the node.html.twig file, the label behaves as expected:
<span class="cp">{{</span> <span class="nv">content.field_tags</span><span class="o">|</span><span class="nf">field_respectful_label</span> <span class="cp">}}</span>
There is now no label showing up on the front end, because we’ve hidden the label in the backend. If I went and changed the label visibility setting to “Above”, the label does show up. No code changes required:
And with that you have a new, custom Twig filter.
In conclusion, I hope this article demystifies the functions and filters you can use with Twig in Drupal 8. And if you’re inspired, feel free to use the example code as a starting point for your own custom functions or filters.