mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-04 22:47:21 +00:00
Merge branch '3.x' into 4.x
* 3.x: Introduce ChainCache and ReadOnlyFilesystemCache Add a note about how macros can override existing functions Fix dumping callables on Node::__toString()
This commit is contained in:
+16
-4
@@ -7,7 +7,7 @@ are useful to reuse template fragments to not repeat yourself.
|
||||
Macros are defined in regular templates.
|
||||
|
||||
Imagine having a generic helper template that define how to render HTML forms
|
||||
via macros (called ``forms.html``):
|
||||
via macros (called ``forms.twig``):
|
||||
|
||||
.. code-block:: html+twig
|
||||
|
||||
@@ -49,9 +49,9 @@ tag:
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
{% import "forms.html" as forms %}
|
||||
{% import "forms.twig" as forms %}
|
||||
|
||||
The above ``import`` call imports the ``forms.html`` file (which can contain
|
||||
The above ``import`` call imports the ``forms.twig`` file (which can contain
|
||||
only macros, or a template and some macros), and import the macros as items of
|
||||
the ``forms`` local variable.
|
||||
|
||||
@@ -67,11 +67,23 @@ via the ``from`` tag:
|
||||
|
||||
.. code-block:: html+twig
|
||||
|
||||
{% from 'forms.html' import input as input_field, textarea %}
|
||||
{% from 'forms.twig' import input as input_field, textarea %}
|
||||
|
||||
<p>{{ input_field('password', '', 'password') }}</p>
|
||||
<p>{{ textarea('comment') }}</p>
|
||||
|
||||
.. caution::
|
||||
|
||||
As macros imported via ``from`` are called like functions, be careful to
|
||||
not override existing functions:
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
{% from 'forms.twig' import input as include %}
|
||||
|
||||
{# include refers to the macro and not to the built-in "include" function #}
|
||||
{{ include() }}
|
||||
|
||||
.. tip::
|
||||
|
||||
When macro usages and definitions are in the same template, you don't need to
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?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\Cache;
|
||||
|
||||
/**
|
||||
* Chains several caches together.
|
||||
*
|
||||
* Cached items are fetched from the first cache having them in its data store.
|
||||
* They are saved and deleted in all adapters at once.
|
||||
*
|
||||
* @author Quentin Devos <quentin@devos.pm>
|
||||
*/
|
||||
final class ChainCache implements CacheInterface
|
||||
{
|
||||
private $caches;
|
||||
|
||||
/**
|
||||
* @param iterable<CacheInterface> $caches The ordered list of caches used to store and fetch cached items
|
||||
*/
|
||||
public function __construct(iterable $caches)
|
||||
{
|
||||
$this->caches = $caches;
|
||||
}
|
||||
|
||||
public function generateKey(string $name, string $className): string
|
||||
{
|
||||
return $className.'#'.$name;
|
||||
}
|
||||
|
||||
public function write(string $key, string $content): void
|
||||
{
|
||||
$splitKey = $this->splitKey($key);
|
||||
|
||||
foreach ($this->caches as $cache) {
|
||||
$cache->write($cache->generateKey(...$splitKey), $content);
|
||||
}
|
||||
}
|
||||
|
||||
public function load(string $key): void
|
||||
{
|
||||
[$name, $className] = $this->splitKey($key);
|
||||
|
||||
foreach ($this->caches as $cache) {
|
||||
$cache->load($cache->generateKey($name, $className));
|
||||
|
||||
if (class_exists($className, false)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getTimestamp(string $key): int
|
||||
{
|
||||
$splitKey = $this->splitKey($key);
|
||||
|
||||
foreach ($this->caches as $cache) {
|
||||
if (0 < $timestamp = $cache->getTimestamp($cache->generateKey(...$splitKey))) {
|
||||
return $timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function splitKey(string $key): array
|
||||
{
|
||||
return array_reverse(explode('#', $key, 2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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\Cache;
|
||||
|
||||
/**
|
||||
* Implements a cache on the filesystem that can only be read, not written to.
|
||||
*
|
||||
* @author Quentin Devos <quentin@devos.pm>
|
||||
*/
|
||||
class ReadOnlyFilesystemCache extends FilesystemCache
|
||||
{
|
||||
public function write(string $key, string $content): void
|
||||
{
|
||||
// Do nothing with the content, it's a read-only filesystem.
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -54,7 +54,7 @@ class Node implements \Countable, \IteratorAggregate
|
||||
{
|
||||
$attributes = [];
|
||||
foreach ($this->attributes as $name => $value) {
|
||||
$attributes[] = \sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true)));
|
||||
$attributes[] = \sprintf('%s: %s', $name, is_callable($value) ? '\Closure' : str_replace("\n", '', var_export($value, true)));
|
||||
}
|
||||
|
||||
$repr = [static::class.'('.implode(', ', $attributes)];
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
namespace Twig\Tests\Cache;
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Cache\ChainCache;
|
||||
use Twig\Cache\FilesystemCache;
|
||||
use Twig\Tests\FilesystemHelper;
|
||||
|
||||
class ChainTest extends TestCase
|
||||
{
|
||||
private $classname;
|
||||
private $directory;
|
||||
private $cache;
|
||||
private $key;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$nonce = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', random_bytes(32));
|
||||
$this->classname = '__Twig_Tests_Cache_ChainTest_Template_'.$nonce;
|
||||
$this->directory = sys_get_temp_dir().'/twig-test';
|
||||
$this->cache = new ChainCache([
|
||||
new FilesystemCache($this->directory.'/A'),
|
||||
new FilesystemCache($this->directory.'/B'),
|
||||
]);
|
||||
$this->key = $this->cache->generateKey('_test_', $this->classname);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if (file_exists($this->directory)) {
|
||||
FilesystemHelper::removeDir($this->directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testLoadInA()
|
||||
{
|
||||
$cache = new FilesystemCache($this->directory.'/A');
|
||||
$key = $cache->generateKey('_test_', $this->classname);
|
||||
|
||||
$dir = \dirname($key);
|
||||
@mkdir($dir, 0777, true);
|
||||
$this->assertDirectoryExists($dir);
|
||||
$this->assertFalse(class_exists($this->classname, false));
|
||||
|
||||
$content = $this->generateSource();
|
||||
file_put_contents($key, $content);
|
||||
var_dump($key);
|
||||
|
||||
$this->cache->load($this->key);
|
||||
|
||||
$this->assertTrue(class_exists($this->classname, false));
|
||||
}
|
||||
|
||||
public function testLoadInB()
|
||||
{
|
||||
$cache = new FilesystemCache($this->directory.'/B');
|
||||
$key = $cache->generateKey('_test_', $this->classname);
|
||||
|
||||
$dir = \dirname($key);
|
||||
@mkdir($dir, 0777, true);
|
||||
$this->assertDirectoryExists($dir);
|
||||
$this->assertFalse(class_exists($this->classname, false));
|
||||
|
||||
$content = $this->generateSource();
|
||||
file_put_contents($key, $content);
|
||||
var_dump($key);
|
||||
|
||||
$this->cache->load($this->key);
|
||||
|
||||
$this->assertTrue(class_exists($this->classname, false));
|
||||
}
|
||||
|
||||
public function testLoadInBoth()
|
||||
{
|
||||
$cache = new FilesystemCache($this->directory.'/A');
|
||||
$key = $cache->generateKey('_test_', $this->classname);
|
||||
|
||||
$dir = \dirname($key);
|
||||
@mkdir($dir, 0777, true);
|
||||
$this->assertDirectoryExists($dir);
|
||||
$this->assertFalse(class_exists($this->classname, false));
|
||||
|
||||
$content = $this->generateSource();
|
||||
file_put_contents($key, $content);
|
||||
|
||||
$cache = new FilesystemCache($this->directory.'/B');
|
||||
$key = $cache->generateKey('_test_', $this->classname);
|
||||
|
||||
$dir = \dirname($key);
|
||||
@mkdir($dir, 0777, true);
|
||||
$this->assertDirectoryExists($dir);
|
||||
$this->assertFalse(class_exists($this->classname, false));
|
||||
|
||||
$content = $this->generateSource();
|
||||
file_put_contents($key, $content);
|
||||
|
||||
$this->cache->load($this->key);
|
||||
|
||||
$this->assertTrue(class_exists($this->classname, false));
|
||||
}
|
||||
|
||||
public function testLoadMissing()
|
||||
{
|
||||
$this->assertFalse(class_exists($this->classname, false));
|
||||
|
||||
$this->cache->load($this->key);
|
||||
|
||||
$this->assertFalse(class_exists($this->classname, false));
|
||||
}
|
||||
|
||||
public function testWrite()
|
||||
{
|
||||
$content = $this->generateSource();
|
||||
|
||||
$cacheA = new FilesystemCache($this->directory.'/A');
|
||||
$keyA = $cacheA->generateKey('_test_', $this->classname);
|
||||
|
||||
$this->assertFileDoesNotExist($keyA);
|
||||
$this->assertFileDoesNotExist($this->directory.'/A');
|
||||
|
||||
$cacheB = new FilesystemCache($this->directory.'/B');
|
||||
$keyB = $cacheB->generateKey('_test_', $this->classname);
|
||||
|
||||
$this->assertFileDoesNotExist($keyB);
|
||||
$this->assertFileDoesNotExist($this->directory.'/B');
|
||||
|
||||
$this->cache->write($this->key, $content);
|
||||
|
||||
$this->assertFileExists($this->directory.'/A');
|
||||
$this->assertFileExists($keyA);
|
||||
$this->assertSame(file_get_contents($keyA), $content);
|
||||
|
||||
$this->assertFileExists($this->directory.'/B');
|
||||
$this->assertFileExists($keyB);
|
||||
$this->assertSame(file_get_contents($keyB), $content);
|
||||
}
|
||||
|
||||
public function testGetTimestampInA()
|
||||
{
|
||||
$cache = new FilesystemCache($this->directory.'/A');
|
||||
$key = $cache->generateKey('_test_', $this->classname);
|
||||
|
||||
$dir = \dirname($key);
|
||||
@mkdir($dir, 0777, true);
|
||||
$this->assertDirectoryExists($dir);
|
||||
|
||||
// Create the file with a specific modification time.
|
||||
touch($key, 1234567890);
|
||||
|
||||
$this->assertSame(1234567890, $this->cache->getTimestamp($this->key));
|
||||
}
|
||||
|
||||
public function testGetTimestampInB()
|
||||
{
|
||||
$cache = new FilesystemCache($this->directory.'/B');
|
||||
$key = $cache->generateKey('_test_', $this->classname);
|
||||
|
||||
$dir = \dirname($key);
|
||||
@mkdir($dir, 0777, true);
|
||||
$this->assertDirectoryExists($dir);
|
||||
|
||||
// Create the file with a specific modification time.
|
||||
touch($key, 1234567890);
|
||||
|
||||
$this->assertSame(1234567890, $this->cache->getTimestamp($this->key));
|
||||
}
|
||||
|
||||
public function testGetTimestampInBoth()
|
||||
{
|
||||
$cacheA = new FilesystemCache($this->directory.'/A');
|
||||
$keyA = $cacheA->generateKey('_test_', $this->classname);
|
||||
|
||||
$dir = \dirname($keyA);
|
||||
@mkdir($dir, 0777, true);
|
||||
$this->assertDirectoryExists($dir);
|
||||
|
||||
// Create the file with a specific modification time.
|
||||
touch($keyA, 1234567890);
|
||||
|
||||
$cacheB = new FilesystemCache($this->directory.'/B');
|
||||
$keyB = $cacheB->generateKey('_test_', $this->classname);
|
||||
|
||||
$dir = \dirname($keyB);
|
||||
@mkdir($dir, 0777, true);
|
||||
$this->assertDirectoryExists($dir);
|
||||
|
||||
// Create the file with a specific modification time.
|
||||
touch($keyB, 1234567891);
|
||||
|
||||
$this->assertSame(1234567890, $this->cache->getTimestamp($this->key));
|
||||
}
|
||||
|
||||
public function testGetTimestampMissingFile()
|
||||
{
|
||||
$this->assertSame(0, $this->cache->getTimestamp($this->key));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideInput
|
||||
*/
|
||||
public function testGenerateKey($expected, $input)
|
||||
{
|
||||
$cache = new ChainCache([]);
|
||||
$this->assertSame($expected, $cache->generateKey($input, static::class));
|
||||
}
|
||||
|
||||
public static function provideInput()
|
||||
{
|
||||
return [
|
||||
['Twig\Tests\Cache\ChainTest#_test_', '_test_'],
|
||||
['Twig\Tests\Cache\ChainTest#_test#with#hashtag_', '_test#with#hashtag_'],
|
||||
];
|
||||
}
|
||||
|
||||
private function generateSource()
|
||||
{
|
||||
return strtr('<?php class {{classname}} {}', [
|
||||
'{{classname}}' => $this->classname,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Twig\Tests\Cache;
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Tests\FilesystemHelper;
|
||||
use Twig\Cache\ReadOnlyFilesystemCache;
|
||||
|
||||
class ReadOnlyFilesystemTest extends TestCase
|
||||
{
|
||||
private $classname;
|
||||
private $directory;
|
||||
private $cache;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$nonce = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', random_bytes(32));
|
||||
$this->classname = '__Twig_Tests_Cache_ReadOnlyFilesystemTest_Template_'.$nonce;
|
||||
$this->directory = sys_get_temp_dir().'/twig-test';
|
||||
$this->cache = new ReadOnlyFilesystemCache($this->directory);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if (file_exists($this->directory)) {
|
||||
FilesystemHelper::removeDir($this->directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testLoad()
|
||||
{
|
||||
$key = $this->directory.'/cache/ro-cachefile.php';
|
||||
|
||||
$dir = \dirname($key);
|
||||
@mkdir($dir, 0777, true);
|
||||
$this->assertDirectoryExists($dir);
|
||||
$this->assertFalse(class_exists($this->classname, false));
|
||||
|
||||
$content = $this->generateSource();
|
||||
file_put_contents($key, $content);
|
||||
|
||||
$this->cache->load($key);
|
||||
|
||||
$this->assertTrue(class_exists($this->classname, false));
|
||||
}
|
||||
|
||||
public function testLoadMissing()
|
||||
{
|
||||
$key = $this->directory.'/cache/cachefile.php';
|
||||
|
||||
$this->assertFalse(class_exists($this->classname, false));
|
||||
|
||||
$this->cache->load($key);
|
||||
|
||||
$this->assertFalse(class_exists($this->classname, false));
|
||||
}
|
||||
|
||||
public function testWrite()
|
||||
{
|
||||
$key = $this->directory.'/cache/cachefile.php';
|
||||
$content = $this->generateSource();
|
||||
|
||||
$this->assertFileDoesNotExist($key);
|
||||
$this->assertFileDoesNotExist($this->directory);
|
||||
|
||||
$this->cache->write($key, $content);
|
||||
|
||||
$this->assertFileDoesNotExist($this->directory);
|
||||
$this->assertFileDoesNotExist($key);
|
||||
}
|
||||
|
||||
public function testGetTimestamp()
|
||||
{
|
||||
$key = $this->directory.'/cache/cachefile.php';
|
||||
|
||||
$dir = \dirname($key);
|
||||
@mkdir($dir, 0777, true);
|
||||
$this->assertDirectoryExists($dir);
|
||||
|
||||
// Create the file with a specific modification time.
|
||||
touch($key, 1234567890);
|
||||
|
||||
$this->assertSame(1234567890, $this->cache->getTimestamp($key));
|
||||
}
|
||||
|
||||
public function testGetTimestampMissingFile()
|
||||
{
|
||||
$key = $this->directory.'/cache/cachefile.php';
|
||||
$this->assertSame(0, $this->cache->getTimestamp($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test file cache is tolerant towards trailing (back)slashes on the configured cache directory.
|
||||
*
|
||||
* @dataProvider provideDirectories
|
||||
*/
|
||||
public function testGenerateKey($expected, $input)
|
||||
{
|
||||
$cache = new ReadOnlyFilesystemCache($input);
|
||||
$this->assertMatchesRegularExpression($expected, $cache->generateKey('_test_', static::class));
|
||||
}
|
||||
|
||||
public static function provideDirectories()
|
||||
{
|
||||
$pattern = '#a/b/[a-zA-Z0-9]+/[a-zA-Z0-9]+.php$#';
|
||||
|
||||
return [
|
||||
[$pattern, 'a/b'],
|
||||
[$pattern, 'a/b/'],
|
||||
[$pattern, 'a/b\\'],
|
||||
[$pattern, 'a/b\\/'],
|
||||
[$pattern, 'a/b\\//'],
|
||||
['#/'.substr($pattern, 1), '/a/b'],
|
||||
];
|
||||
}
|
||||
|
||||
private function generateSource()
|
||||
{
|
||||
return strtr('<?php class {{classname}} {}', [
|
||||
'{{classname}}' => $this->classname,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Twig\Tests\Node;
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Node\Node;
|
||||
|
||||
class NodeTest extends TestCase
|
||||
{
|
||||
public function testToString()
|
||||
{
|
||||
// callable is not a supported type for a Node attribute, but Drupal uses some apparently
|
||||
$node = new Node([], ['value' => function () { return '1'; }], 1);
|
||||
|
||||
$this->assertEquals('Twig\Node\Node(value: \Closure)', (string) $node);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user