🎯 Цель урока

Освоить продвинутые возможности Twig: систему наследования шаблонов, работу с блоками, использование инклюдов для компонентов, передачу сложных данных из контроллеров и создание чистых, структурированных шаблонов для нашего блога.

📚 Теоретическая часть (40 минут)

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

Наследование — одна из самых мощных возможностей Twig, позволяющая создавать иерархию шаблонов и избегать дублирования кода.

🔍 Как работает наследование:

  1. Базовый шаблон определяет общую структуру (скелет) страницы
  2. Блоки (blocks) — это "дырки" в базовом шаблоне, которые могут быть заполнены в дочерних
  3. Дочерний шаблон расширяет базовый и переопределяет нужные блоки
  4. 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     # Макросы для статей
  1. Создайте указанную структуру папок в вашем проекте
  2. Перенесите существующие файлы в соответствующие папки

Шаг 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: Тестирование обновленного блога

  1. Запустите локальный сервер и откройте главную страницу:
    http://localhost/mvc_blog/public/?page=home
    Проверьте:
    • Наследование шаблона работает
    • Все компоненты (header, footer, sidebar) отображаются
    • Карточки статей используют компонент post_card
    • Мета-теги корректно заполнены
  2. Создайте временный маршрут для просмотра статьи:
    // Добавьте в public/index.php
    case 'post':
        $controller = 'App\\Controllers\\HomeController';
        $action = 'postShow';
        $id = $_GET['id'] ?? 1;
        $controllerInstance = new $controller($twig);
        $controllerInstance->$action($id);
        break;
  3. Откройте страницу статьи:
    http://localhost/mvc_blog/public/?page=post&id=1
    Проверьте:
    • Блоки корректно переопределяются
    • Хлебные крошки работают
    • Сайдбар переопределен для страницы статьи
    • Мета-теги специфичны для статьи
    • Навигация по статьям отображается
  4. Проверьте исходный код страниц (Ctrl+U):
    • HTML чистый, без PHP тегов
    • Структура соответствует базовому шаблону
    • Скрипты подключаются в правильном порядке

🔍 Важное наблюдение: Обратите внимание, как Twig автоматически экранирует все переменные. В статье с тегом <script> в заголовке, тег отображается как текст, а не выполняется!

🧪 Эксперименты с наследованием и инклюдами

Эксперимент 1: Создайте шаблон для админ-панели

  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 %}
  2. Создайте дочерний шаблон: admin/dashboard.html.twig
  3. Посмотрите, как работает наследование второго уровня

Эксперимент 2: Создайте динамический сайдбар

  1. В базовом шаблоне добавьте передачу данных в инклюд:
    {{ include('components/sidebar.html.twig', {
        'widgets': ['categories', 'popular_posts', 'tags_cloud']
    }) }}
  2. В компоненте сайдбара проверяйте, какие виджеты показывать
  3. Разные страницы могут иметь разные наборы виджетов

Эксперимент 3: Используйте макросы для форм

  1. Создайте 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 %}
  2. Используйте в шаблоне:
    {% import 'macros/form.html.twig' as form %}
    
    {{ form.input('username', user.username) }}
    {{ form.textarea('bio', user.bio, 3) }}

📋 Домашнее задание

✏️ Задание на день 3

  1. Реорганизуйте структуру шаблонов как показано в уроке
  2. Создайте полноценный базовый шаблон с использованием блоков:
    • Минимум 6 блоков (title, content, sidebar, scripts, styles, meta)
    • Используйте parent() для наследования содержимого
  3. Создайте 3 компонента (инклюда):
    • Компонент "подвал сайта" с контактами и соцсетями
    • Компонент "хлебные крошки" с динамическими данными
    • Компонент "пагинация" с параметрами
  4. Создайте страницу списка статей (posts.html.twig):
    • Наследует базовый шаблон
    • Использует компонент post_card для каждой статьи
    • Имеет пагинацию (можно заглушку)
    • Имеет фильтры по категориям и датам
  5. Реализуйте шаблон для страницы "О блоге" (about.html.twig):
    • Использует уникальный макет (двухколоночный)
    • Содержит информацию о проекте и команде
    • Имеет блок "преимущества" с иконками
  6. Бонус: Создайте шаблон для 404 ошибки с наследованием, но своим уникальным дизайном
  7. Бонус 2: Реализуйте систему виджетов для сайдбара, где разные страницы могут показывать разные наборы виджетов

🧠 Проверка понимания

Вопросы для самопроверки

  1. Как работает система наследования в Twig и чем она лучше простых инклюдов?
  2. Что делает функция parent() и когда её нужно использовать?
  3. Чем отличается include от extends в Twig?
  4. Как передать переменные в инклюдируемый компонент?
  5. Какие преимущества дает разбиение шаблонов на компоненты?
  6. Как организовать разные сайдбары для разных страниц?
  7. Что такое блоки в Twig и как они помогают в создании шаблонов?
  8. Как Twig обрабатывает отсутствие переопределенного блока в дочернем шаблоне?

💡 Продвинутые советы по Twig

🏗️

Планируйте иерархию шаблонов

Создайте схему наследования перед началом разработки. Например: base → layout → section → page. Это упростит поддержку.

🧩

Декомпозируйте на компоненты

Разбивайте интерфейс на маленькие переиспользуемые компоненты. Компонент должен делать одну вещь и делать её хорошо.

Используйте кэширование блоков

Для редко меняющихся блоков используйте: {% cache 'block_name' 3600 %}...{% endcache %}. Это ускорит рендеринг.

🔧

Создайте библиотеку макросов

Часто используемые элементы (кнопки, формы, алерты) выносите в макросы. Это обеспечит единообразие интерфейса.

📱

Учитывайте mobile-first

Проектируйте шаблоны сначала для мобильных, затем адаптируйте для десктопов. Twig отлично работает с CSS-фреймворками.

🔍

Оптимизируйте для SEO

Используйте блоки для мета-тегов, Open Graph и структурированных данных. Каждая страница должна иметь уникальные мета-данные.