Шаблоны в PHP без фреймворка: короткая инструкция
За полчаса вы отделите HTML от логики в PHP-проекте без Blade и Twig — на чистом PHP. Разберём подключение шаблона, передачу данных и защиту от XSS.

К концу этой инструкции у вас будет рабочая функция рендеринга шаблонов на чистом PHP: контроллер готовит данные, отдельный файл-шаблон выводит HTML, а вывод экранируется от XSS. Никакого Composer, Twig или Blade — только то, что есть в PHP 8.x из коробки. Займёт около 30 минут вместе с проверкой.
Подход подходит для небольших сайтов, лендингов и админок, где тянуть шаблонизатор ради трёх страниц избыточно. Если проект вырастет — переезд на Twig будет проще, потому что данные уже отделены от разметки.
Что нужно до начала
- PHP 8.1 или новее (проверьте командой
php -v). Код работает и на 7.4, но примеры используют типизацию, доступную с 8.x. - Локальный сервер: встроенный
php -S localhost:8000или любой Apache/Nginx с PHP-FPM. - Права на запись в папку проекта — вы будете создавать файлы.
- Текстовый редактор. IDE не обязательна.
Структура, к которой придём:
project/
├── index.php # точка входа
├── lib/
│ └── render.php # функция рендеринга
└── templates/
└── page.php # сам шаблонШаг 1. Создайте функцию рендеринга
Идея простая: шаблон — это обычный PHP-файл с HTML. Мы подключаем его через include, но так, чтобы переданные переменные были ему доступны, а вывод возвращался строкой, а не сразу летел в браузер. За это отвечают буфер вывода и функция extract().
Создайте файл lib/render.php:
<?php
declare(strict_types=1);
/**
* Рендерит шаблон и возвращает результат строкой.
*
* @param string $template Имя файла в папке templates без расширения
* @param array $data Переменные для шаблона
*/
function render(string $template, array $data = []): string
{
$path = __DIR__ . '/../templates/' . $template . '.php';
if (!is_file($path)) {
throw new RuntimeException("Шаблон не найден: {$path}");
}
// Делаем ключи массива локальными переменными шаблона.
// EXTR_SKIP не даёт затереть уже существующие переменные.
extract($data, EXTR_SKIP);
// Ловим вывод шаблона в буфер вместо прямой отправки в браузер.
ob_start();
include $path;
return (string) ob_get_clean();
}Проверка: файл создан, синтаксических ошибок нет — выполните php -l lib/render.php, должно вывести No syntax errors detected.
Шаг 2. Напишите шаблон
Создайте templates/page.php. Внутри — обычный HTML с вкраплениями PHP. Переменные $title и $users придут из массива $data.
<?php /** @var string $title */ ?>
<?php /** @var array $users */ ?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?></title>
</head>
<body>
<h1><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?></h1>
<ul>
<?php foreach ($users as $user): ?>
<li><?= htmlspecialchars($user, ENT_QUOTES, 'UTF-8') ?></li>
<?php endforeach; ?>
</ul>
</body>
</html>Обратите внимание на альтернативный синтаксис foreach (...): ... endforeach; — в шаблонах он читается лучше, чем фигурные скобки вперемешку с HTML. Комментарии @var вверху нужны только для подсказок IDE, на выполнение не влияют.
Почему везде htmlspecialchars
Любые данные, которые могли прийти от пользователя, перед выводом в HTML нужно экранировать. Иначе строка вроде <script>alert(1)</script> в имени пользователя выполнится в браузере. Это XSS* — самая частая уязвимость в самописных шаблонах. Правило простое: нет htmlspecialchars — есть дыра.
* XSS (Cross-Site Scripting) — внедрение чужого скрипта на страницу через непроверенные данные. Экранирование превращает символы < > " & в безопасные HTML-сущности.
Шаг 3. Соберите точку входа
Создайте index.php. Здесь готовятся данные и вызывается рендеринг.
<?php
declare(strict_types=1);
require __DIR__ . '/lib/render.php';
// Данные обычно приходят из БД или API, здесь — фиксированные.
$html = render('page', [
'title' => 'Список пользователей',
'users' => ['Анна', 'Борис', '<b>Взлом</b>'],
]);
echo $html;Запустите встроенный сервер из корня проекта:
php -S localhost:8000Проверка: откройте http://localhost:8000. Должен появиться заголовок «Список пользователей» и список из трёх пунктов. Третий пункт выведется как текст <b>Взлом</b>, а не жирным — значит, экранирование работает.
Шаг 4. Сократите повторяющийся вызов экранирования
Писать htmlspecialchars($x, ENT_QUOTES, 'UTF-8') в каждой строке шаблона утомительно. Заведите короткий хелпер. Добавьте в lib/render.php:
function e(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}Теперь в шаблоне вместо длинной формы — <?= e($title) ?>. Функция принимает ?string, поэтому null не вызовет предупреждения в PHP 8.1+, где передача null в строковый параметр стала deprecated.
Экранирование по умолчанию, а не по памяти. Самое надёжное правило для самописного шаблонизатора: любой вывод переменной идёт через e(). Если решили вставить готовый HTML осознанно — это исключение, которое видно в коде, а не наоборот.
Если не получилось
- Warning: Undefined variable $users — вы не передали ключ
usersв массив$data, либо опечатались в имени. Проверьте, что имена вrender()и в шаблоне совпадают буквально. - RuntimeException: Шаблон не найден — путь к файлу неверный. Проверьте, что
page.phpлежит вtemplates/, а расширение вы не дублируете в вызове (render('page'), а неrender('page.php')). - HTML отдаётся как чистый текст в браузере — сервер не обрабатывает PHP. Убедитесь, что запускаете через
php -Sили через веб-сервер с PHP, а не открываете файл двойным кликом (протоколfile://). - Пустая страница, в логах Parse error — синтаксическая ошибка в шаблоне. Частая причина: незакрытый
<?phpили пропущенная:вforeach (...):. Проверьте файл черезphp -l templates/page.php. - Русские буквы превратились в кракозябры — проверьте, что файл сохранён в UTF-8 без BOM и что в
<head>указан<meta charset="utf-8">.
Что делать дальше
База готова. Следующие шаги по мере роста проекта:
- Общий макет. Вынесите шапку и подвал в
layout.php, а контент страницы вставляйте в него. Реализуется тем жеrender(): сначала рендерите контент в строку, потом передаёте её в макет как переменную. - Когда переезжать на Twig. Как только шаблонов становится больше десятка, появляются наследование и повторяющиеся блоки — ручной подход начинает мешать. Twig даёт автоэкранирование по умолчанию и наследование шаблонов из коробки.
- Что мерить. Если страниц много и они тяжёлые — включите OPcache в
php.ini(opcache.enable=1). Include шаблонов кешируется как обычный PHP-код, и рендеринг ускоряется без изменений в вашем коде.
Кеширование готового HTML — отдельная тема, здесь мы разбирали только сам механизм рендеринга.
Инфобез и 152-ФЗ: Аудит политик конфиденциальности, правила сбора cookie.
Спросить за 20 ₽Источники: PHP Manual: htmlspecialchars, PHP Manual: Output Control (ob_start, ob_get_clean)
Частые вопросы
А если у меня PHP 7.4, код заработает?
Чем это отличается от простого include шаблона напрямую?
Нужно ли экранировать данные, которые я сам вписал в код и уверен, что там нет тегов?
Не сработало: вижу Warning об undefined variable в шаблоне. Что делать?
Безопасно ли это для продакшена?
Можно ли так делать вложенные шаблоны, например шапку и подвал?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.