Это короткая заметка о том, как можно организовать использование скинов для брендирования страниц в 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 штук.