From b813a05aa5358c7e7de53a664090b10ac34a50cb Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Fri, 22 Jun 2018 12:44:17 +0200 Subject: [PATCH] [FrameworkBundle] Debug container environment variables --- .../Command/ContainerDebugCommand.php | 25 +++++-- .../Console/Descriptor/Descriptor.php | 39 +++++++++++ .../Console/Descriptor/JsonDescriptor.php | 9 +++ .../Console/Descriptor/MarkdownDescriptor.php | 9 +++ .../Console/Descriptor/TextDescriptor.php | 66 ++++++++++++++++++ .../Console/Descriptor/XmlDescriptor.php | 9 +++ .../Functional/ContainerDebugCommandTest.php | 69 +++++++++++++++++++ .../Functional/app/ContainerDebug/config.yml | 10 +++ .../Bundle/FrameworkBundle/composer.json | 6 +- 9 files changed, 235 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php index 9b5193d63b..13ddc96276 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php @@ -58,6 +58,8 @@ class ContainerDebugCommand extends Command new InputOption('parameter', null, InputOption::VALUE_REQUIRED, 'Displays a specific parameter for an application'), new InputOption('parameters', null, InputOption::VALUE_NONE, 'Displays parameters for an application'), new InputOption('types', null, InputOption::VALUE_NONE, 'Displays types (classes/interfaces) available in the container'), + new InputOption('env-var', null, InputOption::VALUE_REQUIRED, 'Displays a specific environment variable used in the container'), + new InputOption('env-vars', null, InputOption::VALUE_NONE, 'Displays environment variables used in the container'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw description'), ]) @@ -75,6 +77,14 @@ To see available types that can be used for autowiring, use the --typesphp %command.full_name% --types +To see environment variables used by the container, use the --env-vars flag: + + php %command.full_name% --env-vars + +Display a specific environment variable by specifying its name with the --env-var option: + + php %command.full_name% --env-var=APP_ENV + Use the --tags option to display tagged public services grouped by tag: php %command.full_name% --tags @@ -116,7 +126,11 @@ EOF $this->validateInput($input); $object = $this->getContainerBuilder(); - if ($input->getOption('types')) { + if ($input->getOption('env-vars')) { + $options = ['env-vars' => true]; + } elseif ($envVar = $input->getOption('env-var')) { + $options = ['env-vars' => true, 'name' => $envVar]; + } elseif ($input->getOption('types')) { $options = []; $options['filter'] = [$this, 'filterToServiceTypes']; } elseif ($input->getOption('parameters')) { @@ -156,7 +170,7 @@ EOF throw $e; } - if (!$input->getArgument('name') && !$input->getOption('tag') && !$input->getOption('parameter') && $input->isInteractive()) { + if (!$input->getArgument('name') && !$input->getOption('tag') && !$input->getOption('parameter') && !$input->getOption('env-vars') && !$input->getOption('env-var') && $input->isInteractive()) { if ($input->getOption('tags')) { $errorIo->comment('To search for a specific tag, re-run this command with a search term. (e.g. debug:container --tag=form.type)'); } elseif ($input->getOption('parameters')) { @@ -209,12 +223,15 @@ EOF if (!$kernel->isDebug() || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) { $buildContainer = \Closure::bind(function () { return $this->buildContainer(); }, $kernel, \get_class($kernel)); $container = $buildContainer(); - $container->getCompilerPassConfig()->setRemovingPasses([]); - $container->compile(); } else { (new XmlFileLoader($container = new ContainerBuilder(), new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump')); + $container->setParameter('container.build_hash', $hash = ContainerBuilder::hash(__METHOD__)); + $container->setParameter('container.build_id', hash('crc32', $hash.time())); } + $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->compile(); + return $this->containerBuilder = $container; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php index 484ca4fefd..7babbd3912 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php @@ -52,6 +52,9 @@ abstract class Descriptor implements DescriptorInterface case $object instanceof ParameterBag: $this->describeContainerParameters($object, $options); break; + case $object instanceof ContainerBuilder && !empty($options['env-vars']): + $this->describeContainerEnvVars($this->getContainerEnvVars($object), $options); + break; case $object instanceof ContainerBuilder && isset($options['group_by']) && 'tags' === $options['group_by']: $this->describeContainerTags($object, $options); break; @@ -157,6 +160,11 @@ abstract class Descriptor implements DescriptorInterface */ abstract protected function describeContainerParameter($parameter, array $options = []); + /** + * Describes container environment variables. + */ + abstract protected function describeContainerEnvVars(array $envs, array $options = []); + /** * Describes event dispatcher listeners. * @@ -311,4 +319,35 @@ abstract class Descriptor implements DescriptorInterface return ''; } + + private function getContainerEnvVars(ContainerBuilder $container): array + { + $getEnvReflection = new \ReflectionMethod($container, 'getEnv'); + $getEnvReflection->setAccessible(true); + $envs = []; + foreach (array_keys($container->getEnvCounters()) as $env) { + $processor = 'string'; + if (false !== $i = strrpos($name = $env, ':')) { + $name = substr($env, $i + 1); + $processor = substr($env, 0, $i); + } + $defaultValue = ($hasDefault = $container->hasParameter("env($name)")) ? $container->getParameter("env($name)") : null; + if (false === ($runtimeValue = $_ENV[$name] ?? $_SERVER[$name] ?? getenv($name))) { + $runtimeValue = null; + } + $processedValue = ($hasRuntime = null !== $runtimeValue) || $hasDefault ? $getEnvReflection->invoke($container, $env) : null; + $envs[$name.$processor] = [ + 'name' => $name, + 'processor' => $processor, + 'default_available' => $hasDefault, + 'default_value' => $defaultValue, + 'runtime_available' => $hasRuntime, + 'runtime_value' => $runtimeValue, + 'processed_value' => $processedValue, + ]; + } + ksort($envs); + + return array_values($envs); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php index 137114ebe7..e50f50722b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Console\Descriptor; +use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; @@ -177,6 +178,14 @@ class JsonDescriptor extends Descriptor $this->writeData([$key => $parameter], $options); } + /** + * {@inheritdoc} + */ + protected function describeContainerEnvVars(array $envs, array $options = []) + { + throw new LogicException('Using the JSON format to debug environment variables is not supported.'); + } + /** * Writes data as json. * diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php index e68152b9a5..162e68f84d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Console\Descriptor; +use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -273,6 +274,14 @@ class MarkdownDescriptor extends Descriptor $this->write(isset($options['parameter']) ? sprintf("%s\n%s\n\n%s", $options['parameter'], str_repeat('=', \strlen($options['parameter'])), $this->formatParameter($parameter)) : $parameter); } + /** + * {@inheritdoc} + */ + protected function describeContainerEnvVars(array $envs, array $options = []) + { + throw new LogicException('Using the markdown format to debug environment variables is not supported.'); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php index 8980a9ee41..1861916896 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Console\Descriptor; use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Helper\Dumper; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\DependencyInjection\Alias; @@ -384,6 +385,71 @@ class TextDescriptor extends Descriptor ]); } + /** + * {@inheritdoc} + */ + protected function describeContainerEnvVars(array $envs, array $options = []) + { + $dump = new Dumper($this->output); + $options['output']->title('Symfony Container Environment Variables'); + + if (null !== $name = $options['name'] ?? null) { + $options['output']->comment('Displaying detailed environment variable usage matching '.$name); + + $matches = false; + foreach ($envs as $env) { + if ($name === $env['name'] || false !== stripos($env['name'], $name)) { + $matches = true; + $options['output']->section('%env('.$env['processor'].':'.$env['name'].')%'); + $options['output']->table([], [ + ['Default value', $env['default_available'] ? $dump($env['default_value']) : 'n/a'], + ['Real value', $env['runtime_available'] ? $dump($env['runtime_value']) : 'n/a'], + ['Processed value', $env['default_available'] || $env['runtime_available'] ? $dump($env['processed_value']) : 'n/a'], + ]); + } + } + + if (!$matches) { + $options['output']->block('None of the environment variables match this name.'); + } else { + $options['output']->comment('Note real values might be different between web and CLI.'); + } + + return; + } + + if (!$envs) { + $options['output']->block('No environment variables are being used.'); + + return; + } + + $rows = []; + $missing = []; + foreach ($envs as $env) { + if (isset($rows[$env['name']])) { + continue; + } + + $rows[$env['name']] = [ + $env['name'], + $env['default_available'] ? $dump($env['default_value']) : 'n/a', + $env['runtime_available'] ? $dump($env['runtime_value']) : 'n/a', + ]; + if (!$env['default_available'] && !$env['runtime_available']) { + $missing[$env['name']] = true; + } + } + + $options['output']->table(['Name', 'Default value', 'Real value'], $rows); + $options['output']->comment('Note real values might be different between web and CLI.'); + + if ($missing) { + $options['output']->warning('The following variables are missing:'); + $options['output']->listing(array_keys($missing)); + } + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php index db0f346ebd..a58c837823 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Console\Descriptor; +use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; @@ -131,6 +132,14 @@ class XmlDescriptor extends Descriptor $this->writeDocument($this->getContainerParameterDocument($parameter, $options)); } + /** + * {@inheritdoc} + */ + protected function describeContainerEnvVars(array $envs, array $options = []) + { + throw new LogicException('Using the XML format to debug environment variables is not supported.'); + } + /** * Writes DOM document. * diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php index ee13386a4b..310646e428 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php @@ -80,6 +80,75 @@ class ContainerDebugCommandTest extends WebTestCase $this->assertNotContains('No services found', $tester->getDisplay()); } + public function testDescribeEnvVars() + { + putenv('REAL=value'); + static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml']); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(['command' => 'debug:container', '--env-vars' => true], ['decorated' => false]); + + $this->assertStringMatchesFormat(<<<'TXT' + +Symfony Container Environment Variables +======================================= + + --------- ----------------- ------------ + Name Default value Real value + --------- ----------------- ------------ + JSON "[1, "2.5", 3]" n/a + REAL n/a "value" + UNKNOWN n/a n/a + --------- ----------------- ------------ + + // Note real values might be different between web and CLI.%w + + [WARNING] The following variables are missing:%w + + * UNKNOWN + +TXT + , $tester->getDisplay(true)); + + putenv('REAL'); + } + + public function testDescribeEnvVar() + { + static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml']); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(['command' => 'debug:container', '--env-var' => 'js'], ['decorated' => false]); + + $this->assertContains(<<<'TXT' +%env(float:key:2:json:JSON)% +---------------------------- + + ----------------- ----------------- + Default value "[1, "2.5", 3]" + Real value n/a + Processed value 3.0 + ----------------- ----------------- + +%env(int:key:2:json:JSON)% +-------------------------- + + ----------------- ----------------- + Default value "[1, "2.5", 3]" + Real value n/a + Processed value 3 + ----------------- ----------------- + +TXT + , $tester->getDisplay(true)); + } + public function provideIgnoreBackslashWhenFindingService() { return [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDebug/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDebug/config.yml index fb3b3b9b9a..0cc73dbc81 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDebug/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDebug/config.yml @@ -1,6 +1,9 @@ imports: - { resource: ../config/default.yml } +parameters: + env(JSON): '[1, "2.5", 3]' + services: _defaults: { public: true } public: @@ -10,3 +13,10 @@ services: public: false Symfony\Bundle\FrameworkBundle\Tests\Fixtures\BackslashClass: class: Symfony\Bundle\FrameworkBundle\Tests\Fixtures\BackslashClass + env: + class: stdClass + arguments: + - '%env(UNKNOWN)%' + - '%env(REAL)%' + - '%env(int:key:2:json:JSON)%' + - '%env(float:key:2:json:JSON)%' diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 681dd45fd3..57a7e25623 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -34,7 +34,7 @@ "fig/link-util": "^1.0", "symfony/asset": "~3.4|~4.0", "symfony/browser-kit": "^4.3", - "symfony/console": "~3.4|~4.0", + "symfony/console": "^4.3", "symfony/css-selector": "~3.4|~4.0", "symfony/dom-crawler": "~3.4|~4.0", "symfony/polyfill-intl-icu": "~1.0", @@ -53,7 +53,7 @@ "symfony/templating": "~3.4|~4.0", "symfony/twig-bundle": "~2.8|~3.2|~4.0", "symfony/validator": "^4.1", - "symfony/var-dumper": "~3.4|~4.0", + "symfony/var-dumper": "^4.3", "symfony/workflow": "^4.3", "symfony/yaml": "~3.4|~4.0", "symfony/property-info": "~3.4|~4.0", @@ -69,7 +69,7 @@ "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", "symfony/asset": "<3.4", "symfony/browser-kit": "<4.3", - "symfony/console": "<3.4", + "symfony/console": "<4.3", "symfony/dotenv": "<4.2", "symfony/form": "<4.3", "symfony/messenger": "<4.3",