Поговорим о том, как прекратить копипастить между проектами и вынести код в переиспользуемый подключаемый бандл Symfony 5. Серия статей, обобщающих мой опыт работы с бандлами, проведет на практике от создания минимального бандла и рефакторинга демо-приложения, до тестов и релизного цикла бандла.


В предыдущей статье мы вынесли в бандл основной код и шаблоны, настроили роутинг и подключение сервисов в Dependency Injection контейнер. В этой статье будем встраивать бандл в приложение-хост:


  • Интеграция шаблонов: 2 пути
  • Интеграция шаблонов: независимый модуль
  • Подключение стилей бандла в сборку
  • Интеграция шаблонов: встраивание в шаблоны хоста
  • Переопределение стилей и JS

Содержание серии

Часть 1. Минимальный бандл
Часть 2. Выносим код и шаблоны в бандл
Часть 3. Интеграция бандла с хостом: шаблоны, стили, JS
Часть 4. Интерфейс для расширения бандла
Часть 5. Параметры и конфигурация
Часть 6. Тестирование, микроприложение
Часть 7. Релизный цикл, установка и обновление


Если вы не последовательно выполняете туториал, то скачайте приложение из репозитория:
https://github.com/bravik/symfony-bundles-tutorial/
и переключитесь на ветку 2-basic-refactoring.


Инструкции по установке и запуску проекта в файле README.md.
Финальную версию кода для этой статьи вы найдете в векте 3-integration.


Интеграция шаблонов


Чтобы переиспользовать бандл, необходимо убрать его зависимости от окружения.


Наше приложение работает и используует шаблоны бандла. Однако шаблоны редактора мероприятий в бандле зависят от базового шабона base.html.twig, который принадлежит приложению-хосту:


{% extends 'base.html.twig' %}

Базовый шаблон определяет костяк HTML-кода страницы и размечает основные блоки, которые в свою очередь уже переопределяются шаблонами каждой из страниц. С одной стороны, редактору мероприятий нужен такой базовый шаблон, но с другой стороны у каждого приложения-хоста может быть свой базовый шаблон с отличным набором блоков. Как можно сделать шаблоны редактора независимыми?


Есть 2 пути: сделать полностью независимый модуль со своим собственным базовым шаблоном и оформлением, либо попытаться органично встроиться в имеющийся шаблон админки приложения.

Какой путь выбрать зависит от вашей задачи и степени, в которой вы готовы пожертвовать переиспользуемостью бандла.


Интеграция шаблонов: независимый редактор


Если мы не готовы жертвовать reusability, то редактор должен быть полностью независимым и иметь свой базовый шаблон. Для этого мы сделаем внутри бандла свой base.html.twig, а точнее скопируем из приложения хоста.


Удалим из скопированного шаблона все лишнее:


  • include menu.html.twig — в бандле нет меню
  • {{ encore_entry_link_tags('host-styles') }} и {{ encore_entry_script_tags('host-app') }} — это entry-файл стилей и скриптов хоста, собираемых webpack encore

После этого в шаблонах templates/editor/* бандла поменяем:


{% extends 'base.html.twig' %}
 на
{% extends '@Calendar/base.html.twig' %}

Теперь редактор мероприятий не зависит от шаблонов хоста. Проверим работает ли приложение.


Отлично, проект запускается и работает. Переходим в Редактор и видим… голый HTML список событий и голую HTML-форму события.


Подключение стилей бандла


Для оформления бандла мы используем SCSS, а для их сборки — Symfony Encore (обертку Symfony, упрощающую работу с Webpack).


В шаблон ассеты подключаются с помощью entry-файлов. Entry-файлы — это итоговые файлы, в которые вебпак соберет ассеты и которые непосредственно подключаются в шаблон. Они регистрируются в webpack.config.js.


Entry-файлы подключаются в шаблон twig-функциями encore_entry_link_tags(entry-file) и encore_entry_script_tags(entry-file), предоставленных Encore.


Добавим бандлу собственные entry-файлы для стилей и скриптов. Для этого скопируем папку assets из хоста в корень бандла:


cp ./assets ./bundles/CalendarBundle/assets -R

Переименуем entry-файлы:


mv host-app.js calendar-editor-app.js
mv host-styles.scss calendar-editor-styles.scss

Удалим лишний файл assets/scss/events/_main.scss и его импорт в calendar-editor-styles.scss.


Заметим так же, что у нас из хоста скопировался файл стилей scss/events/calendar.scss, — это стили виджета календаря. Они тоже должны быть внутри бандла, поэтому мы оставляем их здесь и удаляем из папки assets хоста. В файле hosts-styles.scss нужно поправить импорт стилей виджета на следующий:


@import "../../vendor/bravik/calendar-bundle/assets/scss/widget/calendar";

На страницах редактора виджет календаря не используется, поэтому убираем его импорт из calendar-editor-styles.scss.


Чтобы WebpackEncore добавил наши entry-файлы в сборку добавим их в webpack.config.js:


Encore
// ...
.addEntry(
    'calendar-editor-app',
     './vendor/bravik/calendar-bundle/assets/js/calendar-editor-app.js'
)
.addStyleEntry(
    'calendar-editor-styles',
     './vendor/bravik/calendar-bundle/assets/scss/calendar-editor-styles.scss'
)

Обратите внимание, что мы указываем пути к файлам не в ./bundles/CalendarBundle, а в папке vendors. Готовый бандл будет подключатся с помощью composer и загружаться именно в эту папку. На время разработки мы в первой статье установили в vendors симлинк(ярлык) на нашу локальную папку.


Проверим сборку ассетов.
Перезапустите Encore-сервер: убейте старый процесс и снова выполните команду npm start.
Если сборка ассетов пройдет без ошибок — вы все сделали правильно.
Если что-то не получилось, вы всегда можете переключиться на git-ветку 3-integration с финальным результатом этой статьи.


Осталось добавить entry-файлы бандла в шаблоны.


В шаблоне @Calendar/base.html.twig в блок stylesheets добавьте:


{{ encore_entry_link_tags('calendar-editor-styles') }}

и в блок javascripts:


{{ encore_entry_script_tags('calendar-editor-app') }}


Посмотрим результат: ассеты должны быть подключены и страница оформлена.


Для многих приложений — это хороший подход: у вас полностью независимое отдельное приложение внутри хоста, со своим интерфейсом, стилями, скриптами, в котором вы делаете всю нужную работу и возвращаетесь обратно.

Интеграция шаблонов: встраиваемый редактор


В предыдущем варианте единственный способ интеграции с хостом — это ссылка на редактор, который будет открываться в отдельном окне и жить своей жизнью. Пользователь должен открыть редактор, поработать с ним, и закрыть, чтобы вернуться обратно.


Но если добиваться такой степени изолированности и разделения,
то нужен ли нам вообще бандл?

Может быть стоит вынести всю эту логику в отдельное полностью независимое микроприложение и взаимодействовать с ним через API? А в бандле оставить только виджет и сервис, извлекающий данные из API? Возможно это хороший вариант для вашей ситуации, но мы хотим более тесной интеграции.


Мы хотим встроить редактор событий в приложение так, чтобы пользователь не замечал переходов: не открывались новые вкладки, не менялся интерфейс. Для этого нам нужно встроить шаблоны бандла в базовый шаблон хоста, а так же подключить его ассеты.


В Symfony предусмотрен механизм для переопределения частей бандла. Например, мы можем переопределить любой его шаблон.

В папке хоста templates создадим папку bundles/CalendarBundle. Имя и структура папок важны.


Когда вы указываете путь шаблона бандла Symfony по умолчанию вначале ищет не в папке templates бандла, а прежде в templates/bundles/<имя бандла> хоста. И только если не найдет запрошенного файла в этом месте, попробует найти его в templates внутри бандла.


Таким образом, придерживаясь в templates/bundles/CalendarBundle хоста структуры папок и именования шаблонов идентичного бандлу, то мы можем переопределить любой его шаблон.


Давайте переопределим в хосте базовый шаблон бандла base.html.twig. Для этого скопируем его из хоста:


cp ./bundles/CalendarBundle/templates/base.html.twig ./templates/bundles/CalendarBundle/base.html.twig

Внутри переопределенного шаблона добавим меню из базового шаблона хоста. Вставьте после <body>:


{% include 'menu.html.twig' %}

Попробуем обновить страницу редактора, — и у нас появилось меню из хоста!


Продвинутое переопределение шаблонов


Но решая одну проблему, мы создали другую. Переопределив базовый шаблон бандла, мы создали копию базового шаблона приложения. Теперь у нас дублируется код.


В примитивном случае там будет немного повторяющегося кода. Но если у нас большое приложение, то базовый шаблон может быть гораздо сложнее. Мы не хотим поддерживать в двух местах один шаблон, поэтому попробуем свести дублирование к минимуму. Для этого унаследуем преопределенный шаблон бандла от базового шаблона приложения.


Добавим в начале переопределенного шаблона бандла
templates/bundles/CalendarBundle/base.html.twig:


{% extends 'base.html.twig' %}

Здесь base.html.twig — это базовый шаблон хоста.


Теперь нам нужно убрать всю лишнюю HTML разметку из шаблона и прокинуть каждый блок базового шаблона бандла в соответствующий блок базового шаблона хоста. То есть, если в бандле используется определенный набор блоков, то мы должны установить соответствие между ними и блоками приложения хоста. Поскольку названия блоков у нас совпадают, то это можно и не делать.


{% extends 'base.html.twig' %}

{% block title %}Редактор событий{% endblock %}

{% block stylesheets %}
    {{ parent() }}
    {{ encore_entry_link_tags('calendar-editor-styles') }}
{% endblock %}

{% block javascripts %}
    {{ parent() }}
    {{ encore_entry_script_tags('calendar-editor-app') }}
{% endblock %}

Мы добавили {{ parent() }} внутри блоков, чтобы не потерять ничего из родительского шаблона:


В этом примере у нас совпадают названия блоков бандла и хоста, но в другом приложении могут не совпадать. И вообще нас смущает такое неявное переопределение блоков хоста блоками бандла.


Все неявное в коде — это зло и ваши потенциальные проблемы в будущем.

Чтобы дать больше контроля над блоками, мы можем добавить блокам бандла префиксы. Тогда базовый шаблон бандла и его переопределенный вариант станет выглядеть вот так:


{% extends 'base.html.twig' %}

{% block title %}
    {%- block calendar_title %}Редактор событий{% endblock -%}
{% endblock %}

{% block stylesheets %}
    {{ parent() }}
    {{ encore_entry_link_tags('calendar-editor-styles') }}
    {% block calendar_styles %}{% endblock %}
{% endblock %}

{% block body %}
    {%- block calendar_body %}{% endblock -%}
{% endblock %}

{% block javascripts %}
    {{ parent() }}
    {{ encore_entry_script_tags('calendar-editor-app') }}
    {% block calendar_scripts %}{% endblock %}
{% endblock %}

В дальнейшем разрабатывая бандл, нужно иметь в виду и даже прописывать в инструкции, что базовый шаблон может/должен быть переопределен.


Переопределение стилей


Переопределение стилей сводится к переопределению entry-файлов бандла.


Таким образом независимо от того, встраиваете ли вы или используете независимо от хоста, везде, где подключается этот entry-файл, вы подключите свой переопределенный.


Например, мы можем создать свой собственный overriden-calendar-styles.scss:


// Импортируем оригинальные стили бандла
@import "../../vendor/bravik/calendar/assets/scss/calendar-styles";

// Добавляем свои модификации
@import "calendar/customizations";

Здесь мы импортируем оригинальный entry-файл стилей бандла, и поверх него добавляем свои кастомизации.


Если нужно что-то кардинально изменить, можно вместо импорта скопировать содержимое entry-файла и переделать по-своему. Однако в этом случае, при обновлениях бандла придется вручную следить за обновлениями переопределенной версии.


Переопределение JavaScript


Таким же образом можно переопределить entry-файлы JavaScript.


Сейчас у нас нет JS-логики. Но entry-файл может выглядеть так:


import CalendarApp from "../calendar/CalendarApp";

global.bravikCalendar = new CalendarApp();
global.bravikCalendar.init();

Entry-файл специально делается минималистичным. Он не должен меняться и рассчитан на то, что его могут скопировать и переопределить.

Вся логика, которая может поменяться прячется внутри CalendarApp.
Это дает возможность переопределить файл в хосте, и добавить нужную логику. Например так:


import CalendarApp from "../../vendor/bravik/calendar/assets/js/calendar/CalendarApp";
import myCustomLogic from "./MyCutomLogic";

global.bravikCalendar = new CalendarApp();
global.bravikCalendar.registerSomeCustomLogic(myCustomLogic);
// And more ...
global.bravikCalendar.init();

Резюме


Мы рассмотрели два способа интеграции шаблонов и логики бандла в приложение-хост: как независимое под-приложение с собственным базовым шаблоном, стилями и JS или как набор шаблонов, встраиваемых в шаблоны хоста. Первый способ прост и позволяет избежать конфликтов с хостом, а второй позволяет сделать более незаметным «шов» на стыке представления бандла и хоста.


Финальную версию кода для этой статьи вы найдете в ветке 3-integration.


В следующей статьеразберемся как предоставить пользователям бандла интерфейс, точки для расширения его функциональности уникальной для проекта логикой.


Другие статьи серии:


Часть 1. Минимальный бандл
Часть 2. Выносим код и шаблоны в бандл
Часть 3. Интеграция бандла с хостом: шаблоны, стили, JS
Часть 4. Интерфейс для расширения бандла
Часть 5. Параметры и конфигурация
Часть 6. Тестирование, микроприложение
Часть 7. Релизный цикл, установка и обновление