Поговорим о том, как прекратить копипастить между проектами и вынести код в переиспользуемый подключаемый бандл Symfony 5. Серия статей, обобщающих мой опыт работы с бандлами, проведет на практике от создания минимального бандла и рефакторинга демо-приложения, до тестов и релизного цикла бандла.
В предыдущей статье мы создали минимальный бандл из двух файлов и подключили его в проект.
В этой статье:
- Перенос кода в бандл
- Dependency Injection: регистрация сервисов бандла в DI-контейнере
- Перенос контроллеров и настройка роутинга
- Механизм определения путей к ресурсам
- Перенос шаблонов в бандл
Содержание серии
Часть 1. Минимальный бандл
Часть 2. Выносим код и шаблоны в бандл
Часть 3. Интеграция бандла с хостом: шаблоны, стили, JS
Часть 4. Интерфейс для расширения бандла
Часть 5. Параметры и конфигурация
Часть 6. Тестирование, микроприложение
Часть 7. Релизный цикл, установка и обновление
Если вы не последовательно выполняете туториал, то скачайте приложение из репозитория и переключитесь на ветку 1-bundle-mockup.
Инструкции по установке и запуску проекта в файле README.md
.
Финальную версию кода для этой статьи вы найдете в ветке 2-basic-refactoring.
Приступим к рефакторингу.
Перемещаем основные файлы
Бандл может содержать все то же самое, что и обычные приложения Symfony: сущности, контроллеры и команды, шаблоны, ассеты, тесты и любой другой код.
Переместите файлы сущностей, репозиториев, формы и необходимые контроллеры в bundles/CalendarBundle/src
(с сохранением структуры папок):
# Crate dirs
cd bundles/CalendarBundle/src/
mkdir Controller Entity Service
cd ../../../src
# Move files
mv Form Repository ../bundles/CalendarBundle/src/
mv Controller/EditorController.php Controller/EventController.php ../bundles/CalendarBundle/src/Controller
mv Entity/Event.php ../bundles/CalendarBundle/src/Entity
mv Service/EventExporter ../bundles/CalendarBundle/src/Service/EventExporter
mv Twig ../bundles/CalendarBundle/src/Twig
Чтобы перемещенный код заработал, нам потребуется обновить пространства имен перемещенных классов. Поменяйте везде после ключевых слов namespace
и use
корень App
на корень бандла bravikCalendarBundle
.
Внутри бандла не должно остаться никаких зависимостей от пространства имен App
: для бандла его не существует.
Все современные IDE имеют функцию поиска и замены по файлам.
Например в IDE PhpStorm выбираем папку бандла и жмем Ctrl/Cmd + Shift + R
.
Кроме папки бандла нам потребуется сделать то же самое для use AppRepositoryEventRepository
в SiteController
Меняем везде и пробуем открыть главную страницу.
Получаем ошибку:
В SiteController
мы внедряем зависимость: репозиторий EventRepository
. С помощью механизма autowiring Symfony автоматически распознает typehints аргументов экшна и ищет нужный сервис среди зарегистрированных в DI-контейнере.
Но если в приложении все классы из папки src/Services
автоматически регистрируются как сервисы, то в бандле никакие классы пока не зарегистрированы.
Регистрация сервисов бандла в Dependency Injection контейнере
Ключевая фишка бандла, это автоматическая подгрузка в DI-контейнер приложения своих зависимостей, прямо при установке.
Но если для пользователя бандла она автоматическая, то его разработчику нужно все настроить.
Как и в обычном приложении, для Symfony-бандла мы можем создать конфиг services.yaml
, в котором будут описаны регистрируемые бандлом в контейнере DI-сервисы.
В корне бандла рядом src
создайте папку и файл config/services.yaml
:
parameters:
# Здесь могут быть параметры бандла
services:
# Конфигурация для всех сервисов этого файла по умолчанию
_defaults:
# Включает механизм автоматической подстановки зависимостей контейнера
# в ваши сервисы по typehints аргументов конструктора (и экшнов контроллеров)
# https://symfony.com/doc/current/service_container.html#the-autowire-option
autowire: true
# Включает механизм автоконфигурации:
# сервисам автоматически добавляются теги по имплементируемым интерфейсам
# https://symfony.com/doc/current/service_container.html#the-autoconfigure-option
autoconfigure: true
# Регистрируем контроллеры бандла и репозиторий как DI-сервисы
bravikCalendarBundleRepositoryEventRepository: ~
bravikCalendarBundleControllerEventController: ~
bravikCalendarBundleControllerEditorController: ~
Мы задали настройки по умолчанию для всего файла и зарегистрировали 3 сервиса.
Но в конфиге приложения-хоста config/services.yaml
у нас осталось еще несколько строк, которые нужно перенести в бандл.
Закомментируйте помеченные @todo
строки и перенесите следующие строки в бандл:
# Фильтр Twig для форматирования даты
bravikCalendarBundleTwigTwigRuDateFilter: ~
# Регистрируем все классы компонента EventExporter как DI-сервисы
bravikCalendarBundleServiceEventExporter:
resource: '../src/Service/EventExporter/*'
# Регистрируем ExporterProvider в качестве DI-сервиса
# и явно инжектим 2 экспортера в конструктор
bravikCalendarBundleServiceEventExporterExporterManager:
arguments:
$exporters:
- '@bravikCalendarBundleServiceEventExporterExportersGoogleCalendarExporter'
- '@bravikCalendarBundleServiceEventExporterExportersICalendarExporter'
Если мы сейчас обновим страницу, то увидим, что ничего не изменилось. Файл конфигурации автоматически не подхватывается фреймворком, и нам нужно подключить его вручную. Для этого в папке src
бандла создадим папку и класс DependencyInjection/CalendarExtension.php
:
<?php
namespace bravikCalendarBundleDependencyInjection;
use Exception;
use SymfonyComponentDependencyInjectionContainerBuilder;
use SymfonyComponentDependencyInjectionExtensionExtension;
class CalendarExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
}
}
Это очень важный для бандла класс. Он должен называться строго в формате <BundleName>Extension
, располагаться в папке src/DependencyInjection
и наследоваться от SymfonyComponentDependencyInjectionExtensionExtension
.
Когда Symfony компилирует DI-контейнер, фреймворк проходится по всем подключенным бандлам и ищет в них именно Extension-файл. Если он существует, то вызывается метод
Extension::load()
в который передается собираемый контейнер приложения.
Это место, где мы можем добавить в контейнер собственные сервисы бандла. Технически мы могли обойтись и без файла конфигурации, и зарегистрировать все нужные сервисы прямо здесь в PHP-коде. Но удобней объявлять их привычным способом в отдельном конфигурационном файле.
Добавим servies.yaml
бандла в сборку:
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__.'/../../config')
);
$loader->load('services.yaml');
}
Снова обновим страницу, и снова получаем ошибку:
На этот раз приложение не может найти роут контроллера.
Настройки роутинга в бандле
В приложениях Symfony настройки роутинга прописываются в файлах:
config/routes.yaml # Здесь указываются роуты
config/routes/annotations.yaml # Здесь подключаются роуты,
# размеченные аннотациями в контроллерах
В контроллерах нашего бандла роуты размечены аннотациями. Посмотрим как они подключаются в annotations.yaml
:
controllers: # Произвольный идентификатор
type: annotation # Тип подкючаемого ресурса
resource: ../../src/Controller/ # Путь к файлам, размеченным аннотациями
Создадим такой же файл в папке с конфигами нашего бандла: config/routes.yaml
.
calendar_routes:
type: annotation
resource: '../src/Controller/'
Теперь нужно подключить настройки бандла к конфигурации аннотаций приложения config/routes/annotations.yaml
.
Добавим туда строки:
calendar_bundle:
resource: '@CalendarBundle/config/routes.yaml'
Здесь в качестве ресурса мы указываем наш конфиг в бандле.
@CalendarBundle
— заменяет относительный путь к корню бандла.
Компонент роутинга Symfony имеет полезные для бандлов настройки префиксов.
Например, что если бы у нас была админка по адресу /admin
и мы хотели бы наш редактор событий как-то в неё интегрировать: чтобы путь к нему начинался так же с /admin и чтобы он был спрятан за общей формой логина?
Мы можем добавить префиксы к роутам бандла, а при желании можем даже добавить префикс к именам роута:
calendar_bundle:
resource: '@CalendarBundle/config/routes.yaml'
prefix: /admin
name_prefix: cms.
А дальше с помощью компонента security
вы можете тонко настроить доступы и роли. Настройки ролей и доступов логично делать вне бандла, это специфичное для каждого приложения поведение.
Чтобы избежать конфликта имен, Symfony рекомендует всегда использовать в названиях роутов бандла префиксы vendor_name_
. Мы не будем придерживаться этой рекомендации.
Пути к ресурсам бандла
Для обращения к ресурсам бандла в конфигах, а так же в шаблонах Twig у фреймворка предусмотрена «логическая» ссылка на бандл.
Её можно использовать в формате:
@<BundleName>Bundle/path/to/config
— в файлах конфига@<BundleName>/path/to/template
— упрощенный вариант в шаблонах.
В Symfony это называется «логические пути». Их использование позволяет легко переопределять любые шаблоны бандла. Об этом позже.
Однако по умолчанию ссылка указывает не на корень бандла а на директорию ./src
. Так сложилось исторически, потому что до Symfony 4 принято было ресурсы приложения и бандлов помещать внутри папки src/Resources
. Начиная с 4 версии рекомендованная Symfony структура приложений и бандлов стала такой, какой вы видите её сейчас — чище и понятней. Однако легаси осталось и нам нужно внести небольшое изменение, чтобы это поправить.
Переопределим в главном файле бандла CalendarBundle
метод getPath()
:
public function getPath(): string
{
return dirname(__DIR__);
}
Теперь @CalendarBundle
будет указывать на корень нашего бандла.
Обновим страницу, и на этот раз мы увидим наш календарь!
Но как же так? Мы ведь не скопировали в бандл шаблоны.
Посмотрим на наши контроллеры бандла. Обратите внимание, что они обращаются к шаблонам по абсолютным путям, а значит используют шаблоны из приложения-хоста.
Перенос шаблонов в бандл
Создадим в корне бандла папку templates
для шаблонов и перенесем туда содержимое папки templates/event
из приложения:
mkdir bundles/CalendarBundle/templates
mv templates/event/* bundles/CalendarBundle/templates
Если мы обновим страницу — увидим, что шаблон не найден. Так и должно быть.
С помощью инструмента «Поиск и замена»вашей IDE замените в контроллерах бандла пути к шаблонам на логические относительно бандла. Для этого вхождения event/
в пути шаблона замените на логическую ссылку @Calendar/
.
На самом деле
@Calendar
в шаблонах Twig это уже не совсем логическая ссылка, а namespace в терминологии Twig. Папкуtemplates
в путях указывать не нужно, так как Symfony автоматически зарегистрирует namespace, и ассоциирует его с папкойtemplates
илиResources/views
(если папки бандла организованы по старой конвенции).
Кроме этого на главной странице приложения в шаблоне site/index.html.twig
мы используем виджет календаря из подключаемого twig-шаблона. Точно так же заменим путь к шаблону виджета на относительный путь из бандла:
Обновим страницу, — календарь снова на месте.
Но с шаблонами все еще остается еще одна проблема: некоторые шаблоны бандла унаследованы от базового шаблона base.html.twig
приложения-хоста:
{% extends 'base.html.twig' %}
Этой проблемой мы займемся в следующей статье.
Резюме
Мы рассмотрели процесс переноса кода, шаблонов и ассетов в бандл, настроили роутинг и подключили сервисы бандла к сборке DI-контейнера. Финальный код Example Project для этой статьи в ветке 2-basic-refactoring.
- Перенос кода в бандл, — это копирование файла с заменой namespace и путей импорта. В бандле не должно остаться зависимостей от пространства имен приложения
App
- Основной класс бандла, — Extension-класс. С его помощью можно вмешаться в компиляцию DI-контейнера приложения и подключить к нему собственные сервисы бандла. Удобно определять сервисы не в самом классе, а с помощью конфигурационного файла.
- Роуты бандла можно определять в конфигурационных файлах внутри бандла. Эти файлы подключаются в настройках роутинга приложения хоста. Можно использовать аннотации в контроллерах бандла.
- Пути к файлом ресурсов, в том числе к конфигах, шаблонам и т.д., определяются с помощью «логической» ссылки на бандл, указывающей на корневую папку бандла. С её помощью ресурсы бандла можно переопределять в приложении хосте.
В следующей статье разберемся как интегрировать шаблоны, JS и стили бандла в приложение-хост.
Другие статьи серии:
Часть 1. Минимальный бандл
Часть 2. Выносим код и шаблоны в бандл
Часть 3. Интеграция бандла с хостом: шаблоны, стили, JS
Часть 4. Интерфейс для расширения бандла
Часть 5. Параметры и конфигурация
Часть 6. Тестирование, микроприложение
Часть 7. Релизный цикл, установка и обновление