From 4c1adc194a399f7692981bd6adb9c285c41c2b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 10 Dec 2023 21:36:26 +0100 Subject: [PATCH] Add docs --- doc/advanced.rst | 150 +++++++++++++++++++++ src/Attribute/AsTwigFilter.php | 32 +++++ src/Attribute/AsTwigFunction.php | 21 +++ src/Attribute/AsTwigTest.php | 8 ++ tests/Extension/AttributeExtensionTest.php | 2 +- 5 files changed, 212 insertions(+), 1 deletion(-) diff --git a/doc/advanced.rst b/doc/advanced.rst index 307650f6a46..17e2c7fd426 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -756,6 +756,156 @@ The ``getTests()`` method lets you add new test functions:: // ... } +Using PHP Attributes to define extensions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 3.9 + + The ``Twig\Extension\AttributeExtension`` was added in Twig 3.9. + +From PHP 8.0, you can use the attributes ``#[AsTwigFilter]``, ``#[AsTwigFunction]``, +and ``#[AsTwigTest]`` on any method of any class to define filters, functions, and tests. + +Create a class, you don't need to extend any class or implement any interface +but it eases integration with frameworks if you use the attribute ``#[AsTwigExtension]``:: + + use Twig\Attribute\AsTwigExtension; + use Twig\Attribute\AsTwigFilter; + use Twig\Attribute\AsTwigFunction; + use Twig\Attribute\AsTwigTest; + + #[AsTwigExtension] + class Project_Twig_Extension + { + #[AsTwigFilter('rot13')] + public static function rot13(string $string): string + { + // ... + } + + #[AsTwigFunction('lipsum')] + public static function lipsum(int $count): string + { + // ... + } + + #[AsTwigTest('even')] + public static function isEven(int $number): bool + { + // ... + } + } + +Then register the class using ``Twig\Extension\AttributeExtension``:: + + $twig = new \Twig\Environment($loader); + $twig->addExtension(new \Twig\Extension\AttributeExtension([ + Project_Twig_Extension::class, + ])); + +.. note:: + + The ``\Twig\Extension\AttributeExtension`` can be added only once to an environment. + +If all the methods are static, you are done. The ``Project_Twig_Extension`` class will +never be instantiated and the class attributes will be scanned only when a template +is compiled. + +Otherwise, if some methods are not static, you need to register the class as +a runtime extension using one of the runtime loaders:: + + use Twig\Attribute\AsTwigExtension; + use Twig\Attribute\AsTwigFunction; + + #[AsTwigExtension] + class Project_Service + { + // Inject hypothetical dependencies + public function __construct(private LipsumProvider $lipsumProvider) {} + + #[AsTwigFunction('lipsum')] + public function lipsum(int $count): string + { + return $this->lipsumProvider->lipsum($count); + } + } + + $twig = new \Twig\Environment($loader); + $twig->addExtension(new \Twig\Extension\AttributeExtension([ + Project_Twig_Extension::class, + ])); + $twig->addRuntimeLoader(new \Twig\RuntimeLoader\FactoryLoader([ + Project_Twig_Extension::class => function () use ($lipsumProvider) { + return new Project_Twig_Extension($lipsumProvider); + }, + ])); + +Or use the instance directly if you don't need lazy-loading:: + + $twig = new \Twig\Environment($loader); + $twig->addExtension(new \Twig\Extension\AttributeExtension([ + new Project_Twig_Extension($lipsumProvider), + ])); + +``#[AsTwigFilter]`` and ``#[AsTwigFunction]`` support ``isSafe``, ``preEscape``, and +``isVariadic`` options:: + + use Twig\Attribute\AsTwigExtension; + use Twig\Attribute\AsTwigFilter; + use Twig\Attribute\AsTwigFunction; + + #[AsTwigExtension] + class Project_Twig_Extension + { + #[AsTwigFilter('rot13', isSafe: ['html'])] + public static function rot13(string $string): string + { + // ... + } + + #[AsTwigFunction('lipsum', isSafe: ['html'], preEscape: 'html')] + public static function lipsum(int $count): string + { + // ... + } + } + +If you want to access the current environment instance in your filter or function, +add the ``Twig\Environment`` type to the first argument of the method:: + + class Project_Twig_Extension + { + #[AsTwigFunction('lipsum')] + public function lipsum(\Twig\Environment $env, int $count): string + { + // ... + } + } + +If you want to access the current context in your filter or function, add an argument +with type and name ``array $context`` first or after ``\Twig\Environment``:: + + class Project_Twig_Extension + { + #[AsTwigFunction('lipsum')] + public function lipsum(array $context, int $count): string + { + // ... + } + } + +``#[AsTwigFilter]`` and ``#[AsTwigFunction]`` support variadic arguments +automatically when applied to variadic methods:: + + class Project_Twig_Extension + { + #[AsTwigFilter('thumbnail')] + public function thumbnail(string $file, mixed ...$options): string + { + // ... + } + } + Definition vs Runtime ~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/Attribute/AsTwigFilter.php b/src/Attribute/AsTwigFilter.php index a253ef353c2..956228c504f 100644 --- a/src/Attribute/AsTwigFilter.php +++ b/src/Attribute/AsTwigFilter.php @@ -11,6 +11,7 @@ namespace Twig\Attribute; +use Twig\Node\Node; use Twig\TwigFilter; /** @@ -35,11 +36,42 @@ public function __construct( * @var non-empty-string $name */ public string $name, + + /** + * List of formats in which you want the raw output to be printed unescaped. + * + * @var list|null $isSafe + */ public ?array $isSafe = null, + + /** + * Function called at compilation time to determine if the filter is safe. + * + * @var callable(Node):bool $isSafeCallback + */ public ?string $isSafeCallback = null, + + /** + * Some filters may need to work on input that is already escaped or safe, for + * example when adding (safe) HTML tags to originally unsafe output. In such a + * case, set preEscape to an escape format to escape the input data before it + * is run through the filter. + */ public ?string $preEscape = null, + + /** + * Preserves the safety of the value that the filter is applied to. + */ public ?array $preservesSafety = null, + + /** + * Set to true if the filter is deprecated. + */ public bool|string $deprecated = false, + + /** + * The alternative filter name to suggest when the deprecated filter is called. + */ public ?string $alternative = null, ) { } diff --git a/src/Attribute/AsTwigFunction.php b/src/Attribute/AsTwigFunction.php index c0116817c88..539cf08bce4 100644 --- a/src/Attribute/AsTwigFunction.php +++ b/src/Attribute/AsTwigFunction.php @@ -11,6 +11,7 @@ namespace Twig\Attribute; +use Twig\Node\Node; use Twig\TwigFunction; /** @@ -35,9 +36,29 @@ public function __construct( * @var non-empty-string $name */ public string $name, + + /** + * List of formats in which you want the raw output to be printed unescaped. + * + * @var list|null $isSafe + */ public ?array $isSafe = null, + + /** + * Function called at compilation time to determine if the function is safe. + * + * @var callable(Node):bool $isSafeCallback + */ public ?string $isSafeCallback = null, + + /** + * Set to true if the function is deprecated. + */ public bool|string $deprecated = false, + + /** + * The alternative function name to suggest when the deprecated function is called. + */ public ?string $alternative = null, ) { } diff --git a/src/Attribute/AsTwigTest.php b/src/Attribute/AsTwigTest.php index e6ea0fbe151..ee4d8c6511e 100644 --- a/src/Attribute/AsTwigTest.php +++ b/src/Attribute/AsTwigTest.php @@ -35,7 +35,15 @@ public function __construct( * @var non-empty-string $name */ public string $name, + + /** + * Set to true if the function is deprecated. + */ public bool|string $deprecated = false, + + /** + * The alternative function name to suggest when the deprecated function is called. + */ public ?string $alternative = null, ) { } diff --git a/tests/Extension/AttributeExtensionTest.php b/tests/Extension/AttributeExtensionTest.php index da07c76b173..e62f15c04b4 100644 --- a/tests/Extension/AttributeExtensionTest.php +++ b/tests/Extension/AttributeExtensionTest.php @@ -111,6 +111,6 @@ public function testRuntimeExtension() public function testLastModified() { $extension = new AttributeExtension([ExtensionWithAttributes::class]); - $this->assertSame(filemtime(__DIR__ . '/Fixtures/ExtensionWithAttributes.php'), $extension->getLastModified()); + $this->assertSame(filemtime(__DIR__.'/Fixtures/ExtensionWithAttributes.php'), $extension->getLastModified()); } }