From 1b032e48e409257a61b41b1cf758bbec0f1ba8f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20=C5=81ebkowski?= Date: Mon, 23 Feb 2026 18:27:09 +0100 Subject: [PATCH 1/2] add console processor for monolog --- composer.json | 6 +- composer.lock | 105 +++++++++++++++++- .../Logging/ConsoleHandlerEventSubscriber.php | 37 ++++++ src/Cli/Logging/ConsoleIo.php | 16 +++ src/Cli/Logging/ConsoleIoStack.php | 36 ++++++ src/Cli/Logging/CurrentConsoleInput.php | 16 +++ src/Cli/Logging/CurrentConsoleOutput.php | 16 +++ .../Logging/Formatter/ConsoleFormatter.php | 100 +++++++++++++++++ src/Cli/Logging/Handler/ConsoleHandler.php | 76 +++++++++++++ .../Processor/ConsoleCommandProcessor.php | 31 ++++++ src/KernelBuilder.php | 1 + .../EventDispatcherServiceFactory.php | 21 ++++ .../SymfonyConsoleServiceFactory.php | 4 +- tests/ConsoleLogger/ConsoleLoggerTest.php | 68 ++++++++++++ tests/Resources/ConsoleLogger/composer.json | 7 ++ .../ConsoleLogger/src/EchoCommand.php | 27 +++++ 16 files changed, 564 insertions(+), 3 deletions(-) create mode 100644 src/Cli/Logging/ConsoleHandlerEventSubscriber.php create mode 100644 src/Cli/Logging/ConsoleIo.php create mode 100644 src/Cli/Logging/ConsoleIoStack.php create mode 100644 src/Cli/Logging/CurrentConsoleInput.php create mode 100644 src/Cli/Logging/CurrentConsoleOutput.php create mode 100644 src/Cli/Logging/Formatter/ConsoleFormatter.php create mode 100644 src/Cli/Logging/Handler/ConsoleHandler.php create mode 100644 src/Cli/Logging/Processor/ConsoleCommandProcessor.php create mode 100644 src/ServiceFactory/EventDispatcherServiceFactory.php create mode 100644 tests/ConsoleLogger/ConsoleLoggerTest.php create mode 100644 tests/Resources/ConsoleLogger/composer.json create mode 100644 tests/Resources/ConsoleLogger/src/EchoCommand.php diff --git a/composer.json b/composer.json index de40a1a..001e250 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ "symfony/serializer": "^7.2", "phpstan/phpdoc-parser": "^2.0", "phpdocumentor/reflection-docblock": "^5.6", - "wondernetwork/php-collection-library": "^10.0" + "wondernetwork/php-collection-library": "^10.0", + "monolog/monolog": "^3.10" }, "license": "MIT", "autoload": { @@ -34,6 +35,9 @@ "tests/Resources/App/src/", "tests/Resources/ErrorMiddleware/src/", "tests/Resources/Messenger/src/" + ], + "Acme\\ConsoleLogger\\": [ + "tests/Resources/ConsoleLogger/src/" ] } }, diff --git a/composer.lock b/composer.lock index 35c3ea2..ae5d3cf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5772a61c3ce824269fccf4122016f2c7", + "content-hash": "898f671ffaee328367e380f2aebe2e1a", "packages": [ { "name": "doctrine/deprecations", @@ -168,6 +168,109 @@ }, "time": "2025-01-24T15:42:37+00:00" }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, { "name": "nikic/fast-route", "version": "v1.3.0", diff --git a/src/Cli/Logging/ConsoleHandlerEventSubscriber.php b/src/Cli/Logging/ConsoleHandlerEventSubscriber.php new file mode 100644 index 0000000..3b84e79 --- /dev/null +++ b/src/Cli/Logging/ConsoleHandlerEventSubscriber.php @@ -0,0 +1,37 @@ + 'onCommand', + ConsoleEvents::TERMINATE => 'onTerminate', + ]; + } + + public function __construct(private ConsoleIoStack $consoleIoStack) { + } + + public function onCommand(ConsoleCommandEvent $event): void { + $input = $event->getInput(); + $output = $event->getOutput(); + + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + $this->consoleIoStack->push(new ConsoleIo($input, $output)); + } + + public function onTerminate(): void { + $this->consoleIoStack->pop(); + } +} diff --git a/src/Cli/Logging/ConsoleIo.php b/src/Cli/Logging/ConsoleIo.php new file mode 100644 index 0000000..abd768f --- /dev/null +++ b/src/Cli/Logging/ConsoleIo.php @@ -0,0 +1,16 @@ + + */ + private SplStack $stack; + + public function __construct() { + $this->stack = new SplStack(); + } + + public function push(ConsoleIo $consoleIo): void { + $this->stack->push($consoleIo); + } + + public function pop(): void { + if (false === $this->stack->isEmpty()) { + $this->stack->pop(); + } + } + + public function current(): ?ConsoleIo { + if ($this->stack->isEmpty()) { + return null; + } + + return $this->stack->top(); + } +} diff --git a/src/Cli/Logging/CurrentConsoleInput.php b/src/Cli/Logging/CurrentConsoleInput.php new file mode 100644 index 0000000..7c509c7 --- /dev/null +++ b/src/Cli/Logging/CurrentConsoleInput.php @@ -0,0 +1,16 @@ +stack->current()?->input; + } +} diff --git a/src/Cli/Logging/CurrentConsoleOutput.php b/src/Cli/Logging/CurrentConsoleOutput.php new file mode 100644 index 0000000..4bbe023 --- /dev/null +++ b/src/Cli/Logging/CurrentConsoleOutput.php @@ -0,0 +1,16 @@ +stack->current()?->output; + } +} diff --git a/src/Cli/Logging/Formatter/ConsoleFormatter.php b/src/Cli/Logging/Formatter/ConsoleFormatter.php new file mode 100644 index 0000000..13af440 --- /dev/null +++ b/src/Cli/Logging/Formatter/ConsoleFormatter.php @@ -0,0 +1,100 @@ +value => 'fg=white', + Level::Info->value => 'fg=green', + Level::Notice->value => 'fg=blue', + Level::Warning->value => 'fg=cyan', + Level::Error->value => 'fg=yellow', + Level::Critical->value => 'fg=red', + Level::Alert->value => 'fg=red', + Level::Emergency->value => 'fg=white;bg=red', + ]; + + public function formatBatch(array $records): mixed { + foreach ($records as $key => $record) { + $records[$key] = $this->format($record); + } + + return $records; + } + + public function format(LogRecord $record): string { + $record = $this->replacePlaceHolder($record); + + $levelColor = \sprintf('<%s>', self::LEVEL_COLOR_MAP[$record->level->value]); + + return \strtr( + "%datetime% %channel% %start_tag%%level_name%%end_tag% %message%\n", + [ + '%datetime%' => $record->datetime->format('H:i:s'), + '%start_tag%' => $levelColor, + '%level_name%' => strtolower($record->level->getName()), + '%end_tag%' => '', + '%channel%' => $record->channel, + '%message%' => $this->replacePlaceHolder($record)->message, + ], + ); + } + + private function replacePlaceHolder(LogRecord $record): LogRecord { + $message = $record->message; + + if (false === \str_contains($message, '{')) { + return $record; + } + + $context = $record->context; + + $replacements = []; + + foreach ($context as $k => $v) { + $v = OutputFormatter::escape($this->dumpData($v)); + $replacements['{'.$k.'}'] = \sprintf('%s', $v); + } + + return $record->with(message: \strtr($message, $replacements)); + } + + private function dumpData(mixed $data): string { + if (\is_string($data) || \is_int($data) || \is_float($data) || $data instanceof Stringable) { + return (string) $data; + } + + if (null === $data) { + return 'N/A'; + } + + if (\is_bool($data)) { + return $data ? 'true' : 'false'; + } + + if (\is_array($data)) { + return \sprintf('array(%d)', \count($data)); + } + + if (\is_resource($data)) { + return \sprintf('resource(%d)', \get_resource_type($data)); + } + + if (\is_object($data)) { + return \sprintf('object(%d)', $data::class); + } + + return \sprintf('unknown(%s)', \get_debug_type($data)); + } +} diff --git a/src/Cli/Logging/Handler/ConsoleHandler.php b/src/Cli/Logging/Handler/ConsoleHandler.php new file mode 100644 index 0000000..1908049 --- /dev/null +++ b/src/Cli/Logging/Handler/ConsoleHandler.php @@ -0,0 +1,76 @@ + Level::Error, + OutputInterface::VERBOSITY_NORMAL => Level::Warning, + OutputInterface::VERBOSITY_VERBOSE => Level::Notice, + OutputInterface::VERBOSITY_VERY_VERBOSE => Level::Info, + OutputInterface::VERBOSITY_DEBUG => Level::Debug, + ]; + + public function __construct(private readonly CurrentConsoleOutput $output) { + parent::__construct(); + } + + public function isHandling(LogRecord $record): bool { + return $this->updateLevel() && parent::isHandling($record); + } + + public function handle(LogRecord $record): bool { + // we have to update the logging level each time because the verbosity of the + // console output might have changed in the meantime (it is not immutable) + return $this->updateLevel() && parent::handle($record); + } + + protected function write(LogRecord $record): void { + if (false === is_string($record->formatted) && false === $record->formatted instanceof Stringable) { + return; + } + + $output = $this->output->currentOutput(); + assert(null !== $output); + + $output->write( + (string) $record->formatted, + false, + $output->getVerbosity(), + ); + } + + protected function getDefaultFormatter(): FormatterInterface { + return new ConsoleFormatter(); + } + + /** + * Updates the logging level based on the verbosity setting of the console output. + * + * @return bool Whether the handler is enabled and verbosity is not set to quiet + */ + private function updateLevel(): bool { + $output = $this->output->currentOutput(); + + if (null === $output) { + return false; + } + + $verbosity = $output->getVerbosity(); + $this->setLevel(self::VERBOSITY_LEVEL_MAP[$verbosity]); + + return true; + } +} diff --git a/src/Cli/Logging/Processor/ConsoleCommandProcessor.php b/src/Cli/Logging/Processor/ConsoleCommandProcessor.php new file mode 100644 index 0000000..586efdd --- /dev/null +++ b/src/Cli/Logging/Processor/ConsoleCommandProcessor.php @@ -0,0 +1,31 @@ +input->currentInput(); + + if (null !== $input) { + $record->extra[$this->nameKey] = $input->getFirstArgument(); + $record->extra[$this->optionsKey] = $input->getOptions(); + $record->extra[$this->argumentsKey] = $input->getArguments(); + } + + return $record; + } +} diff --git a/src/KernelBuilder.php b/src/KernelBuilder.php index 9b144c9..b14553f 100644 --- a/src/KernelBuilder.php +++ b/src/KernelBuilder.php @@ -26,6 +26,7 @@ private function __construct(ServicesBuilder $servicesBuilder) { $this->builder = new ContainerBuilder(); $this->startupHook = new HookCollection(); $this->register(new ServiceFactory\SlimServiceFactory()); + $this->register(new ServiceFactory\EventDispatcherServiceFactory()); } /** @param array $definitions */ diff --git a/src/ServiceFactory/EventDispatcherServiceFactory.php b/src/ServiceFactory/EventDispatcherServiceFactory.php new file mode 100644 index 0000000..9e726c4 --- /dev/null +++ b/src/ServiceFactory/EventDispatcherServiceFactory.php @@ -0,0 +1,21 @@ + autowire(); + yield EventDispatcherInterface::class => get(EventDispatcher::class); + yield Psr\EventDispatcherInterface::class => get(EventDispatcher::class); + } +} diff --git a/src/ServiceFactory/SymfonyConsoleServiceFactory.php b/src/ServiceFactory/SymfonyConsoleServiceFactory.php index 4a70f16..9d836f4 100644 --- a/src/ServiceFactory/SymfonyConsoleServiceFactory.php +++ b/src/ServiceFactory/SymfonyConsoleServiceFactory.php @@ -6,6 +6,7 @@ use DI\Definition\Helper\CreateDefinitionHelper; use Symfony\Component\Console; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use WonderNetwork\SlimKernel\Cli\AutoExit; use WonderNetwork\SlimKernel\ServiceFactory; use WonderNetwork\SlimKernel\ServicesBuilder; @@ -34,7 +35,8 @@ public function __invoke(ServicesBuilder $builder): iterable { ), autowire() ->constructor($this->name) - ->method('setAutoExit', static fn (AutoExit $autoExit) => $autoExit->value()), + ->method('setAutoExit', static fn (AutoExit $autoExit) => $autoExit->value()) + ->method('setDispatcher', get(EventDispatcherInterface::class)), ); } } diff --git a/tests/ConsoleLogger/ConsoleLoggerTest.php b/tests/ConsoleLogger/ConsoleLoggerTest.php new file mode 100644 index 0000000..1360b6c --- /dev/null +++ b/tests/ConsoleLogger/ConsoleLoggerTest.php @@ -0,0 +1,68 @@ +fail('Root path does not exist'); + } + + $container = KernelBuilder::start($rootPath) + ->add( + [ + ConsoleIoStack::class => autowire(), + ConsoleHandler::class => autowire(), + CurrentConsoleOutput::class => autowire(), + CurrentConsoleInput::class => autowire(), + LoggerInterface::class => function (ConsoleHandler $consoleHandler) { + $logger = new Logger('channel'); + $logger->pushHandler($consoleHandler); + + return $logger; + }, + ConsoleHandlerEventSubscriber::class => autowire(), + EventDispatcher::class => decorate( + function (EventDispatcher $dispatcher, ContainerInterface $container) { + $dispatcher->addSubscriber($container->get(ConsoleHandlerEventSubscriber::class)); + return $dispatcher; + }, + ), + ], + ) + ->register( + new SymfonyConsoleServiceFactory( + path: '/src/*Command.php', + ), + ) + ->build(); + + /** @var Application $app */ + $app = $container->get(Application::class); + $output = new BufferedOutput(); + $app->run(input: new ArrayInput(['echo', '-vv', 'message' => 'Hello World']), output: $output); + + self::assertStringContainsString("channel info Received Hello World", $output->fetch()); + } +} diff --git a/tests/Resources/ConsoleLogger/composer.json b/tests/Resources/ConsoleLogger/composer.json new file mode 100644 index 0000000..bb59a8e --- /dev/null +++ b/tests/Resources/ConsoleLogger/composer.json @@ -0,0 +1,7 @@ +{ + "autoload": { + "psr-4": { + "Acme\\ConsoleLogger\\": "src/" + } + } +} diff --git a/tests/Resources/ConsoleLogger/src/EchoCommand.php b/tests/Resources/ConsoleLogger/src/EchoCommand.php new file mode 100644 index 0000000..1ec2bf2 --- /dev/null +++ b/tests/Resources/ConsoleLogger/src/EchoCommand.php @@ -0,0 +1,27 @@ +addArgument('message', InputArgument::REQUIRED); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->logger->info("Received {message}", ['message' => $input->getArgument('message')]); + + return self::SUCCESS; + } +} From 1c31881fbd196f1312ef8cc3c70c46718d04b6cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20=C5=81ebkowski?= Date: Mon, 23 Feb 2026 18:52:41 +0100 Subject: [PATCH 2/2] lazy event subscribers --- .../EventSubscribersCollection.php | 72 +++++++++++++++++++ src/EventDispatcher/LazyListener.php | 21 ++++++ src/EventDispatcher/LazyListenerFactory.php | 29 ++++++++ tests/ConsoleLogger/ConsoleLoggerTest.php | 14 ++-- 4 files changed, 127 insertions(+), 9 deletions(-) create mode 100644 src/EventDispatcher/EventSubscribersCollection.php create mode 100644 src/EventDispatcher/LazyListener.php create mode 100644 src/EventDispatcher/LazyListenerFactory.php diff --git a/src/EventDispatcher/EventSubscribersCollection.php b/src/EventDispatcher/EventSubscribersCollection.php new file mode 100644 index 0000000..4c6133d --- /dev/null +++ b/src/EventDispatcher/EventSubscribersCollection.php @@ -0,0 +1,72 @@ +> $subscribers + */ + public function __construct(private array $subscribers) { + } + + /** + * @param class-string ...$subscribers + */ + public function add(string ...$subscribers): self { + return new self([...$this->subscribers, ...array_values($subscribers)]); + } + + public function addLazyListeners(EventDispatcher $dispatcher, ContainerInterface $container): EventDispatcher { + foreach ($this->subscribers as $subscriber) { + $factory = LazyListenerFactory::of($container, $subscriber); + + /** + * @see EventDispatcher::addSubscriber() + */ + foreach ($subscriber::getSubscribedEvents() as $eventName => $params) { + if (is_string($params)) { + $dispatcher->addListener($eventName, $factory->create($params)); + } elseif (\is_string($params[0])) { + $dispatcher->addListener($eventName, $factory->create($params[0]), (int) ($params[1] ?? 0)); + } else { + foreach ($params as $listener) { + if (is_string($listener)) { + $dispatcher->addListener($eventName, $factory->create($listener)); + } elseif (is_array($listener)) { + $priority = (int) ($listener[1] ?? 0); + $dispatcher->addListener($eventName, $factory->create($listener[0]), $priority); + } else { + throw new RuntimeException('Invalid event listener: '.$subscriber); + } + } + } + } + } + + return $dispatcher; + } + + /** + * @return iterable + */ + public function register(): iterable { + yield self::class => $this; + yield EventDispatcher::class => decorate( + fn (EventDispatcher $dispatcher, ContainerInterface $container) => $container + ->get(EventSubscribersCollection::class) + ->addLazyListeners($dispatcher, $container), + ); + } +} diff --git a/src/EventDispatcher/LazyListener.php b/src/EventDispatcher/LazyListener.php new file mode 100644 index 0000000..3e04acd --- /dev/null +++ b/src/EventDispatcher/LazyListener.php @@ -0,0 +1,21 @@ +container->get($this->className); + $listener->{$this->method}(...$arguments); + } +} diff --git a/src/EventDispatcher/LazyListenerFactory.php b/src/EventDispatcher/LazyListenerFactory.php new file mode 100644 index 0000000..a1e3eee --- /dev/null +++ b/src/EventDispatcher/LazyListenerFactory.php @@ -0,0 +1,29 @@ +container, $this->subscriberClass, $method); + } +} diff --git a/tests/ConsoleLogger/ConsoleLoggerTest.php b/tests/ConsoleLogger/ConsoleLoggerTest.php index 1360b6c..6678d6d 100644 --- a/tests/ConsoleLogger/ConsoleLoggerTest.php +++ b/tests/ConsoleLogger/ConsoleLoggerTest.php @@ -6,25 +6,24 @@ use Monolog\Logger; use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\BufferedOutput; -use Symfony\Component\EventDispatcher\EventDispatcher; use WonderNetwork\SlimKernel\Cli\Logging\ConsoleHandlerEventSubscriber; use WonderNetwork\SlimKernel\Cli\Logging\ConsoleIoStack; use WonderNetwork\SlimKernel\Cli\Logging\CurrentConsoleInput; use WonderNetwork\SlimKernel\Cli\Logging\CurrentConsoleOutput; use WonderNetwork\SlimKernel\Cli\Logging\Handler\ConsoleHandler; +use WonderNetwork\SlimKernel\EventDispatcher\EventSubscribersCollection; use WonderNetwork\SlimKernel\KernelBuilder; use WonderNetwork\SlimKernel\ServiceFactory\SymfonyConsoleServiceFactory; use function DI\autowire; -use function DI\decorate; final class ConsoleLoggerTest extends TestCase { public function testConsoleLogger(): void { $rootPath = realpath(__DIR__.'/../Resources/ConsoleLogger'); + if (false === $rootPath) { $this->fail('Root path does not exist'); } @@ -43,12 +42,9 @@ public function testConsoleLogger(): void { return $logger; }, ConsoleHandlerEventSubscriber::class => autowire(), - EventDispatcher::class => decorate( - function (EventDispatcher $dispatcher, ContainerInterface $container) { - $dispatcher->addSubscriber($container->get(ConsoleHandlerEventSubscriber::class)); - return $dispatcher; - }, - ), + ...EventSubscribersCollection::start() + ->add(ConsoleHandlerEventSubscriber::class) + ->register(), ], ) ->register(