diff --git a/core/includes/common.inc b/core/includes/common.inc index ffdd3fa2cf370d4ba7716d7c531a040f393f46c0..04324f60fcf27bc19720b3d709e8cd7faa959118 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -17,7 +17,6 @@ use Drupal\Core\Render\Element\Link; use Drupal\Core\Render\Markup; use Drupal\Core\StringTranslation\TranslatableMarkup; -use Drupal\Core\PhpStorage\PhpStorageFactory; use Drupal\Core\StringTranslation\PluralTranslatableMarkup; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Element; @@ -1117,7 +1116,7 @@ function drupal_flush_all_caches() { \Drupal::service('kernel')->invalidateContainer(); // Wipe the Twig PHP Storage cache. - PhpStorageFactory::get('twig')->deleteAll(); + \Drupal::service('twig')->invalidate(); // Rebuild module and theme data. $module_data = system_rebuild_module_data(); diff --git a/core/lib/Drupal/Core/Template/TwigEnvironment.php b/core/lib/Drupal/Core/Template/TwigEnvironment.php index 21755a5057c728d0e12fc46e3b81eb455a2963f3..a3a830411686f3fbb7c84c87e81427cc578722a7 100644 --- a/core/lib/Drupal/Core/Template/TwigEnvironment.php +++ b/core/lib/Drupal/Core/Template/TwigEnvironment.php @@ -3,6 +3,7 @@ namespace Drupal\Core\Template; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\PhpStorage\PhpStorageFactory; use Drupal\Core\Render\Markup; use Drupal\Core\State\StateInterface; @@ -16,6 +17,18 @@ */ class TwigEnvironment extends \Twig_Environment { + /** + * Key name of the Twig cache prefix metadata key-value pair in State. + */ + const CACHE_PREFIX_METADATA_KEY = 'twig_extension_hash_prefix'; + + /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + /** * Static cache of template classes. * @@ -43,6 +56,8 @@ class TwigEnvironment extends \Twig_Environment { * The options for the Twig environment. */ public function __construct($root, CacheBackendInterface $cache, $twig_extension_hash, StateInterface $state, \Twig_LoaderInterface $loader = NULL, $options = []) { + $this->state = $state; + // 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'; @@ -63,7 +78,7 @@ public function __construct($root, CacheBackendInterface $cache, $twig_extension $this->addExtension($sandbox); if ($options['cache'] === TRUE) { - $current = $state->get('twig_extension_hash_prefix', ['twig_extension_hash' => '']); + $current = $state->get(static::CACHE_PREFIX_METADATA_KEY, ['twig_extension_hash' => '']); if ($current['twig_extension_hash'] !== $twig_extension_hash || empty($current['twig_cache_prefix'])) { $current = [ 'twig_extension_hash' => $twig_extension_hash, @@ -71,7 +86,7 @@ public function __construct($root, CacheBackendInterface $cache, $twig_extension 'twig_cache_prefix' => uniqid(), ]; - $state->set('twig_extension_hash_prefix', $current); + $state->set(static::CACHE_PREFIX_METADATA_KEY, $current); } $this->twigCachePrefix = $current['twig_cache_prefix']; @@ -82,6 +97,18 @@ public function __construct($root, CacheBackendInterface $cache, $twig_extension parent::__construct($this->loader, $options); } + /** + * Invalidates all compiled Twig templates. + * + * @see \drupal_flush_all_caches + */ + public function invalidate() { + PhpStorageFactory::get('twig')->deleteAll(); + $this->templateClasses = []; + $this->loadedTemplates = []; + $this->state->delete(static::CACHE_PREFIX_METADATA_KEY); + } + /** * Get the cache prefixed used by \Drupal\Core\Template\TwigPhpStorageCache * diff --git a/core/tests/Drupal/KernelTests/Core/Theme/TwigEnvironmentTest.php b/core/tests/Drupal/KernelTests/Core/Theme/TwigEnvironmentTest.php index 5d6f61f205be8a4dd088e38a40cdef9c0a9a5d2d..854c25c8dc6317cbfb1bb3814b811db8f0fd3b96 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/TwigEnvironmentTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/TwigEnvironmentTest.php @@ -4,9 +4,11 @@ use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Html; +use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Site\Settings; use Drupal\Core\Template\TwigPhpStorageCache; use Drupal\KernelTests\KernelTestBase; +use Symfony\Component\DependencyInjection\Definition; /** * Tests the twig environment. @@ -143,4 +145,52 @@ public function testCacheFilename() { $this->assertNotEqual($new_extension_filename, $original_filename); } + /** + * {@inheritdoc} + */ + public function register(ContainerBuilder $container) { + parent::register($container); + + $container->setDefinition('twig_loader__file_system', new Definition('Twig_Loader_Filesystem', [[sys_get_temp_dir()]])) + ->addTag('twig.loader'); + } + + + /** + * Test template invalidation. + */ + public function testTemplateInvalidation() { + $template_before = <<Hello before +TWIG; + $template_after = <<Hello after +TWIG; + + $tempfile = tempnam(sys_get_temp_dir(), '__METHOD__') . '.html.twig'; + file_put_contents($tempfile, $template_before); + + /** @var \Drupal\Core\Template\TwigEnvironment $environment */ + $environment = \Drupal::service('twig'); + + $output = $environment->load(basename($tempfile))->render(); + $this->assertEquals($template_before, $output); + + file_put_contents($tempfile, $template_after); + $output = $environment->load(basename($tempfile))->render(); + $this->assertEquals($template_before, $output); + + $environment->invalidate(); + // Manually change $templateClassPrefix to force a different template + // classname, as the other class is still loaded. This wouldn't be a problem + // on a real site where you reload the page. + $reflection = new \ReflectionClass($environment); + $property_reflection = $reflection->getProperty('templateClassPrefix'); + $property_reflection->setAccessible(TRUE); + $property_reflection->setValue($environment, 'otherPrefix'); + + $output = $environment->load(basename($tempfile))->render(); + $this->assertEquals($template_after, $output); + } + }