Это короткая заметка о том, как можно организовать использование скинов для брендирования страниц в Twig на примере Symfony. Это решение не привязано к Symfony. По аналогии можно реализовать скины в любом проекте, использующем Twig.
У вас интернет-магазин, онлайн-кинотеатр, афиша мероприятий, каталог телепередач и т.д. В один прекрасный день вам поступает задача по брендированию страницы каталога для привлечения пользователей и повышения продаж под какую-то акцию. Как это сделать, если для движка все продукты в каталоге равнозначны?
Самое простое решение — это захардкодить ID продукта из каталога. Можно добавить условие в шаблон и накладывать на body
тег дополнительный CSS класс, по которому потом стилизовать страницу в общих стилях.
{% block body_class -%}
{{ parent () }} product-{{ product.id }}
{%- endblock %}
body.product-12345 {
# custom style
}
Стилями можно сделать очень многое, особенно если вы используете flex, но стили не всесильны. Иногда возможностей стилей недостаточно для брендирования страницы и необходимо изменить HTML-разметку (вёрстку) страницы, и делается это по аналогии со стилями.
{% if product.id == 12345 %}
{# custom code #}
{% else %}
{# original code #}
{% endif %}
Решение, конечно, некрасивое, но для единичного случая вполне приемлемо (YAGNI и KISS). Сказано — сделано. Закоммитил, запушил, забыл.
Проходит неделя, две, месяц — и к вам снова прилетает задача по брендированию уже другого товара. Вы делаете по аналогии с прошлым решением, коммитите и забываете. Потом прилетает ещё задача и ещё. Потом до менеджеров продаж или руководства доходит, что брендирование можно продавать за хорошие деньги. Вы и оглянуться не успели, как уже завалены всевозможными задачами по брендированию и правкам оформления. Становится очевидно, что надо что-то менять (DRY).
Лучшим решением в этом случае будет добавить дополнительное поле в сущность с названием скина. Кому как не сущности лучше знать, забрендирована она или нет, и если забрендирована, то как. Добавив поле в сущность, мы убираем всю лишнюю логику из шаблонов и стилей, и остаётся только вопрос организации стилизации по полю скина из сущности.
Рассмотрим страницу продукта с шаблоном product/show.html.twig
. Заведем новую структуру папок product/skin/<skin_name>/
, где <skin_name>
— это значение поля скин у сущности. В качестве скина по умолчанию возьмём default
и переместим наш шаблон страницы продукта по соответствующему адресу product/skin/default/show.html.twig
. Теперь осталось только поправить контроллер, и можно пользоваться.
public function show(Product $product): Response
{
return $this->render(sprintf('product/skin/%s/show.html.twig', $product->skin), [
'product' => $product,
]);
}
Используйте наследование, чтобы не переписывать с нуля каждый раз весь шаблон просмотра продукта, когда нужно поменять только один блок.
{# product/skin/custom_skin/show.html.twig #}
{% extends 'product/skin/default/show.html.twig' %}
{% blocksome_block %}
{{ parent() }}
{# customise something #}
{% endblock %}
Кроме страницы просмотра самого товара, бывают другие с ней связанные. Например, страница отзывов, вопросы и ответы, хронология для фильмов и сериалов, актерский состав и т. д. На них часто есть блоки, общие для всех страниц продукта. В таких случаях обычно создают общий лейаут, который тоже можно загружать из скинов.
{# product/skin/default/show.html.twig #}
{% extends 'product/skin/' ~ product.skin ~ '/layout.html.twig' %}
{# ... #}
{# product/skin/custom_skin/layout.html.twig #}
{% extends 'product/skin/default/layout.html.twig' %}
{# ... #}
В результате мы получаем такую схема расширения:
product/skin/<skin_name>/show.html.twig
?Ўproduct/skin/default/show.html.twig
?Ўproduct/skin/<skin_name>/layout.html.twig
?Ўproduct/skin/default/layout.html.twig
?Ў- ...
У такого подхода есть один недостаток — вам нужно повторять структуру файлов из скина по умолчанию в каждом новом скине.
default
layout.html.twig
show.html.twig
qa.html.twig
similar.html.twig
first_skin
layout.html.twig
show.html.twig
qa.html.twig
similar.html.twig
second_skin
layout.html.twig
show.html.twig
qa.html.twig
similar.html.twig
Особенно это неприятно, если нужно переопределить только пару строчек в лейауте. Решить эту проблему можно с помощью функций Twig.
public function show(Product $product, Twig $twig): Response
{
$template = $twig->resolveTemplate([
sprintf('product/skin/%s/show.html.twig', $product->skin),
'product/skin/default/show.html.twig',
]);
$content = $template->render([
'product' => $product,
]);
return new Response($content);
}
Метод resolveTemplate()
принимает список шаблонов и поочередно пытается их резолвить. Таким образом будет рендерится шаблон страницы товара из скина, если он есть, а если нет — то шаблон по умолчанию. Код получился громоздкий, но ребята из Symfony не хотят его сокращать, поэтому придется писать так или создавать расширение в своем проекте.
Шаблон product/skin/default/show.html.twig
расширяет лейаут скина, и мы вынуждены в каждом скине создать как минимум шаблон layout.html.twig
. Решить эту проблему можно, передав Twig тегу extends
список шаблонов, и тогда extends
будет внутри вызывать resolveTemplate()
и расширять только существующий шаблон. Это вообще избавит нас от необходимости создавать какой-либо шаблон в папке со скином.
{# product/skin/default/show.html.twig #}
{% extends [
'product/skin/' ~ product.skin ~ '/layout.html.twig',
'product/skin/default/layout.html.twig',
] %}
{# ... #}
Ещё хорошим решением будет вынести стили конкретных скинов в отдельные файлы и подключать только на брендированной странице. Так мы не будем засорять основные стили мусором, который применяется всего на паре страниц. Если кому интересно, то могу в отдельной статье рассказать, как настроить gulp для сборки большого количества скинов вместе и по отдельности. У нас на проекте таких скинов более 300 штук.
pbatanov
Главное не перестараться. Я в далеком 2014 году упоролся настолько, что у меня была сущность «Сайт», у которого можно было выбрать и настроить тему (less переменные бутстрапа) и вывести весь сайт в этом скинчике. Работало поверх этой штуки github.com/braincrafted/bootstrap-bundle и похожего подхода как в статье, только выбирался базовый шаблон для всего ответа в твиге. Но было задорно иметь одну инсталляцию, а не 7.
ghost404 Автор
Вы говорите об использовании одного приложения, одного инстанса на несколько сайтов как в Битрикс? Такой подход был популярен в начале нулевых. Я и сейчас иногда встречаю легаси проекты использующие такой подход.
На мой взгляд, подход с настройкой LESS переменных применим для CMS и готовых движков форумов.
В статье говорится о брендировании. Это несколько другой подход. Брендирование бывает всего сайта. Его проще захардкодить в шаблонах если не нужно будет его отключить чётко по времени.
pbatanov
Подход точно такой же, просто на другом уровне (уровне конечного блока «Продукт»), который выводится. «Витрина» (афиша, каталог) принципиально тот же самый CMS, только с другой формой представления данных (в некоторых случаях). Поле ".skin" можно воткнуть на любом уровне — продукта, категории, раздела сайта. В моем случае я дошел до уровня «сайта». В остальном все очень похоже.