Около года назад наша компания взяла курс на разделение огромного монолита на Magento 1 на микросервисы. Как основу выбрали только вышедшую в релиз Symfony 4. За это время я разработал несколько проектов на этом фреймворке, но особо интересной мне показалась разработка бандлов, переиспользуемых компонентов для Symfony. Под катом пошаговое руководство по разработке HealthCheck бандла для получения статуса/здоровья микросервиса под Syfmony 4.1, в котором я постарался затронуть наиболее интересные и сложные (для меня когда-то) моменты.  В нашей компании этот бандл используется, например, для получения статуса реиндекса продуктов в ElasticSearch — сколько товаров содержится в Elastic с актуальными данными, а сколько требуют индексации. В Symfony 3 для генерации скелетов бандлов был удобный бандл, однако в Symfony 4 он более не поддерживается и потому скелет приходится создавать самому. Разработку каждого нового проекта я начинаю с запуска команды Обратите внимание, что Symfony 4 поддерживает PHP 7.1+, соответственно если запустить эту команду на версии ниже, то вы получите скелет проекта на Symfony 3. Эта команда создаёт новый проект Symfony 4.1 со следующей структурой:  В принципе, это не обязательно, поскольку из созданных файлов нам в итоге пригодится не так уж много, но мне удобнее почистить всё не нужное, нежели руками создавать нужное. Следующим шагом будет редактирование composer.json под наши нужды. В первую очередь, нужно изменить тип проекта type на symfony-bundle это поможет Symfony Flex определить при добавлении бандла в проект, что это действительно бандл Symfony, автоматически подключить его и установить рецепт (но об этом позже). Далее, обязательно добавляем поля name и description. name важно ещё и потому, что определяет в какую папку внутри vendor будет помещён бандл.  Следующий важный шаг отредактировать раздел autoload, который отвечает за загрузку классов бандла. autoload для рабочего окружения, autoload-dev — для рабочего. Раздел scripts можно удалить. Там содержатся скрипты для сборки ассетов и очистки кэша после выполнения команд composer install и composer update, однако у нас бандл не содержит ни ассеты, ни кэш, поэтому и команды эти бесполезны. Последним шагом отредактируем разделы require и require-dev. В итоге получаем следующее: Отмечу, что зависимости из require будут установлены при подключении бандла к рабочему проекту. Запускаем composer update — зависимости установлены. Итак, из полученных файлов можно смело удалять следующие папки: Так же удаляем файлы src/Kernel.php, .env, .env.dist
Всё это нам не нужно, поскольку мы разрабатываем бандл, а не приложение. Итак, мы добавили необходимые зависимости и вычистили всё не нужное из нашего бандла. Пришло время создавать необходимые файлы и папки для успешного подключения бандла к проекту. В первую очередь в папке src создадим файл HealthCheckBundle.php с следующим содержимым: Такой класс должен быть в каждом бандле, который вы создаёте. Именно он будет подключаться в файле config/bundles.php основного проекта. Помимо этого он может влиять на "билд" бандла. Следующий необходимый компонент бандла — это раздел DependencyInjection. Создаём одноимённую папку с 2 файлами: Этот файл отвечает за парсинг и валидацию конфигурации бандла из Yaml или xml файлов. Его мы ещё модицифируем позже. Этот файл отвечает за загрузку конфигурационных файлов бандла, создание и регистрацию "definition" сервисов, загрузку параметров в контейнер и т.д. И последний на данном этапе шаг — это добавление файла src/Resources/services.yaml Который будет содержать описание сервисов нашего бандла. Пока оставим его пустым. Основной задачей нашего бандла будет отдача данных о проекте, в котором он используется. А вот сбор информации — это работа непосредственно самого сервиса, наш бандл может только указать формат информации, которую должен передать ему сервис, и метод, который эту информацию будет получать. В моей реализации все сервисы (а их может быть несколько), которые собирают информацию должны реализовывать интерфейс HealthInterface с 2 методами: getName и getHealthInfo. Последний должен вернуть объект реализующий интерфейс HealthDataInterface. Для начала создадим интерфейс сущности (entity) данных src/Entity/HealthDataInterface.php: Данные должны содержать целочисленный статус и дополнительную информацию (которая, к слову, может быть и пустой). Посколько вероятнее всего реализация этого интерфейса будет типична для большинства наследников, я решил добавить её в бандл src/Entity/CommonHealthData.php: И наконец добавим интерфейс для сервисов сбора данных src/Service/HealthInterface.php: Отдавать данные о проекте будет контроллер в всего одним роутом. Зато этот роут будет одинаков для всех проектов, использующих данный бандл: /health Однако, задача нашего контроллера не только в том, чтобы отдать данные, но и в том, чтобы вытащить их из сервисов, реализующих HealthInterface, соответственно контроллер должен хранить в себе ссылки на каждый из этих сервисов. За добавление сервисов в контроллер будет отвечать метод addHealthService Добавим контроллер src/Controller/HealthController.php: Symfony может выполнять определённые действия с сервисами, реализующими определённый интерфейс. Можно вызвать определённый метод, добавить тэг, однако нельзя взять и проинжектить все такие сервисы в другой сервис (которым является контроллер). Такая задача решается в 4 этапа: Добавим каждому нашему сервису, реализующему HealthInterface тэг. Добавим константу TAG в интерфейс: Далее необходимо добавить этот тэг каждому сервису. В случае конфигурации проекта это можно
реализовать в файле config/services.yaml в разделе _instanceof. В нашем случае эта
запись выглядела бы следующим образом: И, в принципе, если возложить заботу о конфигурации бандла на пользователя, это сработает, но на мой взгляд это не правильный подход, бандл сам при добавлении в проект должен правильно подключиться и сконфигурироваться с минимальным вмешательством пользователя. Кто-то возможно вспомнит о том, что у нас же есть свой services.yaml внутри бандла, но нет, он нам не поможет. Эта настройка работает только если находится в файле проекта, а не бандла.
Не знаю, баг это или фича, но сейчас имеем то, что имеем. Поэтому придётся нам внедриться в процесс компиляции бандла. Переходим в файл src/HealthCheckBundle.php и переопределяем метод build: Теперь каждый класс, который реализует HealthInterface будет отмечен тэгом. Регистрация контроллера, как сервиса На следующем шаге нам необходимо будет обратиться к контроллеру, как к сервису, на этапе компиляции бандла. В случае работы с проектом, там все классы по умолчанию регистрируются как сервисы, однако в случае работы с бандлом мы должны явно определять, какие классы будут сервисами, проставлять им аргументы, обозначать будут ли они публичными. Открываем файл src/Resources/config/services.yaml и добавляем следующее содержимое Мы явно зарегистрировали контроллер как сервис, теперь к нему можно будет обратиться на этапе компиляции. Добавление сервисов в контроллер. На этапе компиляции контейнера и бандлов, мы можем оперировать только definition'ами (определениями) сервисов. На данном этапе нам необходимо взять definition HealthController и указать, что после его создания в него необходимо добавить все сервисы, которые отмечены нашим тэгом. За подобные операции в бандлах отвечают классы, реализующие интерфейс
Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface Создадим такой класс src/DependencyInjection/Compiler/HealthServicePath.php: Как видно мы сначала с помощью метода findDefinition берём контроллер, далее — все сервисы по тегу и после, в цикле, на каждый найденный сервис добавляем вызов метода addHealthService, куда передаём ссылку на этот сервис. Использование CompilerPath Последним шагом будет добавление нашего HealthServicePath в процесс компиляции бандла. Вернёмся в класс HealthCheckBundle и ещё немного изменим метод build. В результате получим: В принципе, на данном этапе наш бандл уже готов к использованию. Он может находить сервисы сбора информации, работать с ними и выдавать ответ при обращении на /health (нужно только добавить настройки роутинга при подключении), однако я решил заложить в него возможность не только отдавать информацию по запросу, но и предусмотреть возможность отправки этой информации куда-либо, например с помощью POST-запроса или через менеджера очередей. Данный интерфейс предназначен для описания классов, ответственных за отправку данных куда-либо. Создадим его в src/Service/HealthSenderInterface Как видно, метод send будет каким-либо образом обрабатывать полученный массив данных из всех классов имплементирующих HealthInterface и далее отправлять туда, куда ему нужно.
Методы getDescription и getName нужны просто для отображения информации при запуске консольной команды. Запускать рассылку данных на сторонние ресурсы будет консольная команда SendDataCommand. Её задача собрать данные для рассылки, а дальше вызвать метод send у каждого из сервисов рассылки. Очевидно, что частично эта команда будет повторять логику работы контроллера, но не во всём. Модифицируем HealthServicesPath, пишем добавление сервисов сбора данных в команду. Как видно, команда в конструкторе принимает массив отправителей. В данном случае не получится воспользоваться фишкой автопривязки зависимостей, нам необходимо самим создать и зарегистрировать команду. Только вопрос ещё в том, какие именно сервисы отправителей добавить в эту команду. Будем указывать их id в конфигурации бандла вот так: Наш бандл ещё не умеет обрабатывать подобные конфигурации, научим его. Переходим в Configuration.php и добавляем дерево конфигурации: Данный код определяет, что корневым узлом у нас будет узел health_check, который будет содержать ноду-массив senders, которая в свою очередь будет содержать какое-то количество строк. Всё, теперь наш бандл знает, как обработать конфигурацию, что мы обозначили выше. Пришло время зарегистрировать команду. Для этого перейдём в HealthCheckExtension и добавим следующий код: Всё, наша команда определена. Теперь, после добавления бандла в проект, при вызове
bin/console мы увидим список команд, в том числе и нашу: health:send-info, вызвать её можно так же: bin/console health:send-info Наш бандл готов. Пришло время протестировать его в проекте. Создадим пустой проект: Добавим в него наш свежеиспечённый бандл, для этого добавим в composer.json раздел repositories: И выполним команду: А ещё, для наиболее быстрого запуска добавим к нашему проекту сервер симфонии: Бандл подключен, Symfony Flex автоматом подключит его в config/bundles.php, а вот для автоматического создания конфигурационных файлов необходимо создавать рецепт. Про рецепты прекрасно расписано в другой статье здесь: https://habr.com/post/345382/ — поэтому расписывать как создавать рецепты и т.д. я тут не буду, да и рецепта для этого бандла пока нет. Тем не менее конфигурационные файлы нужны, поэтому создадим их ручками: Теперь необходимо имплементировать классы отправки информации для команды и класс сбора информации Тут всё предельно просто А тут ещё проще Готово! Почистим кэш и запустим сервер  Теперь можно испытать нашу команду: Получаем такой вот красивый вывод:  Наконец стукнемся на наш роут http://127.0.0.1:8000/health и получим менее красивый, но тоже вывод: Вот и всё! Надеюсь этот незамысловатый туториал поможет кому-то разобраться в основах написания бандлов для Symfony 4.  P.S. Исходный код доступен здесь. Backend Engineer