Commit 0f93b42d authored by alexpott's avatar alexpott

Issue #2291449 by Cottser: Add Twig template inheritance based on the theme...

Issue #2291449 by Cottser: Add Twig template inheritance based on the theme registry, enable adding Twig loaders
parent 82fad36b
......@@ -1125,7 +1125,7 @@ services:
class: Drupal\Core\Extension\InfoParser
twig:
class: Drupal\Core\Template\TwigEnvironment
arguments: ['@app.root', '@twig.loader', '@module_handler', '@theme_handler', '%twig.config%']
arguments: ['@app.root', '@twig.loader', '%twig.config%']
tags:
- { name: service_collector, tag: 'twig.extension', call: addExtension }
twig.extension:
......@@ -1142,10 +1142,24 @@ services:
tags:
- { name: twig.extension }
twig.loader:
alias: twig.loader.filesystem
class: Twig_Loader_Chain
public: false
tags:
- { name: service_collector, tag: twig.loader, call: addLoader, required: TRUE }
twig.loader.filesystem:
class: Twig_Loader_Filesystem
arguments: ['@app.root']
class: Drupal\Core\Template\Loader\FilesystemLoader
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: Twig_Loader_String
tags:
- { name: twig.loader, priority: -100 }
element_info:
alias: plugin.manager.element_info
file.mime_type.guesser:
......
......@@ -53,6 +53,8 @@ class TaggedHandlersPass implements CompilerPassInterface {
* - Optionally the handler's priority as second argument, if the method
* accepts a second parameter and its name is "priority". In any case, all
* handlers registered at compile time are sorted already.
* - required: Boolean indicating if at least one handler service is required.
* Defaults to FALSE.
*
* Example (YAML):
* @code
......@@ -74,12 +76,15 @@ class TaggedHandlersPass implements CompilerPassInterface {
* interface.
* @throws \Symfony\Component\DependencyInjection\Exception\LogicException
* If a tagged handler does not implement the required interface.
* @throws \Symfony\Component\DependencyInjection\Exception\LogicException
* If at least one tagged service is required but none are found.
*/
public function process(ContainerBuilder $container) {
foreach ($container->findTaggedServiceIds('service_collector') as $consumer_id => $passes) {
foreach ($passes as $pass) {
$tag = isset($pass['tag']) ? $pass['tag'] : $consumer_id;
$method_name = isset($pass['call']) ? $pass['call'] : 'addHandler';
$required = isset($pass['required']) ? $pass['required'] : FALSE;
// Determine parameters.
$consumer = $container->getDefinition($consumer_id);
......@@ -122,6 +127,9 @@ public function process(ContainerBuilder $container) {
$handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
}
if (empty($handlers)) {
if ($required) {
throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag));
}
continue;
}
// Sort all handlers by priority.
......
<?php
/**
* @file
* Contains \Drupal\Core\Template\Loader\FilesystemLoader.
*/
namespace Drupal\Core\Template\Loader;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
/**
* Loads templates from the filesystem.
*
* This loader adds module and theme template paths as namespaces to the Twig
* filesystem loader so that templates can be referenced by namespace, like
* @block/block.html.twig or @mytheme/page.html.twig.
*/
class FilesystemLoader extends \Twig_Loader_Filesystem {
/**
* Constructs a new FilesystemLoader object.
*
* @param string|array $paths
* A path or an array of paths to check for templates.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler service.
*/
public function __construct($paths = array(), ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
parent::__construct($paths);
// Add namespaced paths for modules and themes.
$namespaces = array();
foreach ($module_handler->getModuleList() as $name => $extension) {
$namespaces[$name] = $extension->getPath();
}
foreach ($theme_handler->listInfo() as $name => $extension) {
$namespaces[$name] = $extension->getPath();
}
foreach ($namespaces as $name => $path) {
$templatesDirectory = $path . '/templates';
if (file_exists($templatesDirectory)) {
$this->addPath($templatesDirectory, $name);
}
}
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Template\Loader\ThemeRegistryLoader.
*/
namespace Drupal\Core\Template\Loader;
use Drupal\Core\Theme\Registry;
/**
* Loads templates based on information from the Drupal theme registry.
*
* Allows for template inheritance based on the currently active template.
*/
class ThemeRegistryLoader extends \Twig_Loader_Filesystem {
/**
* The theme registry used to determine which template to use.
*
* @var \Drupal\Core\Theme\Registry
*/
protected $themeRegistry;
/**
* Constructs a new ThemeRegistryLoader object.
*
* @param \Drupal\Core\Theme\Registry $theme_registry
* The theme registry.
*/
public function __construct(Registry $theme_registry) {
$this->themeRegistry = $theme_registry;
}
/**
* Finds the path to the requested template.
*
* @param string $name
* The name of the template to load.
*
* @return string
* The path to the template.
*
* @throws \Twig_Error_Loader
* Thrown if a template matching $name cannot be found.
*/
protected function findTemplate($name) {
// 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;
}
}
throw new \Twig_Error_Loader(sprintf('Unable to find template "%s" in the Drupal theme registry.', $name));
}
}
......@@ -8,8 +8,6 @@
namespace Drupal\Core\Template;
use Drupal\Core\PhpStorage\PhpStorageFactory;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
/**
* A class that defines a Twig environment for Drupal.
......@@ -35,9 +33,13 @@ class TwigEnvironment extends \Twig_Environment {
* internally.
*
* @param string $root
* The app root;
* The app root.
* @param \Twig_LoaderInterface $loader
* The Twig loader or loader chain.
* @param array $options
* The options for the Twig environment.
*/
public function __construct($root, \Twig_LoaderInterface $loader = NULL, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, $options = array()) {
public function __construct($root, \Twig_LoaderInterface $loader = NULL, $options = array()) {
// @todo Pass as arguments from the DIC.
$this->cache_object = \Drupal::cache();
......@@ -45,22 +47,6 @@ public function __construct($root, \Twig_LoaderInterface $loader = NULL, ModuleH
// template because functions like twig_drupal_escape_filter are called.
require_once $root . '/core/themes/engines/twig/twig.engine';
// Set twig path namespace for themes and modules.
$namespaces = array();
foreach ($module_handler->getModuleList() as $name => $extension) {
$namespaces[$name] = $extension->getPath();
}
foreach ($theme_handler->listInfo() as $name => $extension) {
$namespaces[$name] = $extension->getPath();
}
foreach ($namespaces as $name => $path) {
$templatesDirectory = $path . '/templates';
if (file_exists($templatesDirectory)) {
$loader->addPath($templatesDirectory, $name);
}
}
$this->templateClasses = array();
$options += array(
......@@ -72,7 +58,7 @@ public function __construct($root, \Twig_LoaderInterface $loader = NULL, ModuleH
// Ensure autoescaping is always on.
$options['autoescape'] = TRUE;
$this->loader = new \Twig_Loader_Chain([$loader, new \Twig_Loader_String()]);
$this->loader = $loader;
parent::__construct($this->loader, $options);
}
......
<?php
/**
* @file
* Contains \Drupal\system\Tests\Theme\TwigLoaderTest.
*/
namespace Drupal\system\Tests\Theme;
use Drupal\simpletest\WebTestBase;
/**
* Tests adding Twig loaders.
*
* @group Theme
*/
class TwigLoaderTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['twig_loader_test'];
/**
* Tests adding an additional twig loader to the loader chain.
*/
public function testTwigLoaderAddition() {
$environment = \Drupal::service('twig');
$template = $environment->loadTemplate('kittens');
$this->assertEqual($template->render(array()), 'kittens', 'Passing "kittens" to the custom Twig loader returns "kittens".');
$template = $environment->loadTemplate('meow');
$this->assertEqual($template->render(array()), 'cats', 'Passing something other than "kittens" to the custom Twig loader returns "cats".');
}
}
<?php
/**
* @file
* Contains \Drupal\system\Tests\Theme\TwigRegistryLoaderTest.
*/
namespace Drupal\system\Tests\Theme;
use Drupal\simpletest\WebTestBase;
/**
* Tests Twig registry loader.
*
* @group Theme
*/
class TwigRegistryLoaderTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('twig_theme_test', 'block');
/**
* @var \Drupal\Core\Template\TwigEnvironment
*/
protected $twig;
protected function setUp() {
parent::setUp();
\Drupal::service('theme_handler')->install(array('test_theme_twig_registry_loader'));
$this->twig = \Drupal::service('twig');
}
/**
* Checks to see if a value is a Twig template.
*/
public function assertTwigTemplate($value, $message = '', $group = 'Other') {
$this->assertTrue($value instanceof \Twig_Template, $message, $group);
}
/**
* Tests template discovery using the Drupal theme registry.
*/
public function testTemplateDiscovery() {
$this->assertTwigTemplate($this->twig->resolveTemplate('block.html.twig'), 'Found block.html.twig in block module.');
}
/**
* Tests template extension and includes using the Drupal theme registry.
*/
public function testTwigNamespaces() {
// Test the module-provided extend and insert templates.
$this->drupalGet('twig-theme-test/registry-loader');
$this->assertText('This line is from twig_theme_test/templates/twig-registry-loader-test-extend.html.twig');
$this->assertText('This line is from twig_theme_test/templates/twig-registry-loader-test-include.html.twig');
// Enable a theme that overrides the extend and insert templates to ensure
// they are picked up by the registry loader.
$this->config('system.theme')
->set('default', 'test_theme_twig_registry_loader')
->save();
$this->drupalGet('twig-theme-test/registry-loader');
$this->assertText('This line is from test_theme_twig_registry_loader/templates/twig-registry-loader-test-extend.html.twig');
$this->assertText('This line is from test_theme_twig_registry_loader/templates/twig-registry-loader-test-include.html.twig');
}
}
{% extends "@block/block.html.twig" %}
{% extends "block.html.twig" %}
{#
/**
* @file
......
<?php
/**
* @file
* Contains \Drupal\twig_loader_test\Loader\TestLoader.
*/
namespace Drupal\twig_loader_test\Loader;
use Drupal\Core\Template\Loader;
/**
* A test Twig loader.
*/
class TestLoader implements \Twig_LoaderInterface, \Twig_ExistsLoaderInterface {
/**
* {@inheritdoc}
*/
public function getSource($name) {
if ($name == 'kittens') {
return $name;
}
else {
return 'cats';
}
}
/**
* {@inheritdoc}
*/
public function exists($name) {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getCacheKey($name) {
return $name;
}
/**
* {@inheritdoc}
*/
public function isFresh($name, $time) {
return TRUE;
}
}
name: 'Twig Loader Test'
type: module
description: 'Support module for testing adding Twig loaders.'
package: Testing
version: VERSION
core: 8.x
services:
twig_loader_test.twig.test_loader:
class: Drupal\twig_loader_test\Loader\TestLoader
tags:
- { name: twig.loader }
......@@ -58,4 +58,11 @@ public function fileUrlRender() {
);
}
/**
* Menu callback for testing the Twig registry loader.
*/
public function registryLoaderRender() {
return array('#theme' => 'twig_registry_loader_test');
}
}
This line is from twig_theme_test/templates/twig-registry-loader-test-extend.html.twig
{% block content %}
This text is in a block.
{% endblock %}
This line is from twig_theme_test/templates/twig-registry-loader-test-include.html.twig
{% extends "twig-registry-loader-test-extend.html.twig" %}
{% block content %}
{% include "twig-registry-loader-test-include.html.twig" %}
{% endblock %}
......@@ -19,6 +19,15 @@ function twig_theme_test_theme($existing, $type, $theme, $path) {
'variables' => array(),
'template' => 'twig_namespace_test',
);
$items['twig_registry_loader_test'] = array(
'variables' => array(),
);
$items['twig_registry_loader_test_include'] = array(
'variables' => array(),
);
$items['twig_registry_loader_test_extend'] = array(
'variables' => array(),
);
$items['twig_raw_test'] = array(
'variables' => array('script' => ''),
);
......
......@@ -32,3 +32,10 @@ twig_theme_test_file_url:
_controller: '\Drupal\twig_theme_test\TwigThemeTestController::fileUrlRender'
requirements:
_access: 'TRUE'
twig_theme_test_registry_loader:
path: '/twig-theme-test/registry-loader'
defaults:
_controller: '\Drupal\twig_theme_test\TwigThemeTestController::registryLoaderRender'
requirements:
_access: 'TRUE'
This line is from test_theme_twig_registry_loader/templates/twig-registry-loader-test-extend.html.twig
{% block content %}
This text is in a block.
{% endblock %}
This line is from test_theme_twig_registry_loader/templates/twig-registry-loader-test-include.html.twig
name: 'Twig registry loader test'
type: theme
description: 'Support module for Twig registry loader testing.'
version: VERSION
core: 8.x
{% extends "@text/field--text.html.twig" %}
{% extends "field--text.html.twig" %}
{% extends "@text/field--text.html.twig" %}
{% extends "field--text.html.twig" %}
{% extends "@system/field.html.twig" %}
{% extends "field.html.twig" %}
{#
/**
* @file
......
......@@ -41,6 +41,25 @@ public function testProcessNoConsumers() {
$this->assertFalse($container->getDefinition('consumer_id')->hasMethodCall('addHandler'));
}
/**
* Tests a required consumer with no handlers.
*
* @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException
* @expectedExceptionMessage At least one service tagged with 'consumer_id' is required.
* @covers ::process
*/
public function testProcessRequiredHandlers() {
$container = $this->buildContainer();
$container
->register('consumer_id', __NAMESPACE__ . '\ValidConsumer')
->addTag('service_collector', array(
'required' => TRUE,
));
$handler_pass = new TaggedHandlersPass();
$handler_pass->process($container);
}
/**
* Tests consumer with missing interface in non-production environment.
*
......
{% extends "@block/block.html.twig" %}
{% extends "block.html.twig" %}
{#
/**
* @file
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment