Егор Банин

Использование шаблонов для генерации HTML

PHP умеет встраиваться в HTML. Это очень классная возможность, которой, однако, не следует злоупотреблять. Смешивая код логики и код генерации HTML вы получите совершенно отвратительнй результат, за который вам будет стыдно, когда вы немного продвинетесь в изучении веб-разработки.

Чтобы получить отличный результат, генерацию HTML следует отделять от остальной логики приложения. Сделать это можно с помощью прекрасной возможности PHP подключать файлы.

Соглашения

Для начала надо условиться, что подключаемый файл будет больше HTML-файлом чем PHP. То есть PHP-код будет встраиваться в HTML, а не наоборот. Вся разметка будет написана как есть (а не через echo).

Следующее условие – использование альтернативного синтаксиса условных операторов и циклов. Это нужно для того, чтобы шаблоны было легко читать, ведь искать в простыне HTML-кода закрывающую фигурную скобочку очень сложно.

Кроме этого важно отказаться от сложных манипуляций с данными в шаблоне. Если данные надо подготовить для вывода, то это надо сделать перед подключением шаблона.

Вот небольшой пример такого шаблона.

<h1>Статьи</h1>
<?php foreach($posts as $post): ?>
    <div>
        <h2><?= htmlspecialchars($post->title) ?></h2>
        <div>
            <?= $post->htmlContent ?>
        </div>
    </div>
<?php endforeach ?>

Подключить такой шаблон можно так.

<?php

//...

$posts = $this->db->table('posts')->select();
require __DIR__ . '/posts.phtml';

Кроме прочего я использую расширение .phtml для файлов шаблонов, чтобы отличать их от других; Использую шорттег <?= (он предназначен специально для таких случаев и работает даже если шортеги отключены); Не ставлю точку с запятой перед закрывающим дескриптором PHP. Это необязательные условия отличного результата, но вы можете скопировать мой стиль.

Не забывайте, что текст и значения атрибутов надо экранировать. Функция htmlspecialchars отлично подходит для этого. Но учтите, что по умолчанию она не экранирует одинарные кавычки. Если вы используете двойные кавычки в HTML, то проблем не будет, а если одинарные, не забудьте заэкранировать их.

Избегайте подстановок переменных в скрипты, будьте внимательны с атрибутами, значения которых, могут быть не просто текстом.

<a href="<?= htmlspecialchars($link) ?>">клёвая ссылка</a>
<!-- если $link будет иметь значение 'javascript: alert('XSS')', то будет совсем не клёво -->

Получение HTML как строки, а не вывод его на экран немедленно

Простое подключение имеет несколько недостатков. Один из них заключается в том, что полученный HTML не всегда надо вывести немедленно. Решить эту проблему помогут функции контроля вывода. Обязательно разберитесь как они работают.

Теперь можно написать функцию, которая будет подключать и файл и захватывать его вывод в буфер.

<?php

/**
 * Подключение файла с буферизацией вывода
 */
function ob_include(string $file): string
{
    ob_start();
    require $file;
    return ob_get_clean();
}

Передача данных в шаблон

Функция ob_include сейчас не работает. Как только мы переместили require внутрь функции, область видимости в подключённом шаблоне стала такой же как внутри функции. Но это легко исправить с помощью extract.

<?php

/**
 * Подключение файла с буферизацией вывода
 */
function ob_include(string $file, array $params): string
{
    extract($params);
    ob_start();
    require $file;
    return ob_get_clean();
}

Теперь в функцию можно передать ассоциативный массив значения из которого будут доступны в подключаемом шаблоне.

<?php

//...

$posts = $this->db->table('posts')->select();
$html = ob_include(__DIR__ . '/posts.phtml', ['posts' => $posts]);
echo $html;

Финальный вариант шаблонизатора

Возможно вы уже обратили внимание, что в текущей реализации есть ошибка. Переменные $file и $params тоже попадают в область видимости шаблона. Кроме того, если что-нибудь передать в $params['file'], то $file перезапишется. Исправим это.

/**
 * Подключение файла с буферизацией вывода
 * @param string $file
 * @param array $params
 */
function ob_include(): string
{
    extract(func_get_arg(1));
    ob_start();
    require func_get_arg(0);
    return ob_get_clean();
}

Готово! Никаких лишних переменных. Используйте эту функцию как есть или переделайте в более подходящий вам вариант.

Примеры использования

Вызывать ob_include внутри шаблона, по-моему, не самая хорошая идея (хотя ничто не мешает вам сделать так), но можно передавать результаты одного вызова ob_include в параметры другого.

<?php

// ...

$posts = $this->db->table('posts')->select();
$postsHtml = ob_include(__DIR__ . '/posts.phtml', ['posts' => $posts]);
$html =  ob_include(__DIR__ . '/layout.phtml', ['content' => $postsHtml]);
echo $html;

При желании можно завернуть вызов ob_include в метод класса и реализовать интерфейс вроде следующего.

<?php

// ...

$posts = $this->db->table('posts')->select();
echo $layout // $layout инстанцирует какой-нибудь код уровнем выше
    ->setTitle('Публикации')
    ->addJs('/popup.js')
    ->addContent(__DIR__ . '/posts.phtml', ['posts' => $posts])
    ->render();

Заключение

Со временем вы познакомитесь с более мощными библиотеками шаблонизации для PHP, но этот простой подход ещё не раз выручит вас там, где нужно просто сгенерировать HTML-документ.