Skip to content

Commit

Permalink
Allows registration of filter/function/test with an attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
GromNaN committed Nov 26, 2023
1 parent aeeec9a commit 20cf159
Show file tree
Hide file tree
Showing 6 changed files with 390 additions and 0 deletions.
29 changes: 29 additions & 0 deletions src/Extension/Attribute/AsTwigFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Twig\Extension\Attribute;

use Twig\TwigFilter;

/**
* Registers a method as template filter.
*
* @see TwigFilter
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class AsTwigFilter
{
public function __construct(
/**
* The name of the filter in Twig (defaults to the method name).
*
* @var non-empty-string|null $name
*/
public ?string $name = null,

/**
* @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}
*/
public array $options = [],
) {
}
}
29 changes: 29 additions & 0 deletions src/Extension/Attribute/AsTwigFunction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Twig\Extension\Attribute;

use Twig\TwigFunction;

/**
* Registers a method as template function.
*
* @see TwigFunction
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class AsTwigFunction
{
public function __construct(
/**
* The name of the function in Twig (defaults to the method name).
*
* @var non-empty-string|null $name
*/
public ?string $name = null,

/**
* @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}
*/
public array $options = [],
) {
}
}
34 changes: 34 additions & 0 deletions src/Extension/Attribute/AsTwigTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Twig\Extension\Attribute;

use Twig\TwigTest;

/**
* Registers a method as template test.
*
* @see TwigTest
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class AsTwigTest
{
public function __construct(
/**
* The name of the filter in Twig (defaults to the method name).
*
* @var non-empty-string|null $name
*/
public ?string $name = null,

/**
* @var array{is_variadic?:bool, node_class?:class-string, deprecated?:bool|string, alternative?:string, one_mandatory_argument?:bool}
*/
public array $options = [],

/**
* @var array<int, mixed>
*/
public array $arguments = [],
) {
}
}
100 changes: 100 additions & 0 deletions src/Extension/Extension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?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\Environment;
use Twig\Extension\Attribute\AsTwigFilter;
use Twig\Extension\Attribute\AsTwigFunction;
use Twig\Extension\Attribute\AsTwigTest;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;

/**
* Abstract class for extension using the new PHP 8 attributes to define filters, functions, and tests.
*
* @author Jérôme Tamarelle <[email protected]>
*/
abstract class Extension extends AbstractExtension
{
public function getFilters(): \Generator
{
$reflectionClass = new \ReflectionClass($this);
foreach ($reflectionClass->getMethods() as $method) {
foreach ($method->getAttributes(AsTwigFilter::class) as $attribute) {
$attribute = $attribute->newInstance();
$options = $attribute->options;
if (!\array_key_exists('needs_environment', $options)) {
$param = $method->getParameters()[0] ?? null;
$options['needs_environment'] = $param && 'env' === $param->getName() && Environment::class === $param->getType()->getName();
}
$firstParam = $options['needs_environment'] ? 1 : 0;
if (!\array_key_exists('needs_context', $options)) {
$param = $method->getParameters()[$firstParam] ?? null;
$options['needs_context'] = $param && 'context' === $param->getName() && 'array' === $param->getType()->getName();
}
$firstParam += $options['needs_context'] ? 1 : 0;
if (!\array_key_exists('is_variadic', $options)) {
$param = $method->getParameters()[$firstParam] ?? null;
$options['is_variadic'] = $param && $param->isVariadic();
}

yield new TwigFilter($attribute->name ?? $method->getName(), [$this, $method->getName()], $options);
}
}
}

public function getFunctions(): \Generator
{
$reflectionClass = new \ReflectionClass($this);
foreach ($reflectionClass->getMethods() as $method) {
foreach ($method->getAttributes(AsTwigFunction::class) as $attribute) {
$attribute = $attribute->newInstance();
$options = $attribute->options;
if (!\array_key_exists('needs_environment', $options)) {
$param = $method->getParameters()[0] ?? null;
$options['needs_environment'] = $param && 'env' === $param->getName() && Environment::class === $param->getType()->getName();
}
$firstParam = $options['needs_environment'] ? 1 : 0;
if (!\array_key_exists('needs_context', $options)) {
$param = $method->getParameters()[$firstParam] ?? null;
$options['needs_context'] = $param && 'context' === $param->getName() && 'array' === $param->getType()->getName();
}
$firstParam += $options['needs_context'] ? 1 : 0;
if (!\array_key_exists('is_variadic', $options)) {
$param = $method->getParameters()[$firstParam] ?? null;
$options['is_variadic'] = $param && $param->isVariadic();
}

yield new TwigFunction($attribute->name ?? $method->getName(), [$this, $method->getName()], $options);
}
}
}

public function getTests(): \Generator
{
$reflectionClass = new \ReflectionClass($this);
foreach ($reflectionClass->getMethods() as $method) {
foreach ($method->getAttributes(AsTwigTest::class) as $attribute) {
$attribute = $attribute->newInstance();
$options = $attribute->options;

if (!\array_key_exists('is_variadic', $options)) {
$param = $method->getParameters()[0] ?? null;
$options['is_variadic'] = $param && $param->isVariadic();
}

yield new TwigTest($attribute->name ?? $method->getName(), [$this, $method->getName()], $options);
}
}
}
}
96 changes: 96 additions & 0 deletions tests/Extension/AttributeExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

namespace Twig\Tests\Extension;

use PHPUnit\Framework\TestCase;
use Twig\Tests\Extension\Fixtures\AttributeExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;

/**
* @requires PHP >= 8.0
*/
class AttributeExtensionTest extends TestCase
{
/**
* @dataProvider provideFilters
*/
public function testFilter(string $name, string $method, array $options)
{
$extension = new AttributeExtension();
foreach ($extension->getFilters() as $filter) {
if ($filter->getName() === $name) {
$this->assertEquals(new TwigFilter($name, [$extension, $method], $options), $filter);

return;
}
}

$this->fail(sprintf('Filter "%s" is not registered.', $name));
}

public static function provideFilters()
{
yield 'basic' => ['fooFilter', 'fooFilter', []];
yield 'with name' => ['bar', 'barFilter', []];
yield 'with env' => ['withEnvFilter', 'withEnvFilter', ['needs_environment' => true]];
yield 'with context' => ['withContextFilter', 'withContextFilter', ['needs_context' => true]];
yield 'with env and context' => ['withEnvAndContextFilter', 'withEnvAndContextFilter', ['needs_environment' => true, 'needs_context' => true]];
yield 'variadic' => ['variadicFilter', 'variadicFilter', ['is_variadic' => true]];
yield 'deprecated' => ['deprecatedFilter', 'deprecatedFilter', ['deprecated' => true, 'alternative' => 'bar']];
}

/**
* @dataProvider provideFunctions
*/
public function testFunction(string $name, string $method, array $options)
{
$extension = new AttributeExtension();
foreach ($extension->getFunctions() as $function) {
if ($function->getName() === $name) {
$this->assertEquals(new TwigFunction($name, [$extension, $method], $options), $function);

return;
}
}

$this->fail(sprintf('Function "%s" is not registered.', $name));
}

public static function provideFunctions()
{
yield 'basic' => ['fooFunction', 'fooFunction', []];
yield 'with name' => ['bar', 'barFunction', []];
yield 'with env' => ['withEnvFunction', 'withEnvFunction', ['needs_environment' => true]];
yield 'with context' => ['withContextFunction', 'withContextFunction', ['needs_context' => true]];
yield 'with env and context' => ['withEnvAndContextFunction', 'withEnvAndContextFunction', ['needs_environment' => true, 'needs_context' => true]];
yield 'variadic' => ['variadicFunction', 'variadicFunction', ['is_variadic' => true]];
yield 'deprecated' => ['deprecatedFunction', 'deprecatedFunction', ['deprecated' => true, 'alternative' => 'bar']];
}

/**
* @dataProvider provideTests
*/
public function testTest(string $name, string $method, array $options)
{
$extension = new AttributeExtension();
foreach ($extension->getTests() as $test) {
if ($test->getName() === $name) {
$this->assertEquals(new TwigTest($name, [$extension, $method], $options), $test);

return;
}
}

$this->fail(sprintf('Function "%s" is not registered.', $name));
}

public static function provideTests()
{
yield 'basic' => ['fooTest', 'fooTest', []];
yield 'with name' => ['bar', 'barTest', []];
yield 'variadic' => ['variadicTest', 'variadicTest', ['is_variadic' => true]];
yield 'deprecated' => ['deprecatedTest', 'deprecatedTest', ['deprecated' => true, 'alternative' => 'bar']];
}
}
102 changes: 102 additions & 0 deletions tests/Extension/Fixtures/AttributeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

namespace Twig\Tests\Extension\Fixtures;

use Twig\Environment;
use Twig\Extension\Attribute\AsTwigFilter;
use Twig\Extension\Attribute\AsTwigFunction;
use Twig\Extension\Attribute\AsTwigTest;
use Twig\Extension\Extension;

class AttributeExtension extends Extension
{
#[AsTwigFilter]
public function fooFilter(string $string)
{
}

#[AsTwigFilter(name: 'bar')]
public function barFilter(string $string)
{
}

#[AsTwigFilter]
public function withContextFilter(array $context, string $string)
{
}

#[AsTwigFilter]
public function withEnvFilter(Environment $env, string $string)
{
}

#[AsTwigFilter]
public function withEnvAndContextFilter(Environment $env, array $context, string $string)
{
}

#[AsTwigFilter]
public function variadicFilter(string ...$strings)
{
}

#[AsTwigFilter(options: ['deprecated' => true, 'alternative' => 'bar'])]
public function deprecatedFilter(string $string)
{
}

#[AsTwigFunction]
public function fooFunction(string $string)
{
}

#[AsTwigFunction(name: 'bar')]
public function barFunction(string $string)
{
}

#[AsTwigFunction]
public function withContextFunction(array $context, string $string)
{
}

#[AsTwigFunction]
public function withEnvFunction(Environment $env, string $string)
{
}

#[AsTwigFunction]
public function withEnvAndContextFunction(Environment $env, array $context, string $string)
{
}

#[AsTwigFunction]
public function variadicFunction(string ...$strings)
{
}

#[AsTwigFunction(options: ['deprecated' => true, 'alternative' => 'bar'])]
public function deprecatedFunction(string $string)
{
}

#[AsTwigTest]
public function fooTest(string $string)
{
}

#[AsTwigTest(name: 'bar')]
public function barTest(string $string)
{
}

#[AsTwigTest]
public function variadicTest(string ...$strings)
{
}

#[AsTwigTest(options: ['deprecated' => true, 'alternative' => 'bar'])]
public function deprecatedTest(string $strings)
{
}
}

0 comments on commit 20cf159

Please sign in to comment.