Skip to content

Commit 6bde02d

Browse files
committed
Allows registration of filter/function/test with an attribute
1 parent aeeec9a commit 6bde02d

File tree

6 files changed

+394
-0
lines changed

6 files changed

+394
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Twig\Extension\Attribute;
4+
5+
use Twig\TwigFilter;
6+
7+
/**
8+
* Registers a method as template filter.
9+
*
10+
* @see TwigFilter
11+
*/
12+
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
13+
class AsTwigFilter
14+
{
15+
public function __construct(
16+
/**
17+
* The name of the filter in Twig (defaults to the method name).
18+
*
19+
* @var non-empty-string|null $name
20+
*/
21+
public ?string $name = null,
22+
23+
/**
24+
* @var array{needs_environment?:bool, needs_context?:bool, is_variadic?:bool, is_safe?:array|null, is_safe_callback?:callable|null, pre_escape?:string|null, preserves_safety?:array|null, node_class?:class-string, deprecated?:bool|string, alternative?:string}
25+
*/
26+
public array $options = [],
27+
) {
28+
}
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Twig\Extension\Attribute;
4+
5+
use Twig\TwigFunction;
6+
7+
/**
8+
* Registers a method as template function.
9+
*
10+
* @see TwigFunction
11+
*/
12+
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
13+
class AsTwigFunction
14+
{
15+
public function __construct(
16+
/**
17+
* The name of the function in Twig (defaults to the method name).
18+
*
19+
* @var non-empty-string|null $name
20+
*/
21+
public ?string $name = null,
22+
23+
/**
24+
* @var array{needs_environment?:bool, needs_context?:bool, is_variadic?:bool, is_safe?:array|null, is_safe_callback?:callable|null, node_class?:class-string, deprecated?:bool|string, alternative?:string}
25+
*/
26+
public array $options = [],
27+
) {
28+
}
29+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Twig\Extension\Attribute;
4+
5+
use Twig\TwigTest;
6+
7+
/**
8+
* Registers a method as template test.
9+
*
10+
* @see TwigTest
11+
*/
12+
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
13+
class AsTwigTest
14+
{
15+
public function __construct(
16+
/**
17+
* The name of the filter in Twig (defaults to the method name).
18+
*
19+
* @var non-empty-string|null $name
20+
*/
21+
public ?string $name = null,
22+
23+
/**
24+
* @var array{is_variadic?:bool, node_class?:class-string, deprecated?:bool|string, alternative?:string, one_mandatory_argument?:bool}
25+
*/
26+
public array $options = [],
27+
28+
/**
29+
* @var array<int, mixed>
30+
*/
31+
public array $arguments = [],
32+
) {
33+
}
34+
}

src/Extension/Extension.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Twig.
5+
*
6+
* (c) Fabien Potencier
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Twig\Extension;
13+
14+
use Twig\Environment;
15+
use Twig\Extension\Attribute\AsTwigFilter;
16+
use Twig\Extension\Attribute\AsTwigFunction;
17+
use Twig\Extension\Attribute\AsTwigTest;
18+
use Twig\TwigFilter;
19+
use Twig\TwigFunction;
20+
use Twig\TwigTest;
21+
22+
/**
23+
* Abstract class for extension using the new PHP 8 attributes to define filters, functions, and tests.
24+
*
25+
* @author Jérôme Tamarelle <[email protected]>
26+
*/
27+
abstract class Extension extends AbstractExtension
28+
{
29+
public function getFilters(): \Generator
30+
{
31+
$reflectionClass = new \ReflectionClass($this);
32+
foreach ($reflectionClass->getMethods() as $method) {
33+
foreach ($method->getAttributes(AsTwigFilter::class) as $attribute) {
34+
$attribute = $attribute->newInstance();
35+
$options = $attribute->options;
36+
if (!\array_key_exists('needs_environment', $options)) {
37+
$param = $method->getParameters()[0] ?? null;
38+
$options['needs_environment'] = $param && 'env' === $param->getName() && Environment::class === $param->getType()->getName();
39+
}
40+
$firstParam = $options['needs_environment'] ? 1 : 0;
41+
if (!\array_key_exists('needs_context', $options)) {
42+
$param = $method->getParameters()[$firstParam] ?? null;
43+
$options['needs_context'] = $param && 'context' === $param->getName() && 'array' === $param->getType()->getName();
44+
}
45+
$firstParam += $options['needs_context'] ? 1 : 0;
46+
if (!\array_key_exists('is_variadic', $options)) {
47+
$param = $method->getParameters()[$firstParam] ?? null;
48+
$options['is_variadic'] = $param && $param->isVariadic();
49+
}
50+
51+
yield new TwigFilter($attribute->name ?? $method->getName(), [$this, $method->getName()], $options);
52+
}
53+
}
54+
}
55+
56+
public function getFunctions(): \Generator
57+
{
58+
$reflectionClass = new \ReflectionClass($this);
59+
foreach ($reflectionClass->getMethods() as $method) {
60+
foreach ($method->getAttributes(AsTwigFunction::class) as $attribute) {
61+
$attribute = $attribute->newInstance();
62+
$options = $attribute->options;
63+
if (!\array_key_exists('needs_environment', $options)) {
64+
$param = $method->getParameters()[0] ?? null;
65+
$options['needs_environment'] = $param && 'env' === $param->getName() && Environment::class === $param->getType()->getName();
66+
}
67+
$firstParam = $options['needs_environment'] ? 1 : 0;
68+
if (!\array_key_exists('needs_context', $options)) {
69+
$param = $method->getParameters()[$firstParam] ?? null;
70+
$options['needs_context'] = $param && 'context' === $param->getName() && 'array' === $param->getType()->getName();
71+
}
72+
$firstParam += $options['needs_context'] ? 1 : 0;
73+
if (!\array_key_exists('is_variadic', $options)) {
74+
$param = $method->getParameters()[$firstParam] ?? null;
75+
$options['is_variadic'] = $param && $param->isVariadic();
76+
}
77+
78+
yield new TwigFunction($attribute->name ?? $method->getName(), [$this, $method->getName()], $options);
79+
}
80+
}
81+
}
82+
83+
public function getTests(): \Generator
84+
{
85+
$reflectionClass = new \ReflectionClass($this);
86+
foreach ($reflectionClass->getMethods() as $method) {
87+
foreach ($method->getAttributes(AsTwigTest::class) as $attribute) {
88+
$attribute = $attribute->newInstance();
89+
$options = $attribute->options;
90+
91+
if (!\array_key_exists('is_variadic', $options)) {
92+
$param = $method->getParameters()[0] ?? null;
93+
$options['is_variadic'] = $param && $param->isVariadic();
94+
}
95+
96+
yield new TwigTest($attribute->name ?? $method->getName(), [$this, $method->getName()], $options);
97+
}
98+
}
99+
}
100+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
namespace Twig\Tests\Extension;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Twig\Tests\Extension\Fixtures\AttributeExtension;
7+
use Twig\TwigFilter;
8+
use Twig\TwigFunction;
9+
use Twig\TwigTest;
10+
11+
/**
12+
* @requires PHP 8.0
13+
*/
14+
class AttributeExtensionTest extends TestCase
15+
{
16+
private AttributeExtension $extension;
17+
18+
protected function setUp(): void
19+
{
20+
$this->extension = new AttributeExtension();
21+
}
22+
23+
/**
24+
* @dataProvider provideFilters
25+
*/
26+
public function testFilter(string $name, string $method, array $options)
27+
{
28+
foreach ($this->extension->getFilters() as $filter) {
29+
if ($filter->getName() === $name) {
30+
$this->assertEquals(new TwigFilter($name, [$this->extension, $method], $options), $filter);
31+
32+
return;
33+
}
34+
}
35+
36+
$this->fail(sprintf('Filter "%s" is not registered.', $name));
37+
}
38+
39+
public static function provideFilters()
40+
{
41+
yield 'basic' => ['fooFilter', 'fooFilter', []];
42+
yield 'with name' => ['bar', 'barFilter', []];
43+
yield 'with env' => ['withEnvFilter', 'withEnvFilter', ['needs_environment' => true]];
44+
yield 'with context' => ['withContextFilter', 'withContextFilter', ['needs_context' => true]];
45+
yield 'with env and context' => ['withEnvAndContextFilter', 'withEnvAndContextFilter', ['needs_environment' => true, 'needs_context' => true]];
46+
yield 'variadic' => ['variadicFilter', 'variadicFilter', ['is_variadic' => true]];
47+
yield 'deprecated' => ['deprecatedFilter', 'deprecatedFilter', ['deprecated' => true, 'alternative' => 'bar']];
48+
}
49+
50+
/**
51+
* @dataProvider provideFunctions
52+
*/
53+
public function testFunction(string $name, string $method, array $options)
54+
{
55+
foreach ($this->extension->getFunctions() as $function) {
56+
if ($function->getName() === $name) {
57+
$this->assertEquals(new TwigFunction($name, [$this->extension, $method], $options), $function);
58+
59+
return;
60+
}
61+
}
62+
63+
$this->fail(sprintf('Function "%s" is not registered.', $name));
64+
}
65+
66+
public static function provideFunctions()
67+
{
68+
yield 'basic' => ['fooFunction', 'fooFunction', []];
69+
yield 'with name' => ['bar', 'barFunction', []];
70+
yield 'with env' => ['withEnvFunction', 'withEnvFunction', ['needs_environment' => true]];
71+
yield 'with context' => ['withContextFunction', 'withContextFunction', ['needs_context' => true]];
72+
yield 'with env and context' => ['withEnvAndContextFunction', 'withEnvAndContextFunction', ['needs_environment' => true, 'needs_context' => true]];
73+
yield 'variadic' => ['variadicFunction', 'variadicFunction', ['is_variadic' => true]];
74+
yield 'deprecated' => ['deprecatedFunction', 'deprecatedFunction', ['deprecated' => true, 'alternative' => 'bar']];
75+
}
76+
77+
/**
78+
* @dataProvider provideTests
79+
*/
80+
public function testTest(string $name, string $method, array $options)
81+
{
82+
foreach ($this->extension->getTests() as $test) {
83+
if ($test->getName() === $name) {
84+
$this->assertEquals(new TwigTest($name, [$this->extension, $method], $options), $test);
85+
86+
return;
87+
}
88+
}
89+
90+
$this->fail(sprintf('Function "%s" is not registered.', $name));
91+
}
92+
93+
public static function provideTests()
94+
{
95+
yield 'basic' => ['fooTest', 'fooTest', []];
96+
yield 'with name' => ['bar', 'barTest', []];
97+
yield 'variadic' => ['variadicTest', 'variadicTest', ['is_variadic' => true]];
98+
yield 'deprecated' => ['deprecatedTest', 'deprecatedTest', ['deprecated' => true, 'alternative' => 'bar']];
99+
}
100+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
namespace Twig\Tests\Extension\Fixtures;
4+
5+
use Twig\Environment;
6+
use Twig\Extension\Attribute\AsTwigFilter;
7+
use Twig\Extension\Attribute\AsTwigFunction;
8+
use Twig\Extension\Attribute\AsTwigTest;
9+
use Twig\Extension\Extension;
10+
11+
class AttributeExtension extends Extension
12+
{
13+
#[AsTwigFilter]
14+
public function fooFilter(string $string)
15+
{
16+
}
17+
18+
#[AsTwigFilter(name: 'bar')]
19+
public function barFilter(string $string)
20+
{
21+
}
22+
23+
#[AsTwigFilter]
24+
public function withContextFilter(array $context, string $string)
25+
{
26+
}
27+
28+
#[AsTwigFilter]
29+
public function withEnvFilter(Environment $env, string $string)
30+
{
31+
}
32+
33+
#[AsTwigFilter]
34+
public function withEnvAndContextFilter(Environment $env, array $context, string $string)
35+
{
36+
}
37+
38+
#[AsTwigFilter]
39+
public function variadicFilter(string ...$strings)
40+
{
41+
}
42+
43+
#[AsTwigFilter(options: ['deprecated' => true, 'alternative' => 'bar'])]
44+
public function deprecatedFilter(string $string)
45+
{
46+
}
47+
48+
#[AsTwigFunction]
49+
public function fooFunction(string $string)
50+
{
51+
}
52+
53+
#[AsTwigFunction(name: 'bar')]
54+
public function barFunction(string $string)
55+
{
56+
}
57+
58+
#[AsTwigFunction]
59+
public function withContextFunction(array $context, string $string)
60+
{
61+
}
62+
63+
#[AsTwigFunction]
64+
public function withEnvFunction(Environment $env, string $string)
65+
{
66+
}
67+
68+
#[AsTwigFunction]
69+
public function withEnvAndContextFunction(Environment $env, array $context, string $string)
70+
{
71+
}
72+
73+
#[AsTwigFunction]
74+
public function variadicFunction(string ...$strings)
75+
{
76+
}
77+
78+
#[AsTwigFunction(options: ['deprecated' => true, 'alternative' => 'bar'])]
79+
public function deprecatedFunction(string $string)
80+
{
81+
}
82+
83+
#[AsTwigTest]
84+
public function fooTest(string $string)
85+
{
86+
}
87+
88+
#[AsTwigTest(name: 'bar')]
89+
public function barTest(string $string)
90+
{
91+
}
92+
93+
#[AsTwigTest]
94+
public function variadicTest(string ...$strings)
95+
{
96+
}
97+
98+
#[AsTwigTest(options: ['deprecated' => true, 'alternative' => 'bar'])]
99+
public function deprecatedTest(string $strings)
100+
{
101+
}
102+
}

0 commit comments

Comments
 (0)