mirror of
https://github.com/twigphp/Twig.git
synced 2026-11-02 09:05:10 +00:00
Create attributes AsTwigFilter, AsTwigFunction and AsTwigTest to ease extension development
This commit is contained in:
committed by
Fabien Potencier
parent
8440cf4ac2
commit
5886907b28
@@ -814,6 +814,107 @@ The ``getTests()`` method lets you add new test functions::
|
||||
// ...
|
||||
}
|
||||
|
||||
Using PHP Attributes to define Extensions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 3.21
|
||||
|
||||
The attribute classes were added in Twig 3.21.
|
||||
|
||||
You can add the ``#[AsTwigFilter]``, ``#[AsTwigFunction]``, and ``#[AsTwigTest]``
|
||||
attributes to public methods of any class to define filters, functions, and tests.
|
||||
|
||||
Create a class using these 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(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
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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.
|
||||
* Additional arguments of the method come from the filter call.
|
||||
*
|
||||
* #[AsTwigFilter(name: 'foo')]
|
||||
* function fooFilter(Environment $env, $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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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.
|
||||
* Additional arguments of the method come from the function call.
|
||||
*
|
||||
* #[AsTwigFunction(name: 'foo')]
|
||||
* function fooFunction(Environment $env, 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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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(name: '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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?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\Extension;
|
||||
|
||||
use Twig\Attribute\AsTwigFilter;
|
||||
use Twig\Attribute\AsTwigFunction;
|
||||
use Twig\Attribute\AsTwigTest;
|
||||
use Twig\Environment;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
use Twig\TwigTest;
|
||||
|
||||
/**
|
||||
* Define Twig filters, functions, and tests with PHP attributes.
|
||||
*
|
||||
* @author Jérôme Tamarelle <jerome@tamarelle.net>
|
||||
*/
|
||||
final class AttributeExtension extends AbstractExtension
|
||||
{
|
||||
private array $filters;
|
||||
private array $functions;
|
||||
private array $tests;
|
||||
|
||||
/**
|
||||
* Use a runtime class using PHP attributes to define filters, functions, and tests.
|
||||
*
|
||||
* @param class-string $class
|
||||
*/
|
||||
public function __construct(private string $class)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string
|
||||
*/
|
||||
public function getClass(): string
|
||||
{
|
||||
return $this->class;
|
||||
}
|
||||
|
||||
public function getFilters(): array
|
||||
{
|
||||
if (!isset($this->filters)) {
|
||||
$this->initFromAttributes();
|
||||
}
|
||||
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
public function getFunctions(): array
|
||||
{
|
||||
if (!isset($this->functions)) {
|
||||
$this->initFromAttributes();
|
||||
}
|
||||
|
||||
return $this->functions;
|
||||
}
|
||||
|
||||
public function getTests(): array
|
||||
{
|
||||
if (!isset($this->tests)) {
|
||||
$this->initFromAttributes();
|
||||
}
|
||||
|
||||
return $this->tests;
|
||||
}
|
||||
|
||||
public function getLastModified(): int
|
||||
{
|
||||
return max(
|
||||
filemtime(__FILE__),
|
||||
is_file($filename = (new \ReflectionClass($this->getClass()))->getFileName()) ? filemtime($filename) : 0,
|
||||
);
|
||||
}
|
||||
|
||||
private function initFromAttributes(): void
|
||||
{
|
||||
$filters = $functions = $tests = [];
|
||||
$reflectionClass = new \ReflectionClass($this->getClass());
|
||||
foreach ($reflectionClass->getMethods() as $method) {
|
||||
foreach ($method->getAttributes(AsTwigFilter::class) as $reflectionAttribute) {
|
||||
/** @var AsTwigFilter $attribute */
|
||||
$attribute = $reflectionAttribute->newInstance();
|
||||
|
||||
$callable = new TwigFilter($attribute->name, [$reflectionClass->name, $method->getName()], [
|
||||
'needs_context' => $attribute->needsContext ?? false,
|
||||
'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method),
|
||||
'needs_charset' => $attribute->needsCharset ?? false,
|
||||
'is_variadic' => $method->isVariadic(),
|
||||
'is_safe' => $attribute->isSafe,
|
||||
'is_safe_callback' => $attribute->isSafeCallback,
|
||||
'pre_escape' => $attribute->preEscape,
|
||||
'preserves_safety' => $attribute->preservesSafety,
|
||||
'deprecation_info' => $attribute->deprecationInfo,
|
||||
]);
|
||||
|
||||
if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) {
|
||||
throw new \LogicException(sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFilter, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
|
||||
}
|
||||
|
||||
$filters[$attribute->name] = $callable;
|
||||
}
|
||||
|
||||
foreach ($method->getAttributes(AsTwigFunction::class) as $reflectionAttribute) {
|
||||
/** @var AsTwigFunction $attribute */
|
||||
$attribute = $reflectionAttribute->newInstance();
|
||||
|
||||
$callable = new TwigFunction($attribute->name, [$reflectionClass->name, $method->getName()], [
|
||||
'needs_context' => $attribute->needsContext ?? false,
|
||||
'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method),
|
||||
'needs_charset' => $attribute->needsCharset ?? false,
|
||||
'is_variadic' => $method->isVariadic(),
|
||||
'is_safe' => $attribute->isSafe,
|
||||
'is_safe_callback' => $attribute->isSafeCallback,
|
||||
'deprecation_info' => $attribute->deprecationInfo,
|
||||
]);
|
||||
|
||||
if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) {
|
||||
throw new \LogicException(sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFunction, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
|
||||
}
|
||||
|
||||
$functions[$attribute->name] = $callable;
|
||||
}
|
||||
|
||||
foreach ($method->getAttributes(AsTwigTest::class) as $reflectionAttribute) {
|
||||
|
||||
/** @var AsTwigTest $attribute */
|
||||
$attribute = $reflectionAttribute->newInstance();
|
||||
|
||||
$callable = new TwigTest($attribute->name, [$reflectionClass->name, $method->getName()], [
|
||||
'needs_context' => $attribute->needsContext ?? false,
|
||||
'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method),
|
||||
'needs_charset' => $attribute->needsCharset ?? false,
|
||||
'is_variadic' => $method->isVariadic(),
|
||||
'deprecation_info' => $attribute->deprecationInfo,
|
||||
]);
|
||||
|
||||
if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) {
|
||||
throw new \LogicException(sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigTest, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
|
||||
}
|
||||
|
||||
$tests[$attribute->name] = $callable;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign all at the end to avoid inconsistent state in case of exception
|
||||
$this->filters = array_values($filters);
|
||||
$this->functions = array_values($functions);
|
||||
$this->tests = array_values($tests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if the first argument of the method is the environment.
|
||||
*/
|
||||
private function needsEnvironment(\ReflectionFunctionAbstract $function): bool
|
||||
{
|
||||
if (!$parameters = $function->getParameters()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $parameters[0]->getType() instanceof \ReflectionNamedType
|
||||
&& Environment::class === $parameters[0]->getType()->getName()
|
||||
&& !$parameters[0]->isVariadic();
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use Twig\ExpressionParser\InfixAssociativity;
|
||||
use Twig\ExpressionParser\InfixExpressionParserInterface;
|
||||
use Twig\ExpressionParser\PrecedenceChange;
|
||||
use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser;
|
||||
use Twig\Extension\AttributeExtension;
|
||||
use Twig\Extension\ExtensionInterface;
|
||||
use Twig\Extension\GlobalsInterface;
|
||||
use Twig\Extension\LastModifiedExtensionInterface;
|
||||
@@ -142,7 +143,11 @@ final class ExtensionSet
|
||||
|
||||
public function addExtension(ExtensionInterface $extension): void
|
||||
{
|
||||
$class = $extension::class;
|
||||
if ($extension instanceof AttributeExtension) {
|
||||
$class = $extension->getClass();
|
||||
} else {
|
||||
$class = $extension::class;
|
||||
}
|
||||
|
||||
if ($this->initialized) {
|
||||
throw new \LogicException(\sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class));
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace Twig\Tests\Extension;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\DeprecatedCallableInfo;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Extension\AttributeExtension;
|
||||
use Twig\ExtensionSet;
|
||||
use Twig\Tests\Extension\Fixtures\FilterWithoutValue;
|
||||
use Twig\Tests\Extension\Fixtures\TestWithoutValue;
|
||||
use Twig\Tests\Extension\Fixtures\ExtensionWithAttributes;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
use Twig\TwigTest;
|
||||
|
||||
class AttributeExtensionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider provideFilters
|
||||
*/
|
||||
public function testFilter(string $name, string $method, array $options)
|
||||
{
|
||||
$extension = new AttributeExtension(ExtensionWithAttributes::class);
|
||||
foreach ($extension->getFilters() as $filter) {
|
||||
if ($filter->getName() === $name) {
|
||||
$this->assertEquals(new TwigFilter($name, [ExtensionWithAttributes::class, $method], $options), $filter);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->fail(sprintf('Filter "%s" is not registered.', $name));
|
||||
}
|
||||
|
||||
public static function provideFilters()
|
||||
{
|
||||
yield 'with name' => ['foo', 'fooFilter', ['is_safe' => ['html']]];
|
||||
yield 'with env' => ['with_env_filter', 'withEnvFilter', ['needs_environment' => true]];
|
||||
yield 'with context' => ['with_context_filter', 'withContextFilter', ['needs_context' => true]];
|
||||
yield 'with env and context' => ['with_env_and_context_filter', 'withEnvAndContextFilter', ['needs_environment' => true, 'needs_context' => true]];
|
||||
yield 'variadic' => ['variadic_filter', 'variadicFilter', ['is_variadic' => true]];
|
||||
yield 'deprecated' => ['deprecated_filter', 'deprecatedFilter', ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.2')]];
|
||||
yield 'pattern' => ['pattern_*_filter', 'patternFilter', []];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideFunctions
|
||||
*/
|
||||
public function testFunction(string $name, string $method, array $options)
|
||||
{
|
||||
$extension = new AttributeExtension(ExtensionWithAttributes::class);
|
||||
foreach ($extension->getFunctions() as $function) {
|
||||
if ($function->getName() === $name) {
|
||||
$this->assertEquals(new TwigFunction($name, [ExtensionWithAttributes::class, $method], $options), $function);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->fail(sprintf('Function "%s" is not registered.', $name));
|
||||
}
|
||||
|
||||
public static function provideFunctions()
|
||||
{
|
||||
yield 'with name' => ['foo', 'fooFunction', ['is_safe' => ['html']]];
|
||||
yield 'with env' => ['with_env_function', 'withEnvFunction', ['needs_environment' => true]];
|
||||
yield 'with context' => ['with_context_function', 'withContextFunction', ['needs_context' => true]];
|
||||
yield 'with env and context' => ['with_env_and_context_function', 'withEnvAndContextFunction', ['needs_environment' => true, 'needs_context' => true]];
|
||||
yield 'no argument' => ['no_arg_function', 'noArgFunction', []];
|
||||
yield 'variadic' => ['variadic_function', 'variadicFunction', ['is_variadic' => true]];
|
||||
yield 'deprecated' => ['deprecated_function', 'deprecatedFunction', ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.2')]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideTests
|
||||
*/
|
||||
public function testTest(string $name, string $method, array $options)
|
||||
{
|
||||
$extension = new AttributeExtension(ExtensionWithAttributes::class);
|
||||
foreach ($extension->getTests() as $test) {
|
||||
if ($test->getName() === $name) {
|
||||
$this->assertEquals(new TwigTest($name, [ExtensionWithAttributes::class, $method], $options), $test);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->fail(sprintf('Test "%s" is not registered.', $name));
|
||||
}
|
||||
|
||||
public static function provideTests()
|
||||
{
|
||||
yield 'with name' => ['foo', 'fooTest', []];
|
||||
yield 'with env' => ['with_env_test', 'withEnvTest', ['needs_environment' => true]];
|
||||
yield 'with context' => ['with_context_test', 'withContextTest', ['needs_context' => true]];
|
||||
yield 'with env and context' => ['with_env_and_context_test', 'withEnvAndContextTest', ['needs_environment' => true, 'needs_context' => true]];
|
||||
yield 'variadic' => ['variadic_test', 'variadicTest', ['is_variadic' => true]];
|
||||
yield 'deprecated' => ['deprecated_test', 'deprecatedTest', ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.2')]];
|
||||
}
|
||||
|
||||
public function testFilterRequireOneArgument()
|
||||
{
|
||||
$extension = new AttributeExtension(FilterWithoutValue::class);
|
||||
|
||||
$this->expectException(\LogicException::class);
|
||||
$this->expectExceptionMessage('"'.FilterWithoutValue::class.'::myFilter()" needs at least 1 arguments to be used AsTwigFilter, but only 0 defined.');
|
||||
|
||||
$extension->getTests();
|
||||
}
|
||||
|
||||
public function testTestRequireOneArgument()
|
||||
{
|
||||
$extension = new AttributeExtension(TestWithoutValue::class);
|
||||
|
||||
$this->expectException(\LogicException::class);
|
||||
$this->expectExceptionMessage('"'.TestWithoutValue::class.'::myTest()" needs at least 1 arguments to be used AsTwigTest, but only 0 defined.');
|
||||
|
||||
$extension->getTests();
|
||||
}
|
||||
|
||||
public function testLastModifiedWithObject()
|
||||
{
|
||||
$extension = new AttributeExtension(\stdClass::class);
|
||||
|
||||
$this->assertSame(filemtime((new \ReflectionClass(AttributeExtension::class))->getFileName()), $extension->getLastModified());
|
||||
}
|
||||
|
||||
public function testLastModifiedWithClass()
|
||||
{
|
||||
$extension = new AttributeExtension('__CLASS_FOR_TEST_LAST_MODIFIED__');
|
||||
|
||||
$filename = tempnam(sys_get_temp_dir(), 'twig');
|
||||
try {
|
||||
file_put_contents($filename, '<?php class __CLASS_FOR_TEST_LAST_MODIFIED__ {}');
|
||||
require $filename;
|
||||
|
||||
$this->assertSame(filemtime($filename), $extension->getLastModified());
|
||||
} finally {
|
||||
unlink($filename);
|
||||
}
|
||||
}
|
||||
|
||||
public function testMultipleRegistrations()
|
||||
{
|
||||
$extensionSet = new ExtensionSet();
|
||||
$extensionSet->addExtension($extension1 = new AttributeExtension(ExtensionWithAttributes::class));
|
||||
$extensionSet->addExtension($extension2 = new AttributeExtension(\stdClass::class));
|
||||
|
||||
$this->assertCount(2, $extensionSet->getExtensions());
|
||||
$this->assertNotNull($extensionSet->getFilter('foo'));
|
||||
|
||||
$this->assertSame($extension1, $extensionSet->getExtension(ExtensionWithAttributes::class));
|
||||
$this->assertSame($extension2, $extensionSet->getExtension(\stdClass::class));
|
||||
|
||||
$this->expectException(RuntimeError::class);
|
||||
$this->expectExceptionMessage('The "Twig\Extension\AttributeExtension" extension is not enabled.');
|
||||
$extensionSet->getExtension(AttributeExtension::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Twig\Tests\Extension\Fixtures;
|
||||
|
||||
use Twig\Attribute\AsTwigFilter;
|
||||
use Twig\Attribute\AsTwigFunction;
|
||||
use Twig\Attribute\AsTwigTest;
|
||||
use Twig\DeprecatedCallableInfo;
|
||||
use Twig\Environment;
|
||||
|
||||
class ExtensionWithAttributes
|
||||
{
|
||||
#[AsTwigFilter(name: 'foo', isSafe: ['html'])]
|
||||
public function fooFilter(string|int $string)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigFilter('with_context_filter', needsContext: true)]
|
||||
public function withContextFilter(array $context, string $string)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigFilter('with_env_filter')]
|
||||
public function withEnvFilter(Environment $env, string $string)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigFilter('with_env_and_context_filter', needsContext: true)]
|
||||
public function withEnvAndContextFilter(Environment $env, array $context, array $data)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigFilter('variadic_filter')]
|
||||
public function variadicFilter(string ...$strings)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigFilter('deprecated_filter', deprecationInfo: new DeprecatedCallableInfo('foo/bar', '1.2'))]
|
||||
public function deprecatedFilter(string $string)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigFilter('pattern_*_filter')]
|
||||
public function patternFilter(string $string)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigFunction(name: 'foo', isSafe: ['html'])]
|
||||
public function fooFunction(string|int $string)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigFunction('with_context_function', needsContext: true)]
|
||||
public function withContextFunction(array $context, string $string)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigFunction('with_env_function')]
|
||||
public function withEnvFunction(Environment $env, string $string)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigFunction('with_env_and_context_function', needsContext: true)]
|
||||
public function withEnvAndContextFunction(Environment $env, array $context, string $string)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigFunction('no_arg_function')]
|
||||
public function noArgFunction()
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigFunction('variadic_function')]
|
||||
public function variadicFunction(string ...$strings)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigFunction('deprecated_function', deprecationInfo: new DeprecatedCallableInfo('foo/bar', '1.2'))]
|
||||
public function deprecatedFunction(string $string)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigTest(name: 'foo')]
|
||||
public function fooTest(string|int $value)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigTest('variadic_test')]
|
||||
public function variadicTest(string ...$value)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigTest('with_context_test', needsContext: true)]
|
||||
public function withContextTest(array $context, $argument)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigTest('with_env_test')]
|
||||
public function withEnvTest(Environment $env, $argument)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigTest('with_env_and_context_test', needsContext: true)]
|
||||
public function withEnvAndContextTest(Environment $env, array $context, $argument)
|
||||
{
|
||||
}
|
||||
|
||||
#[AsTwigTest('deprecated_test', deprecationInfo: new DeprecatedCallableInfo('foo/bar', '1.2'))]
|
||||
public function deprecatedTest($value, $argument)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Twig\Tests\Extension\Fixtures;
|
||||
|
||||
use Twig\Attribute\AsTwigFilter;
|
||||
|
||||
class FilterWithoutValue
|
||||
{
|
||||
#[AsTwigFilter('my_filter')]
|
||||
public function myFilter()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Twig\Tests\Extension\Fixtures;
|
||||
|
||||
use Twig\Attribute\AsTwigTest;
|
||||
|
||||
class TestWithoutValue
|
||||
{
|
||||
#[AsTwigTest('my_test')]
|
||||
public function myTest()
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user