Jérôme Tamarelle
Contributed by Jérôme Tamarelle in #52748

Twig extensions allow you to add new features to Twig templates using your own logic. In Symfony applications, they are created by extending the AbstractExtension class and defining the custom filters and functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Twig/AppExtension.php
namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

class AppExtension extends AbstractExtension
{
    public function getFilters(): array
    {
        return [
            new TwigFilter('product_number', [$this, 'formatProductNumber']),
        ];
    }

    public function formatProductNumber(string $number): string
    {
        // ...
    }
}

To improve performance, it's common to split the extension into two classes: one to declare the filters/functions and another to contain the logic, which is loaded only when the filter or function is actually used in a template:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

class AppExtension extends AbstractExtension
{
    public function getFilters(): array
    {
        return [
            new TwigFilter('product_number', [AppRuntime::class, 'formatProductNumber']),
        ];
    }
}


use Twig\Extension\RuntimeExtensionInterface;

class AppRuntime implements RuntimeExtensionInterface
{
    public function formatProductNumber(string $number): string
    {
        // ...
    }
}

In Symfony 7.3, we're making custom Twig extensions even simpler and more powerful thanks to PHP attributes. Here's what the same extension looks like now:

1
2
3
4
5
6
7
8
9
10
11
12
namespace App\Twig;

use Twig\Attribute\AsTwigFilter;

class AppExtension
{
    #[AsTwigFilter('product_number')]
    public function formatProductNumber(string $number): string
    {
        // ...
    }
}

With this new approach:

  • You no longer need to extend the AbstractExtension base class;
  • Your extensions are lazy-loaded by default, so you get the same performance as before without having to split them into two classes;
  • You don't need to define getFilters() and getFunctions() methods; just add the corresponding attribute (#[AsTwigFilter], #[AsTwigFunction]) directly to your methods.

The new attributes also allow you to fully configure each filter and function using named arguments:

1
2
3
4
#[AsTwigFilter('...', needsEnvironment: true)]
#[AsTwigFilter('...', needsEnvironment: true, preEscape: true)]
#[AsTwigFunction('...', needsContext: true)]
#[AsTwigFunction('...', needsCharset: true, isSafe: ['html'])]

This new syntax makes your extensions cleaner, faster to write, and easier to maintain while keeping the full power of the Twig integration.

Published in #Living on the edge