Skip to content

[Console][FrameworkBundle] Add DotenvDebugCommand #42580

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Dotenv\Command\DebugCommand;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
Expand Down Expand Up @@ -251,6 +252,10 @@ public function load(array $configs, ContainerBuilder $container)
if (!class_exists(BaseYamlLintCommand::class)) {
$container->removeDefinition('console.command.yaml_lint');
}

if (!class_exists(DebugCommand::class)) {
$container->removeDefinition('console.command.dotenv_debug');
}
}

// Load Cache configuration first as it is used by other components
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand;
use Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber;
use Symfony\Component\Console\EventListener\ErrorListener;
use Symfony\Component\Dotenv\Command\DebugCommand as DotenvDebugCommand;
use Symfony\Component\Messenger\Command\ConsumeMessagesCommand;
use Symfony\Component\Messenger\Command\DebugCommand;
use Symfony\Component\Messenger\Command\FailedMessagesRemoveCommand;
Expand Down Expand Up @@ -129,6 +130,13 @@
])
->tag('console.command')

->set('console.command.dotenv_debug', DotenvDebugCommand::class)
->args([
param('kernel.environment'),
param('kernel.project_dir'),
])
->tag('console.command')

->set('console.command.event_dispatcher_debug', EventDispatcherDebugCommand::class)
->args([
tagged_locator('event_dispatcher.dispatcher', 'name'),
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Dotenv/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
---

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

5.1.0
-----
Expand Down
143 changes: 143 additions & 0 deletions src/Symfony/Component/Dotenv/Command/DebugCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Dotenv\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Dotenv\Dotenv;

/**
* A console command to debug current dotenv files with variables and values.
*
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class DebugCommand extends Command
{
protected static $defaultName = 'debug:dotenv';
protected static $defaultDescription = 'Lists all dotenv files with variables and values';

private $kernelEnvironment;
private $projectDirectory;

public function __construct(string $kernelEnvironment, string $projectDirectory)
{
$this->kernelEnvironment = $kernelEnvironment;
$this->projectDirectory = $projectDirectory;

parent::__construct();
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Dotenv Variables & Files');

if (!\array_key_exists('SYMFONY_DOTENV_VARS', $_SERVER)) {
$io->error('Dotenv component is not initialized.');

return 1;
}

$envFiles = $this->getEnvFiles();
$availableFiles = array_filter($envFiles, function (string $file) {
return is_file($this->getFilePath($file));
});

if (\in_array('.env.local.php', $availableFiles, true)) {
$io->warning('Due to existing dump file (.env.local.php) all other dotenv files are skipped.');
}

if (is_file($this->getFilePath('.env')) && is_file($this->getFilePath('.env.dist'))) {
$io->warning('The file .env.dist gets skipped due to the existence of .env.');
}

$io->section('Scanned Files (in descending priority)');
$io->listing(array_map(static function (string $envFile) use ($availableFiles) {
return \in_array($envFile, $availableFiles, true)
? sprintf('<fg=green>✓</> %s', $envFile)
: sprintf('<fg=red>⨯</> %s', $envFile);
}, $envFiles));

$io->section('Variables');
$io->table(
array_merge(['Variable', 'Value'], $availableFiles),
$this->getVariables($availableFiles)
);

$io->comment('Note real values might be different between web and CLI.');

return 0;
}

private function getVariables(array $envFiles): array
{
$vars = explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? '');
sort($vars);

$output = [];
$fileValues = [];
foreach ($vars as $var) {
$realValue = $_SERVER[$var];
$varDetails = [$var, $realValue];
foreach ($envFiles as $envFile) {
$values = $fileValues[$envFile] ?? $fileValues[$envFile] = $this->loadValues($envFile);

$varString = $values[$var] ?? '<fg=yellow>n/a</>';
$shortenedVar = $this->getHelper('formatter')->truncate($varString, 30);
$varDetails[] = $varString === $realValue ? '<fg=green>'.$shortenedVar.'</>' : $shortenedVar;
}

$output[] = $varDetails;
}

return $output;
}

private function getEnvFiles(): array
{
$files = [
'.env.local.php',
sprintf('.env.%s.local', $this->kernelEnvironment),
sprintf('.env.%s', $this->kernelEnvironment),
];

if ('test' !== $this->kernelEnvironment) {
$files[] = '.env.local';
}

if (!is_file($this->getFilePath('.env')) && is_file($this->getFilePath('.env.dist'))) {
$files[] = '.env.dist';
} else {
$files[] = '.env';
}

return $files;
}

private function getFilePath(string $file): string
{
return $this->projectDirectory.\DIRECTORY_SEPARATOR.$file;
}

private function loadValues(string $file): array
{
$filePath = $this->getFilePath($file);

if (str_ends_with($filePath, '.php')) {
return include $filePath;
}

return (new Dotenv())->parse(file_get_contents($filePath));
}
}
153 changes: 153 additions & 0 deletions src/Symfony/Component/Dotenv/Tests/Command/DebugCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Dotenv\Tests\Command;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Dotenv\Command\DebugCommand;
use Symfony\Component\Dotenv\Dotenv;

class DebugCommandTest extends TestCase
{
/**
* @runInSeparateProcess
*/
public function testErrorOnUninitializedDotenv()
{
$command = new DebugCommand('dev', __DIR__.'/Fixtures/Scenario1');
$command->setHelperSet(new HelperSet([new FormatterHelper()]));
$tester = new CommandTester($command);
$tester->execute([]);
$output = $tester->getDisplay();

$this->assertStringContainsString('[ERROR] Dotenv component is not initialized', $output);
}

public function testScenario1InDevEnv()
{
$output = $this->executeCommand(__DIR__.'/Fixtures/Scenario1', 'dev');

// Scanned Files
$this->assertStringContainsString('⨯ .env.local.php', $output);
$this->assertStringContainsString('⨯ .env.dev.local', $output);
$this->assertStringContainsString('⨯ .env.dev', $output);
$this->assertStringContainsString('✓ .env.local', $output);
$this->assertStringContainsString('✓ .env'.\PHP_EOL, $output);

// Skipped Files
$this->assertStringNotContainsString('.env.prod', $output);
$this->assertStringNotContainsString('.env.test', $output);
$this->assertStringNotContainsString('.env.dist', $output);

// Variables
$this->assertStringContainsString('Variable Value .env.local .env', $output);
$this->assertStringContainsString('FOO baz baz bar', $output);
$this->assertStringContainsString('TEST123 true n/a true', $output);
}

public function testScenario1InTestEnv()
{
$output = $this->executeCommand(__DIR__.'/Fixtures/Scenario1', 'test');

// Scanned Files
$this->assertStringContainsString('⨯ .env.local.php', $output);
$this->assertStringContainsString('⨯ .env.test.local', $output);
$this->assertStringContainsString('✓ .env.test', $output);
$this->assertStringContainsString('✓ .env'.\PHP_EOL, $output);

// Skipped Files
$this->assertStringNotContainsString('.env.prod', $output);
$this->assertStringNotContainsString('.env.dev', $output);
$this->assertStringNotContainsString('.env.dist', $output);

// Variables
$this->assertStringContainsString('Variable Value .env.test .env', $output);
$this->assertStringContainsString('FOO bar n/a bar', $output);
$this->assertStringContainsString('TEST123 false false true', $output);
}

public function testScenario1InProdEnv()
{
$output = $this->executeCommand(__DIR__.'/Fixtures/Scenario1', 'prod');

// Scanned Files
$this->assertStringContainsString('⨯ .env.local.php', $output);
$this->assertStringContainsString('✓ .env.prod.local', $output);
$this->assertStringContainsString('⨯ .env.prod', $output);
$this->assertStringContainsString('✓ .env.local', $output);
$this->assertStringContainsString('✓ .env'.\PHP_EOL, $output);

// Skipped Files
$this->assertStringNotContainsString('.env.dev', $output);
$this->assertStringNotContainsString('.env.test', $output);
$this->assertStringNotContainsString('.env.dist', $output);

// Variables
$this->assertStringContainsString('Variable Value .env.prod.local .env.local .env', $output);
$this->assertStringContainsString('FOO baz n/a baz bar', $output);
$this->assertStringContainsString('HELLO world world n/a n/a', $output);
$this->assertStringContainsString('TEST123 true n/a n/a true', $output);
}

public function testScenario2InProdEnv()
{
$output = $this->executeCommand(__DIR__.'/Fixtures/Scenario2', 'prod');

// Scanned Files
$this->assertStringContainsString('✓ .env.local.php', $output);
$this->assertStringContainsString('⨯ .env.prod.local', $output);
$this->assertStringContainsString('✓ .env.prod', $output);
$this->assertStringContainsString('⨯ .env.local', $output);
$this->assertStringContainsString('✓ .env.dist', $output);

// Skipped Files
$this->assertStringNotContainsString('.env'.\PHP_EOL, $output);
$this->assertStringNotContainsString('.env.dev', $output);
$this->assertStringNotContainsString('.env.test', $output);

// Variables
$this->assertStringContainsString('Variable Value .env.local.php .env.prod .env.dist', $output);
$this->assertStringContainsString('FOO BaR BaR BaR n/a', $output);
$this->assertStringContainsString('TEST 1234 1234 1234 0000', $output);
}

public function testWarningOnEnvAndEnvDistFile()
{
$output = $this->executeCommand(__DIR__.'/Fixtures/Scenario3', 'dev');

// Warning
$this->assertStringContainsString('[WARNING] The file .env.dist gets skipped', $output);
}

public function testWarningOnPhpEnvFile()
{
$output = $this->executeCommand(__DIR__.'/Fixtures/Scenario2', 'prod');

// Warning
$this->assertStringContainsString('[WARNING] Due to existing dump file (.env.local.php)', $output);
}

private function executeCommand(string $projectDirectory, string $env): string
{
$_SERVER['TEST_ENV_KEY'] = $env;
(new Dotenv('TEST_ENV_KEY'))->bootEnv($projectDirectory.'/.env');

$command = new DebugCommand($env, $projectDirectory);
$command->setHelperSet(new HelperSet([new FormatterHelper()]));
$tester = new CommandTester($command);
$tester->execute([]);

return $tester->getDisplay();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TEST123=true
FOO=bar
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FOO=baz
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
HELLO=world
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TEST123=false
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TEST=0000
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php
return [
'FOO' => 'BaR',
'TEST' => '1234',
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FOO=BaR
TEST=1234
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FOO=BAR
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FOO=BAZ