From c8c2f57f9b5eec3dc488112682853442989b6b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 26 Nov 2023 00:43:09 +0100 Subject: [PATCH 01/17] Allows registration of filter/function/test with an attribute --- src/Extension/Attribute/AsTwigFilter.php | 29 +++++ src/Extension/Attribute/AsTwigFunction.php | 29 +++++ src/Extension/Attribute/AsTwigTest.php | 29 +++++ src/Extension/Extension.php | 100 +++++++++++++++++ tests/Extension/AttributeExtensionTest.php | 96 +++++++++++++++++ .../Extension/Fixtures/AttributeExtension.php | 102 ++++++++++++++++++ 6 files changed, 385 insertions(+) create mode 100644 src/Extension/Attribute/AsTwigFilter.php create mode 100644 src/Extension/Attribute/AsTwigFunction.php create mode 100644 src/Extension/Attribute/AsTwigTest.php create mode 100644 src/Extension/Extension.php create mode 100644 tests/Extension/AttributeExtensionTest.php create mode 100644 tests/Extension/Fixtures/AttributeExtension.php 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) + { + } +} From 4d9ae3f8016c8cc6a8dfce1bc5f295fb1a515711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 26 Nov 2023 01:51:12 +0100 Subject: [PATCH 02/17] Undocument detected options --- src/Extension/Attribute/AsTwigFilter.php | 2 +- src/Extension/Attribute/AsTwigFunction.php | 2 +- src/Extension/Attribute/AsTwigTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Extension/Attribute/AsTwigFilter.php b/src/Extension/Attribute/AsTwigFilter.php index 6f94cfaa8f6..8b7e88d36b2 100644 --- a/src/Extension/Attribute/AsTwigFilter.php +++ b/src/Extension/Attribute/AsTwigFilter.php @@ -21,7 +21,7 @@ public function __construct( 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} + * @var array{is_safe?:array|null, is_safe_callback?:callable|null, pre_escape?:string|null, preserves_safety?:array|null, deprecated?:bool|string, alternative?:string} */ public array $options = [], ) { diff --git a/src/Extension/Attribute/AsTwigFunction.php b/src/Extension/Attribute/AsTwigFunction.php index 5344a8c8e4b..489abda74b8 100644 --- a/src/Extension/Attribute/AsTwigFunction.php +++ b/src/Extension/Attribute/AsTwigFunction.php @@ -21,7 +21,7 @@ public function __construct( 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} + * @var array{is_safe?:array|null, is_safe_callback?:callable|null, deprecated?:bool|string, alternative?:string} */ public array $options = [], ) { diff --git a/src/Extension/Attribute/AsTwigTest.php b/src/Extension/Attribute/AsTwigTest.php index d0312f207dd..9624d0fec85 100644 --- a/src/Extension/Attribute/AsTwigTest.php +++ b/src/Extension/Attribute/AsTwigTest.php @@ -21,7 +21,7 @@ public function __construct( public ?string $name = null, /** - * @var array{is_variadic?:bool, node_class?:class-string, deprecated?:bool|string, alternative?:string, one_mandatory_argument?:bool} + * @var array{is_variadic?:bool, deprecated?:bool|string, alternative?:string} */ public array $options = [], ) { From 22aeb4fb74e75b6fb7a8e44ed67860523f6d9a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 26 Nov 2023 01:54:27 +0100 Subject: [PATCH 03/17] Test repeated attributes --- tests/Extension/AttributeExtensionTest.php | 6 +++--- .../Extension/Fixtures/AttributeExtension.php | 18 +++--------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/tests/Extension/AttributeExtensionTest.php b/tests/Extension/AttributeExtensionTest.php index 48230ff1139..fb842b57e4b 100644 --- a/tests/Extension/AttributeExtensionTest.php +++ b/tests/Extension/AttributeExtensionTest.php @@ -33,7 +33,7 @@ public function testFilter(string $name, string $method, array $options) public static function provideFilters() { yield 'basic' => ['fooFilter', 'fooFilter', []]; - yield 'with name' => ['bar', 'barFilter', []]; + yield 'with name' => ['foo', 'fooFilter', []]; 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]]; @@ -61,7 +61,7 @@ public function testFunction(string $name, string $method, array $options) public static function provideFunctions() { yield 'basic' => ['fooFunction', 'fooFunction', []]; - yield 'with name' => ['bar', 'barFunction', []]; + yield 'with name' => ['foo', 'fooFunction', []]; 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]]; @@ -89,7 +89,7 @@ public function testTest(string $name, string $method, array $options) public static function provideTests() { yield 'basic' => ['fooTest', 'fooTest', []]; - yield 'with name' => ['bar', 'barTest', []]; + yield 'with name' => ['foo', 'fooTest', []]; 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 index 15352677794..0bb8f024c3b 100644 --- a/tests/Extension/Fixtures/AttributeExtension.php +++ b/tests/Extension/Fixtures/AttributeExtension.php @@ -11,15 +11,11 @@ class AttributeExtension extends Extension { #[AsTwigFilter] + #[AsTwigFilter(name: 'foo')] public function fooFilter(string $string) { } - #[AsTwigFilter(name: 'bar')] - public function barFilter(string $string) - { - } - #[AsTwigFilter] public function withContextFilter(array $context, string $string) { @@ -46,15 +42,11 @@ public function deprecatedFilter(string $string) } #[AsTwigFunction] + #[AsTwigFunction(name: 'foo')] public function fooFunction(string $string) { } - #[AsTwigFunction(name: 'bar')] - public function barFunction(string $string) - { - } - #[AsTwigFunction] public function withContextFunction(array $context, string $string) { @@ -81,15 +73,11 @@ public function deprecatedFunction(string $string) } #[AsTwigTest] + #[AsTwigTest(name: 'foo')] public function fooTest(string $string) { } - #[AsTwigTest(name: 'bar')] - public function barTest(string $string) - { - } - #[AsTwigTest] public function variadicTest(string ...$strings) { From 3f84792fcb890f5bb9d38db76c2018652a8880b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 26 Nov 2023 15:40:02 +0100 Subject: [PATCH 04/17] Use attributes in standalone classes and add flatten promote options as attribute properties --- src/Extension/Attribute/AsTwigFilter.php | 11 +- src/Extension/Attribute/AsTwigFunction.php | 9 +- src/Extension/Attribute/AsTwigTest.php | 7 +- src/Extension/AttributeExtension.php | 161 ++++++++++++++++++ src/Extension/Extension.php | 100 ----------- tests/Extension/AttributeExtensionTest.php | 18 +- ...Extension.php => ObjectWithAttributes.php} | 9 +- 7 files changed, 188 insertions(+), 127 deletions(-) create mode 100644 src/Extension/AttributeExtension.php delete mode 100644 src/Extension/Extension.php rename tests/Extension/Fixtures/{AttributeExtension.php => ObjectWithAttributes.php} (84%) diff --git a/src/Extension/Attribute/AsTwigFilter.php b/src/Extension/Attribute/AsTwigFilter.php index 8b7e88d36b2..137fe79c7ce 100644 --- a/src/Extension/Attribute/AsTwigFilter.php +++ b/src/Extension/Attribute/AsTwigFilter.php @@ -19,11 +19,12 @@ public function __construct( * @var non-empty-string|null $name */ public ?string $name = null, - - /** - * @var array{is_safe?:array|null, is_safe_callback?:callable|null, pre_escape?:string|null, preserves_safety?:array|null, deprecated?:bool|string, alternative?:string} - */ - public array $options = [], + public bool $isSafe = false, + public ?string $isSafeCallback = null, + public ?string $preEscape = null, + public ?array $preservesSafety = null, + public bool|string $deprecated = false, + public ?string $alternative = null, ) { } } diff --git a/src/Extension/Attribute/AsTwigFunction.php b/src/Extension/Attribute/AsTwigFunction.php index 489abda74b8..825f1960c65 100644 --- a/src/Extension/Attribute/AsTwigFunction.php +++ b/src/Extension/Attribute/AsTwigFunction.php @@ -19,11 +19,10 @@ public function __construct( * @var non-empty-string|null $name */ public ?string $name = null, - - /** - * @var array{is_safe?:array|null, is_safe_callback?:callable|null, deprecated?:bool|string, alternative?:string} - */ - public array $options = [], + public bool $isSafe = false, + public ?string $isSafeCallback = null, + public bool|string $deprecated = false, + public ?string $alternative = null, ) { } } diff --git a/src/Extension/Attribute/AsTwigTest.php b/src/Extension/Attribute/AsTwigTest.php index 9624d0fec85..7aab61d8524 100644 --- a/src/Extension/Attribute/AsTwigTest.php +++ b/src/Extension/Attribute/AsTwigTest.php @@ -19,11 +19,8 @@ public function __construct( * @var non-empty-string|null $name */ public ?string $name = null, - - /** - * @var array{is_variadic?:bool, deprecated?:bool|string, alternative?:string} - */ - public array $options = [], + public bool|string $deprecated = false, + public ?string $alternative = null, ) { } } diff --git a/src/Extension/AttributeExtension.php b/src/Extension/AttributeExtension.php new file mode 100644 index 00000000000..9a518e7a563 --- /dev/null +++ b/src/Extension/AttributeExtension.php @@ -0,0 +1,161 @@ + + */ +final class AttributeExtension extends AbstractExtension +{ + private array $filters; + private array $functions; + private array $tests; + + public function __construct( + /** + * @var iterable + */ + private iterable $objects, + ) { + } + + 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; + } + + private function initFromAttributes() + { + $filters = $functions = $tests = []; + + foreach ($this->objects as $object) { + if (!\is_object($object)) { + throw new \LogicException(sprintf('"%s" class requires a list of objects, "%s" given.', __CLASS__, get_debug_type($object))); + } + $reflectionClass = new \ReflectionClass($object); + + foreach ($reflectionClass->getMethods() as $method) { + // Filters + foreach ($method->getAttributes(AsTwigFilter::class) as $attribute) { + $attribute = $attribute->newInstance(); + + $name = $attribute->name ?? $method->getName(); + if (isset($filters[$name])) { + throw new \LogicException(sprintf('Multiple definitions of the "%s" filter', $name)); + } + + $parameters = $method->getParameters(); + $needsEnvironment = isset($parameters[0]) && 'env' === $parameters[0]->getName() && Environment::class === $parameters[0]->getType()->getName(); + $firstParam = $needsEnvironment ? 1 : 0; + $needsContext = isset($parameters[$firstParam]) && 'context' === $parameters[$firstParam]->getName() && 'array' === $parameters[$firstParam]->getType()->getName(); + $firstParam += $needsContext ? 1 : 0; + $isVariadic = isset($parameters[$firstParam]) && $parameters[$firstParam]->isVariadic(); + + $filters[$name] = new TwigFilter($name, [$object, $method->getName()], [ + 'needs_environment' => $needsEnvironment, + 'needs_context' => $needsContext, + 'is_variadic' => $isVariadic, + 'is_safe' => $attribute->isSafe, + 'is_safe_callback' => $attribute->isSafeCallback, + 'pre_escape' => $attribute->preEscape, + 'preserves_safety' => $attribute->preservesSafety, + 'deprecated' => $attribute->deprecated, + 'alternative' => $attribute->alternative, + ]); + } + + // Functions + foreach ($method->getAttributes(AsTwigFunction::class) as $attribute) { + $attribute = $attribute->newInstance(); + + $name = $attribute->name ?? $method->getName(); + if (isset($functions[$name])) { + throw new \LogicException(sprintf('Multiple definitions of the "%s" function', $name)); + } + + $parameters = $method->getParameters(); + $needsEnvironment = isset($parameters[0]) && Environment::class === $parameters[0]->getType()->getName(); + $firstParam = $needsEnvironment ? 1 : 0; + $needsContext = isset($parameters[$firstParam]) && 'context' === $parameters[$firstParam]->getName() && 'array' === $parameters[$firstParam]->getType()->getName(); + $firstParam += $needsContext ? 1 : 0; + $isVariadic = isset($parameters[$firstParam]) && $parameters[$firstParam]->isVariadic(); + + $functions[$name] = new TwigFunction($name, [$object, $method->getName()], [ + 'needs_environment' => $needsEnvironment, + 'needs_context' => $needsContext, + 'is_variadic' => $isVariadic, + 'is_safe' => $attribute->isSafe, + 'is_safe_callback' => $attribute->isSafeCallback, + 'deprecated' => $attribute->deprecated, + 'alternative' => $attribute->alternative, + ]); + } + + // Tests + foreach ($method->getAttributes(AsTwigTest::class) as $attribute) { + $attribute = $attribute->newInstance(); + + $name = $attribute->name ?? $method->getName(); + if (isset($tests[$name])) { + throw new \LogicException(sprintf('Multiple definitions of the "%s" test', $name)); + } + + $parameters = $method->getParameters(); + $isVariadic = isset($parameters[0]) && $parameters[0]->isVariadic(); + + $tests[$name] = new TwigTest($name, [$object, $method->getName()], [ + 'is_variadic' => $isVariadic, + 'deprecated' => $attribute->deprecated, + 'alternative' => $attribute->alternative, + ]); + } + } + } + + // Assign all at the end to avoid inconsistent state in case of exception + $this->filters = $filters; + $this->functions = $functions; + $this->tests = $tests; + } +} diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php deleted file mode 100644 index 478625d30d2..00000000000 --- a/src/Extension/Extension.php +++ /dev/null @@ -1,100 +0,0 @@ - - */ -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 index fb842b57e4b..e2c3d33b62a 100644 --- a/tests/Extension/AttributeExtensionTest.php +++ b/tests/Extension/AttributeExtensionTest.php @@ -3,7 +3,8 @@ namespace Twig\Tests\Extension; use PHPUnit\Framework\TestCase; -use Twig\Tests\Extension\Fixtures\AttributeExtension; +use Twig\Extension\AttributeExtension; +use Twig\Tests\Extension\Fixtures\ObjectWithAttributes; use Twig\TwigFilter; use Twig\TwigFunction; use Twig\TwigTest; @@ -18,10 +19,11 @@ class AttributeExtensionTest extends TestCase */ public function testFilter(string $name, string $method, array $options) { - $extension = new AttributeExtension(); + $object = new ObjectWithAttributes(); + $extension = new AttributeExtension([$object]); foreach ($extension->getFilters() as $filter) { if ($filter->getName() === $name) { - $this->assertEquals(new TwigFilter($name, [$extension, $method], $options), $filter); + $this->assertEquals(new TwigFilter($name, [$object, $method], $options), $filter); return; } @@ -46,10 +48,11 @@ public static function provideFilters() */ public function testFunction(string $name, string $method, array $options) { - $extension = new AttributeExtension(); + $object = new ObjectWithAttributes(); + $extension = new AttributeExtension([$object]); foreach ($extension->getFunctions() as $function) { if ($function->getName() === $name) { - $this->assertEquals(new TwigFunction($name, [$extension, $method], $options), $function); + $this->assertEquals(new TwigFunction($name, [$object, $method], $options), $function); return; } @@ -74,10 +77,11 @@ public static function provideFunctions() */ public function testTest(string $name, string $method, array $options) { - $extension = new AttributeExtension(); + $object = new ObjectWithAttributes(); + $extension = new AttributeExtension([$object]); foreach ($extension->getTests() as $test) { if ($test->getName() === $name) { - $this->assertEquals(new TwigTest($name, [$extension, $method], $options), $test); + $this->assertEquals(new TwigTest($name, [$object, $method], $options), $test); return; } diff --git a/tests/Extension/Fixtures/AttributeExtension.php b/tests/Extension/Fixtures/ObjectWithAttributes.php similarity index 84% rename from tests/Extension/Fixtures/AttributeExtension.php rename to tests/Extension/Fixtures/ObjectWithAttributes.php index 0bb8f024c3b..efe6c4bea07 100644 --- a/tests/Extension/Fixtures/AttributeExtension.php +++ b/tests/Extension/Fixtures/ObjectWithAttributes.php @@ -6,9 +6,8 @@ use Twig\Extension\Attribute\AsTwigFilter; use Twig\Extension\Attribute\AsTwigFunction; use Twig\Extension\Attribute\AsTwigTest; -use Twig\Extension\Extension; -class AttributeExtension extends Extension +class ObjectWithAttributes { #[AsTwigFilter] #[AsTwigFilter(name: 'foo')] @@ -36,7 +35,7 @@ public function variadicFilter(string ...$strings) { } - #[AsTwigFilter(options: ['deprecated' => true, 'alternative' => 'bar'])] + #[AsTwigFilter(deprecated: true, alternative: 'bar')] public function deprecatedFilter(string $string) { } @@ -67,7 +66,7 @@ public function variadicFunction(string ...$strings) { } - #[AsTwigFunction(options: ['deprecated' => true, 'alternative' => 'bar'])] + #[AsTwigFunction(deprecated: true, alternative: 'bar')] public function deprecatedFunction(string $string) { } @@ -83,7 +82,7 @@ public function variadicTest(string ...$strings) { } - #[AsTwigTest(options: ['deprecated' => true, 'alternative' => 'bar'])] + #[AsTwigTest(deprecated: true, alternative: 'bar')] public function deprecatedTest(string $strings) { } From d441d0a08211e3446b0b3d09fb89a04164f6cb26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 26 Nov 2023 18:11:16 +0100 Subject: [PATCH 05/17] Accept a class name to leverage runtime lazy-loading --- src/Extension/AttributeExtension.php | 24 +++++++++++-------- tests/Extension/AttributeExtensionTest.php | 18 ++++++++++---- ...ibutes.php => ExtensionWithAttributes.php} | 3 ++- tests/IntegrationTest.php | 2 ++ 4 files changed, 32 insertions(+), 15 deletions(-) rename tests/Extension/Fixtures/{ObjectWithAttributes.php => ExtensionWithAttributes.php} (94%) diff --git a/src/Extension/AttributeExtension.php b/src/Extension/AttributeExtension.php index 9a518e7a563..afa613f59f0 100644 --- a/src/Extension/AttributeExtension.php +++ b/src/Extension/AttributeExtension.php @@ -20,7 +20,7 @@ use Twig\TwigTest; /** - * Register extension using the new PHP 8 attributes to define filters, functions, and tests. + * Define Twig filters, functions, and tests with PHP attributes. * * @author Jérôme Tamarelle */ @@ -32,9 +32,12 @@ final class AttributeExtension extends AbstractExtension public function __construct( /** - * @var iterable + * A list of objects or class names defining filters, functions, and tests using PHP attributes. + * When passing a class name, it must be available in runtimes. + * + * @var iterable */ - private iterable $objects, + private iterable $objectsOrClasses, ) { } @@ -69,11 +72,12 @@ private function initFromAttributes() { $filters = $functions = $tests = []; - foreach ($this->objects as $object) { - if (!\is_object($object)) { - throw new \LogicException(sprintf('"%s" class requires a list of objects, "%s" given.', __CLASS__, get_debug_type($object))); + foreach ($this->objectsOrClasses as $objectOrClass) { + try { + $reflectionClass = new \ReflectionClass($objectOrClass); + } catch (\ReflectionException $e) { + throw new \LogicException(sprintf('"%s" class requires a list of objects or class name, "%s" given.', __CLASS__, get_debug_type($objectOrClass)), 0, $e); } - $reflectionClass = new \ReflectionClass($object); foreach ($reflectionClass->getMethods() as $method) { // Filters @@ -92,7 +96,7 @@ private function initFromAttributes() $firstParam += $needsContext ? 1 : 0; $isVariadic = isset($parameters[$firstParam]) && $parameters[$firstParam]->isVariadic(); - $filters[$name] = new TwigFilter($name, [$object, $method->getName()], [ + $filters[$name] = new TwigFilter($name, [$objectOrClass, $method->getName()], [ 'needs_environment' => $needsEnvironment, 'needs_context' => $needsContext, 'is_variadic' => $isVariadic, @@ -121,7 +125,7 @@ private function initFromAttributes() $firstParam += $needsContext ? 1 : 0; $isVariadic = isset($parameters[$firstParam]) && $parameters[$firstParam]->isVariadic(); - $functions[$name] = new TwigFunction($name, [$object, $method->getName()], [ + $functions[$name] = new TwigFunction($name, [$objectOrClass, $method->getName()], [ 'needs_environment' => $needsEnvironment, 'needs_context' => $needsContext, 'is_variadic' => $isVariadic, @@ -144,7 +148,7 @@ private function initFromAttributes() $parameters = $method->getParameters(); $isVariadic = isset($parameters[0]) && $parameters[0]->isVariadic(); - $tests[$name] = new TwigTest($name, [$object, $method->getName()], [ + $tests[$name] = new TwigTest($name, [$objectOrClass, $method->getName()], [ 'is_variadic' => $isVariadic, 'deprecated' => $attribute->deprecated, 'alternative' => $attribute->alternative, diff --git a/tests/Extension/AttributeExtensionTest.php b/tests/Extension/AttributeExtensionTest.php index e2c3d33b62a..9a5c2e841ff 100644 --- a/tests/Extension/AttributeExtensionTest.php +++ b/tests/Extension/AttributeExtensionTest.php @@ -4,7 +4,7 @@ use PHPUnit\Framework\TestCase; use Twig\Extension\AttributeExtension; -use Twig\Tests\Extension\Fixtures\ObjectWithAttributes; +use Twig\Tests\Extension\Fixtures\ExtensionWithAttributes; use Twig\TwigFilter; use Twig\TwigFunction; use Twig\TwigTest; @@ -19,7 +19,7 @@ class AttributeExtensionTest extends TestCase */ public function testFilter(string $name, string $method, array $options) { - $object = new ObjectWithAttributes(); + $object = new ExtensionWithAttributes(); $extension = new AttributeExtension([$object]); foreach ($extension->getFilters() as $filter) { if ($filter->getName() === $name) { @@ -48,7 +48,7 @@ public static function provideFilters() */ public function testFunction(string $name, string $method, array $options) { - $object = new ObjectWithAttributes(); + $object = new ExtensionWithAttributes(); $extension = new AttributeExtension([$object]); foreach ($extension->getFunctions() as $function) { if ($function->getName() === $name) { @@ -77,7 +77,7 @@ public static function provideFunctions() */ public function testTest(string $name, string $method, array $options) { - $object = new ObjectWithAttributes(); + $object = new ExtensionWithAttributes(); $extension = new AttributeExtension([$object]); foreach ($extension->getTests() as $test) { if ($test->getName() === $name) { @@ -97,4 +97,14 @@ public static function provideTests() yield 'variadic' => ['variadicTest', 'variadicTest', ['is_variadic' => true]]; yield 'deprecated' => ['deprecatedTest', 'deprecatedTest', ['deprecated' => true, 'alternative' => 'bar']]; } + + public function testRuntimeExtension() + { + $class = ExtensionWithAttributes::class; + $extension = new AttributeExtension([$class]); + + $this->assertSame([$class, 'fooFilter'], $extension->getFilters()['foo']->getCallable()); + $this->assertSame([$class, 'fooFunction'], $extension->getFunctions()['foo']->getCallable()); + $this->assertSame([$class, 'fooTest'], $extension->getTests()['foo']->getCallable()); + } } diff --git a/tests/Extension/Fixtures/ObjectWithAttributes.php b/tests/Extension/Fixtures/ExtensionWithAttributes.php similarity index 94% rename from tests/Extension/Fixtures/ObjectWithAttributes.php rename to tests/Extension/Fixtures/ExtensionWithAttributes.php index efe6c4bea07..34866b062aa 100644 --- a/tests/Extension/Fixtures/ObjectWithAttributes.php +++ b/tests/Extension/Fixtures/ExtensionWithAttributes.php @@ -6,8 +6,9 @@ use Twig\Extension\Attribute\AsTwigFilter; use Twig\Extension\Attribute\AsTwigFunction; use Twig\Extension\Attribute\AsTwigTest; +use Twig\Extension\RuntimeExtensionInterface; -class ObjectWithAttributes +class ExtensionWithAttributes implements RuntimeExtensionInterface { #[AsTwigFilter] #[AsTwigFilter(name: 'foo')] diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index e2b211a01de..064084207cb 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -12,6 +12,7 @@ */ use Twig\Extension\AbstractExtension; +use Twig\Extension\AttributeExtension; use Twig\Extension\DebugExtension; use Twig\Extension\EscaperExtension; use Twig\Extension\SandboxExtension; @@ -45,6 +46,7 @@ public function getExtensions() new SandboxExtension($policy, false), new StringLoaderExtension(), new TwigTestExtension(), + new AttributeExtension(new \ArrayIterator([''])) ]; } From 153e0288e8832be3fb678e91a3fa96ca1cf6879a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 27 Nov 2023 00:32:12 +0100 Subject: [PATCH 06/17] Fix CS --- src/Extension/AttributeExtension.php | 6 +++--- tests/Extension/Fixtures/ExtensionWithAttributes.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Extension/AttributeExtension.php b/src/Extension/AttributeExtension.php index afa613f59f0..2bca2e291ae 100644 --- a/src/Extension/AttributeExtension.php +++ b/src/Extension/AttributeExtension.php @@ -86,7 +86,7 @@ private function initFromAttributes() $name = $attribute->name ?? $method->getName(); if (isset($filters[$name])) { - throw new \LogicException(sprintf('Multiple definitions of the "%s" filter', $name)); + throw new \LogicException(sprintf('Multiple definitions of the "%s" filter.', $name)); } $parameters = $method->getParameters(); @@ -115,7 +115,7 @@ private function initFromAttributes() $name = $attribute->name ?? $method->getName(); if (isset($functions[$name])) { - throw new \LogicException(sprintf('Multiple definitions of the "%s" function', $name)); + throw new \LogicException(sprintf('Multiple definitions of the "%s" function.', $name)); } $parameters = $method->getParameters(); @@ -142,7 +142,7 @@ private function initFromAttributes() $name = $attribute->name ?? $method->getName(); if (isset($tests[$name])) { - throw new \LogicException(sprintf('Multiple definitions of the "%s" test', $name)); + throw new \LogicException(sprintf('Multiple definitions of the "%s" test.', $name)); } $parameters = $method->getParameters(); diff --git a/tests/Extension/Fixtures/ExtensionWithAttributes.php b/tests/Extension/Fixtures/ExtensionWithAttributes.php index 34866b062aa..ca214a73382 100644 --- a/tests/Extension/Fixtures/ExtensionWithAttributes.php +++ b/tests/Extension/Fixtures/ExtensionWithAttributes.php @@ -84,7 +84,7 @@ public function variadicTest(string ...$strings) } #[AsTwigTest(deprecated: true, alternative: 'bar')] - public function deprecatedTest(string $strings) + public function deprecatedTest(string $string) { } } From 7b881384eaa5d646ba26507bda3011036ecb916d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 27 Nov 2023 01:46:04 +0100 Subject: [PATCH 07/17] Track changes in runtime extensions --- src/Extension/AttributeExtension.php | 16 +++++++++++++++- src/Extension/WithLastModified.php | 8 ++++++++ src/ExtensionSet.php | 4 ++++ tests/Extension/AttributeExtensionTest.php | 6 ++++++ 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/Extension/WithLastModified.php diff --git a/src/Extension/AttributeExtension.php b/src/Extension/AttributeExtension.php index 2bca2e291ae..e6ea06a1d73 100644 --- a/src/Extension/AttributeExtension.php +++ b/src/Extension/AttributeExtension.php @@ -24,7 +24,7 @@ * * @author Jérôme Tamarelle */ -final class AttributeExtension extends AbstractExtension +final class AttributeExtension extends AbstractExtension implements WithLastModified { private array $filters; private array $functions; @@ -41,6 +41,20 @@ public function __construct( ) { } + public function getLastModified(): int + { + $lastModified = 0; + + foreach ($this->objectsOrClasses as $objectOrClass) { + $r = new \ReflectionClass($objectOrClass); + if (is_file($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $lastModified) { + $lastModified = $extensionTime; + } + } + + return $lastModified; + } + public function getFilters(): array { if (!isset($this->filters)) { diff --git a/src/Extension/WithLastModified.php b/src/Extension/WithLastModified.php new file mode 100644 index 00000000000..255586308d2 --- /dev/null +++ b/src/Extension/WithLastModified.php @@ -0,0 +1,8 @@ +getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $this->lastModified) { $this->lastModified = $extensionTime; } + if ($extension instanceof WithLastModified && ($extensionTime = $extension->getLastModified()) > $this->lastModified) { + $this->lastModified = $extensionTime; + } } return $this->lastModified; diff --git a/tests/Extension/AttributeExtensionTest.php b/tests/Extension/AttributeExtensionTest.php index 9a5c2e841ff..5a0689ab46e 100644 --- a/tests/Extension/AttributeExtensionTest.php +++ b/tests/Extension/AttributeExtensionTest.php @@ -107,4 +107,10 @@ public function testRuntimeExtension() $this->assertSame([$class, 'fooFunction'], $extension->getFunctions()['foo']->getCallable()); $this->assertSame([$class, 'fooTest'], $extension->getTests()['foo']->getCallable()); } + + public function testLastModified() + { + $extension = new AttributeExtension([ExtensionWithAttributes::class]); + $this->assertSame(filemtime(__DIR__ . '/Fixtures/ExtensionWithAttributes.php'), $extension->getLastModified()); + } } From 76b28303a7d89f19212cf9c1b566d856b474367d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 27 Nov 2023 02:19:51 +0100 Subject: [PATCH 08/17] Fix types --- src/Extension/Attribute/AsTwigFilter.php | 2 +- src/Extension/Attribute/AsTwigFunction.php | 2 +- src/Extension/AttributeExtension.php | 14 +++++++------- tests/Extension/AttributeExtensionTest.php | 2 ++ .../Extension/Fixtures/ExtensionWithAttributes.php | 10 ++++++++++ tests/IntegrationTest.php | 2 -- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Extension/Attribute/AsTwigFilter.php b/src/Extension/Attribute/AsTwigFilter.php index 137fe79c7ce..950a5d50dd4 100644 --- a/src/Extension/Attribute/AsTwigFilter.php +++ b/src/Extension/Attribute/AsTwigFilter.php @@ -19,7 +19,7 @@ public function __construct( * @var non-empty-string|null $name */ public ?string $name = null, - public bool $isSafe = false, + public ?array $isSafe = null, public ?string $isSafeCallback = null, public ?string $preEscape = null, public ?array $preservesSafety = null, diff --git a/src/Extension/Attribute/AsTwigFunction.php b/src/Extension/Attribute/AsTwigFunction.php index 825f1960c65..3a09e374e07 100644 --- a/src/Extension/Attribute/AsTwigFunction.php +++ b/src/Extension/Attribute/AsTwigFunction.php @@ -19,7 +19,7 @@ public function __construct( * @var non-empty-string|null $name */ public ?string $name = null, - public bool $isSafe = false, + public ?array $isSafe = null, public ?string $isSafeCallback = null, public bool|string $deprecated = false, public ?string $alternative = null, diff --git a/src/Extension/AttributeExtension.php b/src/Extension/AttributeExtension.php index e6ea06a1d73..5bd5cdfa5f0 100644 --- a/src/Extension/AttributeExtension.php +++ b/src/Extension/AttributeExtension.php @@ -104,11 +104,11 @@ private function initFromAttributes() } $parameters = $method->getParameters(); - $needsEnvironment = isset($parameters[0]) && 'env' === $parameters[0]->getName() && Environment::class === $parameters[0]->getType()->getName(); + $needsEnvironment = isset($parameters[0]) && Environment::class === $parameters[0]->getType()?->getName(); $firstParam = $needsEnvironment ? 1 : 0; - $needsContext = isset($parameters[$firstParam]) && 'context' === $parameters[$firstParam]->getName() && 'array' === $parameters[$firstParam]->getType()->getName(); + $needsContext = isset($parameters[$firstParam]) && 'context' === $parameters[$firstParam]->getName() && 'array' === $parameters[$firstParam]->getType()?->getName(); $firstParam += $needsContext ? 1 : 0; - $isVariadic = isset($parameters[$firstParam]) && $parameters[$firstParam]->isVariadic(); + $isVariadic = isset($parameters[$firstParam]) && end($parameters)->isVariadic(); $filters[$name] = new TwigFilter($name, [$objectOrClass, $method->getName()], [ 'needs_environment' => $needsEnvironment, @@ -133,11 +133,11 @@ private function initFromAttributes() } $parameters = $method->getParameters(); - $needsEnvironment = isset($parameters[0]) && Environment::class === $parameters[0]->getType()->getName(); + $needsEnvironment = isset($parameters[0]) && Environment::class === $parameters[0]->getType()?->getName(); $firstParam = $needsEnvironment ? 1 : 0; - $needsContext = isset($parameters[$firstParam]) && 'context' === $parameters[$firstParam]->getName() && 'array' === $parameters[$firstParam]->getType()->getName(); + $needsContext = isset($parameters[$firstParam]) && 'context' === $parameters[$firstParam]->getName() && 'array' === $parameters[$firstParam]->getType()?->getName(); $firstParam += $needsContext ? 1 : 0; - $isVariadic = isset($parameters[$firstParam]) && $parameters[$firstParam]->isVariadic(); + $isVariadic = isset($parameters[$firstParam]) && end($parameters)->isVariadic(); $functions[$name] = new TwigFunction($name, [$objectOrClass, $method->getName()], [ 'needs_environment' => $needsEnvironment, @@ -160,7 +160,7 @@ private function initFromAttributes() } $parameters = $method->getParameters(); - $isVariadic = isset($parameters[0]) && $parameters[0]->isVariadic(); + $isVariadic = isset($parameters[$firstParam]) && end($parameters)->isVariadic(); $tests[$name] = new TwigTest($name, [$objectOrClass, $method->getName()], [ 'is_variadic' => $isVariadic, diff --git a/tests/Extension/AttributeExtensionTest.php b/tests/Extension/AttributeExtensionTest.php index 5a0689ab46e..ebbd5e002a8 100644 --- a/tests/Extension/AttributeExtensionTest.php +++ b/tests/Extension/AttributeExtensionTest.php @@ -39,6 +39,7 @@ public static function provideFilters() 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 'no argument' => ['noArgFilter', 'noArgFilter', []]; yield 'variadic' => ['variadicFilter', 'variadicFilter', ['is_variadic' => true]]; yield 'deprecated' => ['deprecatedFilter', 'deprecatedFilter', ['deprecated' => true, 'alternative' => 'bar']]; } @@ -68,6 +69,7 @@ public static function provideFunctions() 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 'no argument' => ['noArgFunction', 'noArgFunction', []]; yield 'variadic' => ['variadicFunction', 'variadicFunction', ['is_variadic' => true]]; yield 'deprecated' => ['deprecatedFunction', 'deprecatedFunction', ['deprecated' => true, 'alternative' => 'bar']]; } diff --git a/tests/Extension/Fixtures/ExtensionWithAttributes.php b/tests/Extension/Fixtures/ExtensionWithAttributes.php index ca214a73382..0429e39e18b 100644 --- a/tests/Extension/Fixtures/ExtensionWithAttributes.php +++ b/tests/Extension/Fixtures/ExtensionWithAttributes.php @@ -31,6 +31,11 @@ public function withEnvAndContextFilter(Environment $env, array $context, string { } + #[AsTwigFilter] + public function noArgFilter() + { + } + #[AsTwigFilter] public function variadicFilter(string ...$strings) { @@ -62,6 +67,11 @@ public function withEnvAndContextFunction(Environment $env, array $context, stri { } + #[AsTwigFunction] + public function noArgFunction() + { + } + #[AsTwigFunction] public function variadicFunction(string ...$strings) { diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 064084207cb..e2b211a01de 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -12,7 +12,6 @@ */ use Twig\Extension\AbstractExtension; -use Twig\Extension\AttributeExtension; use Twig\Extension\DebugExtension; use Twig\Extension\EscaperExtension; use Twig\Extension\SandboxExtension; @@ -46,7 +45,6 @@ public function getExtensions() new SandboxExtension($policy, false), new StringLoaderExtension(), new TwigTestExtension(), - new AttributeExtension(new \ArrayIterator([''])) ]; } From 200c0ba5ba40c7497489f1bf78c6c722196658ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 30 Nov 2023 09:39:47 +0100 Subject: [PATCH 09/17] Add AsTwigExtension attribute and make function name required --- src/Extension/Attribute/AsTwigExtension.php | 13 +++++++ src/Extension/Attribute/AsTwigFilter.php | 6 ++-- src/Extension/Attribute/AsTwigFunction.php | 6 ++-- src/Extension/Attribute/AsTwigTest.php | 6 ++-- tests/Extension/AttributeExtensionTest.php | 31 ++++++++-------- .../Fixtures/ExtensionWithAttributes.php | 36 +++++++++---------- 6 files changed, 53 insertions(+), 45 deletions(-) create mode 100644 src/Extension/Attribute/AsTwigExtension.php diff --git a/src/Extension/Attribute/AsTwigExtension.php b/src/Extension/Attribute/AsTwigExtension.php new file mode 100644 index 00000000000..ef0edeb656d --- /dev/null +++ b/src/Extension/Attribute/AsTwigExtension.php @@ -0,0 +1,13 @@ + ['fooFilter', 'fooFilter', []]; yield 'with name' => ['foo', 'fooFilter', []]; - 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 'no argument' => ['noArgFilter', 'noArgFilter', []]; - yield 'variadic' => ['variadicFilter', 'variadicFilter', ['is_variadic' => true]]; - yield 'deprecated' => ['deprecatedFilter', 'deprecatedFilter', ['deprecated' => true, 'alternative' => 'bar']]; + 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 'no argument' => ['no_arg_filter', 'noArgFilter', []]; + yield 'variadic' => ['variadic_filter', 'variadicFilter', ['is_variadic' => true]]; + yield 'deprecated' => ['deprecated_filter', 'deprecatedFilter', ['deprecated' => true, 'alternative' => 'bar']]; } /** @@ -64,14 +63,13 @@ public function testFunction(string $name, string $method, array $options) public static function provideFunctions() { - yield 'basic' => ['fooFunction', 'fooFunction', []]; yield 'with name' => ['foo', 'fooFunction', []]; - 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 'no argument' => ['noArgFunction', 'noArgFunction', []]; - yield 'variadic' => ['variadicFunction', 'variadicFunction', ['is_variadic' => true]]; - yield 'deprecated' => ['deprecatedFunction', 'deprecatedFunction', ['deprecated' => true, 'alternative' => 'bar']]; + 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', ['deprecated' => true, 'alternative' => 'bar']]; } /** @@ -94,10 +92,9 @@ public function testTest(string $name, string $method, array $options) public static function provideTests() { - yield 'basic' => ['fooTest', 'fooTest', []]; yield 'with name' => ['foo', 'fooTest', []]; - yield 'variadic' => ['variadicTest', 'variadicTest', ['is_variadic' => true]]; - yield 'deprecated' => ['deprecatedTest', 'deprecatedTest', ['deprecated' => true, 'alternative' => 'bar']]; + yield 'variadic' => ['variadic_test', 'variadicTest', ['is_variadic' => true]]; + yield 'deprecated' => ['deprecated_test', 'deprecatedTest', ['deprecated' => true, 'alternative' => 'bar']]; } public function testRuntimeExtension() diff --git a/tests/Extension/Fixtures/ExtensionWithAttributes.php b/tests/Extension/Fixtures/ExtensionWithAttributes.php index 0429e39e18b..3cc44bc0064 100644 --- a/tests/Extension/Fixtures/ExtensionWithAttributes.php +++ b/tests/Extension/Fixtures/ExtensionWithAttributes.php @@ -3,97 +3,95 @@ namespace Twig\Tests\Extension\Fixtures; use Twig\Environment; +use Twig\Extension\Attribute\AsTwigExtension; use Twig\Extension\Attribute\AsTwigFilter; use Twig\Extension\Attribute\AsTwigFunction; use Twig\Extension\Attribute\AsTwigTest; -use Twig\Extension\RuntimeExtensionInterface; -class ExtensionWithAttributes implements RuntimeExtensionInterface +#[AsTwigExtension] +class ExtensionWithAttributes { - #[AsTwigFilter] #[AsTwigFilter(name: 'foo')] public function fooFilter(string $string) { } - #[AsTwigFilter] + #[AsTwigFilter('with_context_filter')] public function withContextFilter(array $context, string $string) { } - #[AsTwigFilter] + #[AsTwigFilter('with_env_filter')] public function withEnvFilter(Environment $env, string $string) { } - #[AsTwigFilter] + #[AsTwigFilter('with_env_and_context_filter')] public function withEnvAndContextFilter(Environment $env, array $context, string $string) { } - #[AsTwigFilter] + #[AsTwigFilter('no_arg_filter')] public function noArgFilter() { } - #[AsTwigFilter] + #[AsTwigFilter('variadic_filter')] public function variadicFilter(string ...$strings) { } - #[AsTwigFilter(deprecated: true, alternative: 'bar')] + #[AsTwigFilter('deprecated_filter', deprecated: true, alternative: 'bar')] public function deprecatedFilter(string $string) { } - #[AsTwigFunction] #[AsTwigFunction(name: 'foo')] public function fooFunction(string $string) { } - #[AsTwigFunction] + #[AsTwigFunction('with_context_function')] public function withContextFunction(array $context, string $string) { } - #[AsTwigFunction] + #[AsTwigFunction('with_env_function')] public function withEnvFunction(Environment $env, string $string) { } - #[AsTwigFunction] + #[AsTwigFunction('with_env_and_context_function')] public function withEnvAndContextFunction(Environment $env, array $context, string $string) { } - #[AsTwigFunction] + #[AsTwigFunction('no_arg_function')] public function noArgFunction() { } - #[AsTwigFunction] + #[AsTwigFunction('variadic_function')] public function variadicFunction(string ...$strings) { } - #[AsTwigFunction(deprecated: true, alternative: 'bar')] + #[AsTwigFunction('deprecated_function', deprecated: true, alternative: 'bar')] public function deprecatedFunction(string $string) { } - #[AsTwigTest] #[AsTwigTest(name: 'foo')] public function fooTest(string $string) { } - #[AsTwigTest] + #[AsTwigTest('variadic_test')] public function variadicTest(string ...$strings) { } - #[AsTwigTest(deprecated: true, alternative: 'bar')] + #[AsTwigTest('deprecated_test', deprecated: true, alternative: 'bar')] public function deprecatedTest(string $string) { } From 29f3b3ea4aa433912c0aad8a5c723fe844f9c8f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 30 Nov 2023 09:47:58 +0100 Subject: [PATCH 10/17] Update phpdoc --- src/Extension/Attribute/AsTwigExtension.php | 11 +++++++++-- src/Extension/Attribute/AsTwigFilter.php | 18 +++++++++++++++++- src/Extension/Attribute/AsTwigFunction.php | 18 +++++++++++++++++- src/Extension/Attribute/AsTwigTest.php | 16 ++++++++++++++++ src/Extension/AttributeExtension.php | 6 +++--- 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/Extension/Attribute/AsTwigExtension.php b/src/Extension/Attribute/AsTwigExtension.php index ef0edeb656d..41540d96968 100644 --- a/src/Extension/Attribute/AsTwigExtension.php +++ b/src/Extension/Attribute/AsTwigExtension.php @@ -1,8 +1,15 @@ getAttributes(AsTwigFilter::class) as $attribute) { $attribute = $attribute->newInstance(); - $name = $attribute->name ?? $method->getName(); + $name = $attribute->name; if (isset($filters[$name])) { throw new \LogicException(sprintf('Multiple definitions of the "%s" filter.', $name)); } @@ -127,7 +127,7 @@ private function initFromAttributes() foreach ($method->getAttributes(AsTwigFunction::class) as $attribute) { $attribute = $attribute->newInstance(); - $name = $attribute->name ?? $method->getName(); + $name = $attribute->name; if (isset($functions[$name])) { throw new \LogicException(sprintf('Multiple definitions of the "%s" function.', $name)); } @@ -154,7 +154,7 @@ private function initFromAttributes() foreach ($method->getAttributes(AsTwigTest::class) as $attribute) { $attribute = $attribute->newInstance(); - $name = $attribute->name ?? $method->getName(); + $name = $attribute->name; if (isset($tests[$name])) { throw new \LogicException(sprintf('Multiple definitions of the "%s" test.', $name)); } From 5dc5e402f1dae3b1700c1173e9a00954110ae0d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 1 Dec 2023 20:27:24 +0100 Subject: [PATCH 11/17] Move attributes to Twig\Attribute namespace --- src/{Extension => }/Attribute/AsTwigExtension.php | 2 +- src/{Extension => }/Attribute/AsTwigFilter.php | 2 +- src/{Extension => }/Attribute/AsTwigFunction.php | 2 +- src/{Extension => }/Attribute/AsTwigTest.php | 2 +- src/Extension/AttributeExtension.php | 6 +++--- tests/Extension/Fixtures/ExtensionWithAttributes.php | 8 ++++---- 6 files changed, 11 insertions(+), 11 deletions(-) rename src/{Extension => }/Attribute/AsTwigExtension.php (91%) rename src/{Extension => }/Attribute/AsTwigFilter.php (97%) rename src/{Extension => }/Attribute/AsTwigFunction.php (97%) rename src/{Extension => }/Attribute/AsTwigTest.php (96%) diff --git a/src/Extension/Attribute/AsTwigExtension.php b/src/Attribute/AsTwigExtension.php similarity index 91% rename from src/Extension/Attribute/AsTwigExtension.php rename to src/Attribute/AsTwigExtension.php index 41540d96968..bdbbd44f159 100644 --- a/src/Extension/Attribute/AsTwigExtension.php +++ b/src/Attribute/AsTwigExtension.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Twig\Extension\Attribute; +namespace Twig\Attribute; /** * Identifies a class that uses PHP attributes to define filters, functions, or tests. diff --git a/src/Extension/Attribute/AsTwigFilter.php b/src/Attribute/AsTwigFilter.php similarity index 97% rename from src/Extension/Attribute/AsTwigFilter.php rename to src/Attribute/AsTwigFilter.php index c14128adb68..a253ef353c2 100644 --- a/src/Extension/Attribute/AsTwigFilter.php +++ b/src/Attribute/AsTwigFilter.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Twig\Extension\Attribute; +namespace Twig\Attribute; use Twig\TwigFilter; diff --git a/src/Extension/Attribute/AsTwigFunction.php b/src/Attribute/AsTwigFunction.php similarity index 97% rename from src/Extension/Attribute/AsTwigFunction.php rename to src/Attribute/AsTwigFunction.php index b3301b80f99..c0116817c88 100644 --- a/src/Extension/Attribute/AsTwigFunction.php +++ b/src/Attribute/AsTwigFunction.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Twig\Extension\Attribute; +namespace Twig\Attribute; use Twig\TwigFunction; diff --git a/src/Extension/Attribute/AsTwigTest.php b/src/Attribute/AsTwigTest.php similarity index 96% rename from src/Extension/Attribute/AsTwigTest.php rename to src/Attribute/AsTwigTest.php index f18737089f2..e6ea0fbe151 100644 --- a/src/Extension/Attribute/AsTwigTest.php +++ b/src/Attribute/AsTwigTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Twig\Extension\Attribute; +namespace Twig\Attribute; use Twig\TwigTest; diff --git a/src/Extension/AttributeExtension.php b/src/Extension/AttributeExtension.php index 1464dcaacd4..1be38b20e18 100644 --- a/src/Extension/AttributeExtension.php +++ b/src/Extension/AttributeExtension.php @@ -11,10 +11,10 @@ namespace Twig\Extension; +use Twig\Attribute\AsTwigFilter; +use Twig\Attribute\AsTwigFunction; +use Twig\Attribute\AsTwigTest; 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; diff --git a/tests/Extension/Fixtures/ExtensionWithAttributes.php b/tests/Extension/Fixtures/ExtensionWithAttributes.php index 3cc44bc0064..f9d65153257 100644 --- a/tests/Extension/Fixtures/ExtensionWithAttributes.php +++ b/tests/Extension/Fixtures/ExtensionWithAttributes.php @@ -2,11 +2,11 @@ namespace Twig\Tests\Extension\Fixtures; +use Twig\Attribute\AsTwigExtension; +use Twig\Attribute\AsTwigFilter; +use Twig\Attribute\AsTwigFunction; +use Twig\Attribute\AsTwigTest; use Twig\Environment; -use Twig\Extension\Attribute\AsTwigExtension; -use Twig\Extension\Attribute\AsTwigFilter; -use Twig\Extension\Attribute\AsTwigFunction; -use Twig\Extension\Attribute\AsTwigTest; #[AsTwigExtension] class ExtensionWithAttributes From 365d1024a4ee93cbead97d160add67c4b91373b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 8 Dec 2023 23:35:54 +0100 Subject: [PATCH 12/17] Add pattern filter test case --- tests/Extension/AttributeExtensionTest.php | 5 +++-- tests/Extension/Fixtures/ExtensionWithAttributes.php | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/Extension/AttributeExtensionTest.php b/tests/Extension/AttributeExtensionTest.php index 9806a56b33c..da07c76b173 100644 --- a/tests/Extension/AttributeExtensionTest.php +++ b/tests/Extension/AttributeExtensionTest.php @@ -34,13 +34,14 @@ public function testFilter(string $name, string $method, array $options) public static function provideFilters() { - yield 'with name' => ['foo', 'fooFilter', []]; + 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 'no argument' => ['no_arg_filter', 'noArgFilter', []]; yield 'variadic' => ['variadic_filter', 'variadicFilter', ['is_variadic' => true]]; yield 'deprecated' => ['deprecated_filter', 'deprecatedFilter', ['deprecated' => true, 'alternative' => 'bar']]; + yield 'pattern' => ['pattern_*_filter', 'patternFilter', []]; } /** @@ -63,7 +64,7 @@ public function testFunction(string $name, string $method, array $options) public static function provideFunctions() { - yield 'with name' => ['foo', 'fooFunction', []]; + 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]]; diff --git a/tests/Extension/Fixtures/ExtensionWithAttributes.php b/tests/Extension/Fixtures/ExtensionWithAttributes.php index f9d65153257..c0aade019fb 100644 --- a/tests/Extension/Fixtures/ExtensionWithAttributes.php +++ b/tests/Extension/Fixtures/ExtensionWithAttributes.php @@ -11,7 +11,7 @@ #[AsTwigExtension] class ExtensionWithAttributes { - #[AsTwigFilter(name: 'foo')] + #[AsTwigFilter(name: 'foo', isSafe: ['html'])] public function fooFilter(string $string) { } @@ -46,7 +46,12 @@ public function deprecatedFilter(string $string) { } - #[AsTwigFunction(name: 'foo')] + #[AsTwigFilter('pattern_*_filter')] + public function patternFilter(string $string) + { + } + + #[AsTwigFunction(name: 'foo', isSafe: ['html'])] public function fooFunction(string $string) { } From 2fd1210b03567330edd9e44cfc2f3aefa646d054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sat, 9 Dec 2023 13:21:57 +0100 Subject: [PATCH 13/17] Remove name unicity constraint, add phpdoc, refactor time computation --- src/Extension/AttributeExtension.php | 14 +------------- src/Extension/WithLastModified.php | 7 +++++++ src/ExtensionSet.php | 4 ++-- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/Extension/AttributeExtension.php b/src/Extension/AttributeExtension.php index 1be38b20e18..c86465da396 100644 --- a/src/Extension/AttributeExtension.php +++ b/src/Extension/AttributeExtension.php @@ -47,7 +47,7 @@ public function getLastModified(): int foreach ($this->objectsOrClasses as $objectOrClass) { $r = new \ReflectionClass($objectOrClass); - if (is_file($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $lastModified) { + if (is_file($r->getFileName()) && $lastModified < $extensionTime = filemtime($r->getFileName())) { $lastModified = $extensionTime; } } @@ -99,10 +99,6 @@ private function initFromAttributes() $attribute = $attribute->newInstance(); $name = $attribute->name; - if (isset($filters[$name])) { - throw new \LogicException(sprintf('Multiple definitions of the "%s" filter.', $name)); - } - $parameters = $method->getParameters(); $needsEnvironment = isset($parameters[0]) && Environment::class === $parameters[0]->getType()?->getName(); $firstParam = $needsEnvironment ? 1 : 0; @@ -128,10 +124,6 @@ private function initFromAttributes() $attribute = $attribute->newInstance(); $name = $attribute->name; - if (isset($functions[$name])) { - throw new \LogicException(sprintf('Multiple definitions of the "%s" function.', $name)); - } - $parameters = $method->getParameters(); $needsEnvironment = isset($parameters[0]) && Environment::class === $parameters[0]->getType()?->getName(); $firstParam = $needsEnvironment ? 1 : 0; @@ -155,10 +147,6 @@ private function initFromAttributes() $attribute = $attribute->newInstance(); $name = $attribute->name; - if (isset($tests[$name])) { - throw new \LogicException(sprintf('Multiple definitions of the "%s" test.', $name)); - } - $parameters = $method->getParameters(); $isVariadic = isset($parameters[$firstParam]) && end($parameters)->isVariadic(); diff --git a/src/Extension/WithLastModified.php b/src/Extension/WithLastModified.php index 255586308d2..055b1df8804 100644 --- a/src/Extension/WithLastModified.php +++ b/src/Extension/WithLastModified.php @@ -2,7 +2,14 @@ namespace Twig\Extension; +/** + * Freshness of templates use the last modification date of each extension class. + * Implement this interface to provide a different last modification date. + */ interface WithLastModified { + /** + * @return int A UNIX timestamp + */ public function getLastModified(): int; } diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 26386cad610..183bb58dc84 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -113,10 +113,10 @@ public function getLastModified(): int foreach ($this->extensions as $extension) { $r = new \ReflectionObject($extension); - if (is_file($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $this->lastModified) { + if (is_file($r->getFileName()) && $this->lastModified < $extensionTime = filemtime($r->getFileName())) { $this->lastModified = $extensionTime; } - if ($extension instanceof WithLastModified && ($extensionTime = $extension->getLastModified()) > $this->lastModified) { + if ($extension instanceof WithLastModified && $this->lastModified < $extensionTime = $extension->getLastModified()) { $this->lastModified = $extensionTime; } } From 4f9e0f843378bdc3c4a07360cf9f9f08f9a112e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 10 Dec 2023 21:36:26 +0100 Subject: [PATCH 14/17] Add docs --- doc/advanced.rst | 150 +++++++++++++++++++++ src/Attribute/AsTwigFilter.php | 32 +++++ src/Attribute/AsTwigFunction.php | 21 +++ src/Attribute/AsTwigTest.php | 8 ++ tests/Extension/AttributeExtensionTest.php | 2 +- 5 files changed, 212 insertions(+), 1 deletion(-) diff --git a/doc/advanced.rst b/doc/advanced.rst index 307650f6a46..17e2c7fd426 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -756,6 +756,156 @@ The ``getTests()`` method lets you add new test functions:: // ... } +Using PHP Attributes to define extensions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 3.9 + + The ``Twig\Extension\AttributeExtension`` was 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, you don't need to extend any class or implement any interface +but it eases integration with frameworks if you use the attribute ``#[AsTwigExtension]``:: + + use Twig\Attribute\AsTwigExtension; + use Twig\Attribute\AsTwigFilter; + use Twig\Attribute\AsTwigFunction; + use Twig\Attribute\AsTwigTest; + + #[AsTwigExtension] + class Project_Twig_Extension + { + #[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 class using ``Twig\Extension\AttributeExtension``:: + + $twig = new \Twig\Environment($loader); + $twig->addExtension(new \Twig\Extension\AttributeExtension([ + Project_Twig_Extension::class, + ])); + +.. note:: + + The ``\Twig\Extension\AttributeExtension`` can be added only once to an environment. + +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 Project_Service + { + // 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([ + Project_Twig_Extension::class, + ])); + $twig->addRuntimeLoader(new \Twig\RuntimeLoader\FactoryLoader([ + Project_Twig_Extension::class => function () use ($lipsumProvider) { + return new Project_Twig_Extension($lipsumProvider); + }, + ])); + +Or use the instance directly if you don't need lazy-loading:: + + $twig = new \Twig\Environment($loader); + $twig->addExtension(new \Twig\Extension\AttributeExtension([ + new Project_Twig_Extension($lipsumProvider), + ])); + +``#[AsTwigFilter]`` and ``#[AsTwigFunction]`` support ``isSafe``, ``preEscape``, and +``isVariadic`` options:: + + use Twig\Attribute\AsTwigExtension; + use Twig\Attribute\AsTwigFilter; + use Twig\Attribute\AsTwigFunction; + + #[AsTwigExtension] + class Project_Twig_Extension + { + #[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 Project_Twig_Extension + { + #[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 Project_Twig_Extension + { + #[AsTwigFunction('lipsum')] + public function lipsum(array $context, int $count): string + { + // ... + } + } + +``#[AsTwigFilter]`` and ``#[AsTwigFunction]`` support variadic arguments +automatically when applied to variadic methods:: + + class Project_Twig_Extension + { + #[AsTwigFilter('thumbnail')] + public function thumbnail(string $file, mixed ...$options): string + { + // ... + } + } + Definition vs Runtime ~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/Attribute/AsTwigFilter.php b/src/Attribute/AsTwigFilter.php index a253ef353c2..956228c504f 100644 --- a/src/Attribute/AsTwigFilter.php +++ b/src/Attribute/AsTwigFilter.php @@ -11,6 +11,7 @@ namespace Twig\Attribute; +use Twig\Node\Node; use Twig\TwigFilter; /** @@ -35,11 +36,42 @@ public function __construct( * @var non-empty-string $name */ public string $name, + + /** + * List of formats in which you want the raw output to be printed unescaped. + * + * @var list|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. + */ public ?array $preservesSafety = null, + + /** + * Set to true if the filter is deprecated. + */ public bool|string $deprecated = false, + + /** + * The alternative filter name to suggest when the deprecated filter is called. + */ public ?string $alternative = null, ) { } diff --git a/src/Attribute/AsTwigFunction.php b/src/Attribute/AsTwigFunction.php index c0116817c88..539cf08bce4 100644 --- a/src/Attribute/AsTwigFunction.php +++ b/src/Attribute/AsTwigFunction.php @@ -11,6 +11,7 @@ namespace Twig\Attribute; +use Twig\Node\Node; use Twig\TwigFunction; /** @@ -35,9 +36,29 @@ public function __construct( * @var non-empty-string $name */ public string $name, + + /** + * List of formats in which you want the raw output to be printed unescaped. + * + * @var list|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, ) { } diff --git a/src/Attribute/AsTwigTest.php b/src/Attribute/AsTwigTest.php index e6ea0fbe151..ee4d8c6511e 100644 --- a/src/Attribute/AsTwigTest.php +++ b/src/Attribute/AsTwigTest.php @@ -35,7 +35,15 @@ public function __construct( * @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, ) { } diff --git a/tests/Extension/AttributeExtensionTest.php b/tests/Extension/AttributeExtensionTest.php index da07c76b173..e62f15c04b4 100644 --- a/tests/Extension/AttributeExtensionTest.php +++ b/tests/Extension/AttributeExtensionTest.php @@ -111,6 +111,6 @@ public function testRuntimeExtension() public function testLastModified() { $extension = new AttributeExtension([ExtensionWithAttributes::class]); - $this->assertSame(filemtime(__DIR__ . '/Fixtures/ExtensionWithAttributes.php'), $extension->getLastModified()); + $this->assertSame(filemtime(__DIR__.'/Fixtures/ExtensionWithAttributes.php'), $extension->getLastModified()); } } From 43bec2ffe974cc4deafc958fd139bd33a526f5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 10 Dec 2023 22:44:14 +0100 Subject: [PATCH 15/17] Mark attributes as final --- src/Attribute/AsTwigExtension.php | 2 +- src/Attribute/AsTwigFilter.php | 2 +- src/Attribute/AsTwigFunction.php | 2 +- src/Attribute/AsTwigTest.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Attribute/AsTwigExtension.php b/src/Attribute/AsTwigExtension.php index bdbbd44f159..40d48915890 100644 --- a/src/Attribute/AsTwigExtension.php +++ b/src/Attribute/AsTwigExtension.php @@ -15,6 +15,6 @@ * Identifies a class that uses PHP attributes to define filters, functions, or tests. */ #[\Attribute(\Attribute::TARGET_CLASS)] -class AsTwigExtension +final class AsTwigExtension { } diff --git a/src/Attribute/AsTwigFilter.php b/src/Attribute/AsTwigFilter.php index 956228c504f..bfa101aaf32 100644 --- a/src/Attribute/AsTwigFilter.php +++ b/src/Attribute/AsTwigFilter.php @@ -27,7 +27,7 @@ * @see TwigFilter */ #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] -class AsTwigFilter +final class AsTwigFilter { public function __construct( /** diff --git a/src/Attribute/AsTwigFunction.php b/src/Attribute/AsTwigFunction.php index 539cf08bce4..fbbf040b012 100644 --- a/src/Attribute/AsTwigFunction.php +++ b/src/Attribute/AsTwigFunction.php @@ -27,7 +27,7 @@ * @see TwigFunction */ #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] -class AsTwigFunction +final class AsTwigFunction { public function __construct( /** diff --git a/src/Attribute/AsTwigTest.php b/src/Attribute/AsTwigTest.php index ee4d8c6511e..17f91cfd0af 100644 --- a/src/Attribute/AsTwigTest.php +++ b/src/Attribute/AsTwigTest.php @@ -26,7 +26,7 @@ * @see TwigTest */ #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] -class AsTwigTest +final class AsTwigTest { public function __construct( /** From 36476c35e563794abffc9af870ea94f8ae1fc916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 26 Dec 2023 21:03:32 +0100 Subject: [PATCH 16/17] Rename ModificationAwareInterface --- src/Extension/AttributeExtension.php | 2 +- .../{WithLastModified.php => ModificationAwareInterface.php} | 4 ++-- src/ExtensionSet.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/Extension/{WithLastModified.php => ModificationAwareInterface.php} (64%) diff --git a/src/Extension/AttributeExtension.php b/src/Extension/AttributeExtension.php index c86465da396..1fc64b69eb7 100644 --- a/src/Extension/AttributeExtension.php +++ b/src/Extension/AttributeExtension.php @@ -24,7 +24,7 @@ * * @author Jérôme Tamarelle */ -final class AttributeExtension extends AbstractExtension implements WithLastModified +final class AttributeExtension extends AbstractExtension implements ModificationAwareInterface { private array $filters; private array $functions; diff --git a/src/Extension/WithLastModified.php b/src/Extension/ModificationAwareInterface.php similarity index 64% rename from src/Extension/WithLastModified.php rename to src/Extension/ModificationAwareInterface.php index 055b1df8804..e85c3ca4ed0 100644 --- a/src/Extension/WithLastModified.php +++ b/src/Extension/ModificationAwareInterface.php @@ -4,9 +4,9 @@ /** * Freshness of templates use the last modification date of each extension class. - * Implement this interface to provide a different last modification date. + * Implement this interface to provide a different modification date for the extension. */ -interface WithLastModified +interface ModificationAwareInterface { /** * @return int A UNIX timestamp diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 183bb58dc84..d77d86e3e18 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -15,7 +15,7 @@ use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; use Twig\Extension\StagingExtension; -use Twig\Extension\WithLastModified; +use Twig\Extension\ModificationAwareInterface; use Twig\Node\Expression\Binary\AbstractBinary; use Twig\Node\Expression\Unary\AbstractUnary; use Twig\NodeVisitor\NodeVisitorInterface; @@ -116,7 +116,7 @@ public function getLastModified(): int if (is_file($r->getFileName()) && $this->lastModified < $extensionTime = filemtime($r->getFileName())) { $this->lastModified = $extensionTime; } - if ($extension instanceof WithLastModified && $this->lastModified < $extensionTime = $extension->getLastModified()) { + if ($extension instanceof ModificationAwareInterface && $this->lastModified < $extensionTime = $extension->getLastModified()) { $this->lastModified = $extensionTime; } } From 57cb0f42ad5a13ec1f40e3b1ca446ad1b2bc3fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 5 Apr 2024 16:34:23 +0200 Subject: [PATCH 17/17] Make AsTwigExtension extension first party instead of relying on the extension to be registered --- doc/advanced.rst | 39 +++++++----------- src/Environment.php | 7 +++- src/Extension/AttributeExtension.php | 43 +++++++++----------- src/Extension/ModificationAwareInterface.php | 15 ------- src/ExtensionSet.php | 30 ++++++++++---- tests/Extension/AttributeExtensionTest.php | 6 --- 6 files changed, 59 insertions(+), 81 deletions(-) delete mode 100644 src/Extension/ModificationAwareInterface.php diff --git a/doc/advanced.rst b/doc/advanced.rst index 17e2c7fd426..e88a8f73907 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -761,13 +761,12 @@ Using PHP Attributes to define extensions .. versionadded:: 3.9 - The ``Twig\Extension\AttributeExtension`` was added in Twig 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, you don't need to extend any class or implement any interface -but it eases integration with frameworks if you use the attribute ``#[AsTwigExtension]``:: +Create a class with the attribute ``#[AsTwigExtension]``:: use Twig\Attribute\AsTwigExtension; use Twig\Attribute\AsTwigFilter; @@ -775,7 +774,7 @@ but it eases integration with frameworks if you use the attribute ``#[AsTwigExte use Twig\Attribute\AsTwigTest; #[AsTwigExtension] - class Project_Twig_Extension + class ProjectExtension { #[AsTwigFilter('rot13')] public static function rot13(string $string): string @@ -796,16 +795,10 @@ but it eases integration with frameworks if you use the attribute ``#[AsTwigExte } } -Then register the class using ``Twig\Extension\AttributeExtension``:: +Then register the extension class:: $twig = new \Twig\Environment($loader); - $twig->addExtension(new \Twig\Extension\AttributeExtension([ - Project_Twig_Extension::class, - ])); - -.. note:: - - The ``\Twig\Extension\AttributeExtension`` can be added only once to an environment. + $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 @@ -818,7 +811,7 @@ a runtime extension using one of the runtime loaders:: use Twig\Attribute\AsTwigFunction; #[AsTwigExtension] - class Project_Service + class ProjectExtension { // Inject hypothetical dependencies public function __construct(private LipsumProvider $lipsumProvider) {} @@ -831,21 +824,17 @@ a runtime extension using one of the runtime loaders:: } $twig = new \Twig\Environment($loader); - $twig->addExtension(new \Twig\Extension\AttributeExtension([ - Project_Twig_Extension::class, - ])); + $twig->addExtension(ProjectExtension::class); $twig->addRuntimeLoader(new \Twig\RuntimeLoader\FactoryLoader([ - Project_Twig_Extension::class => function () use ($lipsumProvider) { - return new Project_Twig_Extension($lipsumProvider); + 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 \Twig\Extension\AttributeExtension([ - new Project_Twig_Extension($lipsumProvider), - ])); + $twig->addExtension(new ProjectExtension($lipsumProvider)); ``#[AsTwigFilter]`` and ``#[AsTwigFunction]`` support ``isSafe``, ``preEscape``, and ``isVariadic`` options:: @@ -855,7 +844,7 @@ Or use the instance directly if you don't need lazy-loading:: use Twig\Attribute\AsTwigFunction; #[AsTwigExtension] - class Project_Twig_Extension + class ProjectExtension { #[AsTwigFilter('rot13', isSafe: ['html'])] public static function rot13(string $string): string @@ -873,7 +862,7 @@ Or use the instance directly if you don't need lazy-loading:: 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 Project_Twig_Extension + class ProjectExtension { #[AsTwigFunction('lipsum')] public function lipsum(\Twig\Environment $env, int $count): string @@ -885,7 +874,7 @@ add the ``Twig\Environment`` type to the first argument of the method:: 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 Project_Twig_Extension + class ProjectExtension { #[AsTwigFunction('lipsum')] public function lipsum(array $context, int $count): string @@ -897,7 +886,7 @@ with type and name ``array $context`` first or after ``\Twig\Environment``:: ``#[AsTwigFilter]`` and ``#[AsTwigFunction]`` support variadic arguments automatically when applied to variadic methods:: - class Project_Twig_Extension + class ProjectExtension { #[AsTwigFilter('thumbnail')] public function thumbnail(string $file, mixed ...$options): string diff --git a/src/Environment.php b/src/Environment.php index ec9c39da5ac..5d350bc5179 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -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) { $this->extensionSet->addExtension($extension); $this->updateOptionsHash(); } /** - * @param ExtensionInterface[] $extensions An array of extensions + * @param list $extensions An array of extensions */ public function setExtensions(array $extensions) { diff --git a/src/Extension/AttributeExtension.php b/src/Extension/AttributeExtension.php index 1fc64b69eb7..c2ec849b464 100644 --- a/src/Extension/AttributeExtension.php +++ b/src/Extension/AttributeExtension.php @@ -11,6 +11,7 @@ namespace Twig\Extension; +use Twig\Attribute\AsTwigExtension; use Twig\Attribute\AsTwigFilter; use Twig\Attribute\AsTwigFunction; use Twig\Attribute\AsTwigTest; @@ -23,36 +24,25 @@ * Define Twig filters, functions, and tests with PHP attributes. * * @author Jérôme Tamarelle + * + * @internal */ -final class AttributeExtension extends AbstractExtension implements ModificationAwareInterface +final class AttributeExtension extends AbstractExtension { + private array $classes; private array $filters; private array $functions; private array $tests; - public function __construct( - /** - * A list of objects or class names defining filters, functions, and tests using PHP attributes. - * When passing a class name, it must be available in runtimes. - * - * @var iterable - */ - private iterable $objectsOrClasses, - ) { - } - - public function getLastModified(): int + /** + * A list of objects or class names defining filters, functions, and tests using PHP attributes. + * When passing a class name, it must be available in runtimes. + * + * @param class-string[] + */ + public function __construct(array $classes) { - $lastModified = 0; - - foreach ($this->objectsOrClasses as $objectOrClass) { - $r = new \ReflectionClass($objectOrClass); - if (is_file($r->getFileName()) && $lastModified < $extensionTime = filemtime($r->getFileName())) { - $lastModified = $extensionTime; - } - } - - return $lastModified; + $this->classes = $classes; } public function getFilters(): array @@ -86,13 +76,18 @@ private function initFromAttributes() { $filters = $functions = $tests = []; - foreach ($this->objectsOrClasses as $objectOrClass) { + foreach ($this->classes as $objectOrClass) { try { $reflectionClass = new \ReflectionClass($objectOrClass); } catch (\ReflectionException $e) { throw new \LogicException(sprintf('"%s" class requires a list of objects or class name, "%s" given.', __CLASS__, get_debug_type($objectOrClass)), 0, $e); } + $attributes = $reflectionClass->getAttributes(AsTwigExtension::class); + if (!$attributes) { + throw new \LogicException(sprintf('Extension class "%s" must have the attribute "%s" in order to use attributes', is_string($objectOrClass) ? $objectOrClass : get_debug_type($objectOrClass), AsTwigExtension::class)); + } + foreach ($reflectionClass->getMethods() as $method) { // Filters foreach ($method->getAttributes(AsTwigFilter::class) as $attribute) { diff --git a/src/Extension/ModificationAwareInterface.php b/src/Extension/ModificationAwareInterface.php deleted file mode 100644 index e85c3ca4ed0..00000000000 --- a/src/Extension/ModificationAwareInterface.php +++ /dev/null @@ -1,15 +0,0 @@ -extensions[$class])) { + throw new RuntimeError(sprintf('The "%s" uses attributes, it requires a runtime.', $class)); + } + return $this->extensions[$class]; } /** - * @param ExtensionInterface[] $extensions + * @param list $extensions */ public function setExtensions(array $extensions): void { @@ -112,11 +117,8 @@ public function getLastModified(): int } foreach ($this->extensions as $extension) { - $r = new \ReflectionObject($extension); - if (is_file($r->getFileName()) && $this->lastModified < $extensionTime = filemtime($r->getFileName())) { - $this->lastModified = $extensionTime; - } - if ($extension instanceof ModificationAwareInterface && $this->lastModified < $extensionTime = $extension->getLastModified()) { + $r = new \ReflectionClass($extension); + if (($filename = $r->getFileName()) && is_file($filename) && $this->lastModified < $extensionTime = filemtime($filename)) { $this->lastModified = $extensionTime; } } @@ -124,9 +126,12 @@ public function getLastModified(): int return $this->lastModified; } - public function addExtension(ExtensionInterface $extension): void + /** + * @param ExtensionInterface|class-string|object $extension + */ + public function addExtension(string|object $extension): void { - $class = \get_class($extension); + $class = is_string($extension) ? $extension : \get_class($extension); if ($this->initialized) { throw new \LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class)); @@ -428,9 +433,16 @@ private function initExtensions(): void $this->unaryOperators = []; $this->binaryOperators = []; + $classes = []; foreach ($this->extensions as $extension) { - $this->initExtension($extension); + if ($extension instanceof ExtensionInterface) { + $this->initExtension($extension); + } else { + $classes[] = $extension; + } } + + $this->initExtension(new AttributeExtension($classes)); $this->initExtension($this->staging); // Done at the end only, so that an exception during initialization does not mark the environment as initialized when catching the exception $this->initialized = true; diff --git a/tests/Extension/AttributeExtensionTest.php b/tests/Extension/AttributeExtensionTest.php index e62f15c04b4..14244fbf8e0 100644 --- a/tests/Extension/AttributeExtensionTest.php +++ b/tests/Extension/AttributeExtensionTest.php @@ -107,10 +107,4 @@ public function testRuntimeExtension() $this->assertSame([$class, 'fooFunction'], $extension->getFunctions()['foo']->getCallable()); $this->assertSame([$class, 'fooTest'], $extension->getTests()['foo']->getCallable()); } - - public function testLastModified() - { - $extension = new AttributeExtension([ExtensionWithAttributes::class]); - $this->assertSame(filemtime(__DIR__.'/Fixtures/ExtensionWithAttributes.php'), $extension->getLastModified()); - } }