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 20 commits 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
139 changes: 139 additions & 0 deletions doc/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,145 @@ The ``getTests()`` method lets you add new test functions::
// ...
}

Using PHP Attributes to define extensions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 3.9

The attribute classes were 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 with the attribute ``#[AsTwigExtension]``::

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

#[AsTwigExtension]
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 extension class::

$twig = new \Twig\Environment($loader);
$twig->addExtension(ProjectExtension::class);

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

Or use the instance directly if you don't need lazy-loading::

$twig = new \Twig\Environment($loader);
$twig->addExtension(new ProjectExtension($lipsumProvider));

``#[AsTwigFilter]`` and ``#[AsTwigFunction]`` support ``isSafe``, ``preEscape``, and
``isVariadic`` options::

use Twig\Attribute\AsTwigExtension;
use Twig\Attribute\AsTwigFilter;
use Twig\Attribute\AsTwigFunction;

#[AsTwigExtension]
class ProjectExtension
{
#[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 ProjectExtension
{
#[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 ProjectExtension
{
#[AsTwigFunction('lipsum')]
public function lipsum(array $context, 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
{
// ...
}
}

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

Expand Down
20 changes: 20 additions & 0 deletions src/Attribute/AsTwigExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?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;

/**
* Identifies a class that uses PHP attributes to define filters, functions, or tests.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final class AsTwigExtension
{
}
55 changes: 55 additions & 0 deletions src/Attribute/AsTwigFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?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\Node\Node;
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 string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped.
* @param null|callable(Node):bool $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, 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.
* @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 ?array $isSafe = null,
public mixed $isSafeCallback = null,
public ?string $preEscape = null,
public ?array $preservesSafety = null,
public ?DeprecatedCallableInfo $deprecationInfo = null,
) {
}
}
48 changes: 48 additions & 0 deletions src/Attribute/AsTwigFunction.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\Node\Node;
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 string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped.
* @param null|callable(Node):bool $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 ?array $isSafe = null,
public mixed $isSafeCallback = null,
public ?DeprecatedCallableInfo $deprecationInfo = null,
) {
}
}
42 changes: 42 additions & 0 deletions src/Attribute/AsTwigTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?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 DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
*/
public function __construct(
public string $name,
public ?DeprecatedCallableInfo $deprecationInfo = null,
) {
}
}
7 changes: 5 additions & 2 deletions src/Environment.php
Original file line number Diff line number Diff line change
Expand Up @@ -650,14 +650,17 @@ public function getRuntime(string $class)
throw new RuntimeError(\sprintf('Unable to load the "%s" runtime.', $class));
}

public function addExtension(ExtensionInterface $extension)
/**
* @param ExtensionInterface|object|class-string $extension
*/
public function addExtension(object|string $extension)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change.

{
$this->extensionSet->addExtension($extension);
$this->updateOptionsHash();
}

/**
* @param ExtensionInterface[] $extensions An array of extensions
* @param list<ExtensionInterface|object|class-string> $extensions An array of extensions
*/
public function setExtensions(array $extensions)
{
Expand Down
Loading
Loading