Twig
Twig copied to clipboard
Create attributes `AsTwigFilter`, `AsTwigFunction` and `AsTwigTest` to ease extension development
One drawback to writing extensions at present is that the declaration of functions/filters/tests is not directly adjacent to the methods. It's worse for runtime extensions because they need to be in 2 different classes. See SerializerExtension
and SerializerRuntime
as an example.
By using attributes for filters, functions and tests definition, we can make writing extensions more expressive, and use reflection to detect particular options (needs_environment
, needs_context
, is_variadic
).
Example if we implemented the serialize
filter: https://github.com/twigphp/Twig/blob/aeeec9a5e907a79e50a6bb78979154599401726e/extra/intl-extra/IntlExtension.php#L392-L395
By using the AsTwigFilter
attribute, it is not necessary to create the getFilters()
method. The needs_environment
option is detected from method signature. The name is still required as the method naming convention (camelCase) doesn't match with Twig naming convention (snake_case).
use Twig\Extension\Attribute\AsTwigFilter;
use Twig\Extension\Attribute\AsTwigExtension;
#[AsTwigExtension]
class IntlExtension
{
#[AsTwigFilter(name: 'format_date')]
public function formatDate(Environment $env, $date, ?string $dateFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string
{
return $this->formatDateTime($env, $date, $dateFormat, 'none', $pattern, $timezone, $calendar, $locale);
}
}
This approach does not totally replace the current definition of extensions, which is still necessary for advanced needs. It does, however, make for more pleasant reading and writing.
This makes writing lazy-loaded runtime extension the easiest way to create Twig extension in Symfony: https://github.com/symfony/symfony/pull/52748
Related to https://github.com/symfony/symfony/issues/50016
Is there any need to cache the parsing of method attributes? They are only read at compile time, but that can have a performance impact during development or when using dynamic templates.
I'll rework the implementation after reading discussions on symfony/symfony#50016
In all honesty, I'm not totally convinced by this implementation. The fact that extensions have to be registered in 2 places (as a runtime extension and for definition in AttributeExtension) makes them difficult to use with Twig standalone, this is totally hidden for Symfony users with the TwigBundle.
I would like to add a new Environment::add(object $extension)
method that would accept any class with attributes. Invocable classes could be used to define filters and functions.
What is a bit strange is having one single instance of AttributeExtension be responsible for registering many runtimes.
Maybe we should instead have one AttributeExtension per runtime?
Then it might be easier to use this standalone : addExtension(new AttributeExtension(new AttributeBasedRuntime)))
(and such runtimes could have a static factory to make this even easier to create).
Note that this suggestion might be wrong as I didn't think of how runtimes should be made lazy-instantiated.
For reference, this is my attempt/experiment to solve the problem of apps having to create a bunch of twig extensions for things: https://github.com/zenstruck/twig-service-bundle