Commit 8b60468d authored by alexpott's avatar alexpott

Issue #2568171 by Cottser, dawehner, hussainweb, Wim Leers, cilefen, znerol:...

Issue #2568171 by Cottser, dawehner, hussainweb, Wim Leers, cilefen, znerol: Upgrade to Twig 1.22 and implement our own cache class
parent c1596550
......@@ -17,7 +17,7 @@
"symfony/validator": "2.7.*",
"symfony/process": "2.7.*",
"symfony/yaml": "2.7.*",
"twig/twig": "~1.21.2",
"twig/twig": "~1.22",
"doctrine/common": "2.5.*",
"doctrine/annotations": "1.2.*",
"guzzlehttp/guzzle": "~6.1",
......
......@@ -4,7 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "1ced6ff7d3b6b67353879644265119bd",
"hash": "e917b58b5b2a847270d4bd49b0bcb22e",
"content-hash": "bbb9004b899df99873971591682b1e42",
"packages": [
{
"name": "doctrine/annotations",
......@@ -1778,16 +1779,16 @@
},
{
"name": "twig/twig",
"version": "v1.21.2",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "ddce1136beb8db29b9cd7dffa8ab518b978c9db3"
"reference": "b7fc2469fa009897871fb95b68237286fc54a5ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/ddce1136beb8db29b9cd7dffa8ab518b978c9db3",
"reference": "ddce1136beb8db29b9cd7dffa8ab518b978c9db3",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/b7fc2469fa009897871fb95b68237286fc54a5ad",
"reference": "b7fc2469fa009897871fb95b68237286fc54a5ad",
"shasum": ""
},
"require": {
......@@ -1800,7 +1801,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.21-dev"
"dev-master": "1.22-dev"
}
},
"autoload": {
......@@ -1835,7 +1836,7 @@
"keywords": [
"templating"
],
"time": "2015-09-09 05:28:51"
"time": "2015-09-15 06:50:16"
},
{
"name": "zendframework/zend-diactoros",
......
......@@ -9,7 +9,6 @@
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\PhpStorage\PhpStorageFactory;
use Drupal\Core\Render\SafeString;
/**
......@@ -22,27 +21,6 @@
*/
class TwigEnvironment extends \Twig_Environment {
/**
* The cache object used for auto-refresh via mtime.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache_object = NULL;
/**
* The PhpStorage object used for storing the templates.
*
* @var \Drupal\Core\PhpStorage\PhpStorageFactory
*/
protected $storage = NULL;
/**
* The template cache filename prefix.
*
* @var string
*/
protected $templateCacheFilenamePrefix;
/**
* Static cache of template classes.
*
......@@ -66,9 +44,6 @@ class TwigEnvironment extends \Twig_Environment {
* The options for the Twig environment.
*/
public function __construct($root, CacheBackendInterface $cache, $twig_extension_hash, \Twig_LoaderInterface $loader = NULL, $options = array()) {
$this->cache_object = $cache;
$this->templateCacheFilenamePrefix = $twig_extension_hash;
// Ensure that twig.engine is loaded, given that it is needed to render a
// template because functions like TwigExtension::escapeFilter() are called.
require_once $root . '/core/themes/engines/twig/twig.engine';
......@@ -84,50 +59,12 @@ public function __construct($root, CacheBackendInterface $cache, $twig_extension
// Ensure autoescaping is always on.
$options['autoescape'] = 'html';
$this->loader = $loader;
parent::__construct($this->loader, $options);
}
/**
* Checks if the compiled template needs an update.
*/
protected function isFresh($cache_filename, $name) {
$cid = 'twig:' . $cache_filename;
$obj = $this->cache_object->get($cid);
$mtime = isset($obj->data) ? $obj->data : FALSE;
return $mtime === FALSE || $this->isTemplateFresh($name, $mtime);
}
/**
* Compile the source and write the compiled template to disk.
*/
public function updateCompiledTemplate($cache_filename, $name) {
$source = $this->loader->getSource($name);
$compiled_source = $this->compileSource($source, $name);
$this->storage()->save($cache_filename, $compiled_source);
// Save the last modification time
$cid = 'twig:' . $cache_filename;
$this->cache_object->set($cid, REQUEST_TIME);
}
/**
* {@inheritdoc}
*/
public function getCacheFilename($name) {
// We override the cache filename in order to avoid issues with not using
// shared filesystems. The Twig templates for example rely on available Twig
// extensions, so we use the twig extension hash which varies by extensions
// and their mtime.
// @see \Drupal\Core\DependencyInjection\Compiler\TwigExtensionPass
if (!$this->cache) {
return FALSE;
if ($options['cache'] === TRUE) {
$options['cache'] = new TwigPhpStorageCache($cache, $twig_extension_hash);
}
$class = substr($this->getTemplateClass($name), strlen($this->templateClassPrefix));
// The first part is what is invalidated.
return $this->templateCacheFilenamePrefix . '_' . basename($name) . '_' . $class;
$this->loader = $loader;
parent::__construct($this->loader, $options);
}
/**
......@@ -159,20 +96,14 @@ public function loadTemplate($name, $index = NULL) {
}
if (!class_exists($cls, FALSE)) {
$cache_filename = $this->getCacheFilename($name);
if ($cache_filename !== FALSE) {
// If autoreload is on, check that the template has not been
// modified since the last compilation.
if ($this->isAutoReload() && !$this->isFresh($cache_filename, $name)) {
$this->updateCompiledTemplate($cache_filename, $name);
}
if (!$this->storage()->load($cache_filename)) {
$this->updateCompiledTemplate($cache_filename, $name);
$this->storage()->load($cache_filename);
}
$key = $this->cache->generateKey($name, $cls);
if (!$this->cache->has($key) || ($this->isAutoReload() && !$this->isTemplateFresh($name, $this->cache->getTimestamp($key)))) {
$this->cache->write($key, $this->compileSource($this->getLoader()->getSource($name), $name));
}
$this->cache->load($key);
if (!class_exists($cls, FALSE)) {
$compiled_source = $this->compileSource($this->loader->getSource($name), $name);
eval('?' . '>' . $compiled_source);
......@@ -186,18 +117,6 @@ public function loadTemplate($name, $index = NULL) {
return $this->loadedTemplates[$cls] = new $cls($this);
}
/**
* Gets the PHP code storage object to use for the compiled Twig files.
*
* @return \Drupal\Component\PhpStorage\PhpStorageInterface
*/
protected function storage() {
if (!isset($this->storage)) {
$this->storage = PhpStorageFactory::get('twig');
}
return $this->storage;
}
/**
* Gets the template class associated with the given string.
*
......
<?php
/**
* @file
* Contains \Drupal\Core\Template\TwigPhpStorageCache.
*/
namespace Drupal\Core\Template;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\PhpStorage\PhpStorageFactory;
/**
* Provides an alternate cache storage for Twig using PhpStorage.
*
* This class is designed to work on setups with multiple webheads using a local
* filesystem for the twig cache. When generating the cache key, a hash value
* depending on the enabled extensions is included. This prevents stale
* templates from being reused when twig extensions are enabled or disabled.
*
* @see \Drupal\Core\DependencyInjection\Compiler\TwigExtensionPass
*/
class TwigPhpStorageCache implements \Twig_CacheInterface {
/**
* The cache object used for auto-refresh via mtime.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The PhpStorage object used for storing the templates.
*
* @var \Drupal\Component\PhpStorage\PhpStorageInterface
*/
protected $storage;
/**
* The template cache filename prefix.
*
* @var string
*/
protected $templateCacheFilenamePrefix;
/**
* Store cache backend and other information internally.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache bin.
* @param string $twig_extension_hash
* The Twig extension hash.
*/
public function __construct(CacheBackendInterface $cache, $twig_extension_hash) {
$this->cache = $cache;
$this->templateCacheFilenamePrefix = $twig_extension_hash;
}
/**
* Gets the PHP code storage object to use for the compiled Twig files.
*
* @return \Drupal\Component\PhpStorage\PhpStorageInterface
*/
protected function storage() {
if (!isset($this->storage)) {
$this->storage = PhpStorageFactory::get('twig');
}
return $this->storage;
}
/**
* {@inheritdoc}
*/
public function generateKey($name, $className) {
$hash = hash('sha256', $className);
// The first part is what is invalidated.
return $this->templateCacheFilenamePrefix . '_' . basename($name) . '_' . $hash;
}
/**
* {@inheritdoc}
*/
public function has($key) {
return $this->storage()->exists($key);
}
/**
* {@inheritdoc}
*/
public function load($key) {
$this->storage()->load($key);
}
/**
* {@inheritdoc}
*/
public function write($key, $content) {
$this->storage()->save($key, $content);
// Save the last mtime.
$cid = 'twig:' . $key;
$this->cache->set($cid, REQUEST_TIME);
}
/**
* {@inheritdoc}
*/
public function getTimestamp($key) {
$cid = 'twig:' . $key;
if ($cache = $this->cache->get($cid)) {
return $cache->data;
}
else {
return 0;
}
}
}
......@@ -114,8 +114,7 @@ function testTwigCacheOverride() {
$this->rebuildContainer();
// This should return false after rebuilding the service container.
$new_cache_filename = $this->container->get('twig')->getCacheFilename($template_filename);
$this->assertFalse($new_cache_filename, 'Twig environment does not return cache filename after caching is disabled.');
$this->assertFalse($this->container->get('twig')->getCache(), 'Twig environment has caching disabled.');
}
/**
......
......@@ -3354,129 +3354,129 @@
"homepage": "https://symfony.com"
},
{
"name": "twig/twig",
"version": "v1.21.2",
"version_normalized": "1.21.2.0",
"name": "symfony/translation",
"version": "v2.7.4",
"version_normalized": "2.7.4.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "ddce1136beb8db29b9cd7dffa8ab518b978c9db3"
"url": "https://github.com/symfony/Translation.git",
"reference": "485877661835e188cd78345c6d4eef1290d17571"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/ddce1136beb8db29b9cd7dffa8ab518b978c9db3",
"reference": "ddce1136beb8db29b9cd7dffa8ab518b978c9db3",
"url": "https://api.github.com/repos/symfony/Translation/zipball/485877661835e188cd78345c6d4eef1290d17571",
"reference": "485877661835e188cd78345c6d4eef1290d17571",
"shasum": ""
},
"require": {
"php": ">=5.2.7"
"php": ">=5.3.9"
},
"conflict": {
"symfony/config": "<2.7"
},
"require-dev": {
"symfony/debug": "~2.7",
"symfony/phpunit-bridge": "~2.7"
"psr/log": "~1.0",
"symfony/config": "~2.7",
"symfony/intl": "~2.4",
"symfony/phpunit-bridge": "~2.7",
"symfony/yaml": "~2.2"
},
"time": "2015-09-09 05:28:51",
"suggest": {
"psr/log": "To use logging capability in translator",
"symfony/config": "",
"symfony/yaml": ""
},
"time": "2015-09-06 08:36:38",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.21-dev"
"dev-master": "2.7-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-0": {
"Twig_": "lib/"
"psr-4": {
"Symfony\\Component\\Translation\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
},
{
"name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com",
"role": "Project Founder"
"email": "fabien@symfony.com"
},
{
"name": "Twig Team",
"homepage": "http://twig.sensiolabs.org/contributors",
"role": "Contributors"
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Twig, the flexible, fast, and secure template language for PHP",
"homepage": "http://twig.sensiolabs.org",
"keywords": [
"templating"
]
"description": "Symfony Translation Component",
"homepage": "https://symfony.com"
},
{
"name": "symfony/translation",
"version": "v2.7.4",
"version_normalized": "2.7.4.0",
"name": "twig/twig",
"version": "v1.22.1",
"version_normalized": "1.22.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/Translation.git",
"reference": "485877661835e188cd78345c6d4eef1290d17571"
"url": "https://github.com/twigphp/Twig.git",
"reference": "b7fc2469fa009897871fb95b68237286fc54a5ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/Translation/zipball/485877661835e188cd78345c6d4eef1290d17571",
"reference": "485877661835e188cd78345c6d4eef1290d17571",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/b7fc2469fa009897871fb95b68237286fc54a5ad",
"reference": "b7fc2469fa009897871fb95b68237286fc54a5ad",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
"conflict": {
"symfony/config": "<2.7"
"php": ">=5.2.7"
},
"require-dev": {
"psr/log": "~1.0",
"symfony/config": "~2.7",
"symfony/intl": "~2.4",
"symfony/phpunit-bridge": "~2.7",
"symfony/yaml": "~2.2"
},
"suggest": {
"psr/log": "To use logging capability in translator",
"symfony/config": "",
"symfony/yaml": ""
"symfony/debug": "~2.7",
"symfony/phpunit-bridge": "~2.7"
},
"time": "2015-09-06 08:36:38",
"time": "2015-09-15 06:50:16",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
"dev-master": "1.22-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Component\\Translation\\": ""
"psr-0": {
"Twig_": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
"BSD-3-Clause"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
"name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com",
"role": "Project Founder"
},
{
"name": "Twig Team",
"homepage": "http://twig.sensiolabs.org/contributors",
"role": "Contributors"
}
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com"
"description": "Twig, the flexible, fast, and secure template language for PHP",
"homepage": "http://twig.sensiolabs.org",
"keywords": [
"templating"
]
}
]
* 1.22.1 (2015-09-15)
* fixed regression in template_from_string
* 1.22.0 (2015-09-13)
* made Twig_Test_IntegrationTestCase more flexible
* added an option to force PHP bytecode invalidation when writing a compiled template into the cache
* fixed the profiler duration for the root node
* changed template cache names to take into account enabled extensions
* deprecated Twig_Environment::clearCacheFiles(), Twig_Environment::getCacheFilename(),
Twig_Environment::writeCacheFile(), and Twig_Environment::getTemplateClassPrefix()
* added a way to override the filesystem template cache system
* added a way to get the original template source from Twig_Template
* 1.21.2 (2015-09-09)
* fixed variable names for the deprecation triggering code
......
......@@ -40,7 +40,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "1.21-dev"
"dev-master": "1.22-dev"
}
}
}
......@@ -117,7 +117,7 @@ Loaders
-------
* As of Twig 1.x, ``Twig_Loader_String`` is deprecated and will be removed in
2.0.
2.0. You can render a string via ``Twig_Environment::createTemplate()``.
Node Visitors
-------------
......@@ -140,8 +140,9 @@ Globals
Miscellaneous
-------------
* As of Twig 1.x, ``Twig_Environment::clearTemplateCache()`` is deprecated and
will be removed in 2.0.
* As of Twig 1.x, ``Twig_Environment::clearTemplateCache()``, ``Twig_Environment::writeCacheFile()``,
``Twig_Environment::clearCacheFiles()``, ``Twig_Environment::getCacheFilename()``, and
``Twig_Environment::getTemplateClassPrefix()`` are deprecated and will be removed in 2.0.
* As of Twig 1.x, ``Twig_Template::getEnvironment()`` and
``Twig_TemplateInterface::getEnvironment()`` are deprecated and will be
......
......@@ -337,24 +337,33 @@ Refreshing modified Templates when OPcache or APC is enabled
When using OPcache with ``opcache.validate_timestamps`` set to ``0`` or APC
with ``apc.stat`` set to ``0`` and Twig cache enabled, clearing the template
cache won't update the cache. To get around this, one can extend
``Twig_Environment`` and force the update of the cache when Twig rewrites the
cache::
cache won't update the cache.
class Twig_Environment_APC extends Twig_Environment
{
protected function writeCacheFile($file, $content)
{
parent::writeCacheFile($file, $content);
To get around this, force Twig to invalidate the bytecode cache::
$twig = new Twig_Environment($loader, array(
'cache' => new Twig_Cache_Filesystem('/some/cache/path', Twig_Cache_Filesystem::FORCE_BYTECODE_INVALIDATION),
// ...
));
// Compile cached file into bytecode cache
if (extension_loaded('Zend OPcache') && ini_get('opcache.enable')) {
opcache_invalidate($file);
} elseif (extension_loaded('apc') && ini_get('apc.enabled')) {
apc_compile_file($file);
.. note::
Before Twig 1.22, you should extend ``Twig_Environment`` instead::
class OpCacheAwareTwigEnvironment extends Twig_Environment
{
protected function writeCacheFile($file, $content)
{
parent::writeCacheFile($file, $content);
// Compile cached file into bytecode cache
if (function_exists('opcache_invalidate')) {
opcache_invalidate($file, true);
} elseif (function_exists('apc_compile_file')) {
apc_compile_file($file);
}
}
}
}
Reusing a stateful Node Visitor
-------------------------------
......@@ -485,4 +494,25 @@ logical name, and not the path from the filesystem::
Now that the ``base.twig`` templates is defined in an array loader, you can
remove it from the database, and everything else will still work as before.
Loading a Template from a String
--------------------------------
From a template, you can easily load a template stored in a string via the
``template_from_string`` function (available as of Twig 1.11 via the
``Twig_Extension_StringLoader`` extension)::
.. code-block:: jinja
{{ include(template_from_string("Hello {{ name }}")) }}
From PHP, it's also possible to load a template stored in a string via
``Twig_Environment::createTemplate()`` (available as of Twig 1.18)::
$template = $twig->createTemplate('hello {{ name }}');
echo $template->render(array('name' => 'Fabien'));
.. note::
Never use the ``Twig_Loader_String`` loader, which has severe limitations.
.. _callback: http://www.php.net/manual/en/function.is-callable.php
......@@ -15,7 +15,7 @@
#ifndef PHP_TWIG_H
#define PHP_TWIG_H
#define PHP_TWIG_VERSION "1.21.2"
#define PHP_TWIG_VERSION "1.22.1"
#include "php.h"
......
<?php
/*
* This file is part of Twig.
*
* (c) 2015 Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Implements a cache on the filesystem.
*
* @author Andrew Tch <andrew@noop.lv>
*/
class Twig_Cache_Filesystem implements Twig_CacheInterface
{
const FORCE_BYTECODE_INVALIDATION = 1;
private $directory;
private $invalidateBytecode;
/**
* @param $directory string The root cache directory
* @param $options int A set of options
*/
public function __construct($directory, $options = 0)
{
$this->directory = $directory;
$this->options = $options;
}
/**
* {@inheritdoc}
*/
public function generateKey($name, $className)
{
$hash = hash('sha256', $className);
return $this->directory.'/'.$hash[0].'/'.$hash[1].'/'.$hash.'.php';
}
/**
* {@inheritdoc}
*/
public function has($key)
{
return is_file($key);
}
/**
* {@inheritdoc}
*/
public function load($key)
{
require_once $key;
}
/**