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.