Концепция наследования
Наследование шаблонов -- ключевая особенность 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>© {{ '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">×</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), шаблон НЕ должен extendsblock('name')-- программный рендер блока по имени