Создание движка шаблонов на 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>
В следующей стать мы реализуем поддержку партиалов, что позволит отделить общие части шаблонов и использовать их в нескольких местах.
Stencil имеет открытый исходный код и размещён на GitHub, если хотите посмотреть на исходный код.