I want to share my solution for this problem. I implemented my own Twig extension, which implements a custom widget tag (I used the Twig embed tag as the source).
Extension
WidgetNode.php:
namespace Artprima\Bundle\TwigWidgetTagBundle\Twig\Extension; class WidgetNode extends \Twig_Node_Include {
WidgetTokenParser.php:
namespace Artprima\Bundle\TwigWidgetTagBundle\Twig\Extension; /** * Class WidgetTokenParser * * @author Denis V * * @package Artprima\Bundle\TwigWidgetTagBundle\Twig\Extension */ class WidgetTokenParser extends \Twig_TokenParser_Include { /** * Parses a token and returns a node. * * @param \Twig_Token $token A Twig_Token instance * * @return \Twig_NodeInterface A Twig_NodeInterface instance */ public function parse(\Twig_Token $token) { $stream = $this->parser->getStream(); $parent = $this->parser->getExpressionParser()->parseExpression(); list($variables, $only, $ignoreMissing) = $this->parseArguments(); // inject a fake parent to make the parent() function work $stream->injectTokens(array( new \Twig_Token(\Twig_Token::BLOCK_START_TYPE, '', $token->getLine()), new \Twig_Token(\Twig_Token::NAME_TYPE, 'extends', $token->getLine()), new \Twig_Token(\Twig_Token::STRING_TYPE, '__parent__', $token->getLine()), new \Twig_Token(\Twig_Token::BLOCK_END_TYPE, '', $token->getLine()), )); $module = $this->parser->parse($stream, array($this, 'decideBlockEnd'), true); // override the parent with the correct one $module->setNode('parent', $parent); $this->parser->embedTemplate($module); $stream->expect(\Twig_Token::BLOCK_END_TYPE); return new WidgetNode($module->getAttribute('filename'), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); } public function decideBlockEnd(\Twig_Token $token) { return $token->test('endwidget'); } /** * Gets the tag name associated with this token parser. * * @return string The tag name */ public function getTag() { return 'widget'; } }
TemplateTagsExtension.php:
namespace Artprima\Bundle\TwigWidgetTagBundle\Twig\Extension; class TemplateTagsExtension extends \Twig_Extension { public function getTokenParsers() { return array( new WidgetTokenParser(), ); } public function getName() { return 'template_tags'; } }
services.yml:
parameters: artprima.twig.extension.template_tags.class: Artprima\Bundle\TwigWidgetTagBundle\Twig\Extension\TemplateTagsExtension services: artprima.twig.extension.template_tags: class: %artprima.twig.extension.template_tags.class% tags: - { name: twig.extension }
Usage example
Views / Blocks / widget.html.twig:
{# please note, that only "widget" block is rendered, all other blocks can be used inside the "widget" block #} {# if you don't define the "widget" block, nothing will be rendered #} {% block widget %} <div class="{{ block('widget_box_class') }}"> {{ block('widget_header') }} {{ block('widget_body') }} </div> {% endblock %} {% block widget_header %} <div class="{{ block('widget_header_class') }}"> {{ block('widget_title') }} {% if display_toolbar is defined and display_toolbar %}{{ block('widget_toolbar') }}{% endif %} </div> {% endblock %} {% block widget_body %} <div class="{{ block('widget_main_class') }}"> {{ block('widget_main') }} </div> {% endblock %} {% block widget_title %} <h5 class="widget-title">{{ block('widget_title_text') }}</h5> {% endblock %} {% block widget_title_text %}(undefined){% endblock %} {% block widget_toolbar %} <div class="widget-toolbar"> {{ block('widget_toolbar_inner') }} </div> {% endblock %} {% block widget_toolbar_inner %}{% endblock %} {% block widget_box_class %}{% spaceless %}widget-box{% endspaceless %}{% endblock %} {% block widget_main_class %}{% spaceless %}widget-main{% endspaceless %}{% endblock %} {% block widget_main %}{% endblock %} {% block widget_header_class %}{% spaceless %}widget-header{% endspaceless %}{% endblock %}
Views / Control Panel / Widgets / sample.html.twig
{% widget "ArtprimaSampleBundle:Blocks:widgets.html.twig" %} {% block widget_title_text %}{{ "widget.records_creation_history"|trans }}{% endblock %} {% block widget_main_class %}{% spaceless %}no-padding {{ parent() }}{% endspaceless %}{% endblock %} {% block widget_main %} <table class="table table-striped table-bordered table-hover no-margin-bottom"> <thead> <tr> <th>Description</th> <th>Amount</th> </tr> </thead> <tbody> <tr> <td>{{ "widget.number_of_countries.created"|trans }}</td> <td>{{ dashboard.countries.created }}</td> </tr> <tr> <td>{{ "widget.number_of_users.created"|trans }}</td> <td>{{ dashboard.users.created }}</td> </tr> </tbody> </table> {% endblock %} {% endwidget %}
Summary
So, as you can see, with my extension you can include a template for reusing blocks inside it. If you need multiple widgets, you can have multiple widget tags in your template using the same source template, and the contents of the block will not overlap. In fact, it works like embedding a template using Twig embed (and I used this tag as a source for my extension), but with the only (and big) difference - it displays ONLY a block called a "widget". All other blocks are ignored, but can be used inside the widget block.