Skip to content

[RFC][TwigBundle] Register custom functions & filters using an attribute #50016

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
artyuum opened this issue Apr 14, 2023 · 6 comments · Fixed by #52748
Closed

[RFC][TwigBundle] Register custom functions & filters using an attribute #50016

artyuum opened this issue Apr 14, 2023 · 6 comments · Fixed by #52748

Comments

@artyuum
Copy link
Contributor

artyuum commented Apr 14, 2023

Description

Currently, in order to create a custom twig function/filter, we need create a class that extends the AbstractExtension, that looks like this:

class TwigExtension extends AbstractExtension
{
    public function getFilters(): array
    {
        return [
            new TwigFilter('my_custom_filter', 'myCustomFilter'),
        ];
    }

    public function getFunctions(): array
    {
        return [
            new TwigFunction('my_custom_function', 'myCustomFunction'),
        ];
    }

    public function myCustomFilter(): string
    {
        // code here
    }

    public function myCustomFunction(): string
    {
        // code here
    }
}

Example

I've always thought that the DX regarding twig extension could be better when working with Symfony. Today, I imagined the following:

class TwigExtension extends AbstractExtension
{
    #[TwigFilter('my_custom_filter')]
    public function myCustomFilter(): string
    {
        // code here
    }

    #[TwigFunction('my_custom_function')]
    public function myCustomFunction(): string
    {
        // code here
    }
}

If we don't want to modify or conflict with the existing TwigFunction / TwigFilter classes, we could name them like this and store them in the twig bridge I think?

  • #[AsTwigFilter]
  • #[AsTwigFunction]

What do you think?

(If the feedbacks are positive, I'll open a PR.) Hum, this seems more difficult than I initially thought.

@evertharmeling
Copy link
Contributor

I guess you could even go as far as even deprecating the AbstractExtension/ExtensionInterface. And maybe introduce an #[AsTwigExtension] for the class (to narrow the classes to search for the attributes). But that could be a separate PR.

Also be sure to include the TokenParserInterface, NodeVisitorInterface, Operator, TwigTest as well, so full list of attributes would become:

  • #[AsTwigFilter]
  • #[AsTwigFunction]
  • #[AsTwigTest]
  • #[AsTwigOperator]
  • #[AsTwigTokenParser] (for consistency include the Twig prefix?)
  • #[AsTwigNodeVisitor] (for consistency include the Twig prefix?)

@stof
Copy link
Member

stof commented Apr 14, 2023

Be careful that Twig itself cannot deprecate the existing system if the new system cannot easily work when using Twig outside Symfony.

And making TwigBundle expose a Symfony-only way of extending Twig might not be the best way to go. Especially when you will still need to go back to the Twig way to use some of the features as an attribute won't be able to configure the is_safe_callback option of Twig filters or functions as you cannot put a closure in an attribute)

I guess you could even go as far as even deprecating the AbstractExtension/ExtensionInterface. And maybe introduce an #[AsTwigExtension] for the class (to narrow the classes to search for the attributes). But that could be a separate PR.

Twig uses the extension instance. So ExtensionInterface makes sense to define its API. And I doubt we can deprecate that without breaking support for standalone usage of Twig.

@artyuum
Copy link
Contributor Author

artyuum commented Apr 14, 2023

I agree that deprecating these interfaces wouldn't make sense since they are not defined nor only used in the framework but are part of the twig package itself and they are still "valid".

Be careful that Twig itself cannot deprecate the existing system if the new system cannot easily work when using Twig outside Symfony.

I was thinking of implementing that only in the framework (either in the TwigBridge or the TwigBundle).

And making TwigBundle expose a Symfony-only way of extending Twig might not be the best way to go. Especially when you will still need to go back to the Twig way to use some of the features as an attribute won't be able to configure the is_safe_callback option of Twig filters or functions as you cannot put a closure in an attribute)

Would it be acceptable to still offer these new attributes as an easier/faster alternative for registering the custom functions/filters when using the framework but still suggesting the Twig way when doing something more complex that wouldn't be compatible with the attribute (e.g. callback)?

@SVillette
Copy link
Contributor

I really love the idea. I only wonder if it is going possible to use the same attribute on a lazy extension. I mean, in your example, you declare the Twig function in the same class as its implementation (in a subclass of AbstractExtension). In case of lazy loaded extensions, the function/filter is declared in the extension which references a twig runtime (in a class that implements RuntimeExtensionInterface).
https://symfony.com/doc/current/templates.html#creating-lazy-loaded-twig-extensions

So my question is (and sorry if it is a dumb one), if you use attributes, how it will create an extension rather than a lazy loaded one ?

@stof
Copy link
Member

stof commented Apr 14, 2023

Well, what we could do in Symfony is have 1 extension implemented in TwigBundle that gets configured based on those services using those attributes (those services will be registered as runtime).
RuntimeExtensionInterface is purely a marker interface to make it easy to identify classes that should be registered as Twig runtimes, but there is no requirement to actually use it.

@artyuum artyuum changed the title [Twig] Register custom functions & filters using an attribute [TwigBundle] Register custom functions & filters using an attribute Apr 17, 2023
@artyuum artyuum changed the title [TwigBundle] Register custom functions & filters using an attribute [RFC][TwigBundle] Register custom functions & filters using an attribute Sep 6, 2023
@GromNaN
Copy link
Member

GromNaN commented Nov 26, 2023

I thought of an implementation in Twig directly, making the getFilters, getFunctions and getTests methods scan the attributes of the methods in the current class twigphp/Twig#3916

fabpot added a commit to twigphp/Twig that referenced this issue Mar 24, 2025
…`AsTwigTest` to ease extension development (GromNaN)

This PR was squashed before being merged into the 3.x branch.

Discussion
----------

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`](https://github.com/symfony/symfony/blob/7.0/src/Symfony/Bridge/Twig/Extension/SerializerExtension.php) and [`SerializerRuntime`](https://github.com/symfony/symfony/blob/7.0/src/Symfony/Bridge/Twig/Extension/SerializerRuntime.php) 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 `formatDate` 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).

```php
use Twig\Extension\Attribute\AsTwigFilter;

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: symfony/symfony#52748

Related to symfony/symfony#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.

Commits
-------

5886907 Create attributes `AsTwigFilter`, `AsTwigFunction` and `AsTwigTest` to ease extension development
@fabpot fabpot closed this as completed in 0b026f9 Mar 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants