Поговорим о том, как прекратить копипастить между проектами и вынести код в переиспользуемый подключаемый бандл 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. Релизный цикл, установка и обновление