Commit 7eda0ecb authored by catch's avatar catch

Issue #2114563 by joelpittet, martin107, Cottser: Remove TwigReference with...

Issue #2114563 by joelpittet, martin107, Cottser: Remove TwigReference with the help of 'without' filter replacing 'show'/'hide' functions.
parent d8c718b2
......@@ -125,7 +125,6 @@ public static function registerTwig(ContainerBuilder $container) {
// When in the installer, twig_cache must be FALSE until we know the
// files folder is writable.
'cache' => drupal_installation_attempted() ? FALSE : settings()->get('twig_cache', TRUE),
'base_template_class' => 'Drupal\Core\Template\TwigTemplate',
// @todo Remove in followup issue
// @see http://drupal.org/node/1712444.
'autoescape' => FALSE,
......
......@@ -21,25 +21,24 @@ public function getFunctions() {
// @todo re-add unset => twig_unset if this is really needed
return array(
// @todo Remove URL function once http://drupal.org/node/1778610 is resolved.
'url' => new \Twig_Function_Function('url'),
// These functions will receive a TwigReference object, if a render array is detected
'hide' => new TwigReferenceFunction('twig_hide'),
'render_var' => new TwigReferenceFunction('twig_render_var'),
'show' => new TwigReferenceFunction('twig_show'),
new \Twig_SimpleFunction('url', 'url'),
// This function will receive a renderable array, if an array is detected.
new \Twig_SimpleFunction('render_var', 'twig_render_var'),
);
}
public function getFilters() {
return array(
't' => new \Twig_Filter_Function('t'),
'trans' => new \Twig_Filter_Function('t'),
new \Twig_SimpleFilter('t', 't'),
new \Twig_SimpleFilter('trans', 't'),
// The "raw" filter is not detectable when parsing "trans" tags. To detect
// which prefix must be used for translation (@, !, %), we must clone the
// "raw" filter and give it identifiable names. These filters should only
// be used in "trans" tags.
// @see TwigNodeTrans::compileString()
'passthrough' => new \Twig_Filter_Function('twig_raw_filter'),
'placeholder' => new \Twig_Filter_Function('twig_raw_filter'),
new \Twig_SimpleFilter('passthrough', 'twig_raw_filter'),
new \Twig_SimpleFilter('placeholder', 'twig_raw_filter'),
new \Twig_SimpleFilter('without', 'twig_without'),
);
}
......@@ -53,8 +52,6 @@ public function getNodeVisitors() {
public function getTokenParsers() {
return array(
new TwigFunctionTokenParser('hide'),
new TwigFunctionTokenParser('show'),
new TwigTransTokenParser(),
);
}
......
<?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(')');
}
}
......@@ -19,36 +19,9 @@
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().
* {@inheritdoc}
*/
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_var needs arguments passed by reference -- in case of render array
$this->isReference = TRUE;
}
return $node;
}
......@@ -62,26 +35,14 @@ function enterNode(\Twig_NodeInterface $node, \Twig_Environment $env) {
*/
function leaveNode(\Twig_NodeInterface $node, \Twig_Environment $env) {
if ($node instanceof \Twig_Node_Print) {
$this->isReference = FALSE;
$class = get_class($node);
$line = $node->getLine();
return new $class(
new \Twig_Node_Expression_Function('render_var', new \Twig_Node(array($node->getNode('expr'))), $node->getLine()),
$node->getLine()
new \Twig_Node_Expression_Function('render_var', new \Twig_Node(array($node->getNode('expr'))), $line),
$line
);
}
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;
}
......
<?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
* The name of the function to call.
* @param $arguments
* The arguments to process and pass to the called function.
*
* @return mixed
* Returns the output of the called function.
*
* @see TwigReference
*/
public static function __callStatic($name, $arguments) {
foreach ($arguments as $key => $val) {
if (is_object($val) && $val instanceof TwigReference) {
$arguments[$key] = &$val->getReference();
}
}
// Needed to pass by reference -- could also restrict to maximum one
// argument instead
$args = array();
foreach ($arguments as $key => &$arg) {
$args[$key] = &$arg;
}
return call_user_func_array($name, $args);
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Template\TwigTemplate.
*/
namespace Drupal\Core\Template;
/**
* This is the base class for compiled Twig templates.
*/
abstract class TwigTemplate extends \Twig_Template {
/**
* A class used to pass variables by reference while they are used in Twig.
*/
protected $twig_reference = NULL;
/**
* List of the name of variables to be passed around as references.
*
* @var array
*/
protected $is_reference = array();
/**
* List of the name of variables to be passed around by value.
*
* @var array
*/
protected $is_no_reference = array();
/**
* @param array $context
* The variables available to the template.
* @param $item
* The name of the variable.
* @return mixed
* The requested variable.
*/
protected function getContextReference(&$context, $item) {
// Optimized version. NULL is a valid value for $context[$item], we only
// want to error if it hasn't been defined at all.
if (!isset($context[$item]) && !array_key_exists($item, $context)) {
// We don't want to throw an exception, but issue a warning instead.
// This is the easiest way to do so.
// @todo Decide based on prod vs. dev setting
$msg = new \Twig_Error(t('@item could not be found in _context', array('@item' => $item)));
trigger_error($msg->getMessage(), E_USER_WARNING);
return NULL;
}
// Return item instead of its reference inside a loop.
// @todo 'hide' and 'show' are not supported inside a loop for now.
// This should be a non-issue as soon as this lands:
// @see http://drupal.org/node/1922304
if (isset($context['_seq'])) {
return $context[$item];
}
// The first test also finds empty / null render arrays
if (!$context[$item] || isset($this->is_no_reference[$item])) {
return $context[$item];
}
if (isset($context['_references'][$item])) {
return $context['_references'][$item];
}
// @todo Check if this is a render array (existence of #theme?)
if ((!isset($this->is_reference[$item])) && ($context[$item] instanceof \TwigMarkup || !is_array($context[$item]))) {
$this->is_no_reference[$item] = TRUE;
return $context[$item];
}
if ($this->twig_reference == NULL) {
$this->twig_reference = new TwigReference();
}
$ref = clone $this->twig_reference; // clone is _much_ faster than new
$ref->setReference($context[$item]);
// Save that this is a reference
$context['_references'][$item] = $ref;
$this->is_reference[$item] = TRUE;
return $ref;
}
}
......@@ -13,10 +13,9 @@
* @ingroup themeable
*/
#}
{% hide(form.place_blocks) %}
<div class="layout-block-list clearfix">
<div class="layout-region block-list-primary">
{{ form }}
{{ form|without('place_blocks') }}
</div>
<div class="layout-region block-list-secondary">
{{ form.place_blocks }}
......
......@@ -7,8 +7,11 @@
* - author: Comment author. Can be a link or plain text.
* - content: The content-related items for the comment display. Use
* {{ content }} to print them all, or print a subset such as
* {{ content.field_example }}. Use hide(content.field_example) to temporarily
* suppress the printing of a given element.
* {{ content.field_example }}. Use the following code to temporarily suppress
* the printing of a given child element:
* @code
* {{ content|without('field_example') }}
* @endcode
* - created: Formatted date and time for when the comment was created.
* Preprocess functions can reformat it by calling format_date() with the
* desired parameters on the 'comment.created' variable.
......@@ -92,9 +95,7 @@
</footer>
<div{{ content_attributes }}>
{# We hide the links now so that we can render them later. #}
{% hide(content.links) %}
{{ content }}
{{ content|without('links') }}
{% if signature %}
<div class="user-signature">
......
......@@ -15,11 +15,9 @@
* @ingroup themeable
*/
#}
{% hide(form.advanced) %}
{% hide(form.actions) %}
<div class="layout-node-form clearfix">
<div class="layout-region layout-region-node-main">
{{ form }}
{{ form|without('advanced', 'actions') }}
</div>
<div class="layout-region layout-region-node-secondary">
{{ form.advanced }}
......
......@@ -18,8 +18,8 @@
* - label: The title of the node.
* - content: All node items. Use {{ content }} to print them all,
* or print a subset such as {{ content.field_example }}. Use
* {% hide(content.field_example) %} to temporarily suppress the printing
* of a given element.
* {{ content|without('field_example') %} to temporarily suppress the printing
* of a given child element.
* - user_picture: The node author's picture from user-picture.html.twig.
* - date: Formatted creation date. Preprocess functions can reformat it by
* calling format_date() with the desired parameters on
......@@ -93,9 +93,7 @@
{% endif %}
<div{{ content_attributes }}>
{# We hide links now so that we can render them later. #}
{% hide(content.links) %}
{{ content }}
{{ content|without('links') }}
</div>
{{ content.links }}
......
<?php
/**
* @file
* Contains \Drupal\system\Tests\Theme\TwigFilterTest.
*/
namespace Drupal\system\Tests\Theme;
use Drupal\simpletest\WebTestBase;
/**
* Tests Drupal's Twig filters.
*/
class TwigFilterTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array(
'twig_theme_test',
);
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => 'Twig Filters',
'description' => 'Test Drupal\'s Twig filters.',
'group' => 'Theme',
);
}
/**
* Test Twig "without" filter.
*/
public function testTwigWithoutFilter() {
$this->drupalGet('/twig-theme-test/filter');
$elements = array(
array(
'expected' => '<div><strong>No author:</strong> You can only find truth with logic if you have already found truth without it.1874-1936.</div>',
'message' => '"No author" was successfully rendered.',
),
array(
'expected' => '<div><strong>Complete quote after without:</strong> You can only find truth with logic if you have already found truth without it.Gilbert Keith Chesterton1874-1936.</div>',
'message' => '"Complete quote after without" was successfully rendered.',
),
array(
'expected' => '<div><strong>Only author:</strong> Gilbert Keith Chesterton.</div>',
'message' => '"Only author:" was successfully rendered.',
),
array(
'expected' => '<div><strong>No author or date:</strong> You can only find truth with logic if you have already found truth without it..</div>',
'message' => '"No author or date" was successfully rendered.',
),
array(
'expected' => '<div><strong>Only date:</strong> 1874-1936.</div>',
'message' => '"Only date" was successfully rendered.',
),
array(
'expected' => '<div><strong>Complete quote again for good measure:</strong> You can only find truth with logic if you have already found truth without it.Gilbert Keith Chesterton1874-1936.</div>',
'message' => '"Complete quote again for good measure" was successfully rendered.',
),
array(
'expected' => '<div><strong>Marked-up:</strong>
<blockquote>
<p>You can only find truth with logic if you have already found truth without it.</p>
<footer>
&ndash; <cite><a href="#">Gilbert Keith Chesterton</a> <em>(1874-1936)</em></cite>
</footer>