Twig icon indicating copy to clipboard operation
Twig copied to clipboard

Create attributes `AsTwigFilter`, `AsTwigFunction` and `AsTwigTest` to ease extension development

Open GromNaN opened this issue 9 months ago • 4 comments

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.

GromNaN avatar Nov 26 '23 00:11 GromNaN

I'll rework the implementation after reading discussions on symfony/symfony#50016

GromNaN avatar Nov 26 '23 09:11 GromNaN

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.

GromNaN avatar Jan 03 '24 09:01 GromNaN

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.

nicolas-grekas avatar Jan 03 '24 09:01 nicolas-grekas

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

kbond avatar Jan 03 '24 16:01 kbond