-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Create attributes AsTwigFilter
, AsTwigFunction
and AsTwigTest
to ease extension development
#3916
base: 3.x
Are you sure you want to change the base?
Conversation
AsTwigFilter
, AsTwigFunction
and AsTwigTest
to improve extension development
AsTwigFilter
, AsTwigFunction
and AsTwigTest
to improve extension developmentAsTwigFilter
, AsTwigFunction
and AsTwigTest
to ease extension development
I'll rework the implementation after reading discussions on symfony/symfony#50016 |
d505321
to
e64a54b
Compare
c627a38
to
4c1adc1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ready for review @fabpot.
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 |
What is a bit strange is having one single instance of AttributeExtension be responsible for registering many runtimes. |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
In the ExtensionSet
, extensions are identified by their class name. The same extension class cannot be registered multiple times with distinct configuration.
I updated this PR so that extension can be registered directly using any class name or object that have the #[AsTwigExtension]
attribute (the attribute becomes required).
$twig = new \Twig\Environment($loader);
- $twig->addExtension(new \Twig\Extension\AttributeExtension([
- ProjectExtension::class,
- ]));
+ $twig->addExtension(ProjectExtension::class);
$twig->addRuntimeLoader(new \Twig\RuntimeLoader\FactoryLoader([
ProjectExtension::class => function () use ($lipsumProvider) {
return new ProjectExtension($lipsumProvider);
},
]));
To use standalone (non-lazy-loaded):
$twig->addExtension(new ProjectExtension($lipsumProvider));
src/Extension/AttributeExtension.php
Outdated
throw new \LogicException(sprintf('Extension class "%s" must have the attribute "%s" in order to use attributes', is_string($objectOrClass) ? $objectOrClass : get_debug_type($objectOrClass), AsTwigExtension::class)); | ||
} | ||
|
||
foreach ($reflectionClass->getMethods() as $method) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thinking about that, I'm wondering whether those asTwigFilter
, AsTwigFunction
and AsTwigTest
attributes should be read by that extension at all.
An alternative solution would be to have an extension in which you inject this list of test, filter and function metadata, and letting TwigBundle perform all this attribute reading at build-time instead.
This would even removed the need for the AsTwigExtension
attribute entirely (which would be quite confusing as the class having this attribute is not a twig extension but a twig runtime) because the autoconfiguration system is able to apply autoconfiguration based on method-level attributes already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the logic for reading attributes should be in Twig,and the extension could receive the configuration already extracted. And Symfony TwigBundle will do the job of serializing and caching the configuration.
The serializing
may be problematic in the future if closures are used in attributes for safeCallback
(RFC for PHP 8.5)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thinking about that, I'm wondering whether those
asTwigFilter
,AsTwigFunction
andAsTwigTest
attributes should be read by that extension at all.
Here is an other approach where attribute class can create the twig callable themself.
https://github.com/twigphp/Twig/blob/36a0732ee66bf405c2164282626fbdc8c23ae81a/src/Attribute/AsTwigFilter.php
src/ExtensionSet.php
Outdated
if ($extension instanceof ExtensionInterface) { | ||
$this->initExtension($extension); | ||
} else { | ||
$classes[] = $extension; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
making $this->extensions
contain a mix of extensions and of runtimes (but not all of them) looks weird to me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if this is a way to solve the case of getLastModified
, I'd rather add a way to make ExtensionSet aware of a separate list of runtimes needing to be checked (which would allow solving the case where we don't invalidate the cache today if you change the argument names in a runtime while it actually impacts the template compilation if you use named parameters in Twig, by making TwigBundle inject the name of all runtimes in the list).
2edd448
to
d16812e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not an extensive review, just things I've spotted while reading the code quickly.
This PR was merged into the 3.x branch. Discussion ---------- Add `getLastModified()` to extensions Give to extensions the ability to set a last modification date for cache invalidation. ### Runtime Currently, the cache is not invalidated when the signature of a runtime method is modified. This is an issue for templates that use named arguments, as argument names have an impact on the generated class. With this change, extensions using runtime classes can compute a modification date by including the files on which they depend. By default, the `AbstractExtension` checks if there is a file for the runtime class with the same name of the This is the convention applied in [Symfony](https://github.com/symfony/symfony/tree/7.3/src/Symfony/Bridge/Twig/Extension) and Twig Extra: `MarkdownExtension` has `MarkdownRuntime`. ### Attribute Contributing to #3916. The extension class that will get the configuration from attributes will be able to track the classes having attributes to find the last modification date of all this classes. ### ~BC break~ ~In Twig 4.0, the method `getLastModified` will be added to `ExtensionInterface`. It is extremely rare to implement this interface without extending `AbstractExtension`. So adding this method to the interface shouldn't be a problem as the base class has an implementation.~ Commits ------- d8fe3bd Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes
e87c1e6
to
60c96db
Compare
I refactored once again to get 1 instance of Also, I removed the ability to pass an instance of an object to The doc is up-to-date on how to use it standalone. The Symfony implementation need to be updated. |
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
andSerializerRuntime
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:Twig/extra/intl-extra/IntlExtension.php
Lines 392 to 395 in aeeec9a
By using the
AsTwigFilter
attribute, it is not necessary to create thegetFilters()
method. Theneeds_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).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.