Repository: twigphp/Twig

Issue #2045: Add SilentNodeInterface to allow creation of tags similar to 'block' or 'set'
Created: 2016-05-22T19:58:46Z
I'm trying to create a new tag that allows me to put JavaScript (or anything else) in its body that will be collected and then output in one location at the bottom of the generated HTML. This tag should be usable anywhere, even outside of blocks in extended templates. Here's a simplified usage example:
{# base.twig #}
<html>
  <head></head>
  <body>

    {% block content '' %}

    {# collected javascript from various templates #}
    <script>{{ dump_javascript() }}</script>

  </body>
</html>
{# some_page.twig #}
{% extends 'base.twig' %}

{% script %}
    console.log("foo"); // this is JS
{% endscript %}

{% block content %}
    ...
{% endblock %}
Now obviously in this simple example I could just be using block instead of script, however my extension does a bunch of additional logic that the native block doesn't. That logic works fine, the only issue is the expected error: "A template that extends another one cannot have a body" thrown inside Twig_Parser::filterBodyNodes. This method is evidently designed to ensure that no Nodes are able to output anything in inappropriate places. However, as the example above hopefully demonstrates, my script tag is never going to output anything and instead collects its body content for later use. It shares this behaviour with the block and set tags and should thus cause no issues when used outside a block.
Twig_Parser::filterBodyNodes checks for several hardcoded cases, namely Twig_Node_Set, Twig_Node_BlockReference and empty Twig_Node_Text instances. It also works with a Twig_NodeOutputInterface, which is used to indicate that a Node does output something.
In the end my suggestion is quite simple: Add another interface, possibly called Twig_NodeNoOutputInterface or Twig_SilentNodeInterface, that can be used to indicate that a Node will never output anything and can safely be used outside of blocks. Add this interface to the BlockReference and Set nodes and replace the hardcoded checks with one for the new interface.
Unless I missed something this should be quite simple. If I am missing something or there is a better way to achieve what I'm trying to do please let me know.
– Regards and thanks for Twig!
PS (updated): I'm able to work around the current limitation by having my ScriptNode extend Twig_Node_Set and bypassing its constructor. It's hacky but works.

Duplicate issue #2340: [feature request] Twig_NodeCaptureInterface
Created: 2017-01-06T13:59:04Z
There is currently no non-hacky way to have a custom Twig tag which captures its sub-nodes’ output (like set...endset) without Twig throwing an error if the tag pair is ever used in an inherited template outside of block...endblock tags.
Example: In Craft CMS we have css and js tags which can be used as tag pairs just like set, which capture the output and register them to be output elsewhere in the template, combined with other registered CSS and JS code.
Twig already has a Twig_NodeOutputInterface interface, for node classes that are meant to output something, which Twig_Parser::filterBodyNodes() checks for when making sure no tags are outputting anything outside of block tags in inherited templates.
What I propose:


There should be an opposite Twig_NodeCaptureInterface interface, which tells Twig that the tag is going to capture the output of any of its sub-nodes, so there’s no need for filterBodyNodes() to loop through them. The interface should be completely empty, just like Twig_NodeOutputInterface.


Twig_Node_Set should implement this interface.


These lines in filterBodyNodes():
// bypass "set" nodes as they "capture" the output
if ($node instanceof Twig_Node_Set) {
should be replaced with:
// bypass nodes that "capture" the output
if ($node instanceof Twig_NodeCaptureInterface) {


To workaround this in Twig 1, we provided our own custom parser class, which extended Twig_Parser and overrode its filterBodyNodes() method:
protected function filterBodyNodes(Twig_NodeInterface $node)
{
    // Bypass CSS/JS registration nodes as they "capture" the output
    if ($node instanceof RegisterResourceNode) {
        return $node;
    }

    return parent::filterBodyNodes($node);
}
However in Twig 2 filterBodyNodes() is now a private function, so not something we can override like that. Instead we have had to adjust our RegisterResourceNode class so that it actually extends Twig_Node_Set, just so it will pass that $node instanceof Twig_Node_Set condition.
class RegisterResourceNode extends Twig_Node_Set
{
    public function __construct(array $nodes = [], array $attributes = [], $lineno = 0, $tag = null)
    {
        // Bypass Twig_Node_Set::__construct()
        Twig_Node::__construct($nodes, $attributes, $lineno, $tag);
    }

    // ...
}
Obviously this is super hacky and a bad idea. We'd much rather be able to change that to:
class RegisterResourceNode extends Twig_Node implements Twig_NodeCaptureInterface
{
    // ...
}

Closing comment (2017-01-06T14:07:09Z): Duplicate of #2045