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 17 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 @@ -756,6 +756,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
{
}
78 changes: 78 additions & 0 deletions src/Attribute/AsTwigFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?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\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, ...) { ... }
*
* @see TwigFilter
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class AsTwigFilter
{
public function __construct(
/**
* The name of the filter in Twig.
*
* @var non-empty-string $name
*/
public string $name,

/**
* List of formats in which you want the raw output to be printed unescaped.
*
* @var list<string>|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.
Copy link
Member

Choose a reason for hiding this comment

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

this misses the doc for the type of array values

*/
public ?array $preservesSafety = null,

/**
* Set to true if the filter is deprecated.
Copy link
Member

Choose a reason for hiding this comment

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

what is the string type about then ?

*/
public bool|string $deprecated = false,

/**
* The alternative filter name to suggest when the deprecated filter is called.
*/
public ?string $alternative = null,
) {
}
}
65 changes: 65 additions & 0 deletions src/Attribute/AsTwigFunction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?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\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, $arg1 = null, ...) { ... }
*
* @see TwigFunction
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class AsTwigFunction
{
public function __construct(
/**
* The name of the function in Twig.
*
* @var non-empty-string $name
*/
public string $name,

/**
* List of formats in which you want the raw output to be printed unescaped.
*
* @var list<string>|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,
) {
}
}
50 changes: 50 additions & 0 deletions src/Attribute/AsTwigTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?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\TwigTest;

/**
* Registers a method as template test.
*
* If the first argument of the method has Twig\Environment type-hint, the test will receive the current environment.
* If the next argument of the method is named $context and has array type-hint, the test will receive the current context.
* The last argument of the method is the value to be tested, if any.
Copy link
Member

Choose a reason for hiding this comment

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

this is wrong. It is not the last argument. It is the following argument. A test can have as many arguments as you want after the value being tested, which become arguments in the Twig signature.

*
* #[AsTwigTest('foo')]
* public function fooTest(Environment $env, array $context, $value, $arg1 = null) { ... }
*
* @see TwigTest
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class AsTwigTest
{
public function __construct(
/**
* The name of the filter in Twig.
*
* @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,
) {
}
}
7 changes: 5 additions & 2 deletions src/Environment.php
Original file line number Diff line number Diff line change
Expand Up @@ -616,14 +616,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