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