Fabien Potencier
Contributed by Fabien Potencier

A CMS often lets each site customize how content fields are displayed. The default theme defines the field wrapper, while a site theme overrides the block used to render an image. The wrapper calls that block, but neither theme extends the other. Inheritance alone cannot make the wrapper see the site's override. BlockChain composes their blocks at runtime. It first shipped in Twig 3.29, so you can use it today.

Using BlockChain

The same pattern works for a data-grid cell: the admin theme provides the wrapper, while the application theme customizes the value:

1
2
3
{# admin_theme.html.twig #}
{% extends 'admin_base.html.twig' %}
{% block cell %}<td class="admin">{{ parent() }}</td>{% endblock %}
1
2
{# admin_base.html.twig #}
{% block cell %}{{ block('cell_value') }}{% endblock %}
1
2
{# application_theme.html.twig #}
{% block cell_value %}<strong>{{ value }}</strong>{% endblock %}
1
2
{# base_theme.html.twig #}
{% block cell_value %}{{ value }}{% endblock %}

Create a chain in order of precedence, then render the block you need:

1
2
3
4
5
6
7
8
9
10
use Twig\BlockChain;

$defaults = new BlockChain($twig, ['base_theme.html.twig']);
$blocks = new BlockChain($twig, [
    'admin_theme.html.twig',
    'application_theme.html.twig',
    $defaults,
]);

echo $blocks->renderBlock('cell', ['value' => 'Pending']);

The result is <td class="admin"><strong>Pending</strong></td>. Each entry can be a template name, a loaded template or another chain. You can reuse $defaults in other theme stacks.

How Block Lookup Works

The first matching block wins: cell comes from the admin theme, while block('cell_value') picks the application override. parent() stays within the defining template's inheritance hierarchy; it does not jump to the next theme. See the BlockChain documentation for the full lookup rules, context handling and other methods.

The same pattern is useful for form controls, CMS fields and email fragments: your renderer picks the block name, and Twig finds the right theme.

How It Helps Open-Source Projects

Symfony Forms: Symfony's form renderer has long combined blocks from unrelated themes: a form view can have its own theme, inherit themes from its parent view and fall back to the default layout. A form_row block from one theme can call block('form_widget') and find an override in another. Before BlockChain, the Twig Bridge had to walk template parents, merge block maps and pass those maps to Twig's internal rendering API.

The Twig Bridge now uses BlockChain when available.

This integration is available in all currently supported Symfony versions. If you upgrade to Twig 3.29, update the Twig Bridge too.

Beyond Symfony: Other renderers implement similar block composition and could benefit from the same Twig API:

  • Ibexa Core selects field-type blocks, such as ibexa_string_field, across local and configured CMS field templates. Its field renderer currently builds the block map itself.
  • Prezent Grid layers grid themes and resolves widget blocks for column types. A chain could compose its themes while the grid renderer keeps its column-type fallback logic.
  • Listing traverses template parents and merges their blocks before rendering listing and column blocks. A chain could take over that traversal.

The need to compose blocks across independent templates is not specific to Symfony, which is why the API landed in Twig.

Published in #Releases #Twig