🎯 Цель урока
Освоить продвинутые возможности Twig: систему наследования шаблонов, работу с блоками, использование инклюдов для компонентов, передачу сложных данных из контроллеров и создание чистых, структурированных шаблонов для нашего блога.
📚 Теоретическая часть (40 минут)
🏗️ Наследование шаблонов в Twig
Наследование — одна из самых мощных возможностей Twig, позволяющая создавать иерархию шаблонов и избегать дублирования кода.
🔍 Как работает наследование:
- Базовый шаблон определяет общую структуру (скелет) страницы
- Блоки (blocks) — это "дырки" в базовом шаблоне, которые могут быть заполнены в дочерних
- Дочерний шаблон расширяет базовый и переопределяет нужные блоки
- Twig собирает финальную страницу из всех частей
| Подход | Пример | Преимущества | Недостатки |
|---|---|---|---|
| Копирование кода | header/footer в каждом файле | Просто понять | Дублирование, сложно обновлять |
| PHP include | <?php include 'header.php'; ?> |
Переиспользование кода | Нет наследования, сложная вложенность |
| Twig наследование | {% extends 'base.html.twig' %} |
Чистая иерархия, переопределение блоков, parent() | Требует понимания концепции |
💡 Реальная аналогия: Представьте строительство дома. Базовый шаблон — это фундамент и стены. Блоки — это окна и двери (их можно менять). Дочерний шаблон — это внутренняя отделка конкретной комнаты.
🧩 Блоки и их возможности
Блоки — это места в шаблоне, которые можно переопределить в дочерних шаблонах.
📝 Синтаксис блоков:
{# Базовый шаблон (base.html.twig) #}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Сайт{% endblock %}</title>
</head>
<body>
{% block header %}{% endblock %}
<main>
{% block content %}
<p>Контент по умолчанию</p>
{% endblock %}
</main>
{% block footer %}{% endblock %}
{% block scripts %}
<script src="/js/common.js"></script>
{% endblock %}
</body>
</html>
{# Дочерний шаблон (page.html.twig) #}
{% extends 'base.html.twig' %}
{% block title %}Специальная страница{% endblock %}
{% block content %}
<h1>Мой контент</h1>
{{ parent() }} {# Вызов содержимого блока из родителя #}
{% endblock %}
{% block scripts %}
{{ parent() }} {# Подключаем скрипты из родителя #}
<script src="/js/page.js"></script> {# Добавляем свои #}
{% endblock %}
✨ Особенности блоков:
- Наследование по умолчанию — если блок не переопределен, используется версия из родителя
parent()— вызов содержимого блока из родительского шаблона- Вложенные блоки — блоки могут содержать другие блоки
- Блоки без содержимого — можно оставить пустыми:
{% block sidebar %}{% endblock %}
🧱 Инклюды (включаемые компоненты)
Инклюды позволяют разбивать шаблоны на переиспользуемые компоненты.
📝 Использование инклюдов:
{# Простой инклюд #}
{{ include('components/header.html.twig') }}
{# Инклюд с передачей переменных #}
{{ include('components/post_card.html.twig', {
'post': post,
'show_excerpt': true
}) }}
{# Инклюд с условием #}
{% if user.is_admin %}
{{ include('admin/panel.html.twig') }}
{% endif %}
{# Инклюд с обработкой отсутствия файла #}
{{ include('components/sidebar.html.twig', ignore_missing = true) }}
📁 Когда использовать инклюды:
- Повторяющиеся компоненты — кнопки, карточки, формы
- Части страницы — header, footer, sidebar, модальные окна
- Условные блоки — рекламные баннеры, уведомления
- Комплексные виджеты — галереи, слайдеры, формы комментариев
⚡ Производительность: Twig кэширует инклюды, поэтому их использование не снижает производительность. Наоборот, разделение на компоненты упрощает поддержку.
📊 Работа с данными в Twig
Twig предоставляет богатые возможности для работы с данными, полученными из контроллеров.
📝 Примеры работы с данными:
{# Работа с массивами #}
{{ post.title }} {# Доступ к элементу массива #}
{{ post.author.name }} {# Вложенный доступ #}
{{ posts|length }} {# Количество элементов #}
{{ posts[0].title }} {# Доступ по индексу #}
{# Работа с объектами #}
{{ user.getName() }} {# Вызов метода #}
{{ user.createdAt|date }} {# Метод + фильтр #}
{# Проверки существования #}
{{ post.image ?? 'default.jpg' }} {# Оператор null coalescing #}
{{ post.description|default('Нет описания') }} {# Фильтр default #}
{{ post.tags is defined ? post.tags : [] }} {# Тернарный оператор #}
🔄 Циклы и условия в Twig:
{# Расширенный цикл for #}
{% for post in posts %}
{{ loop.index }} {# Текущий индекс (начинается с 1) #}
{{ loop.index0 }} {# Текущий индекс (начинается с 0) #}
{{ loop.first }} {# true для первой итерации #}
{{ loop.last }} {# true для последней итерации #}
{{ loop.length }} {# Общее количество элементов #}
{{ loop.revindex }} {# Обратный индекс #}
{# Пропуск итераций #}
{% if loop.index is odd %}
{# Нечетные элементы #}
{% endif %}
{% else %}
<p>Элементов нет</p> {# Выполняется если массив пустой #}
{% endfor %}
{# Вложенные циклы #}
{% for category in categories %}
<h3>{{ category.name }}</h3>
{% for post in category.posts %}
{{ post.title }}
{% endfor %}
{% endfor %}
{# Сложные условия #}
{% if user.isActive and user.hasPosts %}
Пользователь активен и имеет статьи
{% elseif user.isActive or user.isAdmin %}
Пользователь активен или является админом
{% else %}
Неактивный пользователь
{% endif %}
💻 Практическая часть (2 часа)
✅ Шаг 1: Создаем улучшенную структуру папок
Переорганизуем папку с шаблонами для лучшей структуризации:
📁 Новая структура папок шаблонов:
app/views/twig/
├── layout/ # Базовые шаблоны
│ ├── base.html.twig # Основной базовый шаблон
│ └── admin.html.twig # Базовый шаблон для админки
├── components/ # Переиспользуемые компоненты
│ ├── header.html.twig # Шапка сайта
│ ├── footer.html.twig # Подвал сайта
│ ├── sidebar.html.twig # Боковая панель
│ ├── post_card.html.twig # Карточка статьи
│ └── pagination.html.twig # Пагинация
├── pages/ # Шаблоны страниц
│ ├── home.html.twig # Главная страница
│ ├── post/ # Страницы статей
│ │ ├── show.html.twig # Просмотр статьи
│ │ └── list.html.twig # Список статей
│ ├── about.html.twig # О блоге
│ └── contact.html.twig # Контакты
├── includes/ # Включаемые части
│ ├── head.html.twig # Секция <head>
│ ├── scripts.html.twig # Скрипты
│ └── meta.html.twig # Мета-теги
└── macros/ # Макросы (Twig-функции)
└── post.html.twig # Макросы для статей
- Создайте указанную структуру папок в вашем проекте
- Перенесите существующие файлы в соответствующие папки
✅ Шаг 2: Создаем улучшенный базовый шаблон
Обновите app/views/twig/layout/base.html.twig:
<!DOCTYPE html>
<html lang="ru">
<head>
{# Подключаем мета-теги из отдельного файла #}
{{ include('includes/meta.html.twig') }}
{# Блок для заголовка страницы #}
<title>{% block title %}{{ site_name }}{% endblock %}</title>
{# Блок для дополнительных мета-тегов #}
{% block meta %}{% endblock %}
{# Подключаем CSS стили #}
<link rel="stylesheet" href="/css/main.css">
{# Блок для дополнительных стилей страницы #}
{% block styles %}{% endblock %}
</head>
<body class="{% block body_class %}{% endblock %}">
{# Подключаем шапку сайта как компонент #}
{{ include('components/header.html.twig') }}
<div class="container">
<div class="row">
{# Блок для основного контента #}
<main class="col-md-9" role="main">
{# Хлебные крошки #}
{% block breadcrumbs %}{% endblock %}
{# Сообщения (уведомления, ошибки) #}
{% block messages %}
{% if flash_messages is defined %}
{{ include('components/flash_messages.html.twig') }}
{% endif %}
{% endblock %}
{# Основной контент страницы #}
{% block content %}
<p>Контент не определен</p>
{% endblock %}
</main>
{# Боковая панель #}
<aside class="col-md-3">
{% block sidebar %}
{{ include('components/sidebar.html.twig') }}
{% endblock %}
</aside>
</div>
</div>
{# Подключаем подвал сайта #}
{{ include('components/footer.html.twig') }}
{# Подключаем общие скрипты #}
{{ include('includes/scripts.html.twig') }}
{# Блок для скриптов конкретной страницы #}
{% block scripts %}{% endblock %}
{# Блок для модальных окон и всплывающих элементов #}
{% block modals %}{% endblock %}
</body>
</html>
✅ Шаг 3: Создаем компоненты
1. Создайте app/views/twig/components/header.html.twig:
<header class="site-header">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url('home') }}">
{{ site_name }}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
{% set nav_items = [
{'name': 'Главная', 'url': 'home', 'icon': '🏠'},
{'name': 'Блог', 'url': 'posts', 'icon': '📝'},
{'name': 'О блоге', 'url': 'about', 'icon': 'ℹ️'},
{'name': 'Контакты', 'url': 'contact', 'icon': '📞'}
] %}
{% for item in nav_items %}
<li class="nav-item">
<a class="nav-link {% if current_page == item.url %}active{% endif %}"
href="{{ url(item.url) }}">
{{ item.icon }} {{ item.name }}
</a>
</li>
{% endfor %}
</ul>
{# Поиск и пользователь #}
<div class="d-flex">
<form class="d-flex me-2" action="{{ url('search') }}" method="GET">
<input class="form-control me-2" type="search" name="q" placeholder="Поиск...">
<button class="btn btn-outline-light" type="submit">Найти</button>
</form>
{% if current_user is defined and current_user %}
<div class="dropdown">
<a class="btn btn-outline-light dropdown-toggle" href="#" role="button"
data-bs-toggle="dropdown">
{{ current_user.name }}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url('profile') }}">Профиль</a></li>
<li><a class="dropdown-item" href="{{ url('logout') }}">Выйти</a></li>
</ul>
</div>
{% else %}
<a class="btn btn-outline-light" href="{{ url('login') }}">Войти</a>
{% endif %}
</div>
</div>
</div>
</nav>
</header>
2. Создайте app/views/twig/components/post_card.html.twig:
{#
Компонент карточки статьи
Принимает переменные: post, show_excerpt (по умолчанию true), class (дополнительные классы)
#}
{% set show_excerpt = show_excerpt ?? true %}
{% set post_class = 'post-card ' ~ (class ?? '') %}
<article class="{{ post_class|trim }}">
<div class="post-card-header">
{# Категория статьи #}
{% if post.category is defined and post.category %}
<span class="post-category badge bg-primary">
{{ post.category.name }}
</span>
{% endif %}
{# Дата публикации #}
<time class="post-date text-muted" datetime="{{ post.created_at }}">
{{ post.created_at|date('d.m.Y H:i') }}
</time>
</div>
<h3 class="post-title">
<a href="{{ url('post_show', {'id': post.id}) }}">
{{ post.title }}
</a>
</h3>
{# Автор статьи #}
{% if post.author is defined and post.author %}
<div class="post-author">
Автор:
{% if post.author.url %}
<a href="{{ post.author.url }}">{{ post.author.name }}</a>
{% else %}
{{ post.author.name }}
{% endif %}
</div>
{% endif %}
{# Краткое содержание #}
{% if show_excerpt and post.excerpt is defined and post.excerpt %}
<div class="post-excerpt">
{{ post.excerpt|truncate(200) }}
</div>
{% endif %}
{# Теги #}
{% if post.tags is defined and post.tags|length > 0 %}
<div class="post-tags">
{% for tag in post.tags %}
<a href="{{ url('tag', {'slug': tag.slug}) }}" class="tag">
#{{ tag.name }}
</a>
{% endfor %}
</div>
{% endif %}
{# Статистика #}
<div class="post-stats">
{% if post.views is defined %}
<span class="stat">👁️ {{ post.views }}</span>
{% endif %}
{% if post.comments_count is defined %}
<span class="stat">💬 {{ post.comments_count }}</span>
{% endif %}
{% if post.likes is defined %}
<span class="stat">👍 {{ post.likes }}</span>
{% endif %}
</div>
</article>
3. Создайте app/views/twig/includes/meta.html.twig:
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{% block meta_description %}Блог о веб-разработке на PHP и Twig{% endblock %}">
<meta name="keywords" content="{% block meta_keywords %}PHP, Twig, MVC, веб-разработка, программирование{% endblock %}">
<meta name="author" content="{{ site_name }}">
{# Open Graph мета-теги для соцсетей #}
<meta property="og:title" content="{% block og_title %}{{ block('title') }}{% endblock %}">
<meta property="og:description" content="{{ block('meta_description') }}">
<meta property="og:type" content="website">
<meta property="og:url" content="{{ app.request.uri }}">
<meta property="og:image" content="{% block og_image %}/images/og-default.jpg{% endblock %}">
<meta property="og:site_name" content="{{ site_name }}">
{# Twitter Card мета-теги #}
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ block('title') }}">
<meta name="twitter:description" content="{{ block('meta_description') }}">
<meta name="twitter:image" content="{{ block('og_image') }}">
{# Канонический URL #}
<link rel="canonical" href="{% block canonical_url %}{{ app.request.uri }}{% endblock %}">
{# Favicon #}
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
✅ Шаг 4: Переписываем главную страницу с наследованием
Обновите app/views/twig/pages/home.html.twig:
{# Наследуем базовый шаблон #}
{% extends 'layout/base.html.twig' %}
{# Переопределяем заголовок страницы #}
{% block title %}Главная | {{ parent() }}{% endblock %}
{# Добавляем класс для body #}
{% block body_class %}home-page{% endblock %}
{# Добавляем мета-описание для главной #}
{% block meta_description %}
Добро пожаловать в блог о веб-разработке! Изучаем PHP, Twig, MVC архитектуру и современные технологии.
{% endblock %}
{# Переопределяем основной контент #}
{% block content %}
<div class="hero-section text-center mb-5">
<h1 class="display-4">Добро пожаловать в наш блог!</h1>
<p class="lead">Здесь мы делимся знаниями о веб-разработке, PHP, Twig и современных технологиях.</p>
<a href="{{ url('posts') }}" class="btn btn-primary btn-lg">Читать статьи</a>
</div>
{# Рекомендуемые статьи #}
{% if featured_posts is defined and featured_posts|length > 0 %}
<section class="featured-posts mb-5">
<h2 class="section-title">🔥 Рекомендуемые статьи</h2>
<div class="row">
{% for post in featured_posts %}
<div class="col-md-4 mb-4">
{{ include('components/post_card.html.twig', {
'post': post,
'class': 'featured'
}) }}
</div>
{% endfor %}
</div>
</section>
{% endif %}
{# Последние статьи #}
<section class="recent-posts">
<h2 class="section-title">📝 Последние статьи</h2>
{% if posts is defined and posts|length > 0 %}
<div class="row">
{% for post in posts %}
{# Чередование колонок: на больших экранах 2 колонки, на средних 2, на маленьких 1 #}
<div class="col-lg-6 col-md-6 col-sm-12 mb-4">
{{ include('components/post_card.html.twig', {
'post': post,
'show_excerpt': true
}) }}
</div>
{# Добавляем разделитель после каждой второй статьи #}
{% if loop.index is even and not loop.last %}
<div class="w-100 d-none d-lg-block"></div>
{% endif %}
{% endfor %}
</div>
{# Пагинация, если есть #}
{% if pagination is defined %}
{{ include('components/pagination.html.twig', pagination) }}
{% endif %}
{% else %}
<div class="alert alert-info">
Статей пока нет. Возвращайтесь позже!
</div>
{% endif %}
</section>
{# Категории #}
{% if categories is defined and categories|length > 0 %}
<section class="categories mt-5">
<h2 class="section-title">🏷️ Категории</h2>
<div class="row">
{% for category in categories %}
<div class="col-md-3 col-sm-6 mb-3">
<div class="card category-card">
<div class="card-body text-center">
<h5 class="card-title">{{ category.name }}</h5>
<p class="card-text">
{{ category.post_count }} статей
</p>
<a href="{{ url('category', {'slug': category.slug}) }}"
class="btn btn-outline-primary">
Смотреть
</a>
</div>
</div>
</div>
{% endfor %}
</div>
</section>
{% endif %}
{% endblock %}
{# Добавляем скрипты только для главной страницы #}
{% block scripts %}
{{ parent() }}
<script>
// Инициализация слайдера для рекомендуемых статей
document.addEventListener('DOMContentLoaded', function() {
const featuredPosts = document.querySelector('.featured-posts');
if (featuredPosts) {
console.log('Главная страница загружена, слайдер готов к работе');
}
});
</script>
{% endblock %}
✅ Шаг 5: Создаем шаблон для отдельной статьи
Создайте app/views/twig/pages/post/show.html.twig:
{# Шаблон для просмотра одной статьи #}
{% extends 'layout/base.html.twig' %}
{% block title %}{{ post.title }} | {{ parent() }}{% endblock %}
{% block meta_description %}{{ post.excerpt|default(post.content|truncate(160)) }}{% endblock %}
{% block meta_keywords %}
{% if post.tags is defined %}
{{ post.tags|map(tag => tag.name)|join(', ') }}
{% else %}
{{ parent() }}
{% endif %}
{% endblock %}
{% block og_title %}{{ post.title }}{% endblock %}
{% block og_image %}{{ post.image|default('/images/og-default.jpg') }}{% endblock %}
{% block canonical_url %}{{ url('post_show', {'id': post.id}) }}{% endblock %}
{% block breadcrumbs %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url('home') }}">Главная</a></li>
<li class="breadcrumb-item"><a href="{{ url('posts') }}">Блог</a></li>
{% if post.category %}
<li class="breadcrumb-item">
<a href="{{ url('category', {'slug': post.category.slug}) }}">
{{ post.category.name }}
</a>
</li>
{% endif %}
<li class="breadcrumb-item active" aria-current="page">{{ post.title|truncate(50) }}</li>
</ol>
</nav>
{% endblock %}
{% block content %}
<article class="single-post">
{# Заголовок статьи #}
<header class="post-header">
{% if post.category %}
<span class="post-category badge bg-primary">
{{ post.category.name }}
</span>
{% endif %}
<h1 class="post-title">{{ post.title }}</h1>
<div class="post-meta">
{# Автор и дата #}
<div class="post-author-date">
{% if post.author %}
<span class="post-author">
👤
{% if post.author.url %}
<a href="{{ post.author.url }}">{{ post.author.name }}</a>
{% else %}
{{ post.author.name }}
{% endif %}
</span>
{% endif %}
<span class="post-date">
📅 {{ post.created_at|date('d.m.Y H:i') }}
</span>
{% if post.updated_at and post.updated_at != post.created_at %}
<span class="post-updated" title="Обновлено">
✏️ {{ post.updated_at|date('d.m.Y H:i') }}
</span>
{% endif %}
</div>
{# Статистика #}
<div class="post-stats">
{% if post.views is defined %}
<span class="stat" title="Просмотры">👁️ {{ post.views }}</span>
{% endif %}
{% if post.reading_time is defined %}
<span class="stat" title="Время чтения">⏱️ {{ post.reading_time }} мин.</span>
{% endif %}
</div>
</div>
{# Изображение статьи #}
{% if post.image %}
<div class="post-image">
<img src="{{ post.image }}" alt="{{ post.title }}" class="img-fluid rounded">
{% if post.image_caption %}
<div class="image-caption text-muted">{{ post.image_caption }}</div>
{% endif %}
</div>
{% endif %}
</header>
{# Контент статьи #}
<div class="post-content">
{{ post.content|raw }}
</div>
{# Теги статьи #}
{% if post.tags is defined and post.tags|length > 0 %}
<footer class="post-footer">
<div class="post-tags">
<strong>Теги:</strong>
{% for tag in post.tags %}
<a href="{{ url('tag', {'slug': tag.slug}) }}" class="tag badge bg-secondary">
#{{ tag.name }}
</a>
{% endfor %}
</div>
{# Кнопки действий #}
<div class="post-actions mt-3">
<button class="btn btn-outline-primary btn-sm" onclick="window.print()">
🖨️ Печать
</button>
<button class="btn btn-outline-secondary btn-sm" id="share-button">
📤 Поделиться
</button>
<a href="#comments" class="btn btn-outline-success btn-sm">
💬 Комментарии ({{ post.comments_count|default(0) }})
</a>
</div>
</footer>
{% endif %}
</article>
{# Навигация по статьям #}
{% if prev_post or next_post %}
<nav class="post-navigation mt-5">
<div class="row">
{% if prev_post %}
<div class="col-md-6">
<div class="card">
<div class="card-body">
<small class="text-muted">Предыдущая статья</small>
<h5 class="card-title">{{ prev_post.title }}</h5>
<a href="{{ url('post_show', {'id': prev_post.id}) }}" class="btn btn-link">
← Читать
</a>
</div>
</div>
</div>
{% endif %}
{% if next_post %}
<div class="col-md-6">
<div class="card">
<div class="card-body text-end">
<small class="text-muted">Следующая статья</small>
<h5 class="card-title">{{ next_post.title }}</h5>
<a href="{{ url('post_show', {'id': next_post.id}) }}" class="btn btn-link">
Читать →
</a>
</div>
</div>
</div>
{% endif %}
</div>
</nav>
{% endif %}
{# Похожие статьи #}
{% if related_posts is defined and related_posts|length > 0 %}
<section class="related-posts mt-5">
<h3>📚 Похожие статьи</h3>
<div class="row">
{% for related in related_posts %}
<div class="col-md-4 mb-3">
{{ include('components/post_card.html.twig', {
'post': related,
'show_excerpt': false
}) }}
</div>
{% endfor %}
</div>
</section>
{% endif %}
{# Комментарии (будет в следующем уроке) #}
<section id="comments" class="comments mt-5">
<h3>💬 Комментарии</h3>
<p class="text-muted">Система комментариев будет добавлена в следующем уроке.</p>
</section>
{% endblock %}
{% block sidebar %}
{# Переопределяем сайдбар для страницы статьи #}
<div class="sidebar-widget">
<h4>📊 Статистика статьи</h4>
<ul class="list-group">
{% if post.views is defined %}
<li class="list-group-item d-flex justify-content-between">
Просмотры
<span class="badge bg-primary rounded-pill">{{ post.views }}</span>
</li>
{% endif %}
{% if post.comments_count is defined %}
<li class="list-group-item d-flex justify-content-between">
Комментарии
<span class="badge bg-success rounded-pill">{{ post.comments_count }}</span>
</li>
{% endif %}
{% if post.likes is defined %}
<li class="list-group-item d-flex justify-content-between">
Лайки
<span class="badge bg-danger rounded-pill">{{ post.likes }}</span>
</li>
{% endif %}
</ul>
</div>
{# Поделиться в соцсетях #}
<div class="sidebar-widget mt-4">
<h4>📤 Поделиться</h4>
<div class="social-share">
<button class="btn btn-outline-primary btn-sm me-1">Facebook</button>
<button class="btn btn-outline-info btn-sm me-1">Twitter</button>
<button class="btn btn-outline-danger btn-sm">VK</button>
</div>
</div>
{{ parent() }} {# Подключаем стандартный сайдбар #}
{% endblock %}
{% block scripts %}
{{ parent() }}
<script>
// Скрипт для кнопки "Поделиться"
document.getElementById('share-button').addEventListener('click', function() {
if (navigator.share) {
navigator.share({
title: '{{ post.title }}',
text: '{{ post.excerpt|default("") }}',
url: window.location.href
});
} else {
// Фолбэк для старых браузеров
navigator.clipboard.writeText(window.location.href);
alert('Ссылка скопирована в буфер обмена!');
}
});
// Подсветка кода в статье (если есть)
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightBlock(block);
});
</script>
{% endblock %}
✅ Шаг 6: Обновляем контроллер для передачи данных
Обновите app/controllers/HomeController.php для передачи расширенных данных:
<?php
// app/controllers/HomeController.php
namespace App\Controllers;
class HomeController
{
private $twig;
public function __construct(\Twig\Environment $twig)
{
$this->twig = $twig;
}
public function index()
{
// Сложная структура данных для главной страницы
$data = [
'current_page' => 'home',
'title' => 'Главная | MVC Блог с Twig',
// Рекомендуемые статьи
'featured_posts' => [
[
'id' => 1,
'title' => 'Введение в Twig: наследование шаблонов',
'excerpt' => 'Узнайте, как использовать мощную систему наследования Twig для создания чистых и структурированных шаблонов.',
'content' => 'Полное содержание статьи...',
'created_at' => '2026-01-01 10:30:00',
'author' => ['name' => 'Иван Петров', 'url' => '/author/1'],
'category' => ['name' => 'Twig', 'slug' => 'twig'],
'views' => 150,
'comments_count' => 5,
'likes' => 42,
'image' => '/images/twig-intro.jpg',
'tags' => [
['name' => 'Twig', 'slug' => 'twig'],
['name' => 'Шаблоны', 'slug' => 'templates']
]
],
// ... другие рекомендованные статьи
],
// Последние статьи
'posts' => [
[
'id' => 2,
'title' => 'MVC архитектура для начинающих',
'excerpt' => 'Разбираем принципы Model-View-Controller на простых примерах.',
'created_at' => '2026-01-02 14:20:00',
'author' => ['name' => 'Анна Сидорова'],
'views' => 89,
'comments_count' => 3
],
[
'id' => 3,
'title' => 'Работа с базами данных в PHP',
'excerpt' => 'Подробное руководство по PDO и безопасным запросам.',
'created_at' => '2026-01-03 09:15:00',
'author' => ['name' => 'Петр Иванов'],
'views' => 120,
'comments_count' => 7
],
// ... больше статей
],
// Категории блога
'categories' => [
['name' => 'PHP', 'slug' => 'php', 'post_count' => 15],
['name' => 'Twig', 'slug' => 'twig', 'post_count' => 8],
['name' => 'Базы данных', 'slug' => 'databases', 'post_count' => 12],
['name' => 'Веб-разработка', 'slug' => 'webdev', 'post_count' => 25]
],
// Пагинация (если есть)
'pagination' => [
'current' => 1,
'total' => 5,
'path' => '/?page=home'
],
// Пользователь (заглушка)
'current_user' => null
];
echo $this->twig->render('pages/home.html.twig', $data);
}
public function postShow($id)
{
// Данные для отдельной статьи (заглушка)
$data = [
'current_page' => 'post',
'post' => [
'id' => $id,
'title' => 'Полное руководство по Twig: наследование, блоки и инклюды',
'excerpt' => 'Исчерпывающее руководство по всем возможностям Twig шаблонизатора.',
'content' => '<h2>Введение</h2>
<p>Twig — это современный шаблонизатор для PHP...</p>
<h2>Наследование шаблонов</h2>
<p>Одна из самых мощных возможностей Twig...</p>
<pre><code class="php">
// Пример кода
$twig->render(\'template.html.twig\', $data);
</code></pre>',
'created_at' => '2026-01-01 10:30:00',
'updated_at' => '2026-01-02 11:45:00',
'author' => ['name' => 'Иван Петров', 'url' => '/author/1'],
'category' => ['name' => 'Twig', 'slug' => 'twig'],
'views' => 250,
'reading_time' => 8,
'comments_count' => 15,
'likes' => 78,
'image' => '/images/twig-guide.jpg',
'image_caption' => 'Twig - современный шаблонизатор для PHP',
'tags' => [
['name' => 'Twig', 'slug' => 'twig'],
['name' => 'PHP', 'slug' => 'php'],
['name' => 'Шаблоны', 'slug' => 'templates'],
['name' => 'Разработка', 'slug' => 'development']
]
],
// Похожие статьи
'related_posts' => [
[
'id' => 4,
'title' => 'Фильтры и функции в Twig',
'excerpt' => 'Изучаем встроенные и создаем свои фильтры.',
'created_at' => '2026-01-04 16:40:00',
'views' => 95
],
// ... другие похожие статьи
],
// Навигация по статьям
'prev_post' => [
'id' => $id - 1,
'title' => 'Предыдущая статья: Введение в MVC'
],
'next_post' => $id < 10 ? [
'id' => $id + 1,
'title' => 'Следующая статья: Работа с формами в Twig'
] : null,
'current_user' => null
];
echo $this->twig->render('pages/post/show.html.twig', $data);
}
// ... остальные методы
}
✅ Шаг 7: Тестирование обновленного блога
- Запустите локальный сервер и откройте главную страницу:
Проверьте:http://localhost/mvc_blog/public/?page=home- Наследование шаблона работает
- Все компоненты (header, footer, sidebar) отображаются
- Карточки статей используют компонент post_card
- Мета-теги корректно заполнены
- Создайте временный маршрут для просмотра статьи:
// Добавьте в public/index.php case 'post': $controller = 'App\\Controllers\\HomeController'; $action = 'postShow'; $id = $_GET['id'] ?? 1; $controllerInstance = new $controller($twig); $controllerInstance->$action($id); break; - Откройте страницу статьи:
Проверьте:http://localhost/mvc_blog/public/?page=post&id=1- Блоки корректно переопределяются
- Хлебные крошки работают
- Сайдбар переопределен для страницы статьи
- Мета-теги специфичны для статьи
- Навигация по статьям отображается
- Проверьте исходный код страниц (Ctrl+U):
- HTML чистый, без PHP тегов
- Структура соответствует базовому шаблону
- Скрипты подключаются в правильном порядке
🔍 Важное наблюдение: Обратите внимание, как Twig автоматически экранирует все переменные. В статье с тегом <script> в заголовке, тег отображается как текст, а не выполняется!
🧪 Эксперименты с наследованием и инклюдами
Эксперимент 1: Создайте шаблон для админ-панели
- Создайте
app/views/twig/layout/admin.html.twig:{# Базовый шаблон для админки #} {% extends 'layout/base.html.twig' %} {% block body_class %}{{ parent() }} admin-panel{% endblock %} {% block header %} {# Упрощенная шапка для админки #} <header class="admin-header"> <nav> <a href="{{ url('admin_dashboard') }}">Панель управления</a> <a href="{{ url('admin_posts') }}">Статьи</a> <a href="{{ url('admin_users') }}">Пользователи</a> </nav> </header> {% endblock %} {% block sidebar %} {# Сайдбар админки #} {{ include('admin/components/sidebar.html.twig') }} {% endblock %} {% block footer %} {# Упрощенный футер для админки #} <footer class="admin-footer"> <p>Админ-панель © {{ current_year }}</p> </footer> {% endblock %} - Создайте дочерний шаблон:
admin/dashboard.html.twig - Посмотрите, как работает наследование второго уровня
Эксперимент 2: Создайте динамический сайдбар
- В базовом шаблоне добавьте передачу данных в инклюд:
{{ include('components/sidebar.html.twig', { 'widgets': ['categories', 'popular_posts', 'tags_cloud'] }) }} - В компоненте сайдбара проверяйте, какие виджеты показывать
- Разные страницы могут иметь разные наборы виджетов
Эксперимент 3: Используйте макросы для форм
- Создайте
app/views/twig/macros/form.html.twig:{# Макрос для создания полей формы #} {% macro input(name, value = '', type = 'text', class = '') %} <input type="{{ type }}" name="{{ name }}" value="{{ value }}" class="form-control {{ class }}" id="{{ name }}"> {% endmacro %} {% macro textarea(name, value = '', rows = 5, class = '') %} <textarea name="{{ name }}" class="form-control {{ class }}" rows="{{ rows }}" id="{{ name }}">{{ value }}</textarea> {% endmacro %} - Используйте в шаблоне:
{% import 'macros/form.html.twig' as form %} {{ form.input('username', user.username) }} {{ form.textarea('bio', user.bio, 3) }}
📋 Домашнее задание
✏️ Задание на день 3
- Реорганизуйте структуру шаблонов как показано в уроке
- Создайте полноценный базовый шаблон с использованием блоков:
- Минимум 6 блоков (title, content, sidebar, scripts, styles, meta)
- Используйте
parent()для наследования содержимого
- Создайте 3 компонента (инклюда):
- Компонент "подвал сайта" с контактами и соцсетями
- Компонент "хлебные крошки" с динамическими данными
- Компонент "пагинация" с параметрами
- Создайте страницу списка статей (
posts.html.twig):- Наследует базовый шаблон
- Использует компонент post_card для каждой статьи
- Имеет пагинацию (можно заглушку)
- Имеет фильтры по категориям и датам
- Реализуйте шаблон для страницы "О блоге" (
about.html.twig):- Использует уникальный макет (двухколоночный)
- Содержит информацию о проекте и команде
- Имеет блок "преимущества" с иконками
- Бонус: Создайте шаблон для 404 ошибки с наследованием, но своим уникальным дизайном
- Бонус 2: Реализуйте систему виджетов для сайдбара, где разные страницы могут показывать разные наборы виджетов
🧠 Проверка понимания
❓ Вопросы для самопроверки
- Как работает система наследования в Twig и чем она лучше простых инклюдов?
- Что делает функция
parent()и когда её нужно использовать? - Чем отличается
includeотextendsв Twig? - Как передать переменные в инклюдируемый компонент?
- Какие преимущества дает разбиение шаблонов на компоненты?
- Как организовать разные сайдбары для разных страниц?
- Что такое блоки в Twig и как они помогают в создании шаблонов?
- Как Twig обрабатывает отсутствие переопределенного блока в дочернем шаблоне?
💡 Продвинутые советы по Twig
Планируйте иерархию шаблонов
Создайте схему наследования перед началом разработки. Например: base → layout → section → page. Это упростит поддержку.
Декомпозируйте на компоненты
Разбивайте интерфейс на маленькие переиспользуемые компоненты. Компонент должен делать одну вещь и делать её хорошо.
Используйте кэширование блоков
Для редко меняющихся блоков используйте: {% cache 'block_name' 3600 %}...{% endcache %}. Это ускорит рендеринг.
Создайте библиотеку макросов
Часто используемые элементы (кнопки, формы, алерты) выносите в макросы. Это обеспечит единообразие интерфейса.
Учитывайте mobile-first
Проектируйте шаблоны сначала для мобильных, затем адаптируйте для десктопов. Twig отлично работает с CSS-фреймворками.
Оптимизируйте для SEO
Используйте блоки для мета-тегов, Open Graph и структурированных данных. Каждая страница должна иметь уникальные мета-данные.
Комментарии
Комментариев пока нет. Будьте первым!