Skip to content
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

Open
wants to merge 1 commit into
base: 3.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading