Как переиспользовать код с бандлами Symfony 5? Часть 2. Выносим код в бандл

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

Специально для сайта ITWORLD.UZ. Новость взята с сайта Хабр