Skip to content

Commit 8a7d912

Browse files
committed
feature #3916 Create attributes AsTwigFilter, AsTwigFunction and 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
2 parents 3c7c97e + 5886907 commit 8a7d912

10 files changed

+735
-1
lines changed

doc/advanced.rst

+101
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,107 @@ The ``getTests()`` method lets you add new test functions::
814814
// ...
815815
}
816816

817+
Using PHP Attributes to define Extensions
818+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
819+
820+
.. versionadded:: 3.21
821+
822+
The attribute classes were added in Twig 3.21.
823+
824+
You can add the ``#[AsTwigFilter]``, ``#[AsTwigFunction]``, and ``#[AsTwigTest]``
825+
attributes to public methods of any class to define filters, functions, and tests.
826+
827+
Create a class using these attributes::
828+
829+
use Twig\Attribute\AsTwigFilter;
830+
use Twig\Attribute\AsTwigFunction;
831+
use Twig\Attribute\AsTwigTest;
832+
833+
class ProjectExtension
834+
{
835+
#[AsTwigFilter('rot13')]
836+
public static function rot13(string $string): string
837+
{
838+
// ...
839+
}
840+
841+
#[AsTwigFunction('lipsum')]
842+
public static function lipsum(int $count): string
843+
{
844+
// ...
845+
}
846+
847+
#[AsTwigTest('even')]
848+
public static function isEven(int $number): bool
849+
{
850+
// ...
851+
}
852+
}
853+
854+
Then register the ``Twig\Extension\AttributeExtension`` with the class name::
855+
856+
$twig = new \Twig\Environment($loader);
857+
$twig->addExtension(new \Twig\Extension\AttributeExtension(ProjectExtension::class));
858+
859+
If all the methods are static, you are done. The ``ProjectExtension`` class will
860+
never be instantiated and the class attributes will be scanned only when a template
861+
is compiled.
862+
863+
Otherwise, if some methods are not static, you need to register the class as
864+
a runtime extension using one of the runtime loaders::
865+
866+
use Twig\Attribute\AsTwigFunction;
867+
868+
class ProjectExtension
869+
{
870+
// Inject hypothetical dependencies
871+
public function __construct(private LipsumProvider $lipsumProvider) {}
872+
873+
#[AsTwigFunction('lipsum')]
874+
public function lipsum(int $count): string
875+
{
876+
return $this->lipsumProvider->lipsum($count);
877+
}
878+
}
879+
880+
$twig = new \Twig\Environment($loader);
881+
$twig->addExtension(new \Twig\Extension\AttributeExtension(ProjectExtension::class);
882+
$twig->addRuntimeLoader(new \Twig\RuntimeLoader\FactoryLoader([
883+
ProjectExtension::class => function () use ($lipsumProvider) {
884+
return new ProjectExtension($lipsumProvider);
885+
},
886+
]));
887+
888+
If you want to access the current environment instance in your filter or function,
889+
add the ``Twig\Environment`` type to the first argument of the method::
890+
891+
class ProjectExtension
892+
{
893+
#[AsTwigFunction('lipsum')]
894+
public function lipsum(\Twig\Environment $env, int $count): string
895+
{
896+
// ...
897+
}
898+
}
899+
900+
``#[AsTwigFilter]`` and ``#[AsTwigFunction]`` support variadic arguments
901+
automatically when applied to variadic methods::
902+
903+
class ProjectExtension
904+
{
905+
#[AsTwigFilter('thumbnail')]
906+
public function thumbnail(string $file, mixed ...$options): string
907+
{
908+
// ...
909+
}
910+
}
911+
912+
The attributes support other options used to configure the Twig Callables:
913+
914+
* ``AsTwigFilter``: ``needsCharset``, ``needsEnvironment``, ``needsContext``, ``isSafe``, ``isSafeCallback``, ``preEscape``, ``preservesSafety``, ``deprecationInfo``
915+
* ``AsTwigFunction``: ``needsCharset``, ``needsEnvironment``, ``needsContext``, ``isSafe``, ``isSafeCallback``, ``deprecationInfo``
916+
* ``AsTwigTest``: ``needsCharset``, ``needsEnvironment``, ``needsContext``, ``deprecationInfo``
917+
817918
Definition vs Runtime
818919
~~~~~~~~~~~~~~~~~~~~~
819920

src/Attribute/AsTwigFilter.php

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Twig.
5+
*
6+
* (c) Fabien Potencier
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Twig\Attribute;
13+
14+
use Twig\DeprecatedCallableInfo;
15+
use Twig\TwigFilter;
16+
17+
/**
18+
* Registers a method as template filter.
19+
*
20+
* If the first argument of the method has Twig\Environment type-hint, the filter will receive the current environment.
21+
* Additional arguments of the method come from the filter call.
22+
*
23+
* #[AsTwigFilter(name: 'foo')]
24+
* function fooFilter(Environment $env, $string, $arg1 = null, ...) { ... }
25+
*
26+
* {{ 'string'|foo(arg1) }}
27+
*
28+
* @see TwigFilter
29+
*/
30+
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
31+
final class AsTwigFilter
32+
{
33+
/**
34+
* @param non-empty-string $name The name of the filter in Twig.
35+
* @param bool|null $needsCharset Whether the filter needs the charset passed as the first argument.
36+
* @param bool|null $needsEnvironment Whether the filter needs the environment passed as the first argument, or after the charset.
37+
* @param bool|null $needsContext Whether the filter needs the context array passed as the first argument, or after the charset and the environment.
38+
* @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped.
39+
* @param string|array|null $isSafeCallback Function called at compilation time to determine if the filter is safe.
40+
* @param string|null $preEscape Some filters may need to work on input that is already escaped or safe
41+
* @param string[]|null $preservesSafety Preserves the safety of the value that the filter is applied to.
42+
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
43+
*/
44+
public function __construct(
45+
public string $name,
46+
public ?bool $needsCharset = null,
47+
public ?bool $needsEnvironment = null,
48+
public ?bool $needsContext = null,
49+
public ?array $isSafe = null,
50+
public string|array|null $isSafeCallback = null,
51+
public ?string $preEscape = null,
52+
public ?array $preservesSafety = null,
53+
public ?DeprecatedCallableInfo $deprecationInfo = null,
54+
) {
55+
}
56+
}

src/Attribute/AsTwigFunction.php

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Twig.
5+
*
6+
* (c) Fabien Potencier
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Twig\Attribute;
13+
14+
use Twig\DeprecatedCallableInfo;
15+
use Twig\TwigFunction;
16+
17+
/**
18+
* Registers a method as template function.
19+
*
20+
* If the first argument of the method has Twig\Environment type-hint, the function will receive the current environment.
21+
* Additional arguments of the method come from the function call.
22+
*
23+
* #[AsTwigFunction(name: 'foo')]
24+
* function fooFunction(Environment $env, string $string, $arg1 = null, ...) { ... }
25+
*
26+
* {{ foo('string', arg1) }}
27+
*
28+
* @see TwigFunction
29+
*/
30+
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
31+
final class AsTwigFunction
32+
{
33+
/**
34+
* @param non-empty-string $name The name of the function in Twig.
35+
* @param bool|null $needsCharset Whether the function needs the charset passed as the first argument.
36+
* @param bool|null $needsEnvironment Whether the function needs the environment passed as the first argument, or after the charset.
37+
* @param bool|null $needsContext Whether the function needs the context array passed as the first argument, or after the charset and the environment.
38+
* @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped.
39+
* @param string|array|null $isSafeCallback Function called at compilation time to determine if the function is safe.
40+
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
41+
*/
42+
public function __construct(
43+
public string $name,
44+
public ?bool $needsCharset = null,
45+
public ?bool $needsEnvironment = null,
46+
public ?bool $needsContext = null,
47+
public ?array $isSafe = null,
48+
public string|array|null $isSafeCallback = null,
49+
public ?DeprecatedCallableInfo $deprecationInfo = null,
50+
) {
51+
}
52+
}

src/Attribute/AsTwigTest.php

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Twig.
5+
*
6+
* (c) Fabien Potencier
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Twig\Attribute;
13+
14+
use Twig\DeprecatedCallableInfo;
15+
use Twig\TwigTest;
16+
17+
/**
18+
* Registers a method as template test.
19+
*
20+
* The first argument is the value to test and the other arguments are the
21+
* arguments passed to the test in the template.
22+
*
23+
* #[AsTwigTest(name: 'foo')]
24+
* public function fooTest($value, $arg1 = null) { ... }
25+
*
26+
* {% if value is foo(arg1) %}
27+
*
28+
* @see TwigTest
29+
*/
30+
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
31+
final class AsTwigTest
32+
{
33+
/**
34+
* @param non-empty-string $name The name of the test in Twig.
35+
* @param bool|null $needsCharset Whether the test needs the charset passed as the first argument.
36+
* @param bool|null $needsEnvironment Whether the test needs the environment passed as the first argument, or after the charset.
37+
* @param bool|null $needsContext Whether the test needs the context array passed as the first argument, or after the charset and the environment.
38+
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
39+
*/
40+
public function __construct(
41+
public string $name,
42+
public ?bool $needsCharset = null,
43+
public ?bool $needsEnvironment = null,
44+
public ?bool $needsContext = null,
45+
public ?DeprecatedCallableInfo $deprecationInfo = null,
46+
) {
47+
}
48+
}

0 commit comments

Comments
 (0)