Создание движка шаблонов на PHP — Рендеринг и Эхо
htmlspecialchars().Прежде чем начнём писать код, необходимо позаботиться о самой важной части любого проекта по программированию — дать имя проекту. Я назову его Stencil
Сами шаблоны будут на простом PHP. Мы не будем создавать какой-либо специальный синтаксис, такой как Twig или Blade, мы сосредоточимся исключительно на функциональности шаблонов.
Начнём с создания основного класса.
class Stencil
{
public function __construct(
protected string $path,
) {}
}Классу Stencil необходимо знать, где находятся шаблоны, чтобы они передавались через конструктор.
Чтобы на самом деле отображать шаблоны, понадобиться метод render().
class Stencil
{
// ...
public function render(string $template, array $data = []): string
{
// ?
}
}Метод render() принимает имя шаблона и массив данных переменных, которые будут доступны внутри указанного шаблона.
Теперь нужно сделать три вещи:
- Сформировать путь к запрашиваемому шаблону.
- Убедится, что шаблон существует.
- Отобразить шаблон с предоставленными данными.
class Stencil
{
// ...
public function render(string $template, array $data = []): string
{
$path = $this->path . DIRECTORY_SEPARATOR . $template . '.php';
if (! file_exists($path)) {
throw TemplateNotFoundException::make($template);
}
// ?
}
}Первые два пункта списка легко сделать. Stencil будет искать только .php файлы, поэтому формирование пути — этого всего лишь случай объединения строк. Если запрошенный шаблон содержит какие-либо разделители каталогов, будут обрабатываться вложение шаблонов в каталоги.
Если файл шаблона не существует, выбрасываем исключение TemplateNotFoundException.
Чтобы охватить третий пункт в списке, фактически отображающий шаблон, нужно создать новый класс Template. В нём будут размещены все методы, доступные для шаблона, и будет обрабатываться реальная сторона рендеринга.
class Template
{
public function __construct(
protected string $path,
protected array $data = [],
) {}
public function render(): string
{
// ?
}
}class Stencil
{
// ...
public function render(string $template, array $data = []): string
{
$path = $this->path . DIRECTORY_SEPARATOR . $template . '.php';
if (! file_exists($path)) {
throw TemplateNotFoundException::make($template);
}
return (new Template($path, $data))->render();
}
}Чтобы получить отображаемый шаблон в виде строки, мы воспользуемся буфером вывода PHP. Когда вызывается ob_start(), PHP начинает захватывать всё, что приложение пытается вывести (эхо, HTMl и т.д.).
Мы можем получить вывод как строку, а затем прекратить захват вывода с помощью ob_get_clean(). Комбинация этих двух функций и include позволит оценить файл шаблона.
class Template
{
// ...
public function render(): string
{
ob_start();
include $this->path;
return ob_get_clean();
}
}Это обработает рендеринг, но не даст шаблону доступ к данным переменных, хранящихся внутри $data. PHP, будучи замечательным языком, предоставляет ещё одну функцию, extract(), которая принимает массив пар ключ-значение.
Ключ для каждого элемента в массиве будет использоваться для создания новой переменной в текущей области видимости с использованием ассоциированного значения. Поскольку include и его родственники всегда выполняют PHP-файл в текущей области видимости, шаблон сможет получить доступ к извлечённым переменным.
class Template
{
// ...
public function render(): string
{
ob_start();
extract($this->data);
include $this->path;
return ob_get_clean();
}
}Идеально! Теперь мы можем рендерить шаблон и предоставить ему доступ к предоставленным переменным. Есть одна вещь, которую мы не учли… если бы мы захотели создать несколько переменных внутри метода render(), наш шаблон также смог бы получить к ним доступ. Это не то, что мы хотим!
Для решения этой проблемы необходимо обернуть extract() и include/включить вызовы в немедленно вызываемое замыкание — таким образом, шаблон будет иметь доступ только к переменным внутри замыкания.
class Template
{
// ...
public function render(): string
{
ob_start();
(function () {
extract($this->data);
include $this->path;
})();
return ob_get_clean();
}
}Последняя часть головоломки — метод экранирования значений при их отображении. Замыкания наследуют $this, это означает, что наш шаблон сможет вызывать любой метод определённый в классе Template. Создадим метод e(), принимающий значение и экранирующий его с помощью htmlspecialchars().
class Template
{
// ...
public function e(?string $value): string
{
return htmlspecialchar($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}Таким образом, у нас есть небольшой движок шаблонов для наших PHP проектов.
<h1>
Hello, <?= $this->e($name) ?>!
</h1>Приведённый выше шаблон можно рендерить с помощью нашего движка:
$stencil->render('hello', [
'name' => 'Ryan'
]);И вывести следующий HTML:
<h1>
Hello, Ryan!
</h1>В следующей стать мы реализуем поддержку партиалов, что позволит отделить общие части шаблонов и использовать их в нескольких местах.