Includes, Embeds and Components

Four ways to reuse markup - include, embed, macros and Twig Components - and the question that picks between them.

Syntax{{ include('_card.html.twig', {article: article}) }} <twig:Alert message="..." />

Inheritance shares a page shell downwards. Reuse goes sideways: the same card, alert or form row appearing in several unrelated templates. Twig offers four tools, and the choice comes down to what the fragment needs.

include, for a fragment

{{ include('article/_card.html.twig', {article: article}) }} renders another template in place. By default the fragment also sees the caller's variables; only on the tag form, or with_context: false, cuts that off, which makes the fragment's inputs explicit. The tag form {% include 'article/_card.html.twig' %} and the function form above do the same work; the function can be piped, as in {{ include(tpl)|trim }}. Naming partials with a leading underscore is the usual convention.

embed, when the fragment has holes

{% embed %} is include plus inheritance: it renders another template and lets the calling site override that template's blocks. Reach for it for a skeleton such as a panel or a modal, where the frame is fixed and the contents differ.

Macros, for a markup function

A macro is a named, parameterised snippet, defined with {% macro %} and brought in with {% import %} or {% from %}. It is the right shape for a form field or a badge called many times with different arguments.

Twig Components, when logic is involved

A component is a PHP class paired with a template. Install symfony/ux-twig-component, mark the class #[AsTwigComponent], and put its template at templates/components/<Name>.html.twig. Public properties become props set from the calling template; public methods are callable on this. The HTML form <twig:Alert message="..." /> and the function form {{ component('Alert', {message: '...'}) }} are the same render.

Example

Example · php
// src/Twig/Components/Alert.php
declare(strict_types=1);

namespace App\Twig\Components;

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

// The namespace App\Twig\Components\ is mapped to templates/components/
// by config/packages/twig_component.yaml, so this class is paired with
// templates/components/Alert.html.twig.
#[AsTwigComponent]
final class Alert
{
    // Public properties are the props. These two defaults apply when the
    // calling template does not pass them.
    public string $type = 'info';

    public string $message = '';

    // Public methods are callable from the component template as
    // {{ this.cssClass }} - logic that would be awkward in Twig.
    public function cssClass(): string
    {
        return 'alert alert-' . ($this->type === 'danger' ? 'danger' : 'info');
    }
}

When to use it

  • Render the same article card on a list page, a search page and a sidebar.
  • Wrap varying content in a fixed panel or modal frame.
  • Build a reusable alert that derives its CSS classes from a severity prop.

More examples

include and embed side by side

Use include when the fragment is self-contained and embed when the fragment is a frame with holes in it. Adding only is worth the extra typing on a partial used in several places, because it turns an accidental dependency on a caller's variable into an immediate error.

Example · twig
{# templates/article/index.html.twig #}

{# Tag form and function form render the same fragment. #}
{% include 'article/_card.html.twig' with {article: articles|first} %}
{{ include('article/_card.html.twig', {article: articles|first}) }}

{# only: the fragment sees nothing except what is passed here. #}
{% include 'article/_card.html.twig' with {article: articles|first} only %}

{# embed: the panel owns the frame, this template fills its blocks. #}
{% embed 'layout/_panel.html.twig' with {title: 'Latest'} %}
    {% block body %}
        <p>{{ articles|length }} articles published.</p>
    {% endblock %}
{% endembed %}

A macro and the two ways to import it

Macros take default argument values, which keeps the common call short. The important difference from include is isolation: user_email has to be passed in, because the macro cannot reach the template's variables. Use macros for small markup called many times with varying arguments.

Example · twig
{# templates/_macros/form.html.twig #}
{% macro field(name, label, value = '', type = 'text') %}
    <label for="{{ name }}">{{ label }}</label>
    <input id="{{ name }}" type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}

{# templates/account/edit.html.twig #}
{# Whole file under an alias. #}
{% import '_macros/form.html.twig' as form %}
{{ form.field('email', 'Email address', user_email, 'email') }}

{# Or one macro by name. #}
{% from '_macros/form.html.twig' import field %}
{{ field('city', 'City') }}

The component template and both call forms

The HTML form reads better in a page full of markup and is what you will see in recent projects; the function form is useful when the component name or the props come from variables. Choose a component over a macro as soon as the fragment needs PHP to decide anything.

Example · twig
{# templates/components/Alert.html.twig #}
{# Props are plain variables; this is the component object itself. #}
<div class="{{ this.cssClass }}" role="alert">
    {{ message }}
</div>

{# templates/checkout/failed.html.twig #}
{# HTML form. #}
<twig:Alert type="danger" message="Payment failed." />

{# Function form - the same render, written as a call. #}
{{ component('Alert', {type: 'danger', message: 'Payment failed.'}) }}

{# type is omitted, so the class default 'info' applies. #}
<twig:Alert message="Your cart was saved." />

Discussion

  • Be the first to comment on this lesson.