Создание движка шаблонов на PHP — Рендеринг и Эхо

Источник: «Build Your Own Template Engine in PHP - Rendering & Echo»
Давайте создадим крошечный движок шаблонов для 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() принимает имя шаблона и массив данных переменных, которые будут доступны внутри указанного шаблона.

Теперь нужно сделать три вещи:

  1. Сформировать путь к запрашиваемому шаблону.
  2. Убедится, что шаблон существует.
  3. Отобразить шаблон с предоставленными данными.
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, если хотите посмотреть на исходный код.

Дополнительные материалы

Предыдущая Статья

Понимание CSS медиа-запросов

Следующая Статья

API Аутентификация в Laravel