Система пакетов
В версиях Symfony до 4.0, было рекомендовано упорядочивать ваш код, используя пакеты. Это более не рекомендуется и пакеты должны быть использованы только для того, чтобы делиться кодом и функциями между многими приложениями.
Пакет похож на плагин в других ПО, только лучше. Базовые функции фреймворка Symfony релизуются с помощью пакетов FrameworkBundle, SecurityBundle, DebugBundle, и др.). Они также используются для добавления новых функций в ваше приложение через сторонние пакеты.
Пакеты, используемые в вашем приложении, должны быть подключены для каждого окружения в файле config/bundles.php :
1 2 3 4 5 6 7 8 9 10 11 12 13
// config/bundles.php return [ // 'all' means that the bundle is enabled for any Symfony environment Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], // этот пакет включается только в 'dev' и 'test', поэтому вы не можете использовать его в 'prod' Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], ];
В приложении Symfony по умолчанию, которое использует Symfony Flex, пакеты подключаются и отключаются для вас автоматически при их установке или удалении, поэтому вам не нужно искать или редактировать файл bundles.php .
Cоздание пакета
Этот раздел создаёт и включает новый пакет, чтобы продемонстрировать, как это легко сделать. Новый пакет называется AcmeTestBundle, где часть Acme — просто шаблонное имя, которое нужно заменить неким именем «поставщика», который представляет вас или вашу организацию (например, ABCTestBundle для компании по имени ABC ).
Начните с создания каталога src/Acme/TestBundle/ и добавьте в него новый файл под названием AcmeTestBundle.php :
1 2 3 4 5 6 7 8
// src/Acme/TestBundle/AcmeTestBundle.php namespace App\Acme\TestBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; class AcmeTestBundle extends Bundle
Имя AcmeTestBundle следует стандартным договоренностям присваивания имен пакетам. Вы также можете решить укоротить имя пакета до TestBundle, назвав этот класс TestBundle (и назвав файл TestBundle.php ).
Этот пустой класс – единственное, что вам понадобится для создания нового пакета. Несмотря на то, что чаще всего он пуст, этот класс мощный по возможностям и может быть использован для настройки поведения пакета. Теперь, когда вы создали пакет, подключите его:
1 2 3 4 5
// config/bundles.php return [ // . App\Acme\TestBundle\AcmeTestBundle::class => ['all' => true], ];
И хотя он пока ничего не делает, AcmeTestBundle готов к использованию.
Структура каталога пакета
Структура каталога пакета простая и гибкая. По умолчанию, система пакетов следует набору договоренностей, которые помогают создавать единообразный код для всех пакетов Symfony. Посмотрите на AcmeDemoBundle, так как он содержит некоторые самые распространенные элементы пакета:
Controller/ Содержит контроллеры пакета (например, RandomController.php ). DependencyInjection/ Содержит некоторые классы расширения внедрения зависимости, которые могут импортировать конфигурацию сервиса, регистрировать пропуски компилятора и т.д. (этот каталог не является обязательным). Resources/config/ Содержит конфигурацию, включая конфигурацию маршрутов (например, routing.yaml ). Resources/views/ Содержит шаблоны, расположенные в алфавитном порядке по имени контроллера (например, Random/index.html.twig ). Resources/public/ Содержит веб-ресурсы (изображения, таблицы стилей и т.д.); он копируется или символически привязывается к каталогу проекта public/ с помощью командной строки assets:install . Tests/ Содержит все тесты пакета.
Пакет может быть настолько большим или маленьким, насколько это диктует реализуемая им функция. Он содержит только те файлы, которые вам нужны, и ничего более.
По ходу прочтения книги, вы узнаете, как сохранять объекты в базе данных, создавать и валидировать формы, создавать переводы вашего приложения, писать тесты и многое другое. Каждая такая возможность имеет свое место и роль в рамках пакета.
Узнайте больше
- Как переопределить любую часть пакета
- Лучшие практики для повторно используемых пакетов
- Как создать дружественную конфигурацию для пакета
- Как загружать конфигурацию сервиса внутри пакета
- Как упростить конфигурацию нескольких пакетов
Как переиспользовать код с бандлами Symfony 5? Часть 1. Минимальный бандл
Поговорим о том, как прекратить копипастить между проектами и вынести код в переиспользуемый подключаемый бандл Symfony 5. Серия статей, обобщающих мой опыт работы с бандлами, проведет на практике от создания минимального бандла и рефакторинга демо-приложения, до тестов и релизного цикла бандла.
- Зачем нужны бандлы
- Example Project: Calendar
- Настраиваем окружение: 2 способа разработки
- Создаем минимальный бандл
- Подключение бандла в проект
Содержание серии
Что такое бандл и зачем он нужен?
Symfony Bundle понадобится вам тогда (и только тогда), когда вы устанете копипастить код из проекта в проект и задумаетесь о его переиспользовании. Рано или поздно приходит понимание, что удобнее выделить код в переиспользуемый подключаемый модуль. Symfony Bundle — это и есть такой модуль в экосистеме Symfony.
Бандл — это пакет переиспользуемого PHP кода на стероидах Symfony Framework.
Бандлы отличатся от обычных PHP-пакетов использованием компонентов и общепринятых соглашений, упрощающих интеграцию с Symfony-приложением. Благодаря следованию принятым в экосистеме соглашениям именования и структуры кода, бандлы могут автоматически подключаться, конфигурироваться и расширяться в приложении-хосте Symfony. Бандлы подключаются к проекту через менеджер зависимостей composer .
У такой тесной интеграции есть и обратная сторона, — пакет становится зависим от фреймворка. Хотя грамотная организация кода, использование DDD-подхода к разработке может помочь эту связность снизить.
Пример KnpMenuBundle
Взгляните на код одного из самых популярных бандлов Symfony: KnpMenuBundle.
Это бандл, упрощающий генерацию и работу с меню сайта. Но можно заметить, что в этом репозитории слишком мало файлов.
Дело в том, что вы смотрите именно на «стероидную» часть бандла, отвечающую за интеграцию с Symfony приложениями. Вся бизнес-логика (домен) вынесена разработчиками в отдельный, независимый от фреймворка PHP-пакет, который сам подключается в бандл через Composer.
Example Project: Calendar
Разбираться с бандлами будем на примере рефакторинга приложения календаря.

Дизайн заимствован у Яндекса и используется исключительно для образовательных целей.
Представьте, что вы написали красивый календарь мероприятий. Посмотрев на этот сайт к вам пришел еще один клиент попросил такой же на свой сайт. Вы копируете код сайта в новый проект, слегка правите дизайн. Спустя какое-то время вы находите ошибку в коде, и вам приходится обновлять код на двух сайтах.
Ваш календарь имеет успех, и еще 10 клиентов заказывают себе такой же.
Один из них заказывает новую фичу: уведомления о новых мероприятиях.
Остальные клиенты тоже хотят получить эту новую фичу, и теперь вам приходится копировать файлы между проектами, вручную обновлять файлы у всех 12 клиентов.
Очень скоро уследить за изменениями в каждом проекте становиться невозможно, поддержка кода превращается в кошмар.
А теперь представьте, что вы выделили весь код вашего календаря в один, независимый от конкретного проекта, бандл. Теперь, при появлении ошибки, вам достаточно исправить код в одном месте, а в каждом из проектов для обновления достаточно выполнить composer update .
Попробуем отрефакторить приложение и вынести общую для календаря логику в бандл.
В README.md короткая инструкция как развернуть и запустить приложение.
Приложение устроено просто:
- сущность Event
- 2 контроллера для отображения и редактирования мероприятий
- сервис для экспорта календаря в различные форматы
- набор шаблонов для виджета календаря и редактора
- немного стилей, собираемых через webpack-encore
Настраиваем окружение
Первый вопрос, а как вообще разрабатывать бандл?
Ведь с одной стороны бандл — это отдельный проект, рука тянется к File -> New. -> Symfony Project . А с другой стороны он не может запускаться отдельно от приложения-хоста, в которое подключается.
Здесь есть 2 пути:
-
Если бандл разрабатывается с нуля, можно создать микроприложение Symfony для разработки прямо внутри бандла. Мы вернемся к этому варианту в любом случае, так как микроприложение понадобится для тестирования.
| Плюсы | Минусы |
|---|---|
| чистота подхода | сложно новичкам, ведь нужно хорошо понимать Symfony |
| — | нужно потратить время на создание микроприложения |
| — | наличие лишнего кода приложения хоста в проекте |
| Плюсы | Минусы |
|---|---|
| быстро и просто | привязка к приложению хосту, для доработки потребуется иметь доступ к двум репозиториям |
| вы можете постепенно выносить логику в бандл и сразу же тестировать его на готовой инфраструктуре приложения-хоста | нужно иметь в виду 2 репозитория в одном проекте (хотя PhpStorm отлично умеет разделять и справляться с этим) |
| все в одном окне IDE, так проще | требует внимательности: можно при разработки неочевидно воспользоваться зависимостями приложения хоста, что породит проблемы при подключении бандла в другой проект |
К первому пути мы вернемся в статье о тестировании бандлов, а сейчас пойдем по второму пути.
Создаем минимальный бандл
Внутри ./bundles создадим основную папку будущего бандла CalendarBundle ,
и внутри неё минимальный набор файлов:
src/CalendarBundle.php composer.json
Разбираемся с composer.json
Скопируйте в composer.json бандла:
< "name": "bravik/calendar-bundle", "version": "0.1.0", "type": "symfony-bundle", "description": "Symfony bundles tutorial example project", "license": "proprietary", "require": < "php": "^7.3" >, "require-dev": < >, "config": < "sort-packages": true >, "autoload": < "psr-4": < "bravik\\CalendarBundle\\": "src/" >>, "autoload-dev": < "psr-4": < "bravik\\CalendarBundle\\Tests\\": "tests/" >>, "scripts": < "test" : "./vendor/bin/simple-phpunit" >, "extra": < "symfony": < "allow-contrib": false, "require": "5.0.*" >> >
"name": "bravik/calendar-bundle", "description": "Health check bundle",
Эти обязательные поля устанавливают название пакета и описание. Для именования пакета по общепринятому соглашению используется название вендора и бандла. При установке с помощью менеджера зависимостей composer , код бандла будет помещен в соответствующую папку vendor/bravik/calendar-bundle
"type": "symfony-bundle"
Укажет Symfony, что этот пакет является бандлом. Благодаря специальному расширению для composer , — Symfony Flex , — при установке в приложение-хост, бандл автоматически будет подключен в ядро хоста (добавится в bundles.php ), а так же запустится его «рецепт».
Рецепты — это механизм Symfony Flex, который позволяет при установке бандла выполнить специальный скрипт, задающий дополнительные действия по первичной настройке бандла.
"version": "0.1.0",
Задаст версию бандла. Менеджер зависимостей composer отслеживает и загружает обновления всех установленных в проект пакетов. С помощью семантического версионирования вы можете контролировать этот процесс. Подробней об этом позже
"autoload": < "psr-4": < "bravik\\CalendarBundle\\": "src" >>, "autoload-dev": < "psr-4": < "bravik\\CalendarBundle\\Tests\\": "tests" >>,
Здесь задается пространство имен бандла для автолоадера composer. По конвенции оно выбирается в формате //Bundle .
Благодаря этим строкам мы указываем механизму автозагрузки composer, что файлы с пространством имен bravik\\CalendarBundle нужно искать в папке ./src относительно расположения composer.json бандла. При установке бандла в приложение-хост, эти настройки будут автоматически добавлены в общий автолоадер хоста, благодаря чему хост сможет использовать код бандла через use / .
Дополнительно укажем пространство имен для тестов для dev -окружения. Оно понадобиться нам позднее.
"require": < "php": "^7.3" >, "require-dev": <>,
В секции require и require-dev определяются зависимости для prod и dev окружения. Все эти зависимости будут добавлены в дерево зависимостей приложения-хоста и загружены автоматически при установке бандла. После установки их обновления будут отслеживаться через composer хоста. Кроме php нам пока ничего здесь не требуется.
Остальные опции нам не интересны.
Основной класс бандла
Создайте в ./src бандла файл CalendarBundle.php и скопируйте туда код:
Это главный класс бандла. Он используется фреймворком для подключения бандла в хост и настройки его поведения.
Имя файла имеет фиксированный формат: Bundle.php , — оно задает имя бандла и заканчивается словом Bundle . Если придерживаться этих конвенций, то Symfony при установке автоматически распознает и подключит ваш бандл, а так же создаст его псевдоним для внутреннего использования в ресурсах проекта (например в шаблонах).
Не нарушайте общепринятые соглашения без необходимости,
чтобы не усложнять себе жизнь.
Внутри файла мы используем корневое пространство имен bravik\CalendarBundle , как мы установили в composer.json . Остальное содержимое главного класса бандла может быть пустым.
Подключение бандла в приложение-хост
Бандлы подключаются через composer с помощью привычной команды:
composer require bravik/calendar-bundle
Но если вы выполните её сейчас, composer не сможет найти нужный пакет: ведь его нет в официальных репозиториях.
Чтобы указать его местоположение, нужно добавить секцию repositories в composer.json хоста.
На время разработки вместо удаленного репозитория, мы подключим локальную папку bundles/CalendarBundle :
"repositories": [ < "type" : "path", "url" : "./bundles/CalendarBundle" >],
При таком подключении в папке vendor создасться не скачанная с репозитория копия нашего проекта, а симлинк bravik/calendar-bundle , указывающий на нашу локальную папку. Это позволит работать с бандлом как с внешней зависимостью из vendors , но иметь возможность редактировать файлы на локальной папки и сразу же видеть изменения.
Когда бандл достигнет релиза, мы вынесем его в отдельный git-репозиторий так:
"repositories": [ < "type" : "vcs", "url" : "git@bitbucket.org:bravik/calendarbundle.git" >],
В такой конфиграции composer склонирует git-репозиторий в папку vendors/bravik/calendar-bundle .
Теперь после выполнения команды composer require bravik/calendar-bundle в composer.json хоста добавиться наш бандл в качестве зависимости, и подключиться к ядру. Чтобы убедиться в последнем, откроем config/bundles.php :
['all' => true], ];
Мы видим, что наш бандл был подключен в проект с помощью его основного класса!
Инициализация репозиториев
На практике создавая бандл в локальной папке приложения-хоста, я сразу добавлю папку ./bundles в .gitignore приложения хоста, а внутри bundles/CalendarBundle инициализирую новый репозиторий: composer init . Зачем засорять репозиторий хоста лишним кодом, сразу можно выносить код бандла в отдельный репозиторий.
Это так же удобно если вы хотите временно подключить уже готовый бандл, чтобы «на лету» внести какие-то изменения отдебажить какой-то баг, проявляющийся на конкретном хосте.
Но для удобства этой статьи, я храню код в монорепозитории.
Резюме
- Минимальный Symfony бандл без полезной нагрузки состоит всего из 2х файлов: composer.json и класс MyBundle .
- Начинать разработку бандла удобно прямо в проекте-доноре в одном окне IDE в одном GIT-репозитории, подключая локальную папку бандла через composer.
- Когда бандл дойдет до стадии самостоятельного работоспособного пакета, выносите его в отдельный репозиторий.
- Чтобы окончательно оторваться от хоста, можно создать микроприложение прямо внутри бандла.
Финальный код Example Project для этой статьи можно посмотреть в ветке 1-bundle-mockup.
В следующей статье займемся переносом кода, шаблонов и ассетов в бандл, рассмотрим настройки роутинга и механизм подключения ресурсов бандла, а так же создадим конфигурационный файл для DI-контейнера.
Пошаговое создание бандла для Symfony 4
Около года назад наша компания взяла курс на разделение огромного монолита на Magento 1 на микросервисы. Как основу выбрали только вышедшую в релиз Symfony 4. За это время я разработал несколько проектов на этом фреймворке, но особо интересной мне показалась разработка бандлов, переиспользуемых компонентов для Symfony. Под катом пошаговое руководство по разработке HealthCheck бандла для получения статуса/здоровья микросервиса под Syfmony 4.1, в котором я постарался затронуть наиболее интересные и сложные (для меня когда-то) моменты.
В нашей компании этот бандл используется, например, для получения статуса реиндекса продуктов в ElasticSearch — сколько товаров содержится в Elastic с актуальными данными, а сколько требуют индексации.
Создание скелета бандла
В Symfony 3 для генерации скелетов бандлов был удобный бандл, однако в Symfony 4 он более не поддерживается и потому скелет приходится создавать самому. Разработку каждого нового проекта я начинаю с запуска команды
composer create-project symfony/skeleton health-check
Обратите внимание, что Symfony 4 поддерживает PHP 7.1+, соответственно если запустить эту команду на версии ниже, то вы получите скелет проекта на Symfony 3.
Эта команда создаёт новый проект Symfony 4.1 со следующей структурой:

В принципе, это не обязательно, поскольку из созданных файлов нам в итоге пригодится не так уж много, но мне удобнее почистить всё не нужное, нежели руками создавать нужное.
composer.json
Следующим шагом будет редактирование composer.json под наши нужды. В первую очередь, нужно изменить тип проекта type на symfony-bundle это поможет Symfony Flex определить при добавлении бандла в проект, что это действительно бандл Symfony, автоматически подключить его и установить рецепт (но об этом позже). Далее, обязательно добавляем поля name и description . name важно ещё и потому, что определяет в какую папку внутри vendor будет помещён бандл.
"name": "niklesh/health-check", "description": "Health check bundle",
Следующий важный шаг отредактировать раздел autoload , который отвечает за загрузку классов бандла. autoload для рабочего окружения, autoload-dev — для рабочего.
"autoload": < "psr-4": < "niklesh\\HealthCheckBundle\\": "src" >>, "autoload-dev": < "psr-4": < "niklesh\\HealthCheckBundle\\Tests\\": "tests" >>,
Раздел scripts можно удалить. Там содержатся скрипты для сборки ассетов и очистки кэша после выполнения команд composer install и composer update , однако у нас бандл не содержит ни ассеты, ни кэш, поэтому и команды эти бесполезны.
Последним шагом отредактируем разделы require и require-dev . В итоге получаем следующее:
"require": < "php": "^7.1.3", "ext-ctype": "*", "ext-iconv": "*", "symfony/flex": "^1.0", "symfony/framework-bundle": "^4.1", "sensio/framework-extra-bundle": "^5.2", "symfony/lts": "^4@dev", "symfony/yaml": "^4.1" >
Отмечу, что зависимости из require будут установлены при подключении бандла к рабочему проекту.
Запускаем composer update — зависимости установлены.
Чистка не нужного
Итак, из полученных файлов можно смело удалять следующие папки:
- bin — содержит файл console , необходимый для запуска команд Symfony
- config — содержит конфигурационные файлы роутинга, подключенных бандлов,
сервисов и т.д. - public — содержит index.php — точка входа в приложение
- var — тут хранятся логи и cache
Так же удаляем файлы src/Kernel.php , .env , .env.dist
Всё это нам не нужно, поскольку мы разрабатываем бандл, а не приложение.
Создание структуры бандла
Итак, мы добавили необходимые зависимости и вычистили всё не нужное из нашего бандла. Пришло время создавать необходимые файлы и папки для успешного подключения бандла к проекту.
В первую очередь в папке src создадим файл HealthCheckBundle.php с следующим содержимым:
Такой класс должен быть в каждом бандле, который вы создаёте. Именно он будет подключаться в файле config/bundles.php основного проекта. Помимо этого он может влиять на «билд» бандла.
Следующий необходимый компонент бандла — это раздел DependencyInjection . Создаём одноимённую папку с 2 файлами:
- src/DependencyInjection/Configuration.php
root('health_check'); return $treeBuilder; > >
Этот файл отвечает за парсинг и валидацию конфигурации бандла из Yaml или xml файлов. Его мы ещё модицифируем позже.
- src/DependencyInjection/HealthCheckExtension.php
*/ public function load(array $configs, ContainerBuilder $container) < $configuration = new Configuration(); $this->processConfiguration($configuration, $configs); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.yaml'); > >
Этот файл отвечает за загрузку конфигурационных файлов бандла, создание и регистрацию «definition» сервисов, загрузку параметров в контейнер и т.д.
И последний на данном этапе шаг — это добавление файла src/Resources/services.yaml Который будет содержать описание сервисов нашего бандла. Пока оставим его пустым.
HealthInterface
Основной задачей нашего бандла будет отдача данных о проекте, в котором он используется. А вот сбор информации — это работа непосредственно самого сервиса, наш бандл может только указать формат информации, которую должен передать ему сервис, и метод, который эту информацию будет получать. В моей реализации все сервисы (а их может быть несколько), которые собирают информацию должны реализовывать интерфейс HealthInterface с 2 методами: getName и getHealthInfo . Последний должен вернуть объект реализующий интерфейс HealthDataInterface .
Для начала создадим интерфейс сущности (entity) данных src/Entity/HealthDataInterface.php :
Данные должны содержать целочисленный статус и дополнительную информацию (которая, к слову, может быть и пустой).
Посколько вероятнее всего реализация этого интерфейса будет типична для большинства наследников, я решил добавить её в бандл src/Entity/CommonHealthData.php :
status = $status; > public function setStatus(int $status) < $this->status = $status; > public function setAdditionalInfo(array $additionalInfo) < $this->additionalInfo = $additionalInfo; > public function getStatus(): int < return $this->status; > public function getAdditionalInfo(): array < return $this->additionalInfo; > >
И наконец добавим интерфейс для сервисов сбора данных src/Service/HealthInterface.php :
Controller
Отдавать данные о проекте будет контроллер в всего одним роутом. Зато этот роут будет одинаков для всех проектов, использующих данный бандл: /health
Однако, задача нашего контроллера не только в том, чтобы отдать данные, но и в том, чтобы вытащить их из сервисов, реализующих HealthInterface , соответственно контроллер должен хранить в себе ссылки на каждый из этих сервисов. За добавление сервисов в контроллер будет отвечать метод addHealthService
Добавим контроллер src/Controller/HealthController.php :
healthServices[] = $healthService; > /** * @Route("/health") * @return JsonResponse */ public function getHealth(): JsonResponse < return $this->json(array_map(function (HealthInterface $healthService) < $info = $healthService->getHealthInfo(); return [ 'name' => $healthService->getName(), 'info' => [ 'status' => $info->getStatus(), 'additional_info' => $info->getAdditionalInfo() ] ]; >, $this->healthServices)); > >
Компиляция
Symfony может выполнять определённые действия с сервисами, реализующими определённый интерфейс. Можно вызвать определённый метод, добавить тэг, однако нельзя взять и проинжектить все такие сервисы в другой сервис (которым является контроллер). Такая задача решается в 4 этапа:
Добавим каждому нашему сервису, реализующему HealthInterface тэг.
Добавим константу TAG в интерфейс:
interface HealthInterface
Далее необходимо добавить этот тэг каждому сервису. В случае конфигурации проекта это можно
реализовать в файле config/services.yaml в разделе _instanceof . В нашем случае эта
запись выглядела бы следующим образом:
serivces: _instanceof: niklesh\HealthCheckBundle\Service\HealthInterface: tags: - !php/const niklesh\HealthCheckBundle\Service\HealthInterface::TAG
И, в принципе, если возложить заботу о конфигурации бандла на пользователя, это сработает, но на мой взгляд это не правильный подход, бандл сам при добавлении в проект должен правильно подключиться и сконфигурироваться с минимальным вмешательством пользователя. Кто-то возможно вспомнит о том, что у нас же есть свой services.yaml внутри бандла, но нет, он нам не поможет. Эта настройка работает только если находится в файле проекта, а не бандла.
Не знаю, баг это или фича, но сейчас имеем то, что имеем. Поэтому придётся нам внедриться в процесс компиляции бандла.
Переходим в файл src/HealthCheckBundle.php и переопределяем метод build :
registerForAutoconfiguration(HealthInterface::class)->addTag(HealthInterface::TAG); > >
Теперь каждый класс, который реализует HealthInterface будет отмечен тэгом.
Регистрация контроллера, как сервиса
На следующем шаге нам необходимо будет обратиться к контроллеру, как к сервису, на этапе компиляции бандла. В случае работы с проектом, там все классы по умолчанию регистрируются как сервисы, однако в случае работы с бандлом мы должны явно определять, какие классы будут сервисами, проставлять им аргументы, обозначать будут ли они публичными.
Открываем файл src/Resources/config/services.yaml и добавляем следующее содержимое
services: niklesh\HealthCheckBundle\Controller\HealthController: autoconfigure: true
Мы явно зарегистрировали контроллер как сервис, теперь к нему можно будет обратиться на этапе компиляции.
Добавление сервисов в контроллер.
На этапе компиляции контейнера и бандлов, мы можем оперировать только definition’ами (определениями) сервисов. На данном этапе нам необходимо взять definition HealthController и указать, что после его создания в него необходимо добавить все сервисы, которые отмечены нашим тэгом. За подобные операции в бандлах отвечают классы, реализующие интерфейс
Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface
Создадим такой класс src/DependencyInjection/Compiler/HealthServicePath.php :
has(HealthController::class)) < return; >$controller = $container->findDefinition(HealthController::class); foreach (array_keys($container->findTaggedServiceIds(HealthInterface::TAG)) as $serviceId) < $controller->addMethodCall('addHealthService', [new Reference($serviceId)]); > > >
Как видно мы сначала с помощью метода findDefinition берём контроллер, далее — все сервисы по тегу и после, в цикле, на каждый найденный сервис добавляем вызов метода addHealthService , куда передаём ссылку на этот сервис.
Последним шагом будет добавление нашего HealthServicePath в процесс компиляции бандла. Вернёмся в класс HealthCheckBundle и ещё немного изменим метод build . В результате получим:
addCompilerPass(new HealthServicesPath()); $container->registerForAutoconfiguration(HealthInterface::class)->addTag(HealthInterface::TAG); > >
В принципе, на данном этапе наш бандл уже готов к использованию. Он может находить сервисы сбора информации, работать с ними и выдавать ответ при обращении на /health (нужно только добавить настройки роутинга при подключении), однако я решил заложить в него возможность не только отдавать информацию по запросу, но и предусмотреть возможность отправки этой информации куда-либо, например с помощью POST-запроса или через менеджера очередей.
HealthSenderInterface
Данный интерфейс предназначен для описания классов, ответственных за отправку данных куда-либо. Создадим его в src/Service/HealthSenderInterface
Как видно, метод send будет каким-либо образом обрабатывать полученный массив данных из всех классов имплементирующих HealthInterface и далее отправлять туда, куда ему нужно.
Методы getDescription и getName нужны просто для отображения информации при запуске консольной команды.
SendDataCommand
Запускать рассылку данных на сторонние ресурсы будет консольная команда SendDataCommand . Её задача собрать данные для рассылки, а дальше вызвать метод send у каждого из сервисов рассылки. Очевидно, что частично эта команда будет повторять логику работы контроллера, но не во всём.
senders = $senders; > public function addHealthService(HealthInterface $healthService) < $this->healthServices[] = $healthService; > protected function configure() < parent::configure(); $this->setDescription('Send health data by senders'); > protected function initialize(InputInterface $input, OutputInterface $output) < parent::initialize($input, $output); $this->io = new SymfonyStyle($input, $output); > protected function execute(InputInterface $input, OutputInterface $output) < $this->io->title('Sending health info'); try < $data = array_map(function (HealthInterface $service): HealthDataInterface < return $service->getHealthInfo(); >, $this->healthServices); foreach ($this->senders as $sender) < $this->outputInfo($sender); $sender->send($data); > $this->io->success('Data is sent by all senders'); > catch (Throwable $exception) < $this->io->error('Exception occurred: ' . $exception->getMessage()); $this->io->text($exception->getTraceAsString()); > > private function outputInfo(HealthSenderInterface $sender) < if ($name = $sender->getName()) < $this->io->writeln($name); > if ($description = $sender->getDescription()) < $this->io->writeln($description); > > >
Модифицируем HealthServicesPath , пишем добавление сервисов сбора данных в команду.
has(HealthController::class)) < return; >$controller = $container->findDefinition(HealthController::class); $commandDefinition = $container->findDefinition(SendDataCommand::class); foreach (array_keys($container->findTaggedServiceIds(HealthInterface::TAG)) as $serviceId) < $controller->addMethodCall('addHealthService', [new Reference($serviceId)]); $commandDefinition->addMethodCall('addHealthService', [new Reference($serviceId)]); > > >
Как видно, команда в конструкторе принимает массив отправителей. В данном случае не получится воспользоваться фишкой автопривязки зависимостей, нам необходимо самим создать и зарегистрировать команду. Только вопрос ещё в том, какие именно сервисы отправителей добавить в эту команду. Будем указывать их id в конфигурации бандла вот так:
health_check: senders: - '@sender.service1' - '@sender.service2'
Наш бандл ещё не умеет обрабатывать подобные конфигурации, научим его. Переходим в Configuration.php и добавляем дерево конфигурации:
root('health_check'); $rootNode ->children() ->arrayNode('senders') ->scalarPrototype()->end() ->end() ->end() ; return $treeBuilder; > >
Данный код определяет, что корневым узлом у нас будет узел health_check , который будет содержать ноду-массив senders , которая в свою очередь будет содержать какое-то количество строк. Всё, теперь наш бандл знает, как обработать конфигурацию, что мы обозначили выше. Пришло время зарегистрировать команду. Для этого перейдём в HealthCheckExtension и добавим следующий код:
*/ public function load(array $configs, ContainerBuilder $container) < $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.yaml'); // создание определения команды $commandDefinition = new Definition(SendDataCommand::class); // добавление ссылок на отправителей в конструктор комманды foreach ($config['senders'] as $serviceId) < $commandDefinition->addArgument(new Reference($serviceId)); > // регистрация сервиса команды как консольной команды $commandDefinition->addTag('console.command', ['command' => SendDataCommand::COMMAND_NAME]); // установка определения в контейнер $container->setDefinition(SendDataCommand::class, $commandDefinition); > >
Всё, наша команда определена. Теперь, после добавления бандла в проект, при вызове
bin/console мы увидим список команд, в том числе и нашу: health:send-info , вызвать её можно так же: bin/console health:send-info
Наш бандл готов. Пришло время протестировать его в проекте. Создадим пустой проект:
composer create-project symfony/skeleton health-test-project
Добавим в него наш свежеиспечённый бандл, для этого добавим в composer.json раздел repositories :
"repositories": [ < "type": "vcs", "url": "https://github.com/HEKET313/health-check" >]
И выполним команду:
composer require niklesh/health-check
А ещё, для наиболее быстрого запуска добавим к нашему проекту сервер симфонии:
composer req --dev server
Бандл подключен, Symfony Flex автоматом подключит его в config/bundles.php , а вот для автоматического создания конфигурационных файлов необходимо создавать рецепт. Про рецепты прекрасно расписано в другой статье здесь: https://habr.com/post/345382/ — поэтому расписывать как создавать рецепты и т.д. я тут не буду, да и рецепта для этого бандла пока нет.
Тем не менее конфигурационные файлы нужны, поэтому создадим их ручками:
- config/routes/niklesh_health.yaml
health_check: resource: "@HealthCheckBundle/Controller/HealthController.php" prefix: / type: annotation
- config/packages/hiklesh_health.yaml
health_check: senders: - 'App\Service\Sender'
Теперь необходимо имплементировать классы отправки информации для команды и класс сбора информации
- src/Service/DataCollector.php
Тут всё предельно просто
public function getHealthInfo(): HealthDataInterface < $data = new CommonHealthData(HealthDataInterface::STATUS_OK); $data->setAdditionalInfo(['some_data' => 'some_value']); return $data; > >
- src/Service/Sender.php
А тут ещё проще
public function getDescription(): string < return 'Sender description'; >public function getName(): string < return 'Sender name'; >>
Готово! Почистим кэш и запустим сервер
bin/console cache:clear bin/console server:start
Теперь можно испытать нашу команду:
bin/console health:send-info
Получаем такой вот красивый вывод:

Наконец стукнемся на наш роут http://127.0.0.1:8000/health и получим менее красивый, но тоже вывод:
Вот и всё! Надеюсь этот незамысловатый туториал поможет кому-то разобраться в основах написания бандлов для Symfony 4.
Bundles
Бандл напоминает плагин используемый программах, даже лучше. Ключевая разница в том, что в Symfony все есть бандл, включая код вашего приложения и основную функциональность фреймворка. Бандл — это первый сорт граждан в Symfony. Он дает гибкость, позволяя использовать готовые пакеты предоставляемые сторонними разработчиками, а так же возможность распространять свои бандлы. Все это позволяет легко выбирать какие функции включить в свое приложение и оптимизировать их под собственные нужды.
Здесь вы узнаете основы, в тоже время есть целая статья посвященная организации и лучшим практикам по бандлам
Бандл это просто структурированный набор файлов внутри директории, который реализует одну функциональность. Вы можете создать BlogBundle, ForumBundle или бандл для управления пользователями (множество из них уже создано и лежит открытом доступе). Каждая директория содержит все что связанно с данной функциональностью, включая PHP файл, шаблоны, файлы стилей, JavaScript файлы, тесты и вообще все что угодно. Каждый аспект функциональности существует в бандле и каждая функциональность живет в бандле.
Бандлы используемые в приложении должны быть подключены в методе registerBundles() класса AppKernel
// app/AppKernel.php public function registerBundles() < $bundles = array( new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\TwigBundle\TwigBundle(), new Symfony\Bundle\MonologBundle\MonologBundle(), new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(), new Symfony\Bundle\DoctrineBundle\DoctrineBundle(), new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), new AppBundle\AppBundle(), ); if (in_array($this->getEnvironment(), array('dev', 'test'))) < $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle(); $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle(); > return $bundles; >
Метод registerBundles() предоставляет полный контроль над теми банлами которые используются в приложении (включая основные бандлы Symfony)
Бандл может жить где угодно так, как он загружается автоматически (через сформированный автозагрузчик app/autoload.php).
Создание
Symfony Standard Edition поставляется с уже созданным полно-функциональным бандлом. Конечно же создать его в ручную также просто.
Чтобы показать простоту системы бандлов, создадим и подключим новый бандл назвав его AcmeTestBundle.
Часть Acme просто вымышленное имя производителя, которое должно быть заменено на имя вашей организации (например ABCTestBundle для компании ABC )
Начнем с создания директории src/Acme/TestBundle/ и создадим в ней новый файл AcmeTestBundle.php
// src/Acme/TestBundle/AcmeTestBundle.php namespace Acme\TestBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; class AcmeTestBundle extends Bundle
Имя AcmeTestBundle соответсвует соглашению об именовании бандлов. Вы можете выбрать более короткое имя сократив название бандла до TestBundle, назвав класс TestBundle (а файл TestBundle.php)
Пустой класс это необходимая часть нового бандла. Несмотря на то, что он обычно пуст, этот класс обладает мощным потенциалом и может быть использован для изменения поведения бандла.
Теперь когда вы создали бандл, подключите его в классе AppKernel:
// app/AppKernel.php public function registerBundles() < $bundles = array( // . // register your bundle new Acme\TestBundle\AcmeTestBundle(), ); // . return $bundles; > And while it doesn't do anyt
И несмотря на то, что он пока ничего не делает, AcmeTestBundle готов к использованию.
И так же легко это можно сделать используя предоставляемый Symfony командный интерфейс для генерации базового скелета бандла:
$ php bin/console generate:bundle —namespace=Acme/TestBundle
Скелет бандла содержит базовый контроллер, шаблон и ресурсы маршрутизации, которые могут быть изменены по вашему усмотрению. Позже вы узнаете больше об инструментах командной строки Symfony.
Когда создаете новый бандл или используете сторонний, всегда помните, что бандл должен быть включен в registerBundles() . При использовании generate:bundle это делается автоматически.
Структура директории
Структура директории бандла простая и гибкая. По умолчанию система бандла следует набору соглашений которые помогают сохранять код согласованным между всеми бандлами Symfony. Взгляните на AcmeDemoBundle, который содержит некоторые общие элементы бандла:
Controller/
Содержит контроллеры бандла (например RandomController.php).
DependencyInjection/
Содержит определеные классы Dependency Injection Extension, которые могут импортировать конфигурацию сервиса, проходящий регистрацию компилятором и другое (данная директория не обязательна).
Resources/config/
Дом конфигурации, включает конфигурацию маршрутизации (например routing.yml).
Resources/views/
Содержит шаблоны упорядоченные по имени контроллера (например Random/index.html.twig).
Resources/public/
Содержит веб ресурсы (картинки, css, и т.п.) и копируется (либо создается символьная ссылка) в директории web/ проекта через команду assets:install.
Tests/
Содержит все тесты бандла.
Еще материалы
- Лучшие практики многоразовых бандлов
- Как создать дружественную конфигурацию для бандла
- Как загрузить конфигурацию сервиса внутри бандла
- Бандлы
- Как использовать наследование для переопределения частей бандла
- Как установить сторонний бандл
- Как переопределить любую часть бандла
- Как упростить конфигурацию множества бандлов
- Как удалить бандл