state = $state; $this->templateClasses = []; $options += [ // @todo Ensure garbage collection of expired files. 'cache' => TRUE, 'debug' => FALSE, 'auto_reload' => NULL, ]; // Ensure autoescaping is always on. $options['autoescape'] = 'html'; if ($options['cache'] === TRUE) { $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, // Generate a new prefix which invalidates any existing cached files. 'twig_cache_prefix' => uniqid(), ]; $state->set(static::CACHE_PREFIX_METADATA_KEY, $current); } $this->twigCachePrefix = $current['twig_cache_prefix']; $options['cache'] = new TwigPhpStorageCache($cache, $this->twigCachePrefix); } $this->setLoader($loader); parent::__construct($this->getLoader(), $options); $policy = new TwigSandboxPolicy(); $sandbox = new SandboxExtension($policy, TRUE); $this->addExtension($sandbox); } /** * {@inheritdoc} */ public function compileSource(Source $source): string { // Note: always use \Drupal\Core\Serialization\Yaml here instead of the // "serializer.yaml" service. This allows the core serializer to utilize // core related functionality which isn't available as the standalone // component based serializer. $frontMatter = FrontMatter::create($source->getCode(), Yaml::class); // Reconstruct the source if there is front matter data detected. Prepend // the source with {% line \d+ %} to inform Twig that the source code // actually starts on a different line past the front matter data. This is // particularly useful when used in error reporting. try { if (($line = $frontMatter->getLine()) > 1) { $content = "{% line $line %}" . $frontMatter->getContent(); $source = new Source($content, $source->getName(), $source->getPath()); } } catch (FrontMatterParseException $exception) { // Convert parse exception into a syntax exception for Twig and append // the path/name of the source to help further identify where it occurred. $message = sprintf($exception->getMessage() . ' in %s', $source->getPath() ?: $source->getName()); throw new SyntaxError($message, $exception->getSourceLine(), $source, $exception); } return parent::compileSource($source); } /** * Invalidates all compiled Twig templates. * * @see \drupal_flush_all_caches */ public function invalidate() { PhpStorageFactory::get('twig')->deleteAll(); $this->templateClasses = []; $this->state->delete(static::CACHE_PREFIX_METADATA_KEY); } /** * Get the cache prefixed used by \Drupal\Core\Template\TwigPhpStorageCache. * * @return string * The file cache prefix, or empty string if the cache is disabled. */ public function getTwigCachePrefix() { return $this->twigCachePrefix; } /** * Retrieves metadata associated with a template. * * @param string $name * The name for which to calculate the template class name. * * @return array * The template metadata, if any. * * @throws \Twig\Error\LoaderError * @throws \Twig\Error\SyntaxError */ public function getTemplateMetadata(string $name): array { $loader = $this->getLoader(); $source = $loader->getSourceContext($name); // Note: always use \Drupal\Core\Serialization\Yaml here instead of the // "serializer.yaml" service. This allows the core serializer to utilize // core related functionality which isn't available as the standalone // component based serializer. try { return FrontMatter::create($source->getCode(), Yaml::class)->getData(); } catch (FrontMatterParseException $exception) { // Convert parse exception into a syntax exception for Twig and append // the path/name of the source to help further identify where it occurred. $message = sprintf($exception->getMessage() . ' in %s', $source->getPath() ?: $source->getName()); throw new SyntaxError($message, $exception->getSourceLine(), $source, $exception); } } /** * Gets the template class associated with the given string. * * @param string $name * The name for which to calculate the template class name. * @param int $index * The index if it is an embedded template. * * @return string * The template class name. */ public function getTemplateClass(string $name, int $index = NULL): string { // We override this method to add caching because it gets called multiple // times when the same template is used more than once. For example, a page // rendering 50 nodes without any node template overrides will use the same // node.html.twig for the output of each node and the same compiled class. $cache_index = $name . (NULL === $index ? '' : '_' . $index); if (!isset($this->templateClasses[$cache_index])) { $this->templateClasses[$cache_index] = parent::getTemplateClass($name, $index); } return $this->templateClasses[$cache_index]; } /** * Renders a twig string directly. * * Warning: You should use the render element 'inline_template' together with * the #template attribute instead of this method directly. * On top of that you have to ensure that the template string is not dynamic * but just an ordinary static php string, because there may be installations * using read-only PHPStorage that want to generate all possible twig * templates as part of a build step. So it is important that an automated * script can find the templates and extract them. This is only possible if * the template is a regular string. * * @param string $template_string * The template string to render with placeholders. * @param array $context * An array of parameters to pass to the template. * * @return \Drupal\Component\Render\MarkupInterface|string * The rendered inline template as a Markup object. * * @see \Drupal\Core\Template\Loader\StringLoader::exists() */ public function renderInline($template_string, array $context = []) { // Prefix all inline templates with a special comment. $template_string = '{# inline_template_start #}' . $template_string; return Markup::create($this->createTemplate($template_string)->render($context)); } }