Skip to content

Commit

Permalink
Allows registration of filter/function/test with an attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
GromNaN committed Jan 3, 2025
1 parent 5dd37d8 commit 6143f32
Show file tree
Hide file tree
Showing 10 changed files with 744 additions and 1 deletion.
104 changes: 104 additions & 0 deletions doc/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,110 @@ The ``getTests()`` method lets you add new test functions::
// ...
}

Using PHP Attributes to define Extensions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 3.19

The attribute classes were added in Twig 3.19.

You can use the attributes ``#[AsTwigFilter]``, ``#[AsTwigFunction]``, and
``#[AsTwigTest]`` on any method of any class to define filters, functions, and tests.

Create a class using this attributes::

use Twig\Attribute\AsTwigFilter;
use Twig\Attribute\AsTwigFunction;
use Twig\Attribute\AsTwigTest;

class ProjectExtension
{
#[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 ``Twig\Extension\AttributeExtension`` with the class name::

$twig = new \Twig\Environment($loader);
$twig->addExtension(new \Twig\Extension\AttributeExtension([
// List of all the classes using AsTwig attributes
ProjectExtension::class,
]);

If all the methods are static, you are done. The ``ProjectExtension`` 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\AsTwigFunction;

class ProjectExtension
{
// 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([ProjectExtension::class]);
$twig->addRuntimeLoader(new \Twig\RuntimeLoader\FactoryLoader([
ProjectExtension::class => function () use ($lipsumProvider) {
return new ProjectExtension($lipsumProvider);
},
]));

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 ProjectExtension
{
#[AsTwigFunction('lipsum')]
public function lipsum(\Twig\Environment $env, int $count): string
{
// ...
}
}

``#[AsTwigFilter]`` and ``#[AsTwigFunction]`` support variadic arguments
automatically when applied to variadic methods::

class ProjectExtension
{
#[AsTwigFilter('thumbnail')]
public function thumbnail(string $file, mixed ...$options): string
{
// ...
}
}

The attributes support other options used to configure the Twig Callables:

* ``AsTwigFilter``: ``needsCharset``, ``needsEnvironment``, ``needsContext``, ``isSafe``, ``isSafeCallback``, ``preEscape``, ``preservesSafety``, ``deprecationInfo``
* ``AsTwigFunction``: ``needsCharset``, ``needsEnvironment``, ``needsContext``, ``isSafe``, ``isSafeCallback``, ``deprecationInfo``
* ``AsTwigTest``: ``needsCharset``, ``needsEnvironment``, ``needsContext``, ``deprecationInfo``

Definition vs Runtime
~~~~~~~~~~~~~~~~~~~~~

Expand Down
57 changes: 57 additions & 0 deletions src/Attribute/AsTwigFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Twig\Attribute;

use Twig\DeprecatedCallableInfo;
use Twig\TwigFilter;

/**
* Registers a method as template filter.
*
* If the first argument of the method has Twig\Environment type-hint, the filter will receive the current environment.
* If the next argument of the method is named $context and has array type-hint, the filter will receive the current context.
* Additional arguments of the method come from the filter call.
*
* #[AsTwigFilter('foo')]
* function fooFilter(Environment $env, array $context, $string, $arg1 = null, ...) { ... }
*
* {{ 'string'|foo(arg1) }}
*
* @see TwigFilter
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class AsTwigFilter
{
/**
* @param non-empty-string $name The name of the filter in Twig.
* @param bool|null $needsCharset Whether the filter needs the charset passed as the first argument.
* @param bool|null $needsEnvironment Whether the filter needs the environment passed as the first argument, or after the charset.
* @param bool|null $needsContext Whether the filter needs the context array passed as the first argument, or after the charset and the environment.
* @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped.
* @param string|array|null $isSafeCallback Function called at compilation time to determine if the filter is safe.
* @param string|null $preEscape Some filters may need to work on input that is already escaped or safe
* @param string[]|null $preservesSafety Preserves the safety of the value that the filter is applied to.
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
*/
public function __construct(
public string $name,
public ?bool $needsCharset = null,
public ?bool $needsEnvironment = null,
public ?bool $needsContext = null,
public ?array $isSafe = null,
public string|array|null $isSafeCallback = null,
public ?string $preEscape = null,
public ?array $preservesSafety = null,
public ?DeprecatedCallableInfo $deprecationInfo = null,
) {
}
}
53 changes: 53 additions & 0 deletions src/Attribute/AsTwigFunction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Twig\Attribute;

use Twig\DeprecatedCallableInfo;
use Twig\TwigFunction;

/**
* Registers a method as template function.
*
* If the first argument of the method has Twig\Environment type-hint, the function will receive the current environment.
* If the next argument of the method is named $context and has array type-hint, the function will receive the current context.
* Additional arguments of the method come from the function call.
*
* #[AsTwigFunction('foo')]
* function fooFunction(Environment $env, array $context, string $string, $arg1 = null, ...) { ... }
*
* {{ foo('string', arg1) }}
*
* @see TwigFunction
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class AsTwigFunction
{
/**
* @param non-empty-string $name The name of the function in Twig.
* @param bool|null $needsCharset Whether the function needs the charset passed as the first argument.
* @param bool|null $needsEnvironment Whether the function needs the environment passed as the first argument, or after the charset.
* @param bool|null $needsContext Whether the function needs the context array passed as the first argument, or after the charset and the environment.
* @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped.
* @param string|array|null $isSafeCallback Function called at compilation time to determine if the function is safe.
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
*/
public function __construct(
public string $name,
public ?bool $needsCharset = null,
public ?bool $needsEnvironment = null,
public ?bool $needsContext = null,
public ?array $isSafe = null,
public string|array|null $isSafeCallback = null,
public ?DeprecatedCallableInfo $deprecationInfo = null,
) {
}
}
48 changes: 48 additions & 0 deletions src/Attribute/AsTwigTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Twig\Attribute;

use Twig\DeprecatedCallableInfo;
use Twig\TwigTest;

/**
* Registers a method as template test.
*
* The first argument is the value to test and the other arguments are the
* arguments passed to the test in the template.
*
* #[AsTwigTest('foo')]
* public function fooTest($value, $arg1 = null) { ... }
*
* {% if value is foo(arg1) %}
*
* @see TwigTest
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class AsTwigTest
{
/**
* @param non-empty-string $name The name of the test in Twig.
* @param bool|null $needsCharset Whether the test needs the charset passed as the first argument.
* @param bool|null $needsEnvironment Whether the test needs the environment passed as the first argument, or after the charset.
* @param bool|null $needsContext Whether the test needs the context array passed as the first argument, or after the charset and the environment.
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
*/
public function __construct(
public string $name,
public ?bool $needsCharset = null,
public ?bool $needsEnvironment = null,
public ?bool $needsContext = null,
public ?DeprecatedCallableInfo $deprecationInfo = null,
) {
}
}
Loading

0 comments on commit 6143f32

Please sign in to comment.