IMP Templates Простая функциональная шаблонизация в PHP Interlabs 2 октября 2014 1 / 37
Nov 28, 2014
IMP TemplatesПростая функциональная шаблонизация в PHP
Interlabs
2 октября 2014
1 / 37
О чем речь
• о разных подходах к шаблонизации• о шаблонизации в новом фреймворке• о структурировании шаблонов• о роли верстальщика в разработке сайта
2 / 37
Шаблонизация в PHPPHP PHP-файлы + include() и разнообразные
вариации на эту тему
vs
Handlebars клон известного шаблонизатора JavaScript,теоретически можно унифицировать шаблоны сJavaScript, на практике — маловероятно
Twig клон питоновского шаблонизатора, стандартSymfony, «давайте сделаем все красиво»
. . . еще много вариантов «более лучший язык шаблонизации»3 / 37
Языки шаблонизации«Верстальщику достаточно знать язык шаблонов».
Цена вопроса:
• объемная реализация (Twig > 7.5 CLOC)• необходимость отдельной стадии компиляции шаблона• как следствие необходимость кеширования шаблона• ограничения языка описания шаблона• сложный API для написания расширений
Но ведь PHP уже язык шаблонизации?4 / 37
PHP как шаблонизатор
Неэстетично. . .
• отсутствие четкой структуры описания шаблона• отсутствие разделения данных между шаблонами• отсутствие готовых средств структурирования шаблонов(блоки, наследование и т.д.)
• можно писать логику приложения непосредственно вшаблоне (но не нужно)
. . . зато дешево, надежно и практично.5 / 37
Шаблоны в новом фреймворкеПочему не Twig и что мы вообще хотим:
• простое в реализации решение• без промежуточной фазы парсинга шаблонов,обязательного кеширования и т.д.
• PHP уже шаблонизатор, еще один язык не нужен• нужна возможность структурирования шаблонов (блоки,наследование, вызов)
• необходима изоляция данных времени выполнения междушаблонами
imp\text\templateфункциональные (типа) шаблоны
6 / 37
imp\text\template• базовая реализация — около 300 строк• функциональные шаблоны: каждый шаблон — замыкание• определение и выполнение шаблона разделены• код шаблона — PHP, свой парсинг и компиляция не нужны• данные периода выполнения разделены• наследование, блочная структура, переопределение• с точки зрения структурирования напоминает Twig
<?php $this->is(function (array $args, $yield) { ?><p>Hello, <?= $args[’name’] ?></p><?php }) ?>
7 / 37
API// Определение шаблона:
$this->is(function (array $args, $yield) { ... } )$this->is($parentTemplate, function (array $args, $yield) { ... } )$this->in($areaName, function (array $args, $yield) { ... } )$this->flags($type, array $flags)$this->defaults(array $defaultArgs);
// Тело шаблона:
$this->area($area, function (array $args, $yield) { ... } )$this->getFlags($type);
// Вызов шаблона:
$bundle->call($template, array $args);$template(array $args);$template->render(array $args);
8 / 37
TemplateBundle• каждый шаблон — отдельный файл в каталоге шаблонов• имя шаблона соответствует пути в этом каталоге• несколько каталогов шаблонов можно объединять —используется первый найденный
• все шаблоны приложения — TemplateBundle
use imp\text\template\TemplateBundle;
$bundle = new TemplateBundle([’tpl/theme’, // - тема оформления’tpl/default’ // - шаблоны по умолчанию
]);
9 / 37
Загрузка шаблона
$front = $bundle[’site.page.front’];$form = $bundle[’site.form’];$menu = $bundle[’site.menu’];
tpl/theme/site/page/
front.phtml <- site.page.frontform.phtml <- site.form
tpl/default/site/form.phtmlmenu.phtml <- site.menu
10 / 37
Template• объект класса imp\text\template\Template• создается и кешируется экземпляром TemplateBundle• загружает один или несколько файлов определения• выполняемый объект, выполнение формирует результат• данные передаются при выполнении• загружается только один раз• может выполняться многократно с различными данными
$page = $templates[’site.page.front’];$page([ ’page.title’ => ’Test page’ ]);$page([ ’page.title’ => ’Another page’ ]);
11 / 37
Определение шаблона// test.phtml<?php $this->is(function (array $args, $yield) { ?><p>Hello, <?= $args[’name’] ?></p>
<?php }) ?>
• выполняется в контексте объекта шаблона• is() определяет содержимое шаблона• любой вывод за пределами is() игнорируется• $args — данные шаблона при его вызове• $yield используется при наследовании шаблонов• внутри замыкания — разметка, возможно, дополнительныепеременные, стараемся минимизировать код
12 / 37
Контекст определенияВ момент определения доступны переменные, которые можнопередать в замыкание:
• по умолчанию: template (он же $this), $bundle• дополнительно — набор пользовательских переменных
$bundle = new TemplateBundle($path, [’app’ => $application, // - пользовательский контекст
]);
// template.phtml
<?php $this->is(function ($args, $yields) use ($bundle, $app) { ?>...<?php }) ?>
13 / 37
Наследование шаблонаБазовый шаблон вызывает производный в виде $yield:
// page.phtml — базовый шаблон, произвольная страница<?php $this->is(function ($args, $yield) { ?><html><body>
<div class="head">...</div><?php $yield($args); ?><div class="foot">...</div>
</body></html><?php }) ?>
// front.phtml — производный шаблон, главная страница<?php $this->is(’page’, function ($args, $yield) { ?><div class="showcase">..</div><div class="news">...</div><?php }) ?>
14 / 37
Наследование шаблона
• устанавливается передачей в качестве первого аргументаis() имени родительского шаблона
• родительский шаблон должен вызывать $yield() внужном месте
• загружаются несколько определений, но объект шаблонавсе равно один
• поэтому отдельно загруженный родительский шаблон —отдельный шаблон
Выполнение сверху вниз, отродительского к производному
15 / 37
Области выводаОпределение в базовом шаблоне, заполнение в производных.
// родительский шаблон html.phtml<?php $this->is(function ($args, $yield) { ?>
<html><head><script>
<?php $this->area(’script’, $args) ?></script>
</head><body><?php $yield($args) ?></body></html>
<?php }) ?>
// производный шаблон news.phtml<?php $this->is(function (array $args, $yield) { ?>
<h1>News...</h1><?php }) ?><?php $this->in(’script’, function (array $args, $yield) { ?>
require([’site/news’], function (news) { ... });<?php }) ?>
16 / 37
Области вывода<?php $this->area(’sidebar’, $args) ?>
<?php $this->in(’sidebar’, function (array $args, $yield) { ?><p>Перед содержимым области родительского шаблона.</p><?php $yield($args) ?><p>После содержимого области родительского шаблона.</p>
<?php }) ?>
• $yield — вызов генерации содержимого областиродительского шаблона
• $args — массив параметров блока, как правилопередается без изменений
Выполнение снизу вверх, отпроизводного к родительскому шаблону
17 / 37
ФлагиНаборы значений, задаваемые производными шаблонами.
// html.phtml$this->is(function (array $args, $yield) { ?>..<script>
require([<?= "’" . implode("’,’", $this->getFlags(’require’)) . "’" ?>],function () {});
</script>...
// page.phtml<?php $this->flags(’require’, array(’common’)) ?><?php $this->is(’html’, function(array $args, $yield) ...
// news.phtml<?php $this->flags(’require’, array(’news’, ’social’)) ?><?php $this->is(’page’, function (array $args, $yield) ...
// Результат для news:<script>
require([ ’common’, ’news’, ’social’ ], function () {});</script>
18 / 37
Флаги: использование• часть определения шаблона, устанавливаются призагрузке, не меняются (многократном выполнении)
• значения по умолчанию — просто true• можно задавать произвольные значения, еслииспользовать ассоциативный массив
• можно получить не только имена, но и значения спомощью дополнительного признака в getFlags()
• порядок следования — от родительского шаблона кпроизводным
<?php $this->flags(’values’, array(’f1’ => 1, ’f2’ => 2)); ?>
<?php foreach ($this->getFlags(’values’, true) as $flag => $value) ... ?>
19 / 37
Выполнение шаблона• шаблон = выполняемый объект• выполнение шаблона приводит к генерации егосодержимого без буферизации
• если нужен строковый результат — метод render()
Используя набор шаблонов, например, из другого шаблона:<?php $bundle->call(’template’, [ ’title’ => ’test’, ... ]); ?>
// Или индивидуально вызывая объект:$template = $bundle[’template’];$template([ ’title’ => ’test’, ’...’ ]);
// Если результат нужен в виде строки:$text = $template->render([ ’title’ => ’test’, ... ]);
20 / 37
Данные шаблона
• шаблон вызывается с набором параметров $args• тип данных набора определяется типом аргументафункции шаблона
• в большинстве случаев — просто массив• рекомендуется использовать плоские массивы бездополнительных уровень вложенности
• иерархия элементов (если необходимо) — в имени ключа(page.meta.title и т.д.)
• в зависимости от ситуации, иногда выгоднее простопередать в вызываемый шаблон параметры вызывающего,иногда — сформировать новый массив параметров.
21 / 37
Вспомогательные функцииШаблонов мало, нужны вспомогательные функции (helpers).
• вызываются много (еще много, много) раз• должны работать максимально быстро, динамическаядиспетчеризация вызова — плохая идея
• группируем функции в простые процедурные классы
<?php use imp\ext\HTML; ?>
<?php $this->is(function(array $data, $yield) { ?><p>Hello, <?= HTML::escape($data[’name’]) ?></p>
<?php }) ?>
22 / 37
Структурированиешаблонов
23 / 37
Главный антипаттернСэндвич привет Битриксу
<?php include(’top.phtml’); ?><?= $content ?><?php include(’bottom.phtml’); ?>
Два файла вместо одного, неудобно сопровождать, тяжелоискать ошибки, невозможно использовать правильнуюабстракцию, все сводится к include.
Не надо так делать.24 / 37
Иерархия шаблонов• начинаем с базового шаблона страницы — заголовки,меты, скрипты
• выполняем специализацию шаблона для различныхразделов сайта
• (без необходимости) не делаем различий на уровнекорневого шаблона для главной и рабочих страниц
• добавляем специфический контент в производныхшаблонах с помощью блоков
• где нужно, отдельно вызываем шаблоны компонент• меньше условий, больше наследования (да, наследования)
В идеале — простая иерархическая структура, каждыйпроизводный шаблон дополняет, но не меняет родительский.
25 / 37
Пример структурыsite/ - шаблоны сайта
html - общий шаблон (js, css, meta)page(html) - страница сайта (+header, +footer, +menu, +sidebar)
pages/ - шаблоны страниц сайта, загружаются контроллеромfront(page) - главная страницаsecondary(page) - рабочая страница (+breadcrumbs)news(secondary) - (+календарь в одном из блоков)catalog(secondary) - (+дополнительная навигация и баннер в блоке)product(catalog) - (+блок «смотри также» в sidebar)
news/calendar - компонент на странице news
showcase - витрина, на главной и страницах каталогаshowcase/
front - вариант витрины для главнойcategory - вариант витрины для каталога
...
26 / 37
Шаблоны компонентов
• можно использовать наследование• можно использовать блоки (без фанатизма , )• без inline-скриптов (для этого есть flight-компоненты)• если в родительском шаблоне $yield вызывается в цикле,производный шаблон может переопределятьповторяющуюся часть (например, для витрины)
• шаблон компонента полностью изолирован отсодержащего его шаблона и это правильно
• поэтому может быть выведен отдельно на страницеруководства по стилям
27 / 37
Процесс разработкипрограммирование и верстка
28 / 37
Наша проблема• верстальщик — статические прототипы• программист — все остальное
Слабая интеграция между статическими прототипами и сайтом,слабая вовлеченность верстальщика в опубликованный проект.
Долой статические прототипы!
• верстальщик работает с шаблонами• не готова серверная часть — используем тестовые данные• отдельный ресурс приложения для тестирования шаблонов
29 / 37
TemplateResource// Минимальное приложение для разработки шаблонов:include(’../lib/autoload.php’);
use imp\text\template\app\TemplateResource; // - шаблонный ресурсuse imp\text\template\TemplateBundle; // - набор шаблонов
use imp\http\app\Application; // - приложениеuse imp\http\app\Services; // - константы сервисовuse imp\http\Request; // - запросuse imp\http\server\Server; // - обработка запроса
$app = new Application();$app->service(Services::TEMPLATE, function (Application $app) {
return new TemplateBundle(__DIR__ . ’/../tpl’); // - шаблоны в tpl})->path(’proto’, function ($request, $match) use ($app) {
$app->rmatch(TemplateResource::SLUG,new TemplateResource($app, __DIR__ . ’/../proto’));
});
Server::send($app(Server::request()));30 / 37
PrototypeApplication
А попроще нельзя? Можно, используя готовый класс:
include(’../ext/imp/lib/autoload.php’);
use imp\http\server\Server;use imp\ext\template\app\PrototypeApplication;
Server::run(new PrototypeApplication());
31 / 37
Генерация прототиповproto/front.php
<?php return [’site.pages.front’, // <- первый элемент — используемый шаблон’document.title’ => ’Front page’,’document.description’ => ’Front page description’,’cart.count’ => 2,’cart.total’ => 2500,’page.menu-top’ => [[ ’title’ => ’Home’, ’active’ => true ],[ ’title’ => ’My Account’ ],...
];
http://site.com/proto/front.php32 / 37
Определение прототипа
• PHP — возможность программной генерации данных• выполняется в контексте ресурса приложения→ можноиспользовать сервис данных и т.д.
• специальный файл _.php — вызывается перед загрузкойлюбого файла каталога
• набор вспомогательных методов Data: генерация объектовданных, случайные наборы данных и т.д.
• нет реализации — можно начинать с простых объектов• по мере реализации — заменяем на настоящие данные
33 / 37
Прототипы• одни и те же шаблоны для прототипов и реальных страниц• могут соответствовать отдельным страницам сайта или ихразным вариантам
• могут состоять из отдельных компонентов страниц• могут образовывать руководство по стилю, развивающеесявместе с сайтом
• позволяют отлаживать JavaScript-компоненты• использование runway и Flight устраняет различие междупрототипами и реальными страницами.
протосайт ,→ реальный сайт
34 / 37
Данные прототипов• реальные данные всегда лучше• нет — начинаем с тестовых, потом адаптируем шаблон
use imp\fake\Data;
use imp\fake\Data;return [
’catalog.items’ => Data::collection(Data::make(Data::rand(10, 20), function () {
return Data::entity([’id’ => Data::id(),’title’ => Data::any([ ’Костюм’, ’Рубашка’, ’Блейзер’, ’Поло’ ]),’size’ => Data::any([ ’S’, ’M’, ’L’, ’XL’, ’XXL’ ]),’price’ => Data::rand(1000, 5000)
]);});
)];
35 / 37
imp\fake\Data• Data::collection(), Data::entity() — коллекции исущности, c т.з. шаблона не должно быть большой разницысо слоем модели
• Data::any(), Data::make() — возвращают массивы, измассива легко сделать коллекцию
• Data::make() — генерация набора данных по исходномунабору значений или диапазону
• Data::any() — N различных значений из набора (Флойд)
$some = Data::any(3, Data::make([ 1 => ’one’, 2 => ’two’, 3 => ’three’, 4 => ’four’, 5 => ’five’ ],function ($value, $key) { return "$key. Item $value"; }
));
36 / 37
Итого• можно писать структурированные шаблоны на PHP• функциональный шаблонный движок не обязательнобольшой и сложный
• анемичная модель хорошо подходит для шаблонизации• верстальщик — это вообще-то не только HTML, но еще ишаблонизация и JavaScript
• верстка и реализация функционала могут выполнятьсяпараллельно
• актуальный Style Guide с минимумом дополнительныхзатрат возможен даже в условиях конвейера
privatehttps://bitbucket.org/interlabs/imp/
37 / 37