Около года назад наша компания взяла курс на разделение огромного монолита на 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
с следующим содержимым:
<?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
Получаем такой вот красивый вывод:
Наконец стукнемся на наш роут http://127.0.0.1:8000/health
и получим менее красивый, но тоже вывод:
[{"name":"Data collector","info":{"status":1,"additional_info":{"some_data":"some_value"}}}]
Вот и всё! Надеюсь этот незамысловатый туториал поможет кому-то разобраться в основах написания бандлов для Symfony 4.
Комментарии (23)
BoShurik
06.08.2018 23:34Раздел с созданием скелета бандла — спорный. Гораздо проще использовать стандартный
composer init
, т.к. в вашем случае удалить придется гораздо больше, чем создать. К тому же вы добавляете лишние зависимости (по сути бандл должен зависеть только отsymfony/framework-bundle
, как минимумsymfony/flex
,sensio/framework-extra-bundle
иsymfony/lts
— лишние)
Ну а в остальном создание ничем не отличается от других версий Symfony
HEKET313 Автор
07.08.2018 18:55На счёт `symfony/flex` и `symfony/lts` в принципе могу согласиться, тем более, что они автоматом ставятся в любой проект, который использует Symfony 4, а вот `sensio/framework-extra-bundle` тут действительно нужен. Он необходим для роутинга через аннотации.
BoShurik
07.08.2018 19:20тем более, что они автоматом ставятся в любой проект, который использует Symfony 4
Речь идет о новых проектах, использующих
flex
. Если кто-то обновил свое приложение с3.4
до4
и захочет поставить ваш бандл, он очень не обрадуется появившемусяflex
, который внезапно переопределит все конфиги :)
а вот
sensio/framework-extra-bundle
тут действительно нужен. Он необходим для роутинга через аннотации.Не рекомендуется использовать такого рода "магию" в бандлах. Равно как и использование автовайринга и автоконфигрирования: Best Practices for Reusable Bundles
Services should not use autowiring or autoconfiguration. Instead, all services should be defined explicitly.
Fesor
08.08.2018 00:18extra bundle для роутинга не нужен. там добавляется лишь возможность лепить аннотацию на сервис что не очень актуально.
springimport
07.08.2018 17:02Как надо делить бандлы: по теме (пользователи, новости) или по функциональности (аутентификация, работа с файлами). Или допустимы оба варианта?
HEKET313 Автор
07.08.2018 18:53Моё мнение — по функциональности, хотя и по теме тоже можно. Например, у нас есть самописный бандл, отвечающий за отправку сообщений в Kafka, у каждого микросервиса, к которому обращаются другие микросервисы, есть свой клиентский бандл, есть бандл авторизации, который содержит кастомную аннотацию, которая добавляет авторизацию к тому или иному action'у контроллера — это что касается функционального разделения. В то же время есть и тематический бандл, который отвечает за определение, с какого именно интернет-магазина (у нас их несколько) пришёл запрос и содержит общие константы, URL'ы и кучу разной статической информации.
Моё мнение — как удобно, так и делите, другое дело, что бандл не должен быть монструозным и содержать чрезмерное количество разрозненного функционала, это не должен быть мини-монолит, который вы таскаете между проектами.springimport
08.08.2018 16:06Тоже к этому пришел: оба варианта.
бандл не должен быть монструозным
Зависит от виртуозности разработчика :) К этому надо стремиться, в этом случае правило unix очень кстати: «сделай мало, но хорошо», главное чтобы не получилось как в npm.Fesor
09.08.2018 01:24главное чтобы не получилось как в npm
с npm проблема только одна — разработчики npm-а которые… ну я могу долго тут их ругать за то что у них странные приоритеты и странное отношение к понятию lock файл. Но главный их косяк — отсутствие возможностти подписывать пакеты, дабы исключить возможность подмены как это случалось не однократно (что приводило к веселым новостям о том что eslint слил чьито ключи в сеть).
Что до "сделай мало но хорошо" — посмотрите на штуки типа grep, less и т.д. Я не могу сказать что они мало делают. Ну то есть… проблема этого "мало" в том что это относительное понятие и оно для каждого свое.
Суть не в том что бы там чего-то было "мало" или "много" а в том, что бы причины для изменений штуки были одни и те же. Если причины разные — рано или поздно они начнут конфликтовать между собой и придется идти на компромисы. так магенты появляются (там еще избыточное обобщение и неверная стратегия "расширяемости" в стиле вротпресса).
Fesor
08.08.2018 00:29Вопервых проясним вопрос с терминами «темы» и «функциональность». По сути это должны быть синонимы. Во вторых ваши «пользователи» являются совокупностью разных функциональностей (и возможно должны быть разбиты), в частности это аутентификация, профили (иногда это разные профили, представьте себе убер где есть профиль пассажира и профиль водителя), есть сквозная функциональность (например у тех же профилей есть рейтинг, который неплохо выделяется в свой модуль) ну и т.д.
Во вторых на тему разделения проекта на модули (а бандлы это вид модуля) пишут книги еще с 70-х, и все сводится к тому что бы уменьшать связанность между ними и повышать cohesion. Лучше с этими вопросами попробуйте разобраться.
Скажем… типичная проблема людей, которую я наблюдал на проектах — желание разделить на бандлы и потом юзать нутро этих бандлов в других бандлах. Типа есть бандл «пользователи» и во всех остальных бандлах идут отсылки на сущность `User` из этого бандла, хотя в целом достаточно лишь айдишки (хотя логика тут ясна — так намного проще, особенно по началу, в плане выплевывания данных в шаблоны). Ну и всякие CommonBundle, CoreBundle и прочие UtilsBundle свидетельствуют о проблемах с декомпозицией на проекте.
Ну и в заключение — не надо делить проект на бандлы. Вообще. Бандлы нужны для реюза кода между проектами, что бы можно было их подключить к проекту и все. Та же аутентификация или восстановление пароля в целом неплохо обобщаются до бандла, но мне не известны примеры где это сделано хорошо (хотя скажем есть вариант подключить какой AuthN-server и не париться с бандлами, но это больше вообще к вопросу о расширении кругозора к тому как не писать код а юзать готовое).
Если вы хотите разделить проект — сделайте нэймспейс. Этого более чем достаточно.springimport
08.08.2018 16:22Я считаю что нужно смотреть реалистично. В теории можно сделать идеальный проект с двадцаткой бандлов всех мастей которые делают «мало, но хорошо». На практике редко когда удается что-то отличное (ну да, квалификация и все такое). И самое главное, не факт что эта нарезанная картошка найдет свой суп в то время когда нужно приготовить пиццу. Пожалуй, такие универсальные штуки во всем может позволить себе делать разве что sensiolab.
Я бы наверное сделал бы этот самый общий User.
уменьшать связанность между ними и повышать cohesion
Вспоминаю модули в magento 2, всегда думал что это не правильно все :)
желание разделить на бандлы и потом юзать нутро этих бандлов в других бандлах
Понятно что наследование лучше избежать.
не надо делить проект на бандлы. Вообще. Бандлы нужны для реюза кода между проектами, что бы можно было их подключить к проекту и все
всякие CoreBundle
Не понял, можно иметь CoreBundle, только называть так нельзя?apapacy
08.08.2018 17:08Это интересный вопрос. Чаще всего приложение пишут в бандле (AppBundle) хотя насколько я понимаю разработчик продукта это не приветствует
symfony.com/doc/current/best_practices/creating-the-project.htmlHEKET313 Автор
08.08.2018 17:49До Symfony 4 писали внутри бандла, в 4 от этого отказались. Теперь все файлы проекта находятся внутри директории `src` без всяких бандлов.
apapacy
08.08.2018 18:24Насколько я понимаю так можно делать было и раньше см. knpuniversity.com/blog/AppBundle
Я конечно не сторонник там всякой отсебятины и наоборот сторонник все делать по мануалам. И меня немного удивило в одной конторе когда я обнаружил AppKernel там где его наверное никто больше не держит. Но это бесконечное bundle -bundle которое слышишь целый день немного утомляло. Интересно как реальные разработчики восприняли такое новшество. Боюсь что все по привычке держат в бандле да ещё и не в одномFesor
09.08.2018 01:19ходят легенды что изначально план был дать возможность людям дробить проекты на кернелы, потому была папка
app
иsrc
. В этом случае удобно было бы отдельные модули подключать в свои кернелы бандлами и т.д.
Люди неправильно поняли и начали воспринимать бандлы как модули. и дробить проект именно таким образом. Причина для этого очень и очень проста — автоконфигурация. Вы создали папку
Entity
внутри бандла и доктрина автоматически подтянула оттуда сущности. Вы создали папкуResources
и… ну вы поняли идею.
Жить без бандлов вообще можно было в целом с самого начала. Но ключевой момент распространенных заблуждений — автоконфигурация и неудобные конфиги.
С выходом symfony 3.3 по сути конфиги симфони стали куда более удобны для "безбандальной структуры". Но проблема того, что люди структурируют проект под автоконфигурацию, все еще никуда не делась.
что до мануалов — best practice симфони появился с выходом symfony 2.8 вроде, то есть люди уже успели набомбить проектов и привыкнуть. В этом бэст практис покрывался вопрос что нет смысла дробить на бандлы но опять же автоконфигурация а потому "если хотите бандл — не делите ничего а юзайте AppBundle".
VolCh
09.08.2018 10:44+1Скорее заготовки под кернелы/приложения — наследие symfony 1.*, фича которой часто пользовались для, например, разделения публичной части и админки. Но особо востребованности не было и забили.
VolCh
09.08.2018 10:40По-моему, уже в 3.3 отказались от бандла, причём по тихому так, уже после релиза, ближе к 3.4
Fesor
09.08.2018 01:12В теории можно сделать идеальный проект с двадцаткой бандлов всех мастей которые делают «мало, но хорошо»
еще раз — не надо делить проект на бандлы. Бандлы для реюза кода между проектами, инфраструктура какая-то, обобщенный функционал. Например вам надо быстро прикрутить websockets — ставим centrifugo bundle. Бизнес логики в бандлах быть не должно.
Я считаю что нужно смотреть реалистично.
То есть забить просто и не пытаться даже делать нормально декомпозицию? ну ладно.
Вспоминаю модули в magento 2
магенту это вообще страх и слезы.
Понятно что наследование лучше избежать.
причем тут наследование? В целом когда у вас есть модуль
Users
и еще десяток и все юзают сущность из этого бедного модуляUsers
, хотя им только айдишка нужна. И это самый лайтовый пример факапов.
Не понял, можно иметь CoreBundle, только называть так нельзя?
Суть не в названии, вы же это понимаете. Я сейчас говорю о разделении приложения на модули. То есть вы можете сделать какой-нибудь модуль
Infrastructure
, но там не должно быть сущностей и бизнес логики.
oxidmod
08.08.2018 16:35По сути бандлы — независимые части. Значит для взаимодействия между ними нужно пилить адаптеры. Пилить адаптеры для собственного кода… Это странно, имхо. Лучше свою БЛ вообще вне бандла держать. В бандлы выносить только вещи, которые сами в себе. К примеру какойто кеш (которому пофиг что кешить), работу с очередями, хранилищами и прочее.
VolCh
09.08.2018 10:47+1Сейчас хорошее использование непереиспользуемых бандлов — подготовка к разделению монолита на (микро)сервисы.
BoShurik
Вместо `CompiledPass` проще использовать `!tagged`: symfony.com/blog/new-in-symfony-3-4-simpler-injection-of-tagged-services
HEKET313 Автор
Действительно, не знал про этот способ. Спасибо.