MidТеория5 min

Наследование шаблонов

Наследование шаблонов Twig: extends, block, parent(), include, embed, use

Концепция наследования

Наследование шаблонов -- ключевая особенность Twig. Позволяет создать базовый layout и переопределять отдельные блоки в дочерних шаблонах. Это аналог наследования классов в ООП.

base.html.twig          -- Base layout (skeleton)
  └── layout.html.twig  -- Section layout (sidebar, breadcrumbs)
       └── page.html.twig -- Specific page content

extends и block

Базовый шаблон

{# templates/base.html.twig #}
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}My Application{% endblock %}</title>
    <meta name="description" content="{% block meta_description %}{% endblock %}">

    {% block stylesheets %}
        <link rel="stylesheet" href="{{ asset('css/app.css') }}">
    {% endblock %}
</head>
<body>
    {% block header %}
        <header>
            <nav>{% block navigation %}{% endblock %}</nav>
        </header>
    {% endblock %}

    <main>
        {% block body %}{% endblock %}
    </main>

    {% block footer %}
        <footer>&copy; {{ 'now'|date('Y') }} My App</footer>
    {% endblock %}

    {% block javascripts %}
        <script src="{{ asset('js/app.js') }}"></script>
    {% endblock %}
</body>
</html>

Дочерний шаблон

{# templates/product/show.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}{{ product.name }} -- {{ parent() }}{% endblock %}

{% block meta_description %}{{ product.description|striptags|truncate(160) }}{% endblock %}

{% block body %}
    <h1>{{ product.name }}</h1>
    <p>{{ product.description }}</p>
    <span class="price">{{ product.price|number_format(2) }} EUR</span>
{% endblock %}

Подвох экзамена: {% extends %} ДОЛЖЕН быть первым тегом в дочернем шаблоне. Исключение -- {% set %}. Любой контент вне блоков в дочернем шаблоне будет проигнорирован.

parent()

Функция parent() возвращает содержимое одноимённого блока из родительского шаблона. Позволяет дополнить, а не заменить, содержимое блока.

{# templates/admin/base.html.twig #}
{% extends 'base.html.twig' %}

{% block stylesheets %}
    {# Include parent stylesheets FIRST #}
    {{ parent() }}

    {# Then add admin-specific styles #}
    <link rel="stylesheet" href="{{ asset('css/admin.css') }}">
{% endblock %}

{% block javascripts %}
    {{ parent() }}
    <script src="{{ asset('js/admin.js') }}"></script>
{% endblock %}

{% block body %}
    <div class="admin-wrapper">
        <aside>{% block sidebar %}{% endblock %}</aside>
        <section>{% block content %}{% endblock %}</section>
    </div>
{% endblock %}
{# templates/admin/users/list.html.twig #}
{% extends 'admin/base.html.twig' %}

{% block title %}Users -- Admin{% endblock %}

{% block sidebar %}
    <ul>
        <li><a href="{{ path('admin_users') }}">Users</a></li>
        <li><a href="{{ path('admin_products') }}">Products</a></li>
    </ul>
{% endblock %}

{% block content %}
    <h1>User Management</h1>
    <table>
        {% for user in users %}
            <tr>
                <td>{{ user.email }}</td>
                <td>{{ user.name }}</td>
            </tr>
        {% endfor %}
    </table>
{% endblock %}

Подвох экзамена: parent() можно вызвать только ВНУТРИ блока, который переопределяет родительский. Вызов parent() вне блока или в блоке, который не существует в родителе, вызовет ошибку.

Динамическое наследование

{# Layout is determined at runtime #}
{% extends ajax ? 'base_ajax.html.twig' : 'base.html.twig' %}

{% block body %}
    <h1>Dynamic Layout</h1>
{% endblock %}
<?php

declare(strict_types=1);

// Controller passes the flag
return $this->render('page.html.twig', [
    'ajax' => $request->isXmlHttpRequest(),
]);

include

Тег include встраивает другой шаблон внутрь текущего. Это не наследование, а композиция -- вставка фрагмента.

{# Basic include #}
{% include 'components/_alert.html.twig' %}

{# Include with extra variables #}
{% include 'components/_card.html.twig' with {
    title: product.name,
    description: product.shortDescription,
    image: product.thumbnail
} %}

{# Include with ONLY specified variables (sandbox mode) #}
{% include 'components/_card.html.twig' with {
    title: product.name
} only %}

Подвох экзамена: По умолчанию включённый шаблон имеет доступ ко ВСЕМ переменным текущего контекста. Ключевое слово only изолирует контекст -- шаблон получит только явно переданные переменные. Это лучшая практика для переиспользуемых компонентов.

Условный include

{# Ignore if template does not exist #}
{% include 'components/_sidebar.html.twig' ignore missing %}

{# Fallback templates #}
{% include [
    'product/' ~ product.type ~ '.html.twig',
    'product/default.html.twig'
] %}

Пример компонента

{# templates/components/_alert.html.twig #}
<div class="alert alert-{{ type|default('info') }}" role="alert">
    {% if dismissible|default(false) %}
        <button type="button" class="close">&times;</button>
    {% endif %}
    {{ message }}
</div>

{# Usage #}
{% include 'components/_alert.html.twig' with {
    type: 'danger',
    message: 'Something went wrong!',
    dismissible: true
} only %}

embed

embed -- комбинация include и extends. Позволяет включить шаблон и переопределить его блоки. Это мощнейший инструмент для создания переиспользуемых компонентов.

{# templates/components/_card.html.twig #}
<div class="card {{ class|default('') }}">
    {% block card_header %}
        <div class="card-header">
            <h3>{% block card_title %}{{ title|default('') }}{% endblock %}</h3>
        </div>
    {% endblock %}

    <div class="card-body">
        {% block card_body %}{% endblock %}
    </div>

    {% block card_footer %}{% endblock %}
</div>
{# Usage with embed -- override blocks! #}
{% embed 'components/_card.html.twig' with {title: 'User Profile'} %}
    {% block card_body %}
        <p>Name: {{ user.name }}</p>
        <p>Email: {{ user.email }}</p>
    {% endblock %}

    {% block card_footer %}
        <div class="card-footer">
            <a href="{{ path('user_edit', {id: user.id}) }}">Edit Profile</a>
        </div>
    {% endblock %}
{% endembed %}

Разница между include и embed

Возможность include embed
Вставка шаблона Да Да
Передача переменных Да Да
only для изоляции Да Да
Переопределение блоков Нет Да
Наследование шаблонов Нет Да (embed файл может extends)

Подвох экзамена: embed создаёт новую область видимости для блоков. Переменные из внешнего контекста доступны внутри embed (если не указано only), но блоки embed изолированы от блоков текущего шаблона.

embed с наследованием

{# templates/components/_modal.html.twig #}
{# This template itself can extend another template #}
<div class="modal" id="{{ id|default('modal') }}">
    <div class="modal-dialog">
        <div class="modal-content">
            {% block modal_header %}
                <div class="modal-header">
                    <h5>{% block modal_title %}Modal{% endblock %}</h5>
                </div>
            {% endblock %}
            <div class="modal-body">
                {% block modal_body %}{% endblock %}
            </div>
            {% block modal_footer %}
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary">Close</button>
                </div>
            {% endblock %}
        </div>
    </div>
</div>

{# Usage: embed AND override #}
{% embed 'components/_modal.html.twig' with {id: 'confirm-delete'} %}
    {% block modal_title %}Confirm Deletion{% endblock %}

    {% block modal_body %}
        <p>Are you sure you want to delete "{{ item.name }}"?</p>
    {% endblock %}

    {% block modal_footer %}
        <div class="modal-footer">
            <button type="button" class="btn btn-secondary">Cancel</button>
            <button type="button" class="btn btn-danger">Delete</button>
        </div>
    {% endblock %}
{% endembed %}

use

Тег use импортирует блоки из другого шаблона (горизонтальное переиспользование). Это аналог trait в PHP. Шаблон, из которого импортируются блоки, не должен extends другой шаблон.

{# templates/blocks/_forms.html.twig #}
{# This template MUST NOT extend any other template #}

{% block form_errors %}
    {% if errors|length > 0 %}
        <ul class="errors">
            {% for error in errors %}
                <li>{{ error.message }}</li>
            {% endfor %}
        </ul>
    {% endif %}
{% endblock %}

{% block form_help %}
    {% if help is defined and help %}
        <small class="form-text text-muted">{{ help }}</small>
    {% endif %}
{% endblock %}
{# templates/product/form.html.twig #}
{% extends 'base.html.twig' %}

{# Import blocks from another template (like PHP traits) #}
{% use 'blocks/_forms.html.twig' %}

{# You can rename imported blocks to avoid conflicts #}
{% use 'blocks/_pagination.html.twig' with pagination as page_pagination %}

{% block body %}
    {{ block('form_errors') }}
    {# Render the imported block #}
{% endblock %}

Подвох экзамена: use -- это горизонтальное переиспользование (trait). Шаблон в use НЕ должен наследовать другой шаблон (extends). use не рендерит шаблон, а только импортирует определённые в нём блоки.

Переименование импортированных блоков

{# Rename to avoid name conflicts #}
{% use 'blocks/_sidebar.html.twig' with
    sidebar as base_sidebar,
    sidebar_menu as base_sidebar_menu
%}

{% block sidebar %}
    {# Override but still access the original #}
    {{ block('base_sidebar') }}
    <div class="custom-addition">Extra content</div>
{% endblock %}

Вложенные блоки и block()

{# Render a block by name (useful inside loops) #}
{{ block('sidebar') }}

{# Check if a block is defined #}
{% if block('optional_sidebar') is defined %}
    {{ block('optional_sidebar') }}
{% endif %}

{# Nested blocks #}
{% block body %}
    <div class="container">
        {% block content %}
            <div class="row">
                {% block main_column %}{% endblock %}
                {% block side_column %}{% endblock %}
            </div>
        {% endblock %}
    </div>
{% endblock %}

Shortcut block syntax

{# Long form #}
{% block title %}
    Page Title
{% endblock title %}

{# Short form -- end tag can have block name for readability #}
{% block title %}Page Title{% endblock title %}

{# The name in endblock is optional but improves readability #}
{% block body %}
    ...content...
{% endblock body %}

Итоги

  • extends -- вертикальное наследование (одна цепочка), должен быть первым тегом
  • block -- определяет переопределяемую область, parent() -- вызывает родительский блок
  • include -- вставка шаблона (композиция), only для изоляции контекста
  • embed -- include + возможность переопределять блоки (самый мощный)
  • use -- горизонтальный импорт блоков (аналог trait), шаблон НЕ должен extends
  • block('name') -- программный рендер блока по имени

Проверь себя

Что произойдёт, если использовать `{% include 'partial.html.twig' %}` без ключевого слова `only`?

Чем `embed` отличается от `include`?

Где можно вызывать функцию `parent()` в Twig?

Какое ограничение имеет тег `use` в Twig?

Какой тег ОБЯЗАТЕЛЬНО должен быть первым в дочернем шаблоне Twig?