commit 51b91c5263d1cc54fb3b3823808e79ba30400a81 Author: Wim Leers Date: Tue Jun 14 20:59:54 2016 +0200 Fix component/template inheritance by fixing a core bug -- includes #2387069-26. diff --git a/core/core.services.yml b/core/core.services.yml index b5d9ad7..fe40a83 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1529,6 +1529,7 @@ services: calls: - [setUrlGenerator, ['@url_generator']] - [setThemeManager, ['@theme.manager']] + - [setThemeRegistry, ['@theme.registry']] - [setDateFormatter, ['@date.formatter']] # @todo Figure out what to do about debugging functions. # @see https://www.drupal.org/node/1804998 @@ -1546,11 +1547,6 @@ services: arguments: ['@app.root', '@module_handler', '@theme_handler'] tags: - { name: twig.loader, priority: 100 } - twig.loader.theme_registry: - class: Drupal\Core\Template\Loader\ThemeRegistryLoader - arguments: ['@theme.registry'] - tags: - - { name: twig.loader, priority: 0 } twig.loader.string: class: Drupal\Core\Template\Loader\StringLoader tags: diff --git a/core/lib/Drupal/Core/Template/Loader/ThemeRegistryLoader.php b/core/lib/Drupal/Core/Template/Loader/ThemeRegistryLoader.php deleted file mode 100644 index 67a82dd..0000000 --- a/core/lib/Drupal/Core/Template/Loader/ThemeRegistryLoader.php +++ /dev/null @@ -1,68 +0,0 @@ -themeRegistry = $theme_registry; - } - - /** - * Finds the path to the requested template. - * - * @param string $name - * The name of the template to load. - * @param bool $throw - * Whether to throw an exception when an error occurs. - * - * @return string - * The path to the template. - * - * @throws \Twig_Error_Loader - * Thrown if a template matching $name cannot be found. - */ - protected function findTemplate($name, $throw = TRUE) { - // Allow for loading based on the Drupal theme registry. - $hook = str_replace('.html.twig', '', strtr($name, '-', '_')); - $theme_registry = $this->themeRegistry->getRuntime(); - - if ($theme_registry->has($hook)) { - $info = $theme_registry->get($hook); - if (isset($info['path'])) { - $path = $info['path'] . '/' . $name; - } - elseif (isset($info['template'])) { - $path = $info['template'] . '.html.twig'; - } - if (isset($path) && is_file($path)) { - return $this->cache[$name] = $path; - } - } - - if ($throw) { - throw new \Twig_Error_Loader(sprintf('Unable to find template "%s" in the Drupal theme registry.', $name)); - } - } - -} diff --git a/core/lib/Drupal/Core/Template/ThemeRegistryNodeVisitor.php b/core/lib/Drupal/Core/Template/ThemeRegistryNodeVisitor.php new file mode 100644 index 0000000..17c0aa9 --- /dev/null +++ b/core/lib/Drupal/Core/Template/ThemeRegistryNodeVisitor.php @@ -0,0 +1,178 @@ +themeRegistry = $theme_registry; + } + + /** + * {@inheritdoc} + */ + protected function doEnterNode(\Twig_Node $node, \Twig_Environment $env) { + if ($node instanceof \Twig_Node_Module && $node->hasNode('parent')) { + $parent = $node->getNode('parent'); + if ($parent && $this->expressionQualifies($parent)) { + $current_file = basename($node->getAttribute('filename')); + $extended_file = $parent->getAttribute('value'); + $candidates = ($current_file === $extended_file) + ? $this->getCandidateParentTemplates($current_file) + : $this->getCandidateTemplates($extended_file); + if ($candidates === FALSE) { + throw new \Twig_Error(sprintf('Template "%s" extends "%s", but no such parent templates exist.', $node->getAttribute('filename'), $parent->getAttribute('value'))); + } + $node->setNode('parent', $this->getReplacementNode($parent, $candidates)); + } + } + elseif ($node instanceof \Twig_Node_Include) { + $include = $node->getNode('expr'); + if ($include && $this->expressionQualifies($include)) { + $candidates = $this->getCandidateTemplates($include->getAttribute('value')); + if (empty($candidates)) { + throw new \Twig_Error(sprintf('Template "%s" includes "%s", but no such templates exist.', $node->getAttribute('filename'), $include->getAttribute('value'))); + } + $node->setNode('expr', $this->getReplacementNode($include, $candidates)); + } + } + return $node; + } + + /** + * Whether the given expression qualifies for theme registry-based expansion. + * + * @param \Twig_Node $node + * The Twig node to evaluate. + * + * @return bool + * Whether the expression node qualifies or not. + */ + protected function expressionQualifies(\Twig_Node $node) { + $extended_file = $node->getAttribute('value'); + return + // Only override when extending a single template. + $node instanceof \Twig_Node_Expression_Constant + // Only override when the extended template is pointing to a file, i.e + // when it doesn't include either a path or a namespace. + && $extended_file === basename($extended_file); + } + + /** + * Replace the existing constant template with a list of templates. + * + * @param \Twig_Node_Expression_Constant $node + * The Twig constant expression node to replace. + * @param string[] $candidates + * The candidate templates that Twig should attempt to load. + * + * @return \Twig_Node_Expression_Array + * The replacing Twig array expression node. + * + * @see http://twig.sensiolabs.org/doc/tags/extends.html#dynamic-inheritance + */ + protected function getReplacementNode(\Twig_Node_Expression_Constant $node, array $candidates) { + $line = $node->getLine(); + $replacement_node = new \Twig_Node_Expression_Array([], $line); + foreach ($candidates as $candidate) { + $replacement_node->addElement(new \Twig_Node_Expression_Constant("{$candidate['path']}/{$candidate['template']}.html.twig", $line)); + } + return $replacement_node; + } + + /** + * Returns candidate templates based on the theme registry. + * + * @param string $template_filepath + * The full path relative to the Drupal root for this template. + * + * @return string[] + * The candidate templates, including the current template. + */ + protected function getCandidateTemplates($template_filepath) { + $template_filename = basename($template_filepath); + $theme_hook = str_replace('.html.twig', '', strtr($template_filename, '-', '_')); + $info = $this->themeRegistry->getRuntime()->get($theme_hook); + + // Put the candidates in the right order. + $candidates = array_reverse($info['template lineage']); + + return $candidates; + } + + /** + * Returns candidate parent templates based on the theme registry. + * + * @param string $template_filepath + * The full path relative to the Drupal root for this template. + * + * @return string[]|false + * The candidate parent templates, or FALSE when none exist. + */ + protected function getCandidateParentTemplates($template_filepath) { + $candidates = $this->getCandidateTemplates($template_filepath); + + // The candidates include the current template too, so there must be >=2. + if (count($candidates) < 2) { + return FALSE; + } + + // Remove the current template. + array_shift($candidates); + + return $candidates; + } + + /** + * {@inheritdoc} + */ + protected function doLeaveNode(\Twig_Node $node, \Twig_Environment $env) { + return $node; + } + + /** + * {@inheritdoc} + */ + public function getPriority() { + // Just above the Optimizer, which is the normal last one. + return 256; + } + +} diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php index 715fd1e..40c6aa8 100644 --- a/core/lib/Drupal/Core/Template/TwigExtension.php +++ b/core/lib/Drupal/Core/Template/TwigExtension.php @@ -11,6 +11,7 @@ use Drupal\Core\Render\RenderableInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Routing\UrlGeneratorInterface; +use Drupal\Core\Theme\Registry; use Drupal\Core\Theme\ThemeManagerInterface; use Drupal\Core\Url; @@ -46,6 +47,13 @@ class TwigExtension extends \Twig_Extension { protected $themeManager; /** + * The theme registry. + * + * @var \Drupal\Core\Theme\Registry + */ + protected $themeRegistry; + + /** * The date formatter. * * @var \Drupal\Core\Datetime\DateFormatterInterface @@ -104,6 +112,19 @@ public function setThemeManager(ThemeManagerInterface $theme_manager) { } /** + * Sets the theme registry. + * + * @param \Drupal\Core\Theme\Registry $theme_registry + * The theme registry. + * + * @return $this + */ + public function setThemeRegistry(Registry $theme_registry) { + $this->themeRegistry = $theme_registry; + return $this; + } + + /** * Sets the date formatter. * * @param \Drupal\Core\Datetime\DateFormatter $date_formatter @@ -176,10 +197,9 @@ public function getFilters() { * {@inheritdoc} */ public function getNodeVisitors() { - // The node visitor is needed to wrap all variables with - // render_var -> TwigExtension->renderVar() function. return array( new TwigNodeVisitor(), + new ThemeRegistryNodeVisitor($this->themeRegistry), ); } diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index 9da6d0f..2e24f58 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -359,6 +359,7 @@ protected function build() { // phase. $modules = array_keys($this->moduleHandler->getModuleList()); foreach (\Drupal::service('theme_component_discovery')->getComponents() as $id => $resolved_component) { + // @todo rename, "tree" should be "lineage" foreach ($resolved_component['_provider tree'] as $extension_name => $component) { // 1. Module components. if (in_array($extension_name, $modules)) { @@ -375,7 +376,7 @@ protected function build() { $type = $index < count(array_keys($resolved_component['_provider tree'])) - 1 ? 'base_theme' : 'theme'; $path = drupal_get_path('theme', $extension_name); } - $cache = $this->updateRegistryWithComponent($cache, $resolved_component, $type, $path); + $cache = $this->updateRegistryWithComponent($cache, $resolved_component, $type, $extension_name, $path); } } @@ -402,12 +403,23 @@ protected function mapComponentVariablesToThemeVariables(array $component_variab return $variables; } - protected function updateRegistryWithComponent(array $registry, array $component_definition, $provider_type, $provider_path) { + protected function updateRegistryWithComponent(array $registry, array $component_definition, $provider_type, $provider_name, $provider_path) { $id = $component_definition['id']; $bc_component_name = str_replace('-', '_', $id); $path = $provider_path . '/components/' . $id; $inherited_path = isset($registry[$bc_component_name]) ? $registry[$bc_component_name]['path'] : $path; + $template_lineage = isset($registry[$bc_component_name]) ? $registry[$bc_component_name]['template lineage'] : []; + + $extension_is_overriding_template = file_exists($path .'/'. $id . '.html.twig'); + + if ($extension_is_overriding_template) { + $template_lineage[] = [ + 'extension' => $provider_name, + 'path' => $path, + 'template' => $id, + ]; + } $registry[$bc_component_name] = [ // New variables are always appended to the list of variables. @@ -416,9 +428,10 @@ protected function updateRegistryWithComponent(array $registry, array $component 'theme path' => $provider_path, 'template' => $id, // Allow themes to extend components without repeating the Twig template. - 'path' => file_exists($path .'/'. $id . '.html.twig') ? $path : $inherited_path, + 'path' => $extension_is_overriding_template ? $path : $inherited_path, // Allow inspectors of the theme registry to detect components. 'component' => TRUE, + 'template lineage' => $template_lineage, ]; return $registry; @@ -557,6 +570,20 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path) $result[$hook]['path'] = $path . '/templates'; } + if (isset($cache[$hook]['template lineage'])) { + $result[$hook]['template lineage'] = $cache[$hook]['template lineage']; + } + else { + $result[$hook]['template lineage'] = array(); + } + if (isset($result[$hook]['template'])) { + $result[$hook]['template lineage'][] = array( + 'extension' => $theme, + 'template' => $result[$hook]['template'], + 'path' => $result[$hook]['path'], + ); + } + // If the default keys are not set, use the default values registered // by the module. if (isset($cache[$hook])) { diff --git a/core/themes/classy/components/image/image.html.twig b/core/themes/classy/components/image/image.html.twig index c03c37c..95d3d8e 100644 --- a/core/themes/classy/components/image/image.html.twig +++ b/core/themes/classy/components/image/image.html.twig @@ -1,4 +1,4 @@ -{% extends "@system/image.html.twig" %} +{% extends "image.html.twig" %} {% set classes = [ style_name ? 'image-style-' ~ style_name|clean_class,