Skip to content

Commit 6143f32

Browse files
committed
Allows registration of filter/function/test with an attribute
1 parent 5dd37d8 commit 6143f32

10 files changed

+744
-1
lines changed

doc/advanced.rst

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,110 @@ The ``getTests()`` method lets you add new test functions::
813813
// ...
814814
}
815815

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

src/Attribute/AsTwigFilter.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
* If the next argument of the method is named $context and has array type-hint, the filter will receive the current context.
22+
* Additional arguments of the method come from the filter call.
23+
*
24+
* #[AsTwigFilter('foo')]
25+
* function fooFilter(Environment $env, array $context, $string, $arg1 = null, ...) { ... }
26+
*
27+
* {{ 'string'|foo(arg1) }}
28+
*
29+
* @see TwigFilter
30+
*/
31+
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
32+
final class AsTwigFilter
33+
{
34+
/**
35+
* @param non-empty-string $name The name of the filter in Twig.
36+
* @param bool|null $needsCharset Whether the filter needs the charset passed as the first argument.
37+
* @param bool|null $needsEnvironment Whether the filter needs the environment passed as the first argument, or after the charset.
38+
* @param bool|null $needsContext Whether the filter needs the context array passed as the first argument, or after the charset and the environment.
39+
* @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped.
40+
* @param string|array|null $isSafeCallback Function called at compilation time to determine if the filter is safe.
41+
* @param string|null $preEscape Some filters may need to work on input that is already escaped or safe
42+
* @param string[]|null $preservesSafety Preserves the safety of the value that the filter is applied to.
43+
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
44+
*/
45+
public function __construct(
46+
public string $name,
47+
public ?bool $needsCharset = null,
48+
public ?bool $needsEnvironment = null,
49+
public ?bool $needsContext = null,
50+
public ?array $isSafe = null,
51+
public string|array|null $isSafeCallback = null,
52+
public ?string $preEscape = null,
53+
public ?array $preservesSafety = null,
54+
public ?DeprecatedCallableInfo $deprecationInfo = null,
55+
) {
56+
}
57+
}

src/Attribute/AsTwigFunction.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
* If the next argument of the method is named $context and has array type-hint, the function will receive the current context.
22+
* Additional arguments of the method come from the function call.
23+
*
24+
* #[AsTwigFunction('foo')]
25+
* function fooFunction(Environment $env, array $context, string $string, $arg1 = null, ...) { ... }
26+
*
27+
* {{ foo('string', arg1) }}
28+
*
29+
* @see TwigFunction
30+
*/
31+
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
32+
final class AsTwigFunction
33+
{
34+
/**
35+
* @param non-empty-string $name The name of the function in Twig.
36+
* @param bool|null $needsCharset Whether the function needs the charset passed as the first argument.
37+
* @param bool|null $needsEnvironment Whether the function needs the environment passed as the first argument, or after the charset.
38+
* @param bool|null $needsContext Whether the function needs the context array passed as the first argument, or after the charset and the environment.
39+
* @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped.
40+
* @param string|array|null $isSafeCallback Function called at compilation time to determine if the function is safe.
41+
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
42+
*/
43+
public function __construct(
44+
public string $name,
45+
public ?bool $needsCharset = null,
46+
public ?bool $needsEnvironment = null,
47+
public ?bool $needsContext = null,
48+
public ?array $isSafe = null,
49+
public string|array|null $isSafeCallback = null,
50+
public ?DeprecatedCallableInfo $deprecationInfo = null,
51+
) {
52+
}
53+
}

src/Attribute/AsTwigTest.php

Lines changed: 48 additions & 0 deletions
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('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)