Создание движка шаблонов на PHP — Рендеринг и Эхо
Давайте создадим крошечный движок шаблонов для 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 проектов.
Приведённый выше шаблон можно рендерить с помощью нашего движка:
$stencil->render('hello', [
'name' => 'Ryan'
]);
В следующей стать мы реализуем поддержку партиалов, что позволит отделить общие части шаблонов и использовать их в нескольких местах.
Rendering(шаблонизация) на чистом PHP без особых зависимостей?
Нужен пример(кусок кода) шаблонизатора на чистом php. То что мы вызываем в контроллере, где выбираем шаблон, и передаем переменные в представление. Решения из фреймворков имеют зависимости, они в данном случае недопустимы
/** * Renders template * * @param string 0 Name of your template * @param mixed 1 Data passed to template * @return string */ function template() < extract(func_get_arg(1)); ob_start(); if (file_exists(func_get_arg(0))) < require func_get_arg(0); >else < echo 'Template not found!'; >return ob_get_clean(); > // Usage echo template('30.php', ['hello' => 'world!']);
Хотя в целом идея использования чистого PHP в качестве языка для шаблонов дурно пахнет. Советую Smarty.
Андрей: это зависит от вашей реализации. Я люблю ООП, поэтому сделал бы абстрактный класс, в котором вызов рендеринга шаблона был в виде отдельного метода.
Все зависит от того, какие цели вы преследуете и здесь нет четкого ответа.
У вас есть роутинг? Как он реализован? У вас много файлов или единая точка входа? Нужна ли шаблонизация для почты или нет?
Кстати, никто не мешает вам делать такие вещи:
echo template('30.php', [ 'leftPanel' => template('leftPanel.php', left_panel()), 'central' => template('central.php', central()), 'aside' => template('aside.php', aside()) ]);
Philipp T: Роутинг имеется, статический. Точка входа единая. Зависимостей не должно быть потому что эту систему в будущем будут использовать просто юзеры, которые понятия не имеют что такое composer.
public $layout = "main"; public function template($content) < return require_once Logic::get() . "/views/layouts/" . $this->layout . ".php"; > public function render($view, Array $params = NULL) < if ($params !== NULL) < extract($params); >ob_start(); if ( file_exists( Logic::get() . "/views/".$view.".php" ) ) < require_once Logic::get() . "/views/".$view.".php"; >return ob_get_clean(); >
вызов в контроллере такой
$this->view->layout = 'news'; $this->view->template( $this->view->render( 'site/news/index', ["model" => $model, "msg" => $this->msg] ) );
Из этого хотелось бы слепить что то получше.
class View < public $data = []; // все данные тут public $template = 'default'; // а здесь фрагмент пути к шаблону // такой конструктор позволяет нам быстро задавать шаблон без лишних движений public function __construct($template = null) < if (!empty($template)) < $this->template = $template; > > // просто построить полный путь к шаблону protected function templatePath() < // понятия не имею, что из себя представляет Logic::get() // предполагаю группа представлений return Logic::get() . "/views/" . $this->template . ".php"; > // обработать переменные и вернуть результат public function render() < extract($this->data); ob_start(); if (file_exists($this->templatePath())) < require $this->templatePath(); > else < echo 'Template not found!'; >return ob_get_clean(); > >
// создаем представление для блока новостей $newsBlockLayout = new View('layouts/news'); $newsBlockLayout->data['news'] = NewsModel::getRecentNews(); // создаем представление для блока последних записей из блога $recentPostsBlockLayout = new View('layouts/recentPosts'); $recentPostsBlockLayout->data['posts'] = BlogModel::getRecentPosts(); // а теперь берем представление для нашей главной страницы $mainPageView = new View('main'); $mainPageView->data['news'] = $newsBlockLayout->render(); $mainPageView->data['posts'] = $recentPostsBlockLayout->render(); echo $mainPageView->render(); // вместо отдачи в браузер можно полученный результат отправить по почте
Андрей: Composer это часть сборки приложения, вы что собираетесь поставлять юзерам не собранное приложение?
Philipp T: да, вы реализовали в тысячу раз лучше чем было. У меня еще один вопрос, как лучше сделать отдачу главного шаблона в браузер? в index.php сделать что то вроде echo call_user_func( [new View, ‘render’] ); ? но тогда это будет постоянно новый объект.
Константин Грачев: там где это приложение будет использоваться, нет доступа в интернет. Что то вроде корпоративной сети. Так что смысла от него нет
Андрей: ещё раз говорю, composer используется для сборки приложения. В вашу корпоративную сеть вы должны отправить уже собранноё приложение, со всеми зависимостями. Если для доставки вы используете git, ничего не мешает поместить в git и папку vendor.
Philipp T: либо в базовом виде создать свойство $content и класть в него render(). и вызывать в представлении как то так
A simple HTML renderer in PHP
Occasionally, projects are done that do not require usage of feature-rich frameworks or libraries. They usually have a short list of requirements and are built on top of simple infrastructure. Such projects are often prototypes that may become a complex system sometime in the future. But, until that happens, as a first step, a quick demo will be assembled and it needs to be as stable as possible. This post describes one possible way to go.
Let’s start with the top-level rendering function. It will output document doctype information and its root tags. Whole content will be rendered in between by the lambda (or anonymous) function passed in as the only parameter: $contentRenderer. A lambda function can contain other lambda functions and thus form a rendering tree that will result with well-formed HTML markup. Simple as that! 🙂
function renderHtml($contentRenderer = null) < echo "n"; echo ''; if ($contentRenderer) $contentRenderer(); echo ''; >
HTML tags can have attributes, so it’s important to make place for them as the first few parameters of a rendering function. Of course, only attributes needed in project’s scope should be listed. Therefore, even though the element can have a handful of parameters, only “href” (as mandatory) and “target” (the optional one) have been picked and will be used. Again, element’s body will be rendered by an outside lambda function.
function renderA($href, $target = '', $contentRenderer = null) < echo '' if ($contentRenderer) $contentRenderer(); echo ''; > function prepareAttribute($name, $value = '', $is_mandatory = true) < $attribute = ''; if ($is_mandatory || $value || $value === 0 || $value === '0') < $attribute = ' '.$name.'="'.htmlspecialchars($value).'"'; >return $attribute; >
A new utility function, prepareAttribute(), has been introduced in this example. It takes attribute name and value and concatenates them in the =”” form. If an attribute is marked as optional (that is, the $is_mandatory flag is set to false), its construction will be skipped unless a non-empty value is provided (which includes zero either as a number or a string). This will avoid having optional parameters rendered in the =”” form and hence leave the final markup as clean as possible.
Now that basic concepts have been explained, here’s an example of a complete page rendering tree. It demonstrates how lambda functions are nested in each other and how they can be leveraged to render dynamic data loaded from DB or some other source.
); renderBody(function() < renderH1('Under Construction'); renderImg('http://www.mysite.com/images/logo.png', 'My Site Ltd.'); renderP(function() < echo htmlspecialchars('This site is still being constructed. '); renderBr(); renderA('http://www.mysite.com/', '', function() < echo htmlspecialchars('>Visit My Site's official website!'); >); renderA('http://www.some-other-site.com/', '_blank', function() < echo htmlspecialchars('>Get more information here.'); >); >); // load and render some text dynamically $textItems = loadTextItems(); foreach ($textItems as $item) < renderP(function() use ($item) < echo htmlspecialchars($item); >); >); >); >); ?>
Since lambda rendering functions are expected parameterless and as such simply executed, parameter passing needs to be done via closures. Where necessary, a rendering function can be linked to its calling scope by binding to that scope’s variables, thus forming a closure. Such variables can be used inside the function normally.
Naturally, this approach has its limits and it should be deprecated as project grows. It is intended for quick prototyping or for simple projects that don’t require heavy machinery. Once development starts “for real”, the code should switch to one of (many) PHP frameworks that can do a better job of rendering HTML markup.