Skip to content

Commit

Permalink
Add LastModifiedExtensionInterface and implementation in `AbstractE…
Browse files Browse the repository at this point in the history
…xtension` to track modification of runtime classes
  • Loading branch information
GromNaN committed Jan 2, 2025
1 parent 4a871c1 commit d8fe3bd
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 11 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
18 changes: 13 additions & 5 deletions doc/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
// ...
Expand All @@ -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;

Expand All @@ -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
--------------------

Expand Down
19 changes: 18 additions & 1 deletion src/Extension/AbstractExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Twig\Extension;

abstract class AbstractExtension implements ExtensionInterface
abstract class AbstractExtension implements LastModifiedExtensionInterface
{
public function getTokenParsers()
{
Expand Down Expand Up @@ -42,4 +42,21 @@ public function getOperators()
{
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 changes: 8 additions & 0 deletions src/Extension/EscaperExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ public function getFilters(): array
];
}

public function getLastModified(): int
{
return max(
parent::getLastModified(),
filemtime((new \ReflectionClass(EscaperRuntime::class))->getFileName()),
);
}

/**
* @deprecated since Twig 3.10
*/
Expand Down
23 changes: 23 additions & 0 deletions src/Extension/LastModifiedExtensionInterface.php
Original file line number Diff line number Diff line change
@@ -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;
}
14 changes: 10 additions & 4 deletions src/ExtensionSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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;
Expand Down Expand Up @@ -116,14 +117,19 @@ public function getLastModified(): int
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
Expand Down
5 changes: 5 additions & 0 deletions tests/Extension/CoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,11 @@ public function testSandboxedIncludeWithPreloadedTemplate()
$this->expectException(SecurityError::class);
$twig->render('index');
}

public function testLastModified()
{
$this->assertGreaterThan(1000000000, (new CoreExtension())->getLastModified());
}
}

final class CoreTestIteratorAggregate implements \IteratorAggregate
Expand Down
5 changes: 5 additions & 0 deletions tests/Extension/EscaperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ public function testCustomEscapersOnMultipleEnvs()
$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)
Expand Down

0 comments on commit d8fe3bd

Please sign in to comment.