diff --git a/CHANGELOG.md b/CHANGELOG.md index 51f9d47..370fdb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 2.1.1 under development - Enh #65: Minor refactoring of `CallableFactory` and `ListenerCollectionFactory` (@vjik) +- New #73: Add `debug:events` console command (@samdark, @xepozz) - Enh #76: Raise minimum PHP version to `^8.1` and refactor code (@vjik) ## 2.1.0 November 04, 2023 diff --git a/README.md b/README.md index b09bf83..3291fa7 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ composer require yiisoft/yii-event ### DI configuration -You can find default configuration in the [config directory](config): +You can find the default configuration in the [config directory](config): - [di.php](config/di.php) contains the configuration for the [PSR-14](https://www.php-fig.org/psr/psr-14/) interfaces. - [di-web.php](config/di-web.php) and [di-console.php](config/di-consle.php) contains the configuration for the `Yiisoft\EventDispatcher\Provider\ListenerCollection`. @@ -72,13 +72,13 @@ return [ // they will be resolved the same way as in the previous example. [SomeClass::class, 'staticMethodName'], - // Non-static methods are allowed too. In this case `SomeClass` will be instantiated by your DI container. + // Non-static methods are allowed too. In this case, `SomeClass` will be instantiated by your DI container. [SomeClass::class, 'methodName'], // An object of a class with the `__invoke` method implemented new InvokableClass(), - // In this case the `InvokableClass` with the `__invoke` method will be instantiated by your DI container + // In this case, the `InvokableClass` with the `__invoke` method will be instantiated by your DI container InvokableClass::class, // Any definition of an invokable class may be here while your `$container->has('the definition)` @@ -88,11 +88,11 @@ return [ ``` The dependency resolving is done in a lazy way: dependencies will not be resolved before the corresponding event -will happen. +happens. ### Configuration checker -To help you with event listener configuration validation there is the `ListenerConfigurationChecker`. It is converting +To help you with event listener configuration validation, there is the `ListenerConfigurationChecker`. It is converting your whole listener config to actual callables at once to validate it. It is intended to be used in development environment or in tests since it is a resource greedy operation in large projects. An `InvalidEventConfigurationFormatException` will be thrown if your configuration contains an invalid listener. @@ -103,6 +103,11 @@ Usage example: $checker->check($configuration->get('events-web')); ``` +## Event configuration debugging + +If you use the package with Yii3, `./yii debug:events` command is available. +It shows information about events and listeners. + ## Documentation - [Internals](docs/internals.md) diff --git a/composer.json b/composer.json index 96d9d77..235ddfb 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,13 @@ "vimeo/psalm": "^5.26", "yiisoft/config": "^1.3", "yiisoft/di": "^1.2", - "yiisoft/test-support": "^3.0" + "yiisoft/test-support": "^3.0", + "symfony/console": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.4" + }, + "suggest": { + "symfony/console": "For debug:events command", + "symfony/var-dumper": "For debug:events command" }, "autoload": { "psr-4": { diff --git a/config/params-console.php b/config/params-console.php index 1a781c5..0ea254a 100644 --- a/config/params-console.php +++ b/config/params-console.php @@ -2,8 +2,18 @@ declare(strict_types=1); +use Yiisoft\Yii\Event\Command\DebugEventsCommand; + return [ 'yiisoft/yii-event' => [ 'eventsConfigGroup' => 'events-console', ], + 'yiisoft/yii-debug' => [ + 'ignoredCommands' => [ + 'debug:events', + ], + ], + 'yiisoft/yii-console' => [ + 'debug:events' => DebugEventsCommand::class, + ], ]; diff --git a/src/Command/DebugEventsCommand.php b/src/Command/DebugEventsCommand.php new file mode 100644 index 0000000..f63463b --- /dev/null +++ b/src/Command/DebugEventsCommand.php @@ -0,0 +1,124 @@ +addArgument('id', InputArgument::IS_ARRAY, 'Service ID') + ->addOption('groups', null, InputOption::VALUE_NONE, 'Show groups') + ->addOption('group', 'g', InputOption::VALUE_REQUIRED, 'Show group'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $config = $this->container->get(ConfigInterface::class); + + $io = new SymfonyStyle($input, $output); + + if ($input->hasOption('groups') && $input->getOption('groups')) { + $build = $this->getConfigBuild($config); + $groups = array_keys($build); + sort($groups); + + $io->table(['Groups'], array_map(static fn ($group) => [$group], $groups)); + + return self::SUCCESS; + } + if ($input->hasOption('group') && !empty($group = $input->getOption('group'))) { + $data = $config->get($group); + ksort($data); + $table = new Table($output); + + foreach ($data as $event => $listeners) { + $io->title($event); + foreach ($listeners as $listener) { + if (is_callable($listener) && !is_array($listener)) { + SymfonyVarDumper::dump($this->export($listener)); + } else { + SymfonyVarDumper::dump($listener); + } + } + $table->render(); + $io->newLine(); + } + return self::SUCCESS; + } + + $data = []; + if ($config->has('events')) { + $data = array_merge($data, $config->get('events')); + } + if ($config->has('events-console')) { + $data = array_merge($data, $config->get('events-console')); + } + $rows = []; + foreach ($data as $event => $listeners) { + $rows[] = [ + $event, + is_countable($listeners) ? count($listeners) : 0, + implode( + "\n", + array_map(function (mixed $listener) { + if (is_array($listener)) { + return sprintf( + '%s::%s', + $listener[0], + $listener[1] + ); + } + return $this->export($listener); + }, $listeners) + ), + ]; + } + $table = new Table($output); + $table + ->setHeaders(['Event', 'Count', 'Listeners']) + ->setRows($rows); + $table->render(); + + return self::SUCCESS; + } + + private function getConfigBuild(mixed $config): array + { + $reflection = new ReflectionClass($config); + $buildReflection = $reflection->getProperty('build'); + $buildReflection->setAccessible(true); + return $buildReflection->getValue($config); + } + + protected function export(mixed $value): string + { + return VarDumper::create($value)->asString(); + } +} diff --git a/tests/Command/DebugEventsCommandTest.php b/tests/Command/DebugEventsCommandTest.php new file mode 100644 index 0000000..83ce53f --- /dev/null +++ b/tests/Command/DebugEventsCommandTest.php @@ -0,0 +1,47 @@ +createContainer(); + $command = new DebugEventsCommand($container); + $commandTester = new CommandTester($command); + $commandTester->execute([]); + + $commandTester->assertCommandIsSuccessful(); + $output = $commandTester->getDisplay(); + $this->assertStringContainsString('Listeners', $output); + } + + private function createContainer(): ContainerInterface + { + $config = ContainerConfig::create() + ->withDefinitions([ + LoggerInterface::class => NullLogger::class, + ConfigInterface::class => [ + 'class' => Config::class, + '__construct()' => [ + new ConfigPaths(__DIR__ . '/config'), + ], + ], + ]); + return new Container($config); + } +} diff --git a/tests/Command/config/.merge-plan.php b/tests/Command/config/.merge-plan.php new file mode 100644 index 0000000..f60be99 --- /dev/null +++ b/tests/Command/config/.merge-plan.php @@ -0,0 +1,11 @@ +[ + 'params' => [ + + ] + ], +]; diff --git a/tests/Command/config/param1.php b/tests/Command/config/param1.php new file mode 100644 index 0000000..c9d9563 --- /dev/null +++ b/tests/Command/config/param1.php @@ -0,0 +1,13 @@ + [ + 'params' => [ + 'yiitest/yii-debug' => [ + 'param1.php', + ], + ], + ], +];