Skip to content

Commit f1b8b6e

Browse files
committed
feature #42580 [Console][FrameworkBundle] Add DotenvDebugCommand (chr-hertel)
This PR was merged into the 5.4 branch. Discussion ---------- [Console][FrameworkBundle] Add DotenvDebugCommand | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #41958 | License | MIT | Doc PR | symfony/symfony-docs#15734 ![Bildschirmfoto von 2021-08-15 13-57-48](https://user-images.githubusercontent.com/2852185/129477842-06f0c680-b311-4411-8734-57579adeadeb.png) - [x] Command Implementation - [x] Tests - [x] Docs Commits ------- 5b2ecee Add DotenvDebugCommand
2 parents 7a7240f + 5b2ecee commit f1b8b6e

File tree

14 files changed

+325
-0
lines changed

14 files changed

+325
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
use Symfony\Component\DependencyInjection\Parameter;
5959
use Symfony\Component\DependencyInjection\Reference;
6060
use Symfony\Component\DependencyInjection\ServiceLocator;
61+
use Symfony\Component\Dotenv\Command\DebugCommand;
6162
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
6263
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
6364
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
@@ -252,6 +253,10 @@ public function load(array $configs, ContainerBuilder $container)
252253
if (!class_exists(BaseYamlLintCommand::class)) {
253254
$container->removeDefinition('console.command.yaml_lint');
254255
}
256+
257+
if (!class_exists(DebugCommand::class)) {
258+
$container->removeDefinition('console.command.dotenv_debug');
259+
}
255260
}
256261

257262
// Load Cache configuration first as it is used by other components

src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand;
4040
use Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber;
4141
use Symfony\Component\Console\EventListener\ErrorListener;
42+
use Symfony\Component\Dotenv\Command\DebugCommand as DotenvDebugCommand;
4243
use Symfony\Component\Messenger\Command\ConsumeMessagesCommand;
4344
use Symfony\Component\Messenger\Command\DebugCommand;
4445
use Symfony\Component\Messenger\Command\FailedMessagesRemoveCommand;
@@ -129,6 +130,13 @@
129130
])
130131
->tag('console.command')
131132

133+
->set('console.command.dotenv_debug', DotenvDebugCommand::class)
134+
->args([
135+
param('kernel.environment'),
136+
param('kernel.project_dir'),
137+
])
138+
->tag('console.command')
139+
132140
->set('console.command.event_dispatcher_debug', EventDispatcherDebugCommand::class)
133141
->args([
134142
tagged_locator('event_dispatcher.dispatcher', 'name'),

src/Symfony/Component/Dotenv/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add `dotenv:dump` command to compile the contents of the .env files into a PHP-optimized file called `.env.local.php`
8+
* Add `debug:dotenv` command to list all dotenv files with variables and values
89

910
5.1.0
1011
-----
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Dotenv\Command;
13+
14+
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Output\OutputInterface;
17+
use Symfony\Component\Console\Style\SymfonyStyle;
18+
use Symfony\Component\Dotenv\Dotenv;
19+
20+
/**
21+
* A console command to debug current dotenv files with variables and values.
22+
*
23+
* @author Christopher Hertel <[email protected]>
24+
*/
25+
final class DebugCommand extends Command
26+
{
27+
protected static $defaultName = 'debug:dotenv';
28+
protected static $defaultDescription = 'Lists all dotenv files with variables and values';
29+
30+
private $kernelEnvironment;
31+
private $projectDirectory;
32+
33+
public function __construct(string $kernelEnvironment, string $projectDirectory)
34+
{
35+
$this->kernelEnvironment = $kernelEnvironment;
36+
$this->projectDirectory = $projectDirectory;
37+
38+
parent::__construct();
39+
}
40+
41+
protected function execute(InputInterface $input, OutputInterface $output): int
42+
{
43+
$io = new SymfonyStyle($input, $output);
44+
$io->title('Dotenv Variables & Files');
45+
46+
if (!\array_key_exists('SYMFONY_DOTENV_VARS', $_SERVER)) {
47+
$io->error('Dotenv component is not initialized.');
48+
49+
return 1;
50+
}
51+
52+
$envFiles = $this->getEnvFiles();
53+
$availableFiles = array_filter($envFiles, function (string $file) {
54+
return is_file($this->getFilePath($file));
55+
});
56+
57+
if (\in_array('.env.local.php', $availableFiles, true)) {
58+
$io->warning('Due to existing dump file (.env.local.php) all other dotenv files are skipped.');
59+
}
60+
61+
if (is_file($this->getFilePath('.env')) && is_file($this->getFilePath('.env.dist'))) {
62+
$io->warning('The file .env.dist gets skipped due to the existence of .env.');
63+
}
64+
65+
$io->section('Scanned Files (in descending priority)');
66+
$io->listing(array_map(static function (string $envFile) use ($availableFiles) {
67+
return \in_array($envFile, $availableFiles, true)
68+
? sprintf('<fg=green>✓</> %s', $envFile)
69+
: sprintf('<fg=red>⨯</> %s', $envFile);
70+
}, $envFiles));
71+
72+
$io->section('Variables');
73+
$io->table(
74+
array_merge(['Variable', 'Value'], $availableFiles),
75+
$this->getVariables($availableFiles)
76+
);
77+
78+
$io->comment('Note real values might be different between web and CLI.');
79+
80+
return 0;
81+
}
82+
83+
private function getVariables(array $envFiles): array
84+
{
85+
$vars = explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? '');
86+
sort($vars);
87+
88+
$output = [];
89+
$fileValues = [];
90+
foreach ($vars as $var) {
91+
$realValue = $_SERVER[$var];
92+
$varDetails = [$var, $realValue];
93+
foreach ($envFiles as $envFile) {
94+
$values = $fileValues[$envFile] ?? $fileValues[$envFile] = $this->loadValues($envFile);
95+
96+
$varString = $values[$var] ?? '<fg=yellow>n/a</>';
97+
$shortenedVar = $this->getHelper('formatter')->truncate($varString, 30);
98+
$varDetails[] = $varString === $realValue ? '<fg=green>'.$shortenedVar.'</>' : $shortenedVar;
99+
}
100+
101+
$output[] = $varDetails;
102+
}
103+
104+
return $output;
105+
}
106+
107+
private function getEnvFiles(): array
108+
{
109+
$files = [
110+
'.env.local.php',
111+
sprintf('.env.%s.local', $this->kernelEnvironment),
112+
sprintf('.env.%s', $this->kernelEnvironment),
113+
];
114+
115+
if ('test' !== $this->kernelEnvironment) {
116+
$files[] = '.env.local';
117+
}
118+
119+
if (!is_file($this->getFilePath('.env')) && is_file($this->getFilePath('.env.dist'))) {
120+
$files[] = '.env.dist';
121+
} else {
122+
$files[] = '.env';
123+
}
124+
125+
return $files;
126+
}
127+
128+
private function getFilePath(string $file): string
129+
{
130+
return $this->projectDirectory.\DIRECTORY_SEPARATOR.$file;
131+
}
132+
133+
private function loadValues(string $file): array
134+
{
135+
$filePath = $this->getFilePath($file);
136+
137+
if (str_ends_with($filePath, '.php')) {
138+
return include $filePath;
139+
}
140+
141+
return (new Dotenv())->parse(file_get_contents($filePath));
142+
}
143+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Dotenv\Tests\Command;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Console\Helper\FormatterHelper;
16+
use Symfony\Component\Console\Helper\HelperSet;
17+
use Symfony\Component\Console\Tester\CommandTester;
18+
use Symfony\Component\Dotenv\Command\DebugCommand;
19+
use Symfony\Component\Dotenv\Dotenv;
20+
21+
class DebugCommandTest extends TestCase
22+
{
23+
/**
24+
* @runInSeparateProcess
25+
*/
26+
public function testErrorOnUninitializedDotenv()
27+
{
28+
$command = new DebugCommand('dev', __DIR__.'/Fixtures/Scenario1');
29+
$command->setHelperSet(new HelperSet([new FormatterHelper()]));
30+
$tester = new CommandTester($command);
31+
$tester->execute([]);
32+
$output = $tester->getDisplay();
33+
34+
$this->assertStringContainsString('[ERROR] Dotenv component is not initialized', $output);
35+
}
36+
37+
public function testScenario1InDevEnv()
38+
{
39+
$output = $this->executeCommand(__DIR__.'/Fixtures/Scenario1', 'dev');
40+
41+
// Scanned Files
42+
$this->assertStringContainsString('⨯ .env.local.php', $output);
43+
$this->assertStringContainsString('⨯ .env.dev.local', $output);
44+
$this->assertStringContainsString('⨯ .env.dev', $output);
45+
$this->assertStringContainsString('✓ .env.local', $output);
46+
$this->assertStringContainsString('✓ .env'.\PHP_EOL, $output);
47+
48+
// Skipped Files
49+
$this->assertStringNotContainsString('.env.prod', $output);
50+
$this->assertStringNotContainsString('.env.test', $output);
51+
$this->assertStringNotContainsString('.env.dist', $output);
52+
53+
// Variables
54+
$this->assertStringContainsString('Variable Value .env.local .env', $output);
55+
$this->assertStringContainsString('FOO baz baz bar', $output);
56+
$this->assertStringContainsString('TEST123 true n/a true', $output);
57+
}
58+
59+
public function testScenario1InTestEnv()
60+
{
61+
$output = $this->executeCommand(__DIR__.'/Fixtures/Scenario1', 'test');
62+
63+
// Scanned Files
64+
$this->assertStringContainsString('⨯ .env.local.php', $output);
65+
$this->assertStringContainsString('⨯ .env.test.local', $output);
66+
$this->assertStringContainsString('✓ .env.test', $output);
67+
$this->assertStringContainsString('✓ .env'.\PHP_EOL, $output);
68+
69+
// Skipped Files
70+
$this->assertStringNotContainsString('.env.prod', $output);
71+
$this->assertStringNotContainsString('.env.dev', $output);
72+
$this->assertStringNotContainsString('.env.dist', $output);
73+
74+
// Variables
75+
$this->assertStringContainsString('Variable Value .env.test .env', $output);
76+
$this->assertStringContainsString('FOO bar n/a bar', $output);
77+
$this->assertStringContainsString('TEST123 false false true', $output);
78+
}
79+
80+
public function testScenario1InProdEnv()
81+
{
82+
$output = $this->executeCommand(__DIR__.'/Fixtures/Scenario1', 'prod');
83+
84+
// Scanned Files
85+
$this->assertStringContainsString('⨯ .env.local.php', $output);
86+
$this->assertStringContainsString('✓ .env.prod.local', $output);
87+
$this->assertStringContainsString('⨯ .env.prod', $output);
88+
$this->assertStringContainsString('✓ .env.local', $output);
89+
$this->assertStringContainsString('✓ .env'.\PHP_EOL, $output);
90+
91+
// Skipped Files
92+
$this->assertStringNotContainsString('.env.dev', $output);
93+
$this->assertStringNotContainsString('.env.test', $output);
94+
$this->assertStringNotContainsString('.env.dist', $output);
95+
96+
// Variables
97+
$this->assertStringContainsString('Variable Value .env.prod.local .env.local .env', $output);
98+
$this->assertStringContainsString('FOO baz n/a baz bar', $output);
99+
$this->assertStringContainsString('HELLO world world n/a n/a', $output);
100+
$this->assertStringContainsString('TEST123 true n/a n/a true', $output);
101+
}
102+
103+
public function testScenario2InProdEnv()
104+
{
105+
$output = $this->executeCommand(__DIR__.'/Fixtures/Scenario2', 'prod');
106+
107+
// Scanned Files
108+
$this->assertStringContainsString('✓ .env.local.php', $output);
109+
$this->assertStringContainsString('⨯ .env.prod.local', $output);
110+
$this->assertStringContainsString('✓ .env.prod', $output);
111+
$this->assertStringContainsString('⨯ .env.local', $output);
112+
$this->assertStringContainsString('✓ .env.dist', $output);
113+
114+
// Skipped Files
115+
$this->assertStringNotContainsString('.env'.\PHP_EOL, $output);
116+
$this->assertStringNotContainsString('.env.dev', $output);
117+
$this->assertStringNotContainsString('.env.test', $output);
118+
119+
// Variables
120+
$this->assertStringContainsString('Variable Value .env.local.php .env.prod .env.dist', $output);
121+
$this->assertStringContainsString('FOO BaR BaR BaR n/a', $output);
122+
$this->assertStringContainsString('TEST 1234 1234 1234 0000', $output);
123+
}
124+
125+
public function testWarningOnEnvAndEnvDistFile()
126+
{
127+
$output = $this->executeCommand(__DIR__.'/Fixtures/Scenario3', 'dev');
128+
129+
// Warning
130+
$this->assertStringContainsString('[WARNING] The file .env.dist gets skipped', $output);
131+
}
132+
133+
public function testWarningOnPhpEnvFile()
134+
{
135+
$output = $this->executeCommand(__DIR__.'/Fixtures/Scenario2', 'prod');
136+
137+
// Warning
138+
$this->assertStringContainsString('[WARNING] Due to existing dump file (.env.local.php)', $output);
139+
}
140+
141+
private function executeCommand(string $projectDirectory, string $env): string
142+
{
143+
$_SERVER['TEST_ENV_KEY'] = $env;
144+
(new Dotenv('TEST_ENV_KEY'))->bootEnv($projectDirectory.'/.env');
145+
146+
$command = new DebugCommand($env, $projectDirectory);
147+
$command->setHelperSet(new HelperSet([new FormatterHelper()]));
148+
$tester = new CommandTester($command);
149+
$tester->execute([]);
150+
151+
return $tester->getDisplay();
152+
}
153+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
TEST123=true
2+
FOO=bar
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FOO=baz
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
HELLO=world
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TEST123=false
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TEST=0000
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
return [
3+
'FOO' => 'BaR',
4+
'TEST' => '1234',
5+
];
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FOO=BaR
2+
TEST=1234
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FOO=BAR
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FOO=BAZ

0 commit comments

Comments
 (0)