🎯 Цель урока
Понять преимущества шаблонизаторов, научиться устанавливать 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 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
- Если Composer не установлен, скачайте с официального сайта
- В корневой папке проекта (
mvc_blog/) создайте файлcomposer.json:{ "require": { "twig/twig": "^3.0" }, "autoload": { "psr-4": { "App\\": "app/" } } } - Откройте терминал в папке проекта и выполните:
composer install - Проверьте, что создалась папка
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
- Запустите локальный сервер и откройте:
http://localhost/mvc_blog/public/?page=home - Проверьте, что:
- Страницы отображаются корректно
- Навигация работает (обратите внимание на активный пункт меню)
- Статья с тегом <script> выводится как текст (не выполняется!)
- Фильтры Twig работают (дата, обрезка текста)
- Откройте страницу 404:
http://localhost/mvc_blog/public/?page=unknown
🔍 Проверка безопасности: Откройте исходный код страницы (Ctrl+U) и найдите статью с тегом <script>. Вы увидите, что тег экранирован: <script>тегом</script> — это автоматическая защита Twig от XSS!
🧪 Эксперименты с Twig
Эксперимент 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); })); - В шаблоне используйте:
{{ post.created_at|russian_date }}
Эксперимент 2: Создайте макрос (функцию) для отображения статьи
- Создайте
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 %} - В основном шаблоне импортируйте и используйте:
{% 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
- Установите Twig через Composer в свой проект
- Перепишите существующие шаблоны (home, about) на Twig
- Создайте страницу "Контакты" с формой используя Twig:
- Шаблон
contact.html.twig - Метод
contact()в контроллере - Добавьте маршрут в
index.php
- Шаблон
- Добавьте в базовый шаблон вывод текущего пользователя (пока можно использовать заглушку:
$twig->addGlobal('current_user', ['name' => 'Гость'])) - Создайте 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> - Бонус: Реализуйте наследование шаблонов второго уровня:
base.html.twig— общая структураadmin_layout.html.twig— наследует base, добавляет меню админкиadmin_dashboard.html.twig— наследует admin_layout
- Бонус 2: Создайте простой шаблон письма (email.html.twig) с переменными и отрендерите его без HTML-обрамления
🧠 Проверка понимания
❓ Вопросы для самопроверки
- Какие преимущества дает использование Twig вместо чистого PHP в шаблонах?
- Как Twig обеспечивает безопасность от XSS-атак?
- Что такое фильтры в Twig и приведите 3 примера их использования?
- Как работает наследование шаблонов в Twig?
- Зачем нужен Composer и что такое автозагрузка классов?
- Как добавить глобальную переменную, доступную во всех шаблонах?
- В чем разница между
{{ variable }}и{{ variable|raw }}? - Как закэшировать фрагмент шаблона в 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
Комментарии
Комментариев пока нет. Будьте первым!