Add LastModifiedExtensionInterface and implementation in AbstractExtension to track modification of runtime classes

This commit is contained in:
Jérôme Tamarelle
2024-12-23 23:58:49 +01:00
parent 4a871c1196
commit d8fe3bd1ec
8 changed files with 83 additions and 11 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# 3.19.0 (2025-XX-XX)
* n/a
* Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes
# 3.18.0 (2024-12-29)
+13 -5
View File
@@ -868,7 +868,7 @@ must be autoload-able)::
// implement the logic to create an instance of $class
// and inject its dependencies
// most of the time, it means using your dependency injection container
if ('CustomRuntimeExtension' === $class) {
if ('CustomTwigRuntime' === $class) {
return new $class(new Rot13Provider());
} else {
// ...
@@ -884,9 +884,9 @@ must be autoload-able)::
(``\Twig\RuntimeLoader\ContainerRuntimeLoader``).
It is now possible to move the runtime logic to a new
``CustomRuntimeExtension`` class and use it directly in the extension::
``CustomTwigRuntime`` class and use it directly in the extension::
class CustomRuntimeExtension
class CustomTwigRuntime
{
private $rot13Provider;
@@ -906,13 +906,21 @@ It is now possible to move the runtime logic to a new
public function getFunctions()
{
return [
new \Twig\TwigFunction('rot13', ['CustomRuntimeExtension', 'rot13']),
new \Twig\TwigFunction('rot13', ['CustomTwigRuntime', 'rot13']),
// or
new \Twig\TwigFunction('rot13', 'CustomRuntimeExtension::rot13'),
new \Twig\TwigFunction('rot13', 'CustomTwigRuntime::rot13'),
];
}
}
.. note::
The extension class should implement the ``Twig\Extension\LastModifiedExtensionInterface``
interface to invalidate the template cache when the runtime class is modified.
The ``AbstractExtension`` class implements this interface and tracks the
runtime class if its name is the same as the extension class but ends with
``Runtime`` instead of ``Extension``.
Testing an Extension
--------------------
+18 -1
View File
@@ -11,7 +11,7 @@
namespace Twig\Extension;
abstract class AbstractExtension implements ExtensionInterface
abstract class AbstractExtension implements LastModifiedExtensionInterface
{
public function getTokenParsers()
{
@@ -42,4 +42,21 @@ abstract class AbstractExtension implements ExtensionInterface
{
return [[], []];
}
public function getLastModified(): int
{
$filename = (new \ReflectionClass($this))->getFileName();
if (!is_file($filename)) {
return 0;
}
$lastModified = filemtime($filename);
// Track modifications of the runtime class if it exists and follows the naming convention
if (str_ends_with($filename, 'Extension.php') && is_file($filename = substr($filename, 0, -13) . 'Runtime.php')) {
$lastModified = max($lastModified, filemtime($filename));
}
return $lastModified;
}
}
+8
View File
@@ -57,6 +57,14 @@ final class EscaperExtension extends AbstractExtension
];
}
public function getLastModified(): int
{
return max(
parent::getLastModified(),
filemtime((new \ReflectionClass(EscaperRuntime::class))->getFileName()),
);
}
/**
* @deprecated since Twig 3.10
*/
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Extension;
interface LastModifiedExtensionInterface extends ExtensionInterface
{
/**
* Returns the last modification time of the extension for cache invalidation.
*
* This timestamp should be the last time the source code of the extension class
* and all its dependencies were modified (including the Runtime class).
*/
public function getLastModified(): int;
}
+10 -4
View File
@@ -14,6 +14,7 @@ namespace Twig;
use Twig\Error\RuntimeError;
use Twig\Extension\ExtensionInterface;
use Twig\Extension\GlobalsInterface;
use Twig\Extension\LastModifiedExtensionInterface;
use Twig\Extension\StagingExtension;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Unary\AbstractUnary;
@@ -116,14 +117,19 @@ final class ExtensionSet
return $this->lastModified;
}
$lastModified = 0;
foreach ($this->extensions as $extension) {
$r = new \ReflectionObject($extension);
if (is_file($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $this->lastModified) {
$this->lastModified = $extensionTime;
if ($extension instanceof LastModifiedExtensionInterface) {
$lastModified = max($extension->getLastModified(), $lastModified);
} else {
$r = new \ReflectionObject($extension);
if (is_file($r->getFileName())) {
$lastModified = max(filemtime($r->getFileName()), $lastModified);
}
}
}
return $this->lastModified;
return $this->lastModified = $lastModified;
}
public function addExtension(ExtensionInterface $extension): void
+5
View File
@@ -393,6 +393,11 @@ class CoreTest extends TestCase
$this->expectException(SecurityError::class);
$twig->render('index');
}
public function testLastModified()
{
$this->assertGreaterThan(1000000000, (new CoreExtension())->getLastModified());
}
}
final class CoreTestIteratorAggregate implements \IteratorAggregate
+5
View File
@@ -70,6 +70,11 @@ class EscaperTest extends TestCase
$this->assertSame('foo**ISO-8859-1**UTF-8', $env1->getRuntime(EscaperRuntime::class)->escape('foo', 'foo', 'ISO-8859-1'));
$this->assertSame('foo**ISO-8859-1**UTF-8**again', $env2->getRuntime(EscaperRuntime::class)->escape('foo', 'foo', 'ISO-8859-1'));
}
public function testLastModified()
{
$this->assertGreaterThan(1000000000, (new EscaperExtension())->getLastModified());
}
}
function legacy_escaper(Environment $twig, $string, $charset)