diff --git a/src/Extension/Attribute/AsTwigFilter.php b/src/Extension/Attribute/AsTwigFilter.php new file mode 100644 index 00000000000..6f94cfaa8f6 --- /dev/null +++ b/src/Extension/Attribute/AsTwigFilter.php @@ -0,0 +1,29 @@ + + */ +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); + } + } + } +} diff --git a/tests/Extension/AttributeExtensionTest.php b/tests/Extension/AttributeExtensionTest.php new file mode 100644 index 00000000000..48230ff1139 --- /dev/null +++ b/tests/Extension/AttributeExtensionTest.php @@ -0,0 +1,96 @@ += 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']]; + } +} diff --git a/tests/Extension/Fixtures/AttributeExtension.php b/tests/Extension/Fixtures/AttributeExtension.php new file mode 100644 index 00000000000..15352677794 --- /dev/null +++ b/tests/Extension/Fixtures/AttributeExtension.php @@ -0,0 +1,102 @@ + 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) + { + } +}