Pull to refresh

Пошаговое создание бандла для Symfony 4

Reading time16 min
Views27K

Около года назад наша компания взяла курс на разделение огромного монолита на 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 со следующей структурой:


image


В принципе, это не обязательно, поскольку из созданных файлов нам в итоге пригодится не так уж много, но мне удобнее почистить всё не нужное, нежели руками создавать нужное.


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 с следующим содержимым:


<?php

namespace niklesh\HealthCheckBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class HealthCheckBundle extends Bundle
{
}

Такой класс должен быть в каждом бандле, который вы создаёте. Именно он будет подключаться в файле config/bundles.php основного проекта. Помимо этого он может влиять на "билд" бандла.


Следующий необходимый компонент бандла — это раздел DependencyInjection. Создаём одноимённую папку с 2 файлами:


  • src/DependencyInjection/Configuration.php

<?php

namespace niklesh\HealthCheckBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $treeBuilder->root('health_check');
        return $treeBuilder;
    }
}

Этот файл отвечает за парсинг и валидацию конфигурации бандла из Yaml или xml файлов. Его мы ещё модицифируем позже.


  • src/DependencyInjection/HealthCheckExtension.php

<?php

namespace niklesh\HealthCheckBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;

class HealthCheckExtension extends Extension
{
    /**
     * {@inheritdoc}
     */
    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:


<?php

namespace niklesh\HealthCheckBundle\Entity;

interface HealthDataInterface
{
    public const STATUS_OK = 1;
    public const STATUS_WARNING = 2;
    public const STATUS_DANGER = 3;
    public const STATUS_CRITICAL = 4;

    public function getStatus(): int;
    public function getAdditionalInfo(): array;
}

Данные должны содержать целочисленный статус и дополнительную информацию (которая, к слову, может быть и пустой).


Посколько вероятнее всего реализация этого интерфейса будет типична для большинства наследников, я решил добавить её в бандл src/Entity/CommonHealthData.php:


<?php

namespace niklesh\HealthCheckBundle\Entity;

class CommonHealthData implements HealthDataInterface
{
    private $status;
    private $additionalInfo = [];

    public function __construct(int $status)
    {
        $this->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:


<?php

namespace niklesh\HealthCheckBundle\Service;

use niklesh\HealthCheckBundle\Entity\HealthDataInterface;

interface HealthInterface
{
    public function getName(): string;
    public function getHealthInfo(): HealthDataInterface;
}

Controller


Отдавать данные о проекте будет контроллер в всего одним роутом. Зато этот роут будет одинаков для всех проектов, использующих данный бандл: /health


Однако, задача нашего контроллера не только в том, чтобы отдать данные, но и в том, чтобы вытащить их из сервисов, реализующих HealthInterface, соответственно контроллер должен хранить в себе ссылки на каждый из этих сервисов. За добавление сервисов в контроллер будет отвечать метод addHealthService


Добавим контроллер src/Controller/HealthController.php:


<?php

namespace niklesh\HealthCheckBundle\Controller;

use niklesh\HealthCheckBundle\Service\HealthInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;

class HealthController extends AbstractController
{
    /** @var HealthInterface[] */
    private $healthServices = [];

    public function addHealthService(HealthInterface $healthService)
    {
        $this->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
{
    public const TAG = 'health.service';
}

Далее необходимо добавить этот тэг каждому сервису. В случае конфигурации проекта это можно
реализовать в файле config/services.yaml в разделе _instanceof. В нашем случае эта
запись выглядела бы следующим образом:


serivces:
  _instanceof:
    niklesh\HealthCheckBundle\Service\HealthInterface:
      tags: 
        - !php/const niklesh\HealthCheckBundle\Service\HealthInterface::TAG

И, в принципе, если возложить заботу о конфигурации бандла на пользователя, это сработает, но на мой взгляд это не правильный подход, бандл сам при добавлении в проект должен правильно подключиться и сконфигурироваться с минимальным вмешательством пользователя. Кто-то возможно вспомнит о том, что у нас же есть свой services.yaml внутри бандла, но нет, он нам не поможет. Эта настройка работает только если находится в файле проекта, а не бандла.
Не знаю, баг это или фича, но сейчас имеем то, что имеем. Поэтому придётся нам внедриться в процесс компиляции бандла.


Переходим в файл src/HealthCheckBundle.php и переопределяем метод build:


<?php

namespace niklesh\HealthCheckBundle;

use niklesh\HealthCheckBundle\Service\HealthInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class HealthCheckBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);
        $container->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:


<?php

namespace niklesh\HealthCheckBundle\DependencyInjection\Compiler;

use niklesh\HealthCheckBundle\Controller\HealthController;
use niklesh\HealthCheckBundle\Service\HealthInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class HealthServicesPath implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->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, куда передаём ссылку на этот сервис.


Использование CompilerPath


Последним шагом будет добавление нашего HealthServicePath в процесс компиляции бандла. Вернёмся в класс HealthCheckBundle и ещё немного изменим метод build. В результате получим:


<?php

namespace niklesh\HealthCheckBundle;

use niklesh\HealthCheckBundle\DependencyInjection\Compiler\HealthServicesPath;
use niklesh\HealthCheckBundle\Service\HealthInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class HealthCheckBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);
        $container->addCompilerPass(new HealthServicesPath());
        $container->registerForAutoconfiguration(HealthInterface::class)->addTag(HealthInterface::TAG);
    }
}

В принципе, на данном этапе наш бандл уже готов к использованию. Он может находить сервисы сбора информации, работать с ними и выдавать ответ при обращении на /health (нужно только добавить настройки роутинга при подключении), однако я решил заложить в него возможность не только отдавать информацию по запросу, но и предусмотреть возможность отправки этой информации куда-либо, например с помощью POST-запроса или через менеджера очередей.


HealthSenderInterface


Данный интерфейс предназначен для описания классов, ответственных за отправку данных куда-либо. Создадим его в src/Service/HealthSenderInterface


<?php

namespace niklesh\HealthCheckBundle\Service;

use niklesh\HealthCheckBundle\Entity\HealthDataInterface;

interface HealthSenderInterface
{
    /**
     * @param HealthDataInterface[] $data
     */
    public function send(array $data): void;
    public function getDescription(): string;
    public function getName(): string;
}

Как видно, метод send будет каким-либо образом обрабатывать полученный массив данных из всех классов имплементирующих HealthInterface и далее отправлять туда, куда ему нужно.
Методы getDescription и getName нужны просто для отображения информации при запуске консольной команды.


SendDataCommand


Запускать рассылку данных на сторонние ресурсы будет консольная команда SendDataCommand. Её задача собрать данные для рассылки, а дальше вызвать метод send у каждого из сервисов рассылки. Очевидно, что частично эта команда будет повторять логику работы контроллера, но не во всём.


<?php

namespace niklesh\HealthCheckBundle\Command;

use niklesh\HealthCheckBundle\Entity\HealthDataInterface;
use niklesh\HealthCheckBundle\Service\HealthInterface;
use niklesh\HealthCheckBundle\Service\HealthSenderInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Throwable;

class SendDataCommand extends Command
{
    public const COMMAND_NAME = 'health:send-info';

    private $senders;
    /** @var HealthInterface[] */
    private $healthServices;
    /** @var SymfonyStyle */
    private $io;

    public function __construct(HealthSenderInterface... $senders)
    {
        parent::__construct(self::COMMAND_NAME);

        $this->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, пишем добавление сервисов сбора данных в команду.


<?php

namespace niklesh\HealthCheckBundle\DependencyInjection\Compiler;

use niklesh\HealthCheckBundle\Command\SendDataCommand;
use niklesh\HealthCheckBundle\Controller\HealthController;
use niklesh\HealthCheckBundle\Service\HealthInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class HealthServicesPath implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->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 и добавляем дерево конфигурации:


<?php

namespace niklesh\HealthCheckBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('health_check');
        $rootNode
            ->children()
                ->arrayNode('senders')
                    ->scalarPrototype()->end()
                ->end()
            ->end()
        ;
        return $treeBuilder;
    }
}

Данный код определяет, что корневым узлом у нас будет узел health_check, который будет содержать ноду-массив senders, которая в свою очередь будет содержать какое-то количество строк. Всё, теперь наш бандл знает, как обработать конфигурацию, что мы обозначили выше. Пришло время зарегистрировать команду. Для этого перейдём в HealthCheckExtension и добавим следующий код:


<?php

namespace niklesh\HealthCheckBundle\DependencyInjection;

use niklesh\HealthCheckBundle\Command\SendDataCommand;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Reference;

class HealthCheckExtension extends Extension
{
    /**
     * {@inheritdoc}
     */
    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

Тут всё предельно просто


<?php

namespace App\Service;

use niklesh\HealthCheckBundle\Entity\CommonHealthData;
use niklesh\HealthCheckBundle\Entity\HealthDataInterface;
use niklesh\HealthCheckBundle\Service\HealthInterface;

class DataCollector implements HealthInterface
{

    public function getName(): string
    {
        return 'Data collector';
    }

    public function getHealthInfo(): HealthDataInterface
    {
        $data = new CommonHealthData(HealthDataInterface::STATUS_OK);
        $data->setAdditionalInfo(['some_data' => 'some_value']);
        return $data;
    }
}

  • src/Service/Sender.php

А тут ещё проще


<?php

namespace App\Service;

use niklesh\HealthCheckBundle\Entity\HealthDataInterface;
use niklesh\HealthCheckBundle\Service\HealthSenderInterface;

class Sender implements HealthSenderInterface
{
    /**
     * @param HealthDataInterface[] $data
     */
    public function send(array $data): void
    {
        print "Data sent\n";
    }

    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

Получаем такой вот красивый вывод:


image


Наконец стукнемся на наш роут http://127.0.0.1:8000/health и получим менее красивый, но тоже вывод:


[{"name":"Data collector","info":{"status":1,"additional_info":{"some_data":"some_value"}}}]

Вот и всё! Надеюсь этот незамысловатый туториал поможет кому-то разобраться в основах написания бандлов для Symfony 4.


P.S. Исходный код доступен здесь.

Tags:
Hubs:
+8
Comments33

Articles