Skip to content

Commit

Permalink
Make AsTwigExtension extension first party instead of relying on the …
Browse files Browse the repository at this point in the history
…extension to be registered
  • Loading branch information
GromNaN committed Apr 5, 2024
1 parent 36476c3 commit 57cb0f4
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 81 deletions.
39 changes: 14 additions & 25 deletions doc/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -761,21 +761,20 @@ 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;
use Twig\Attribute\AsTwigFunction;
use Twig\Attribute\AsTwigTest;

#[AsTwigExtension]
class Project_Twig_Extension
class ProjectExtension
{
#[AsTwigFilter('rot13')]
public static function rot13(string $string): string
Expand All @@ -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
Expand All @@ -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) {}
Expand All @@ -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::
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions src/Environment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExtensionInterface|object|class-string> $extensions An array of extensions
*/
public function setExtensions(array $extensions)
{
Expand Down
43 changes: 19 additions & 24 deletions src/Extension/AttributeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Twig\Extension;

use Twig\Attribute\AsTwigExtension;
use Twig\Attribute\AsTwigFilter;
use Twig\Attribute\AsTwigFunction;
use Twig\Attribute\AsTwigTest;
Expand All @@ -23,36 +24,25 @@
* Define Twig filters, functions, and tests with PHP attributes.
*
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*
* @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<object|class-string>
*/
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
Expand Down Expand Up @@ -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) {
Expand Down
15 changes: 0 additions & 15 deletions src/Extension/ModificationAwareInterface.php

This file was deleted.

30 changes: 21 additions & 9 deletions src/ExtensionSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Twig;

use Twig\Error\RuntimeError;
use Twig\Extension\AttributeExtension;
use Twig\Extension\ExtensionInterface;
use Twig\Extension\GlobalsInterface;
use Twig\Extension\StagingExtension;
Expand Down Expand Up @@ -74,11 +75,15 @@ public function getExtension(string $class): ExtensionInterface
throw new RuntimeError(sprintf('The "%s" extension is not enabled.', $class));
}

if (is_string($this->extensions[$class])) {
throw new RuntimeError(sprintf('The "%s" uses attributes, it requires a runtime.', $class));
}

return $this->extensions[$class];
}

/**
* @param ExtensionInterface[] $extensions
* @param list<ExtensionInterface|class-string> $extensions
*/
public function setExtensions(array $extensions): void
{
Expand Down Expand Up @@ -112,21 +117,21 @@ 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;
}
}

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));
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 0 additions & 6 deletions tests/Extension/AttributeExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}

0 comments on commit 57cb0f4

Please sign in to comment.