🎯 Цель урока

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

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

🤔 Зачем нужны шаблонизаторы?

В предыдущем уроке мы уже разделили логику и представление с помощью MVC. Но наши PHP-шаблоны все еще содержат код вроде <?php echo htmlspecialchars(...); ?>. Шаблонизаторы решают эту и другие проблемы.

🔍 Проблемы "чистого" PHP в шаблонах:

  • Безопасность — легко забыть про htmlspecialchars() (XSS-уязвимости)
  • Сложный синтаксис — много открывающих/закрывающих тегов PHP
  • Нет наследования — сложно создавать базовые шаблоны (layout)
  • Смешивание логики — возникает соблазн добавить PHP-код в шаблоны
  • Низкая производительность — PHP каждый раз компилирует шаблоны

💡 Реальная аналогия: Представьте, что вы пишете письмо чернилами без возможности исправить ошибку (PHP в шаблонах). Шаблонизатор — это текстовый редактор с проверкой орфографии, автозаменой и шаблонами документов.

🚀 Что такое Twig и его преимущества

Twig — современный шаблонизатор для PHP, созданный разработчиками Symfony. Он компилирует шаблоны в оптимизированный PHP-код.

Сравнение PHP и Twig в шаблонах
Задача PHP в шаблоне Twig
Вывод переменной <?php echo htmlspecialchars($title); ?>
Цикл по массиву <?php foreach($posts as $post): ?>...<?php endforeach; ?> {% for post in posts %}...{% endfor %}
Условие <?php if($user): ?>...<?php endif; ?> {% if user %}...{% endif %}
Наследование шаблона Сложно, через include/require {% extends 'base.html.twig' %}
Безопасность Нужно помнить про htmlspecialchars() Автоматическое экранирование

✨ Основные возможности Twig:

  • Автоматическое экранирование — защита от XSS по умолчанию
  • Наследование шаблонов — мощная система наследования с блоками
  • Фильтры и функции{{ post.title|upper }}, {{ path('route_name') }}
  • Кэширование — шаблоны компилируются в PHP-код один раз
  • Расширяемость — можно создавать свои фильтры, функции, теги
  • Безопасная среда — в шаблонах нельзя выполнять произвольный PHP-код

📦 Установка зависимостей через Composer

Composer — менеджер зависимостей для PHP. Он позволяет легко подключать и обновлять библиотеки.

📁 Новая структура проекта с зависимостями:

mvc_blog/
├── app/
│   ├── controllers/
│   ├── models/
│   └── views/           # Теперь здесь будут Twig-шаблоны
│       └── twig/       # Новый каталог для Twig-шаблонов
├── public/
│   └── index.php
├── config/
├── vendor/              # Папка для зависимостей (создаст Composer)
└── composer.json        # Файл конфигурации Composer

🔧 Что делает Composer:

  • Скачивает библиотеки (Twig, Symfony компоненты и т.д.)
  • Управляет версиями и зависимостями
  • Автозагружает классы (не нужно писать require_once)
  • Создает карту классов для быстрой загрузки

📝 Базовый синтаксис Twig

1. Вывод переменных (экранированный):

{{ variable }}          {# Безопасный вывод с экранированием #}
{{ variable|raw }}      {# Без экранирования (осторожно!) #}
{{ variable|default('Нет данных') }} {# Значение по умолчанию #}

2. Управляющие конструкции:

{# Условия #}
{% if user.isAdmin %}
    Панель администратора
{% elseif user.isModerator %}
    Панель модератора
{% else %}
    Обычный пользователь
{% endif %}

{# Циклы #}
{% for post in posts %}
    {{ loop.index }}. {{ post.title }}
{% else %}
    <p>Статей пока нет</p>
{% endfor %}

3. Фильтры (модификаторы):

{{ title|upper }}                 {# В верхний регистр #}
{{ content|truncate(100) }}       {# Обрезать до 100 символов #}
{{ date|date('d.m.Y H:i') }}      {# Форматирование даты #}
{{ text|escape('html') }}         {# Явное экранирование #}
{{ array|join(', ') }}            {# Объединить массив в строку #}

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

{# base.html.twig #}
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}Мой сайт{% endblock %}</title>
</head>
<body>
    {% block content %}{% endblock %}
</body>
</html>

{# child.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}Название страницы{% endblock %}

{% block content %}
    Содержимое страницы
{% endblock %}

💻 Практическая часть (2 часа)

Шаг 1: Установка Composer и Twig

  1. Если Composer не установлен, скачайте с официального сайта
  2. В корневой папке проекта (mvc_blog/) создайте файл composer.json:
    {
        "require": {
            "twig/twig": "^3.0"
        },
        "autoload": {
            "psr-4": {
                "App\\": "app/"
            }
        }
    }
  3. Откройте терминал в папке проекта и выполните:
    composer install
  4. Проверьте, что создалась папка vendor/ с файлами Twig

📁 Что произошло: Composer создал папку vendor/, скачал Twig и настроил автозагрузку классов. Теперь нам не нужно писать require_once для классов в папке app/.

Шаг 2: Настройка Twig в проекте

1. Создайте конфигурационный файл config/twig.php:

<?php
// config/twig.php - Конфигурация Twig

// Автозагрузка классов через Composer
require_once dirname(__DIR__) . '/vendor/autoload.php';

// Создаем папку для кэша шаблонов (если её нет)
$cachePath = dirname(__DIR__) . '/var/cache/twig';
if (!file_exists($cachePath)) {
    mkdir($cachePath, 0777, true);
}

// Настройка Twig
$loader = new \Twig\Loader\FilesystemLoader(dirname(__DIR__) . '/app/views/twig');
$twig = new \Twig\Environment($loader, [
    'cache' => $cachePath,
    'debug' => true, // Включаем отладку для разработки
    'auto_reload' => true, // Автоматически перекомпилировать при изменении
]);

// Добавляем глобальные переменные (доступны во всех шаблонах)
$twig->addGlobal('current_year', date('Y'));
$twig->addGlobal('site_name', 'MVC Блог');

// Добавляем функцию для генерации URL (пока упрощенная)
$twig->addFunction(new \Twig\TwigFunction('url', function ($page = 'home', $params = []) {
    $query = http_build_query(array_merge(['page' => $page], $params));
    return "/mvc_blog/public/?{$query}";
}));

return $twig;

2. Обновите файл public/index.php:

<?php
// public/index.php - Обновленная точка входа с Twig

// Автозагрузка Composer
require_once dirname(__DIR__) . '/vendor/autoload.php';

// Базовая конфигурация
define('ROOT_PATH', dirname(__DIR__));
define('APP_PATH', ROOT_PATH . '/app');

// Подключаем Twig
$twig = require_once ROOT_PATH . '/config/twig.php';

// Простейший роутинг (как было)
$page = $_GET['page'] ?? 'home';

switch ($page) {
    case 'home':
        $controller = 'App\\Controllers\\HomeController';
        $action = 'index';
        break;
    case 'about':
        $controller = 'App\\Controllers\\HomeController';
        $action = 'about';
        break;
    default:
        $controller = 'App\\Controllers\\HomeController';
        $action = 'notFound';
        break;
}

// Подключаем и запускаем контроллер
if (class_exists($controller)) {
    $controllerInstance = new $controller($twig);
    if (method_exists($controllerInstance, $action)) {
        $controllerInstance->$action();
    } else {
        echo "Метод {$action} не найден";
    }
} else {
    echo "Контроллер {$controller} не найден";
}

Шаг 3: Обновляем контроллер для работы с Twig

Обновите app/controllers/HomeController.php:

<?php
// app/controllers/HomeController.php

namespace App\Controllers;

class HomeController
{
    private $twig;
    
    // Конструктор теперь принимает объект Twig
    public function __construct(\Twig\Environment $twig)
    {
        $this->twig = $twig;
    }
    
    public function index()
    {
        // Данные для главной страницы
        $data = [
            'title' => 'Добро пожаловать в MVC блог с Twig!',
            'posts' => [
                ['id' => 1, 'title' => 'Первая статья на Twig', 'content' => 'Содержание первой статьи...', 'created_at' => '2026-01-01'],
                ['id' => 2, 'title' => 'Вторая статья', 'content' => 'Содержание второй статьи...', 'created_at' => '2026-01-02'],
                ['id' => 3, 'title' => 'Статья с <script>тегом</script>', 'content' => 'Проверка безопасности Twig', 'created_at' => '2026-01-03'],
            ],
        ];
        
        // Рендерим шаблон Twig
        echo $this->twig->render('home.html.twig', $data);
    }
    
    public function about()
    {
        $data = [
            'title' => 'О нашем блоге',
            'description' => 'Этот блог теперь использует Twig для шаблонов.',
            'features' => ['MVC архитектура', 'Twig шаблонизатор', 'Роутинг', 'Безопасность'],
        ];
        
        echo $this->twig->render('about.html.twig', $data);
    }
    
    public function notFound()
    {
        $data = [
            'title' => 'Страница не найдена',
            'error_code' => 404,
            'message' => 'Запрошенная страница не существует.',
        ];
        
        // Используем отдельный шаблон для ошибок
        echo $this->twig->render('errors/404.html.twig', $data);
    }
}

📖 Изменения в контроллере:

  • namespace App\Controllers; — добавляем пространство имен для автозагрузки
  • __construct(\Twig\Environment $twig) — принимаем Twig через конструктор
  • $this->twig->render() — используем Twig для рендеринга вместо своего метода render
  • Больше не нужны методы render() и extract()

Шаг 4: Создаем базовый шаблон Twig

Создайте app/views/twig/layout/base.html.twig:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}{{ site_name }}{% endblock %}</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: Arial, sans-serif; line-height: 1.6; background: #f5f5f5; }
        .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
        header { background: #2c3e50; color: white; padding: 1rem 0; }
        nav ul { display: flex; list-style: none; gap: 20px; }
        nav a { color: white; text-decoration: none; padding: 5px 10px; border-radius: 3px; }
        nav a:hover, nav a.active { background: rgba(255,255,255,0.1); }
        main { padding: 2rem 0; min-height: 70vh; }
        .post { background: white; padding: 1.5rem; margin-bottom: 1rem; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
        .post-meta { color: #7f8c8d; font-size: 0.9rem; margin-bottom: 10px; }
        footer { background: #34495e; color: white; padding: 2rem 0; margin-top: 3rem; }
        .alert { padding: 15px; border-radius: 5px; margin-bottom: 20px; }
        .alert-danger { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
        .breadcrumb { margin-bottom: 20px; }
        .breadcrumb a { color: #3498db; }
    </style>
</head>
<body>
    <header>
        <div class="container">
            <h1><a href="{{ url('home') }}" style="color: white; text-decoration: none;">{{ site_name }}</a></h1>
            <nav>
                <ul>
                    <li><a href="{{ url('home') }}" {% if current_page == 'home' %}class="active"{% endif %}>Главная</a></li>
                    <li><a href="{{ url('about') }}" {% if current_page == 'about' %}class="active"{% endif %}>О блоге</a></li>
                    <li><a href="{{ url('contact') }}" {% if current_page == 'contact' %}class="active"{% endif %}>Контакты</a></li>
                </ul>
            </nav>
        </div>
    </header>
    
    <main class="container">
        {# Хлебные крошки (опционально) #}
        {% block breadcrumb %}{% endblock %}
        
        {# Сообщения (например, после отправки формы) #}
        {% block messages %}{% endblock %}
        
        {# Основное содержимое страницы #}
        {% block content %}{% endblock %}
    </main>
    
    <footer>
        <div class="container">
            <p>© {{ current_year }} {{ site_name }}. Все права защищены.</p>
            <p>Сайт работает на PHP + Twig</p>
        </div>
    </footer>
    
    <!-- Можно добавить общие скрипты -->
    {% block scripts %}{% endblock %}
</body>
</html>

Шаг 5: Создаем Twig-шаблоны страниц

1. Создайте app/views/twig/home.html.twig:

{# Наследуем базовый шаблон #}
{% extends 'layout/base.html.twig' %}

{% block title %}{{ title }} | {{ parent() }}{% endblock %}

{% block content %}
    <h2>{{ title }}</h2>
    <p>Это главная страница нашего блога с использованием Twig.</p>
    
    {# Пример работы фильтров Twig #}
    <p>Всего статей: <strong>{{ posts|length }}</strong></p>
    
    <h3>Последние статьи:</h3>
    
    {% if posts|length > 0 %}
        {% for post in posts %}
            <article class="post">
                <h4>{{ post.title }}</h4>
                <div class="post-meta">
                    Опубликовано: {{ post.created_at|date('d.m.Y') }}
                    | ID: {{ post.id }}
                </div>
                <p>{{ post.content|truncate(150) }}</p>
                <a href="{{ url('post', {'id': post.id}) }}">Читать далее →</a>
            </article>
        {% endfor %}
    {% else %}
        <div class="alert">Статей пока нет.</div>
    {% endif %}
    
    {# Пример вложенных условий #}
    {% if posts|length > 5 %}
        <p><small>Показаны последние 5 статей из {{ posts|length }}</small></p>
    {% endif %}
{% endblock %}

{% block scripts %}
    <script>
        console.log('Главная страница загружена');
    </script>
{% endblock %}

2. Создайте app/views/twig/about.html.twig:

{% extends 'layout/base.html.twig' %}

{% block title %}{{ title }} | {{ parent() }}{% endblock %}

{% block breadcrumb %}
    <div class="breadcrumb">
        <a href="{{ url('home') }}">Главная</a> / О блоге
    </div>
{% endblock %}

{% block content %}
    <h2>{{ title }}</h2>
    <p>{{ description }}</p>
    
    <h3>Особенности проекта:</h3>
    <ul>
        {% for feature in features %}
            <li>{{ feature }}</li>
        {% endfor %}
    </ul>
    
    <h3>Технологии:</h3>
    <table border="1" style="border-collapse: collapse; width: 100%;">
        <tr>
            <th>Компонент</th>
            <th>Версия</th>
            <th>Назначение</th>
        </tr>
        <tr>
            <td>PHP</td>
            <td>8.1+</td>
            <td>Бэкенд</td>
        </tr>
        <tr>
            <td>Twig</td>
            <td>3.0</td>
            <td>Шаблонизатор</td>
        </tr>
        <tr>
            <td>Composer</td>
            <td>2.0</td>
            <td>Менеджер зависимостей</td>
        </tr>
    </table>
{% endblock %}

3. Создайте app/views/twig/errors/404.html.twig:

{# Этот шаблон НЕ наследует базовый, у него свой дизайн #}
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>{{ title }}</title>
    <style>
        body { 
            font-family: Arial, sans-serif; 
            display: flex; 
            justify-content: center; 
            align-items: center; 
            height: 100vh; 
            background: #f8f9fa; 
            color: #333;
            text-align: center;
        }
        .error-container { max-width: 500px; }
        .error-code { 
            font-size: 120px; 
            color: #e74c3c; 
            margin: 0; 
            font-weight: bold;
        }
        .error-message { 
            font-size: 24px; 
            margin: 20px 0; 
        }
        .back-link { 
            display: inline-block; 
            margin-top: 20px; 
            padding: 10px 20px; 
            background: #3498db; 
            color: white; 
            text-decoration: none; 
            border-radius: 5px;
        }
        .back-link:hover { background: #2980b9; }
    </style>
</head>
<body>
    <div class="error-container">
        <h1 class="error-code">{{ error_code }}</h1>
        <p class="error-message">{{ message }}</p>
        <p>Возможно, страница была перемещена или вы ошиблись в адресе.</p>
        <a href="{{ url('home') }}" class="back-link">Вернуться на главную</a>
    </div>
</body>
</html>

Шаг 6: Тестирование приложения с Twig

  1. Запустите локальный сервер и откройте:
    http://localhost/mvc_blog/public/?page=home
  2. Проверьте, что:
    • Страницы отображаются корректно
    • Навигация работает (обратите внимание на активный пункт меню)
    • Статья с тегом <script> выводится как текст (не выполняется!)
    • Фильтры Twig работают (дата, обрезка текста)
  3. Откройте страницу 404:
    http://localhost/mvc_blog/public/?page=unknown

🔍 Проверка безопасности: Откройте исходный код страницы (Ctrl+U) и найдите статью с тегом <script>. Вы увидите, что тег экранирован: &lt;script&gt;тегом&lt;/script&gt; — это автоматическая защита Twig от XSS!

🧪 Эксперименты с Twig

Эксперимент 1: Добавьте фильтр для форматирования даты

  1. В конфигурации Twig (config/twig.php) добавьте:
    // Добавляем фильтр для русских названий месяцев
    $twig->addFilter(new \Twig\TwigFilter('russian_date', function($date) {
        $months = [
            1 => 'января', 2 => 'февраля', 3 => 'марта',
            4 => 'апреля', 5 => 'мая', 6 => 'июня',
            7 => 'июля', 8 => 'августа', 9 => 'сентября',
            10 => 'октября', 11 => 'ноября', 12 => 'декабря'
        ];
        
        $timestamp = strtotime($date);
        return date('d', $timestamp) . ' ' . $months[date('n', $timestamp)] . ' ' . date('Y', $timestamp);
    }));
  2. В шаблоне используйте: {{ post.created_at|russian_date }}

Эксперимент 2: Создайте макрос (функцию) для отображения статьи

  1. Создайте app/views/twig/macros/post.html.twig:
    {# Макрос для отображения статьи #}
    {% macro display(post) %}
        <div class="post">
            <h4>{{ post.title }}</h4>
            <p>{{ post.content|truncate(200) }}</p>
            <a href="{{ url('post', {'id': post.id}) }}">Читать далее</a>
        </div>
    {% endmacro %}
  2. В основном шаблоне импортируйте и используйте:
    {% import 'macros/post.html.twig' as post_macro %}
    
    {{ post_macro.display(current_post) }}

Эксперимент 3: Добавьте кэширование фрагментов

В шаблоне используйте тег cache:

{% cache 'sidebar' 600 %} {# Кэшировать на 10 минут #}
    <div class="sidebar">
        <h3>Популярные статьи</h3>
        {% for post in popular_posts %}
            ... 
        {% endfor %}
    </div>
{% endcache %}

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

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

  1. Установите Twig через Composer в свой проект
  2. Перепишите существующие шаблоны (home, about) на Twig
  3. Создайте страницу "Контакты" с формой используя Twig:
    • Шаблон contact.html.twig
    • Метод contact() в контроллере
    • Добавьте маршрут в index.php
  4. Добавьте в базовый шаблон вывод текущего пользователя (пока можно использовать заглушку: $twig->addGlobal('current_user', ['name' => 'Гость']))
  5. Создайте Twig-фильтр для подсветки синтаксиса кода:
    // В config/twig.php
    $twig->addFilter(new \Twig\TwigFilter('highlight_php', function($code) {
        return highlight_string('<?php ' . $code, true);
    }));
    
    // В шаблоне
    <pre>{{ 'echo "Hello World!";'|highlight_php|raw }}</pre>
  6. Бонус: Реализуйте наследование шаблонов второго уровня:
    • base.html.twig — общая структура
    • admin_layout.html.twig — наследует base, добавляет меню админки
    • admin_dashboard.html.twig — наследует admin_layout
  7. Бонус 2: Создайте простой шаблон письма (email.html.twig) с переменными и отрендерите его без HTML-обрамления

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

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

  1. Какие преимущества дает использование Twig вместо чистого PHP в шаблонах?
  2. Как Twig обеспечивает безопасность от XSS-атак?
  3. Что такое фильтры в Twig и приведите 3 примера их использования?
  4. Как работает наследование шаблонов в Twig?
  5. Зачем нужен Composer и что такое автозагрузка классов?
  6. Как добавить глобальную переменную, доступную во всех шаблонах?
  7. В чем разница между {{ variable }} и {{ variable|raw }}?
  8. Как закэшировать фрагмент шаблона в Twig?

💡 Советы по работе с Twig

🚀

Начинайте с простых шаблонов

Не пытайтесь сразу использовать все возможности Twig. Сначала освоите базовый синтаксис, затем фильтры, потом наследование и макросы.

🛡️

Всегда используйте экранирование

Избегайте |raw для пользовательских данных. Twig экранирует по умолчанию — это ваша защита от XSS. Используйте |raw только для доверенного HTML.

📁

Организуйте шаблоны в папки

Создавайте структуру: layout/, pages/, components/, macros/, errors/. Это упростит навигацию в больших проектах.

Включите кэширование в продакшене

В разработке используйте 'debug' => true, в продакшене — 'cache' => 'path/to/cache' и 'auto_reload' => false для производительности.

🔧

Создайте свои фильтры и функции

Часто используемые преобразования выносите в кастомные фильтры: форматирование цен, дат, телефонных номеров и т.д.

📚

Изучите документацию Twig

В Twig много встроенных функций и фильтров: twig.symfony.com