From 624482fe83eff7a639f2caa8d1344481418bdcad Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 13 Sep 2024 12:58:27 +0300 Subject: [PATCH 1/8] Add debug command --- README.md | 5 + composer.json | 20 +- config/params.php | 16 ++ src/Command/DebugContainerCommand.php | 191 ++++++++++++++++++ .../Command/DebugContainerCommandTest.php | 49 +++++ tests/Unit/Command/config/.merge-plan.php | 11 + tests/Unit/Command/config/param1.php | 13 ++ 7 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 config/params.php create mode 100644 src/Command/DebugContainerCommand.php create mode 100644 tests/Unit/Command/DebugContainerCommandTest.php create mode 100644 tests/Unit/Command/config/.merge-plan.php create mode 100644 tests/Unit/Command/config/param1.php diff --git a/README.md b/README.md index cdcaa046..3dd4fa0e 100644 --- a/README.md +++ b/README.md @@ -493,6 +493,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 f3698848..adf721a9 100644 --- a/composer.json +++ b/composer.json @@ -32,10 +32,12 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "ext-mbstring": "*", "psr/container": "^1.1|^2.0", - "yiisoft/definitions": "^3.0" + "yiisoft/definitions": "^3.0", + "yiisoft/var-dumper": "^1.7", + "symfony/console": "^5.4|^6.0" }, "require-dev": { "league/container": "^4.2", @@ -47,7 +49,8 @@ "spatie/phpunit-watcher": "^1.23", "vimeo/psalm": "^4.30|^5.7", "yiisoft/injector": "^1.0", - "yiisoft/test-support": "^3.0" + "yiisoft/test-support": "^3.0", + "yiisoft/config": "^1.3" }, "suggest": { "yiisoft/injector": "^1.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 00000000..8f805018 --- /dev/null +++ b/config/params.php @@ -0,0 +1,16 @@ + [ + '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 00000000..1427ea7b --- /dev/null +++ b/src/Command/DebugContainerCommand.php @@ -0,0 +1,191 @@ +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 00000000..5ecd6ea5 --- /dev/null +++ b/tests/Unit/Command/DebugContainerCommandTest.php @@ -0,0 +1,49 @@ +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 00000000..f60be993 --- /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 00000000..c9d95632 --- /dev/null +++ b/tests/Unit/Command/config/param1.php @@ -0,0 +1,13 @@ + [ + 'params' => [ + 'yiitest/yii-debug' => [ + 'param1.php', + ], + ], + ], +]; From bd2dcea71be9be8485f0c8c94b434c6963d2901d Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 13 Sep 2024 09:58:45 +0000 Subject: [PATCH 2/8] Apply fixes from StyleCI --- config/params.php | 2 ++ src/Command/DebugContainerCommand.php | 1 - tests/Unit/Command/DebugContainerCommandTest.php | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/params.php b/config/params.php index 8f805018..549a0e92 100644 --- a/config/params.php +++ b/config/params.php @@ -1,5 +1,7 @@ asString(); } } - diff --git a/tests/Unit/Command/DebugContainerCommandTest.php b/tests/Unit/Command/DebugContainerCommandTest.php index 5ecd6ea5..bcad82ce 100644 --- a/tests/Unit/Command/DebugContainerCommandTest.php +++ b/tests/Unit/Command/DebugContainerCommandTest.php @@ -1,5 +1,7 @@ Date: Fri, 13 Sep 2024 12:59:54 +0300 Subject: [PATCH 3/8] Add CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe0220d3..8c570a2a 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) ## 1.2.1 December 23, 2022 From 8e0d0fe7e18417d5980717b069f4dc1e610473f3 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 13 Sep 2024 21:44:50 +0300 Subject: [PATCH 4/8] Make command dependencies optional --- .gitignore | 1 + composer.json | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 6dd15181..b69d32d3 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/composer.json b/composer.json index 0d73aaef..02fabac2 100644 --- a/composer.json +++ b/composer.json @@ -35,9 +35,7 @@ "php": "^8.1", "ext-mbstring": "*", "psr/container": "^1.1|^2.0", - "yiisoft/definitions": "^3.0", - "yiisoft/var-dumper": "^1.7", - "symfony/console": "^5.4|^6.0" + "yiisoft/definitions": "^3.0" }, "require-dev": { "league/container": "^4.2", @@ -50,11 +48,15 @@ "vimeo/psalm": "^5.26", "yiisoft/injector": "^1.0", "yiisoft/test-support": "^3.0", - "yiisoft/config": "^1.3" + "yiisoft/config": "^1.3", + "yiisoft/var-dumper": "^1.7", + "symfony/console": "^5.4|^6.0" }, "suggest": { "yiisoft/injector": "^1.0", - "phpbench/phpbench": "To run benchmarks." + "phpbench/phpbench": "To run benchmarks.", + "symfony/console": "For debug:commands command", + "symfony/var-dumper": "For debug:commands command" }, "provide": { "psr/container-implementation": "1.0.0" From f1d6923573c090ba2f035bc2239bb3f14b893f87 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 13 Sep 2024 21:46:32 +0300 Subject: [PATCH 5/8] Update symfony/console version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 02fabac2..bd0558b6 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "yiisoft/test-support": "^3.0", "yiisoft/config": "^1.3", "yiisoft/var-dumper": "^1.7", - "symfony/console": "^5.4|^6.0" + "symfony/console": "^5.4|^6.0|^7.0" }, "suggest": { "yiisoft/injector": "^1.0", From da38a9f07fbd694096e9c9ff5dd80880d82a3251 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 13 Sep 2024 21:47:17 +0300 Subject: [PATCH 6/8] Fix suggestion --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bd0558b6..d014e016 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "yiisoft/injector": "^1.0", "phpbench/phpbench": "To run benchmarks.", "symfony/console": "For debug:commands command", - "symfony/var-dumper": "For debug:commands command" + "yiisoft/var-dumper": "For debug:commands command" }, "provide": { "psr/container-implementation": "1.0.0" From 79b8ef68718f71115f24f58858e60eba46213b14 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 13 Sep 2024 21:49:10 +0300 Subject: [PATCH 7/8] Fix suggestion --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index d014e016..11decdd5 100644 --- a/composer.json +++ b/composer.json @@ -55,8 +55,8 @@ "suggest": { "yiisoft/injector": "^1.0", "phpbench/phpbench": "To run benchmarks.", - "symfony/console": "For debug:commands command", - "yiisoft/var-dumper": "For debug:commands command" + "symfony/console": "For debug:container command", + "yiisoft/var-dumper": "For debug:container command" }, "provide": { "psr/container-implementation": "1.0.0" From c5eae16f0e07214ce1b3ccba655b16eed242e8a6 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Mon, 16 Sep 2024 23:48:05 +0300 Subject: [PATCH 8/8] Remove suggestions that are for development --- composer.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/composer.json b/composer.json index 11decdd5..ccb9cbe4 100644 --- a/composer.json +++ b/composer.json @@ -53,8 +53,6 @@ "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" },