diff --git a/.gitignore b/.gitignore index 6dd1518..b69d32d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ composer.phar /phpunit.phar /phpunit.xml /.phpunit.cache +.phpunit.result.cache # Static analysis analysis.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 90988b4..ffcb69a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Enh #353: Add shortcut for tag reference #333 (@xepozz) - Enh #356: Improve usage `NotFoundException` for cases with definitions (@vjik) - Enh #364: Minor refactoring to improve performance of container (@samdark) +- New #372: Add `debug:container` console command (@samdark, @xepozz) - Enh #375: Raise minimum PHP version to `^8.1` and refactor code (@vjik) ## 1.2.1 December 23, 2022 diff --git a/README.md b/README.md index e4b54db..6172776 100644 --- a/README.md +++ b/README.md @@ -492,6 +492,11 @@ $config = ContainerConfig::create() $container = new Container($config); ``` +## Configuration debugging + +If you use the package with Yii3, `./yii debug:container` command is available. +It shows information about container. + ## Documentation - [Internals](docs/internals.md) diff --git a/composer.json b/composer.json index 5d7a32c..ccb9cbe 100644 --- a/composer.json +++ b/composer.json @@ -47,11 +47,14 @@ "spatie/phpunit-watcher": "^1.23", "vimeo/psalm": "^5.26", "yiisoft/injector": "^1.0", - "yiisoft/test-support": "^3.0" + "yiisoft/test-support": "^3.0", + "yiisoft/config": "^1.3", + "yiisoft/var-dumper": "^1.7", + "symfony/console": "^5.4|^6.0|^7.0" }, "suggest": { - "yiisoft/injector": "^1.0", - "phpbench/phpbench": "To run benchmarks." + "symfony/console": "For debug:container command", + "yiisoft/var-dumper": "For debug:container command" }, "provide": { "psr/container-implementation": "1.0.0" @@ -66,6 +69,14 @@ "Yiisoft\\Di\\Tests\\": "tests" } }, + "extra": { + "config-plugin-options": { + "source-directory": "config" + }, + "config-plugin": { + "params": "params.php" + } + }, "scripts": { "test": "phpunit --testdox --no-interaction", "test-watch": "phpunit-watcher watch" @@ -77,7 +88,8 @@ "sort-packages": true, "allow-plugins": { "infection/extension-installer": true, - "composer/package-versions-deprecated": true + "composer/package-versions-deprecated": true, + "yiisoft/config": false } } } diff --git a/config/params.php b/config/params.php new file mode 100644 index 0000000..549a0e9 --- /dev/null +++ b/config/params.php @@ -0,0 +1,18 @@ + [ + 'ignoredCommands' => [ + 'debug:container', + ], + ], + 'yiisoft/yii-console' => [ + 'commands' => [ + 'debug:container' => DebugContainerCommand::class, + ], + ], +]; diff --git a/src/Command/DebugContainerCommand.php b/src/Command/DebugContainerCommand.php new file mode 100644 index 0000000..8682803 --- /dev/null +++ b/src/Command/DebugContainerCommand.php @@ -0,0 +1,190 @@ +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->hasArgument('id') && !empty($ids = $input->getArgument('id'))) { + $build = $this->getConfigBuild($config); + foreach ($ids as $id) { + $definition = null; + foreach ($build as $definitions) { + if (array_key_exists($id, $definitions)) { + $definition = $definitions[$id]; + } + } + if ($definition === null) { + $io->error( + sprintf( + 'Service "%s" not found.', + $id, + ) + ); + continue; + } + $io->title($id); + + $normalizedDefinition = DefinitionNormalizer::normalize($definition, $id); + if ($normalizedDefinition instanceof ArrayDefinition) { + $definitionList = ['ID' => $id]; + if (class_exists($normalizedDefinition->getClass())) { + $definitionList[] = ['Class' => $normalizedDefinition->getClass()]; + } + if (!empty($normalizedDefinition->getConstructorArguments())) { + $definitionList[] = [ + 'Constructor' => $this->export( + $normalizedDefinition->getConstructorArguments() + ), + ]; + } + if (!empty($normalizedDefinition->getMethodsAndProperties())) { + $definitionList[] = [ + 'Methods' => $this->export( + $normalizedDefinition->getMethodsAndProperties() + ), + ]; + } + if (isset($definition['tags'])) { + $definitionList[] = ['Tags' => $this->export($definition['tags'])]; + } + + $io->definitionList(...$definitionList); + + continue; + } + if ($normalizedDefinition instanceof CallableDefinition || $normalizedDefinition instanceof ValueDefinition) { + $io->text( + $this->export($definition) + ); + continue; + } + + $output->writeln([ + $id, + VarDumper::create($normalizedDefinition)->asString(), + ]); + } + + return self::SUCCESS; + } + + 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); + + $rows = $this->getGroupServices($data); + + $table = new Table($output); + $table + ->setHeaderTitle($group) + ->setHeaders(['Service', 'Definition']) + ->setRows($rows); + $table->render(); + + return self::SUCCESS; + } + + $build = $this->getConfigBuild($config); + + foreach ($build as $group => $data) { + $rows = $this->getGroupServices($data); + + $table = new Table($output); + $table + ->setHeaderTitle($group) + ->setHeaders(['Group', 'Services']) + ->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 getGroupServices(array $data): array + { + $rows = []; + foreach ($data as $id => $definition) { + $class = ''; + if (is_string($definition)) { + $class = $definition; + } + if (is_array($definition)) { + $class = $definition['class'] ?? $id; + } + if (is_object($definition)) { + $class = $definition::class; + } + + $rows[] = [ + $id, + $class, + ]; + } + return $rows; + } + + protected function export(mixed $value): string + { + return VarDumper::create($value)->asString(); + } +} diff --git a/tests/Unit/Command/DebugContainerCommandTest.php b/tests/Unit/Command/DebugContainerCommandTest.php new file mode 100644 index 0000000..bcad82c --- /dev/null +++ b/tests/Unit/Command/DebugContainerCommandTest.php @@ -0,0 +1,50 @@ +createContainer(); + $config = $container->get(ConfigInterface::class); + // trigger config build + $config->get('params'); + + $command = new DebugContainerCommand($container); + $commandTester = new CommandTester($command); + + $commandTester->execute([]); + + $this->assertEquals(0, $commandTester->getStatusCode()); + } + + 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/Unit/Command/config/.merge-plan.php b/tests/Unit/Command/config/.merge-plan.php new file mode 100644 index 0000000..f60be99 --- /dev/null +++ b/tests/Unit/Command/config/.merge-plan.php @@ -0,0 +1,11 @@ +[ + 'params' => [ + + ] + ], +]; diff --git a/tests/Unit/Command/config/param1.php b/tests/Unit/Command/config/param1.php new file mode 100644 index 0000000..c9d9563 --- /dev/null +++ b/tests/Unit/Command/config/param1.php @@ -0,0 +1,13 @@ + [ + 'params' => [ + 'yiitest/yii-debug' => [ + 'param1.php', + ], + ], + ], +];