Создание простого шаблонизатора на PHP - часть 1
Недавно я писал несколько составных пакетов для работы с файлами и файловой системой: Affinity4File, Affinity4Config и Affinity4Concat. При работе над ними мне понадобилось протестировать их на файлах. Вначале мои тесты успешно выполнялись на компьютере, который работал под управлением Windows. Но при сохранении утилита TravisCI говорила мне, что пути не совпадали. Потому что разделители папок в адресах Windows и Linux не совпадали.
Затем я наткнулся на виртуальные файловые системы StreamWrappers, в частности vfsStream. Но не все файловые тесты могут быть переписаны с использованием vfsStream. Например, тесты, в которых класс/метод использует SplFileInfo:
$pattern = '/^test[wd-]*.txt$/';
$dir = 'tests/files/01/02';
$this->assertContainsOnlyInstancesOf(
'SplFileInfo',
$this->file->find($pattern)->in($dir)->get()
);
Как ко мне пришла идея
В vfsStream обработчики потоков используются разными способами. Изучая их, я заметил две интересные:
- Могут обрабатываться как потоки. Это значит, что они хранятся как переменные и могут быть изменены в памяти без необходимости изменения самого файл.
- После этого файл может быть подключен через include или require.
Поэтому мне в голову пришла идея написать простой шаблонизатор PHP, в котором файл шаблона открывается в потоке, изменяется и преобразуется в корректный PHP-код. А затем другой поток используется, чтобы смоделировать файл, который отображается через буфер вывода и функцию include. Таким образом исключаются временные файлы и легко могут быть созданы фильтры, делая синтаксис шаблона простым для изменения.
Синтаксис
Пример реализации механизма шаблонизатора находится здесь -Affinity4/Template. Он поддерживает PHP, поэтому синтаксис шаблонов не является обязательным. На это меня вдохновил Sass.
Вторая ключевая особенность – это то, что синтаксис шаблона не будет отображаться без применения механизма шаблонов или в случае ошибки компиляции потока в PHP. Это означает, что если вы решите использовать другой язык шаблонов или включите файлы напрямую, то при просмотре страницы вы не увидите блоков {{ var }} или @foreach(thingy in thingys), оставленных в коде.
Третьей особенностью является то, что синтаксис по умолчанию должен работать во всех IDE, текстовых редакторах и быть понятен всем, знающим HTML.
Чтобы шаблонизатор соответствовал второму и третьему требованию, нужно использовать комментарии HTML.
Вот пример:
<h1><!-- :title --></h1>
<!-- @if :show_list is true -->
<ul>
<!-- @each :item in :items -->
<li><!-- :item --></li>
<!-- @/each -->
<ul>
<!-- @/if -->
Недостаток заключается в том, что здесь больше кода. Но он может быть быстро написан, используя горячие клавиши комментариев в редакторе или IDE (обычно Ctrl + / или Cmd + /).
Достоинства шаблонизатора:
- Поддержку IDE/редакторов по умолчанию.
- Возможность использования чистого PHP-код. Это позволяет быстро усвоить механизм.
- Специфический язык шаблона может быть полностью проигнорирован любым, кто хочет работать только над разметкой.
Наш механизм шаблонов не будет включать в себя инструменты макетов или блоков, которые есть в Twig, Blade, Latte и т.д. Это важные средства, и я планирую реализовать их в будущем. Но нам они не нужны.
Структура проекта
Мы поместим всё в два класса: один для обработки правил синтаксиса (регулярные выражения), другой - для открытия файлов исходного кода, их компиляции в PHP, выделения переменных и вывода.
Звучит запутанно? Но с помощью vfsStream это становится тривиальной задачей по сравнению с традиционным методом создания лексеров/парсеров, токенизаторов и разработкой некоторой формы AST (абстрактного синтаксического дерева).
Мы используем менеджер зависимостей Composer, чтобы подключать vfsStream и загружать наши собственные классы.
composer require mikey179/vfsStream
Далее создадим в корне проекта папку под названием src. Внутри нее разместим два файла классов (Engine.php и Syntax.php). Структура папок должна выглядеть следующим образом:
template
|-- composer.json
|-- src
|-- Engine.php
|-- Syntax.php
|-- views
|-- home.php
Затем я записываю всё в файл index.php, чтобы увидеть правильность реализации. Для механизма шаблонов это достаточно просто. Поэтому можно сделать следующее:
Файл: index.php
<?php
use TemplateEngine;
require_once __DIR__ . '/vendor/autoload.php';
$template = new Engine;
$template->render('views/home.php', [
'show_title' => true,
'title' => 'Home'
]);?>
Чтобы получить рабочую версию шаблонизатора, нужно следующее:
- Метод прорисовки шаблона (render).
- Метод render должен получать путь к представлению в качестве первого аргумента.
- Необязательный массив параметров может передаваться в представление.
- Параметры должны быть выделены в переменные, если второй аргумент – не пустой массив.
- Загрузка представления в vfsStream для дальнейшей обработки.
- Включение обработанного (скомпилированного) «файла» в буфер вывода для отображения.
- Файл представления: views/home.php.
Сначала создадим наше представление с простым PHP кодом, чтобы убедиться, что переменные передаются в представление:
Файл: views/home.php
<?php if ($show_title) : ?>
<h1><?php echo $title; ?></h1>
<?php endif; ?>
Теперь можно сфокусироваться на простейшей реализации класса шаблонизатора на PHP (Engine.php).
Файл: src/Engine.php
<?php
namespace TemplateEngine;
use orgbovigovfsvfsStream;
class Engine
{
public function render($view, $params = [])
{
// 1. Выделить параметры при непустом массиве params
// 2. Если представление существует, включить его в буфер вывода
// 3. Если файл представления не найден, вывести исключение
}
}?>
Так мы получим рабочую версию метода render, хотя и без желаемой функциональности.
Перед тем, как двигаться дальше, нужно добавить папку src в автозагрузчик PSR-4, расположенный в файле composer.json:
Файл: composer.json
{
"autoload": {
"psr-4": {
"Template": "src/"
}
},
"require": {
"mikey179/vfsStream": "^1.6"
}
}
Теперь требуется повторно сгенерировать файлы автозагрузки и файл composer.lock:
composer dumpautoload
Реализуем логику получения представления для отображения с параметрами, которые мы передали.
Файл: src/Engine.php
<?php
...
class Engine
{
public function render($view, $params = [])
{
// 1. Выделить параметры при непустом массиве params
if (!empty($params)) extract($params);
// 2. Если наше представление существует, включить его в буфер вывода
if (file_exists($view)) {
ob_start();
include $view;
ob_get_flush();
} else {
// 3. Если файл представления не найден, вывести исключение.
throw new Exception(sprintf('Файл %s не найден.', $view));
}
}
}
?>
Запустите локальный сервер для PHP. Перейдите в корень проекта и откройте командную строку. В командной строке наберите:
php -S localhost:80
Вы увидите сообщение о том, что сервер запущен. Откройте браузер, перейдите по адресу http://localhost/ и вы должны увидеть:

Теперь мы знаем, что метод render будет работать, так как ожидается. В следующей части мы подключим vfsStream.