Commit ee2acd68 authored by Dries's avatar Dries

Issue #1696786 by Fabianx, jenlampton, stevector, steveoliver, jwilson3,...

Issue #1696786 by Fabianx, jenlampton, stevector, steveoliver, jwilson3, amateescu, chx: Integrate Twig into core: Implementation issue.
parent d57d4e24
......@@ -5799,6 +5799,9 @@ function drupal_render_children(&$element, $children_keys = NULL) {
* @see hide()
*/
function render(&$element) {
if (!$element && $element !== 0) {
return NULL;
}
if (is_array($element)) {
show($element);
return drupal_render($element);
......@@ -6878,8 +6881,9 @@ function drupal_flush_all_caches() {
drupal_container()->get('router.builder')->rebuild();
menu_router_rebuild();
// Wipe the DIC cache.
// Wipe the PHP Storage caches.
drupal_php_storage('service_container')->deleteAll();
drupal_php_storage('twig')->deleteAll();
// Re-initialize the maintenance theme, if the current request attempted to
// use it. Unlike regular usages of this function, the installer and update
......
......@@ -229,6 +229,12 @@ function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callb
include_once DRUPAL_ROOT . '/' . $theme->owner;
}
}
// Load twig as secondary always available engine.
// @todo Make twig the default engine and remove this. This is required
// because (by design) the theme system doesn't allow modules to register more
// than one type of extension. We need a temporary backwards compatibility
// layer to allow us to perform core-wide .tpl.php to .twig conversion.
include_once DRUPAL_ROOT . '/core/themes/engines/twig/twig.engine';
if (isset($registry_callback)) {
_theme_registry_callback($registry_callback, array($theme, $base_theme, $theme_engine));
......@@ -468,6 +474,28 @@ function _theme_process_registry(&$cache, $name, $type, $theme, $path) {
if (!isset($info['path'])) {
$result[$hook]['template'] = $path . '/templates/' . $info['template'];
}
if ($type == 'module') {
// Add two render engines for modules.
// @todo Remove and make twig the default engine.
$render_engines = array(
'.twig' => 'twig',
'.tpl.php' => 'phptemplate'
);
// Find the best engine for this template.
foreach ($render_engines as $extension => $engine) {
// Render the output using the template file.
$template_file = $result[$hook]['template'] . $extension;
if (isset($info['path'])) {
$template_file = $info['path'] . '/' . $template_file;
}
if (file_exists($template_file)) {
$result[$hook]['template_file'] = $template_file;
$result[$hook]['engine'] = $engine;
break;
}
}
}
}
// Allow variable processors for all theming hooks, whether the hook is
......@@ -1054,6 +1082,11 @@ function theme($hook, $variables = array()) {
$extension = $extension_function();
}
}
elseif (isset($info['engine'])) {
if (function_exists($info['engine'] . '_render_template')) {
$render_function = $info['engine'] . '_render_template';
}
}
}
// In some cases, a template implementation may not have had
......@@ -1077,6 +1110,12 @@ function theme($hook, $variables = array()) {
if (isset($info['path'])) {
$template_file = $info['path'] . '/' . $template_file;
}
// Modules can override this.
if (isset($info['template_file'])) {
$template_file = $info['template_file'];
}
$output = $render_function($template_file, $variables);
}
......@@ -1252,7 +1291,14 @@ function drupal_find_theme_templates($cache, $extension, $path) {
$matches = preg_grep('/^' . $pattern . '/', $patterns);
if ($matches) {
foreach ($matches as $match) {
$file = substr($match, 0, strpos($match, '.'));
$file = $match;
// Chop off the remaining extensions if there are any. $template
// already has the rightmost extension removed, but there might still
// be more, such as with .tpl.php, which still has .tpl in $template
// at this point.
if (($pos = strpos($match, '.')) !== FALSE) {
$file = substr($match, 0, $pos);
}
// Put the underscores back in for the hook name and register this pattern.
$arg_name = isset($info['variables']) ? 'variables' : 'render element';
$implementations[strtr($file, '-', '_')] = array(
......@@ -2915,6 +2961,7 @@ function drupal_common_theme() {
),
'datetime' => array(
'variables' => array('timestamp' => NULL, 'text' => NULL, 'attributes' => array(), 'html' => FALSE),
'template' => 'datetime',
),
'status_messages' => array(
'variables' => array('display' => NULL),
......
......@@ -59,6 +59,9 @@ public function build(ContainerBuilder $container) {
$container->register('user.tempstore', 'Drupal\user\TempStoreFactory')
->addArgument(new Reference('database'))
->addArgument(new Reference('lock'));
$container->register('twig', 'Drupal\Core\Template\TwigEnvironment')
->setFactoryClass('Drupal\Core\Template\TwigFactory')
->setFactoryMethod('get');
// Add the entity query factory.
$container->register('entity.query', 'Drupal\Core\Entity\Query\QueryFactory')
......
<?php
/**
* @file
* Definition of Drupal\Core\Template\TwigEnvironment.
*/
namespace Drupal\Core\Template;
/**
* A class that defines a Twig environment for Drupal.
*
* Instances of this class are used to store the configuration and extensions,
* and are used to load templates from the file system or other locations.
*
* @see core\vendor\twig\twig\lib\Twig\Enviornment.php
*/
class TwigEnvironment extends \Twig_Environment {
protected $cache_object = NULL;
protected $storage = NULL;
/**
* Constructs a TwigEnvironment object and stores cache and storage
* internally.
*/
public function __construct(\Twig_LoaderInterface $loader = NULL, $options = array()) {
// @todo Pass as arguments from the DIC?
$this->cache_object = cache();
$this->storage = drupal_php_storage('twig');
parent::__construct($loader, $options);
}
/**
* Checks if the compiled template needs an update.
*/
public function needsUpdate($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);
}
/**
* Implements Twig_Environment::loadTemplate().
*
* We need to overwrite this function to integrate with drupal_php_storage().
*
* This is a straight copy from loadTemplate() changed to use
* drupal_php_storage().
*/
public function loadTemplate($name, $index = NULL) {
$cls = $this->getTemplateClass($name, $index);
if (isset($this->loadedTemplates[$cls])) {
return $this->loadedTemplates[$cls];
}
if (!class_exists($cls, FALSE)) {
$cache_filename = $this->getCacheFilename($name);
if ($cache_filename === FALSE) {
$source = $this->loader->getSource($name);
$compiled_source = $this->compileSource($source, $name);
eval('?' . '>' . $compiled_source);
} else {
// If autoreload is on, check that the template has not been
// modified since the last compilation.
if ($this->isAutoReload() && $this->needsUpdate($cache_filename, $name)) {
$this->updateCompiledTemplate($cache_filename, $name);
}
if (!$this->storage->load($cache_filename)) {
$this->updateCompiledTemplate($cache_filename, $name);
$this->storage->load($cache_filename);
}
}
}
if (!$this->runtimeInitialized) {
$this->initRuntime();
}
return $this->loadedTemplates[$cls] = new $cls($this);
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Template\TwigFactory.
*
* This provides a factory class to construct Twig_Environment objects and use
* them in combination with the Drupal Injection Container.
*
* @see \Drupal\Core\CoreBundle
*/
namespace Drupal\Core\Template;
/**
* A class for constructing Twig_Environment objects.
*
* This is used for constructing and configuring a system wide Twig_Environment
* object that is integrated with the Drupal Injection Container.
*
* @see \Drupal\Core\CoreBundle
*/
class TwigFactory {
/**
* Returns a fully initialized Twig_Environment object.
*
* This constructs and configures a Twig_Environment. It also adds Drupal
* specific Twig_NodeVisitors, filters and functions.
*
* To retrieve the system wide Twig_Environment object you should use:
* @code
* $twig = drupal_container()->get('twig');
* @endcode
* This will retrieve the Twig_Environment object from the DIC.
*
* @return Twig_Environment
* The fully initialized Twig_Environment object.
*
* @see twig_render
* @see TwigNodeVisitor
* @see TwigReference
* @see TwigReferenceFunction
*/
public static function get() {
// @todo Maybe we will have our own loader later.
$loader = new \Twig_Loader_Filesystem(DRUPAL_ROOT);
$twig = new TwigEnvironment($loader, array(
// This is saved / loaded via drupal_php_storage().
// All files can be refreshed by clearing caches.
// @todo ensure garbage collection of expired files.
'cache' => TRUE,
'base_template_class' => 'Drupal\Core\Template\TwigTemplate',
// @todo Remove in followup issue
// @see http://drupal.org/node/1712444.
'autoescape' => FALSE,
// @todo Remove in followup issue
// @see http://drupal.org/node/1806538.
'strict_variables' => FALSE,
// @todo Maybe make debug mode dependent on "production mode" setting.
'debug' => TRUE,
// @todo Make auto reload mode dependent on "production mode" setting.
'auto_reload' => FALSE,
));
// The node visitor is needed to wrap all variables with
// render -> twig_render() function.
$twig->addNodeVisitor(new TwigNodeVisitor());
$twig->addTokenParser(new TwigFunctionTokenParser('hide'));
$twig->addTokenParser(new TwigFunctionTokenParser('show'));
// @todo Figure out what to do about debugging functions.
// @see http://drupal.org/node/1804998
$twig->addExtension(new \Twig_Extension_Debug());
$reference_functions = array(
'hide' => 'twig_hide',
'render' => 'twig_render',
'show' => 'twig_show',
// @todo re-add unset => twig_unset if this is really needed
);
$filters = array(
't' => 't'
);
// These functions will receive a TwigReference object, if a render array is detected
foreach ($reference_functions as $function => $php_function) {
$twig->addFunction($function, new TwigReferenceFunction($php_function));
}
foreach ($filters as $filter => $php_function) {
$twig->addFilter($filter, new \Twig_Filter_Function($php_function));
}
// @todo Remove URL function once http://drupal.org/node/1778610 is resolved.
$twig->addFunction('url', new \Twig_Function_Function('url'));
return $twig;
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Template\TwigFunctionTokenParser.
*/
namespace Drupal\Core\Template;
/**
* A class that defines the Twig token parser for Drupal.
*
* The token parser converts a token stream created from template source
* code into an Abstract Syntax Tree (AST). The AST will later be compiled
* into PHP code usable for runtime execution of the template.
*
* @see core\vendor\twig\twig\lib\Twig\TokenParser.php
*/
class TwigFunctionTokenParser extends \Twig_TokenParser {
/**
* The name of tag. Can be 'hide' or 'show'.
*
* @var string
*/
protected $tag;
/**
* Constructor for TwigFunctionTokenParser.
*
* Locally scope variables.
*/
public function __construct($tag = 'hide') {
$this->tag = $tag;
}
/**
* Parses a token and returns a node.
*
* @param Twig_Token $token A Twig_Token instance.
*
* @return Twig_Node_Print A Twig_Node_Print instance.
*/
public function parse(\Twig_Token $token) {
$lineno = $token->getLine();
$expr = $this->parser->getExpressionParser()->parseExpression();
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
return new \Twig_Node_Print(new \Twig_Node_Expression_Function($this->tag, new \Twig_Node(array($expr)), $lineno), $lineno);
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag() {
return $this->tag;
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Template\TwigNodeExpressionNameReference
*/
namespace Drupal\Core\Template;
/**
* A class that defines a reference to the name for all nodes that represent
* expressions.
*
* @see core\vendor\twig\twig\lib\Twig\Node\Expression\Name.php
*/
class TwigNodeExpressionNameReference extends \Twig_Node_Expression_Name {
/**
* Overrides Twig_Node_Expression_Name::compile().
*/
public function compile(\Twig_Compiler $compiler) {
$name = $this->getAttribute('name');
$compiler
->raw('$this->getContextReference($context, ')
->string($name)
->raw(')');
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Template\TwigNodeVisitor.
*/
namespace Drupal\Core\Template;
/**
* Provides a Twig_NodeVisitor to change the generated parse-tree.
*
* This is used to ensure that everything that is printed is wrapped via
* twig_render() function so that we can write for example just {{ content }}
* in templates instead of having to write {{ render(content) }}.
*
* @see twig_render
*/
class TwigNodeVisitor implements \Twig_NodeVisitorInterface {
/**
* TRUE when this node is a function getting arguments by reference.
*
* For example: 'hide' or 'render' are such functions.
*
* @var bool
*/
protected $isReference = FALSE;
/**
* Implements Twig_NodeVisitorInterface::enterNode().
*/
function enterNode(\Twig_NodeInterface $node, \Twig_Environment $env) {
if ($node instanceof \Twig_Node_Expression_Function) {
$name = $node->getAttribute('name');
$func = $env->getFunction($name);
// Optimization: Do not support nested functions.
if ($this->isReference && $func instanceof \Twig_Function_Function) {
$this->isReference = FALSE;
}
if ($func instanceof TwigReferenceFunction) {
// We need to create a TwigReference
$this->isReference = TRUE;
}
}
if ($node instanceof \Twig_Node_Print) {
// Our injected render needs arguments passed by reference -- in case of render array
$this->isReference = TRUE;
}
return $node;
}
/**
* Implements Twig_NodeVisitorInterface::leaveNode().
*
* We use this to inject a call to render -> twig_render()
* before anything is printed.
*
* @see twig_render
*/
function leaveNode(\Twig_NodeInterface $node, \Twig_Environment $env) {
if ($node instanceof \Twig_Node_Print) {
$this->isReference = FALSE;
$class = get_class($node);
return new $class(
new \Twig_Node_Expression_Function('render', new \Twig_Node(array($node->getNode('expr'))), $node->getLine()),
$node->getLine()
);
}
if ($this->isReference) {
if ($node instanceof \Twig_Node_Expression_Name) {
$name = $node->getAttribute('name');
return new TwigNodeExpressionNameReference($name, $node->getLine());
}
elseif ($node instanceof \Twig_Function_Function) {
// Do something!
$this->isReference = FALSE;
}
}
return $node;
}
/**
* Implements Twig_NodeVisitorInterface::getPriority().
*/
function getPriority() {
// We want to run before other NodeVisitors like Escape or Optimizer
return -1;
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Template\TwigReference.
*/
namespace Drupal\Core\Template;
/**
* A class used to pass variables by reference while they are used in twig.
*
* This is done by saving a reference to the original render array within a
* TwigReference via the setReference() method like this:
* @code
* $obj = new TwigReference();
* $obj->setReference($variable);
* @endcode
*
* When a TwigReference is accessed via the offsetGet method the resulting
* reference is again wrapped within a TwigReference. Therefore references to
* render arrays within render arrays are also retained.
*
* To unwrap TwigReference objects the reference can be retrieved out of the
* object by calling the getReference() method like this:
* @code
* $variable = &$obj->getReference();
* @endcode
* This allows render(), hide() and show() to access the original variable and
* change it. The process of unwrapping and passing by reference to this
* functions is done transparently by the TwigReferenceFunctions helper class.
*
* @see TwigReferenceFunction
* @see TwigReferenceFunctions
*/
class TwigReference extends \ArrayObject {
/**
* Holds an internal reference to the original array.
*
* @var array
*/
protected $writableRef = array();
/**
* Constructs a \Drupal\Core\Template\TwigReference object.
*
* The argument to the constructor is ignored as it is not safe that this will
* always be a reference.
*
* To set a reference use:
* @code
* $obj = new TwigReference();
* $obj->setReference($variable);
* @endcode
*
* @param $array
* The array parameter is ignored and not passed to the parent
*/
public function __construct($array = NULL) {
parent::__construct();
}
/**
* Sets a reference in the internal storage.
*
* @param $array
* The array to set as internal reference.
*/
public function setReference(&$array) {
$this->exchangeArray($array);
$this->writableRef = &$array;
}
/**
* Gets a reference to the internal storage.
*
* Should be called like:
* @code
* $reference = &$obj->getReference();
* @endcode
*
* @return
* Returns the stored internal reference.
*/
public function &getReference() {
return $this->writableRef;
}
/**
* Sets offset in internal reference and internal storage to value.
*
* This is just for completeness, but should never be used, because
* twig cannot set properties and should not.
*
* @link http://php.net/manual/en/arrayaccess.offsetset.php
* @param mixed $offset
* The offset to assign the value to.
* @param mixed $value
* The value to set.
*/
public function offsetSet($offset, $value) {
$this->writableRef[$offset] = $value;
parent::offsetSet($offset, $value);
}
/**
* Retrieves offset from internal reference.
*
* In case of a render array, it is wrapped again within a TwigReference
* object.
*
* @param mixed $offset
* The offset to retrieve.
*
* @return mixed
* Returns a TwigReference object wrapping the array if the retrieved offset
* is a complex array (i.e. not an attribute). Else it returns the retrived
* offset directly.
*/
public function offsetGet($offset) {
if (!is_array($this->writableRef[$offset]) || $offset[0] == '#') {
return $this->writableRef[$offset];
}
// Wrap the returned array in a new TwigReference.
$x = clone $this; // clone is faster than new
$x->setReference($this->writableRef[$offset]);
return $x;
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Template\TwigReferenceFunction.
*/
namespace Drupal\Core\Template;
/**
* This class is used to create functions requiring references like 'hide'.
*/
class TwigReferenceFunction extends \Twig_Function_Function {
}
<?php
/**
* @file
* Definition of Drupal\Core\Template\TwigReferenceFunctions.
*/
namespace Drupal\Core\Template;
/**
* A helper used to unwrap TwigReference objects transparently.
*
* This is providing a static magic function that makes it easier to unwrap
* TwigReference objects and pass variables by reference to show(), hide() and
* render().
*
* The problem is that twig passes variables only by value. The following is a
* simplified version of the generated code by twig when the property "links" of
* a render array stored in $content should be hidden:
* @code
* $_content_ = $content;
* hide(getAttribute($_content_, 'links'));
* @endcode
* As hide() is operating on a copy of the original array the hidden property
* is not set on the original $content variable.
*
* TwigReferenceFunctions can be used in combination with TwigReference to solve
* this problem:
* @code
* // Internally getContextReference returns the array wrapped in a
* // TwigReference if certain criteria are met
* function getContextReference(&$content) {
* $obj = new Drupal\Core\Template\TwigReference();
* $obj->setReference($content);
* return $obj;
* }
*
* // [...]
* // Simplified, generated twig code
* $_content_ = getContextReference($content);
*
* Drupal\Core\Template\TwigReferenceFunctions::hide(
* getAttribute($_content_, 'links')
* );
* @endcode
* A TwigReference object is passed to the __callStatic function of
* TwigReferenceFunctions. The method unwraps the TwigReference and calls the
* hide() method essentially with a reference to $content['links'].
*
* Therefore the hidden property is correctly set and a successive call to
* render() will not render the content twice.
*
* @see TwigReference
* @see TwigReferenceFunction
* @see TwigFactory
*
*/
class TwigReferenceFunctions {
/**
* Magic function to call functions called from twig templates with a
* reference to the original variable.
*
* This checks if the array provided by value is containing a reference to
* the original version. If yes it replaces the argument with its reference.
*
* @param $name