From 234c0b300d13f43abe871d99943e779e1b4051f6 Mon Sep 17 00:00:00 2001 From: Abdiel Carrazana Date: Tue, 17 Aug 2021 18:20:25 -0400 Subject: [PATCH 1/2] [Dotenv] Reimplementing symfony/flex' dump-env as a Symfony command --- src/Symfony/Component/Dotenv/CHANGELOG.md | 5 + .../Dotenv/Command/DotenvDumpCommand.php | 128 ++++++++++++++++++ src/Symfony/Component/Dotenv/composer.json | 2 + 3 files changed, 135 insertions(+) create mode 100644 src/Symfony/Component/Dotenv/Command/DotenvDumpCommand.php diff --git a/src/Symfony/Component/Dotenv/CHANGELOG.md b/src/Symfony/Component/Dotenv/CHANGELOG.md index e004ed8d75d67..29fa3681bf557 100644 --- a/src/Symfony/Component/Dotenv/CHANGELOG.md +++ b/src/Symfony/Component/Dotenv/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.4 +--- + + * Add `dotenv:dump` command to compile the contents of the .env files into a PHP-optimized file called `.env.local.php` + 5.1.0 ----- diff --git a/src/Symfony/Component/Dotenv/Command/DotenvDumpCommand.php b/src/Symfony/Component/Dotenv/Command/DotenvDumpCommand.php new file mode 100644 index 0000000000000..43456c8581a7e --- /dev/null +++ b/src/Symfony/Component/Dotenv/Command/DotenvDumpCommand.php @@ -0,0 +1,128 @@ + + * + * 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\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Dotenv\Dotenv; + +/** + * A console command to compile the contents of the .env files into a PHP-optimized file called .env.local.php. + * + * @internal + */ +final class DotenvDumpCommand extends Command +{ + protected static $defaultName = 'dotenv:dump'; + protected static $defaultDescription = 'Compiles .env files to .env.local.php'; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setDescription('Compiles .env files to .env.local.php.') + ->setDefinition([ + new InputArgument('env', InputArgument::OPTIONAL, 'The application environment to dump .env files for - e.g. "prod".'), + ]) + ->addOption('empty', null, InputOption::VALUE_NONE, 'Ignore the content of .env files') + ->setHelp(<<<'EOT' +The %command.name% command compiles the contents of the .env files into a PHP-optimized file called .env.local.php. + + %command.full_name% +EOT + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + /** @var Application $application */ + $application = $this->getApplication(); + $kernel = $application->getKernel(); + + if ($env = $input->getArgument('env')) { + $_SERVER['APP_ENV'] = $env; + } + + $path = $kernel->getProjectDir().'/.env'; + + if (!$env || !$input->getOption('empty')) { + $vars = $this->loadEnv($path, $env, $kernel->getProjectDir().'/composer.json'); + $env = $vars['APP_ENV']; + } + + if ($input->getOption('empty')) { + $vars = ['APP_ENV' => $env]; + } + + $vars = var_export($vars, true); + $vars = <<writeln('Successfully dumped .env files in .env.local.php'); + + return Command::SUCCESS; + } + + private function loadEnv(string $path, ?string $env, $composerFilePath): array + { + $globalsBackup = [$_SERVER, $_ENV]; + unset($_SERVER['APP_ENV']); + $_ENV = ['APP_ENV' => $env]; + $_SERVER['SYMFONY_DOTENV_VARS'] = implode(',', array_keys($_SERVER)); + putenv('SYMFONY_DOTENV_VARS='.$_SERVER['SYMFONY_DOTENV_VARS']); + + try { + $dotenv = new Dotenv(); + + if (!$env && file_exists($p = "$path.local")) { + $env = $_ENV['APP_ENV'] = $dotenv->parse(file_get_contents($p), $p)['APP_ENV'] ?? null; + } + + if (!$env) { + throw new \RuntimeException('Please provide the name of the environment either by passing it as command line argument or by defining the "APP_ENV" variable in the ".env.local" file.'); + } + + $dotenv->loadEnv( + $path, + null, + 'dev', + json_decode(file_get_contents($composerFilePath), true)['extra']['runtime']['test_envs'] ?? ['test'] + ); + + if (isset($_ENV['SYMFONY_DOTENV_VARS'])) { + unset($_ENV['SYMFONY_DOTENV_VARS']); + } + $env = $_ENV; + } finally { + [$_SERVER, $_ENV] = $globalsBackup; + } + + return $env; + } +} diff --git a/src/Symfony/Component/Dotenv/composer.json b/src/Symfony/Component/Dotenv/composer.json index 9bd6cda177f1c..89de2c5a4fffe 100644 --- a/src/Symfony/Component/Dotenv/composer.json +++ b/src/Symfony/Component/Dotenv/composer.json @@ -20,6 +20,8 @@ "symfony/deprecation-contracts": "^2.1" }, "require-dev": { + "symfony/console": "^5.4|^6.0", + "symfony/filesystem": "^4.4|^5.0|^6.0", "symfony/process": "^4.4|^5.0|^6.0" }, "autoload": { From e8575e32f4c59f7a4dd59a51ef0b97dd31381eb6 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 30 Sep 2021 11:54:16 +0200 Subject: [PATCH 2/2] [Dotenv] Decouple DotenvDumpCommand from FrameworkBundle --- .../Dotenv/Command/DotenvDumpCommand.php | 79 ++++++-------- .../Tests/Command/DotenvDumpCommandTest.php | 102 ++++++++++++++++++ src/Symfony/Component/Dotenv/composer.json | 3 +- 3 files changed, 135 insertions(+), 49 deletions(-) create mode 100644 src/Symfony/Component/Dotenv/Tests/Command/DotenvDumpCommandTest.php diff --git a/src/Symfony/Component/Dotenv/Command/DotenvDumpCommand.php b/src/Symfony/Component/Dotenv/Command/DotenvDumpCommand.php index 43456c8581a7e..e044199433f0f 100644 --- a/src/Symfony/Component/Dotenv/Command/DotenvDumpCommand.php +++ b/src/Symfony/Component/Dotenv/Command/DotenvDumpCommand.php @@ -11,37 +11,48 @@ namespace Symfony\Component\Dotenv\Command; -use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; use Symfony\Component\Dotenv\Dotenv; /** - * A console command to compile the contents of the .env files into a PHP-optimized file called .env.local.php. + * A console command to compile .env files into a PHP-optimized file called .env.local.php. * * @internal */ +#[Autoconfigure(bind: ['$dotenvPath' => '%kernel.project_dir%/.env', '$defaultEnv' => '%kernel.environment%'])] final class DotenvDumpCommand extends Command { protected static $defaultName = 'dotenv:dump'; protected static $defaultDescription = 'Compiles .env files to .env.local.php'; + private $dotenvPath; + private $defaultEnv; + + public function __construct(string $dotenvPath, string $defaultEnv = null) + { + $this->dotenvPath = $dotenvPath; + $this->defaultEnv = $defaultEnv; + + parent::__construct(); + } + /** * {@inheritdoc} */ protected function configure() { $this - ->setDescription('Compiles .env files to .env.local.php.') ->setDefinition([ - new InputArgument('env', InputArgument::OPTIONAL, 'The application environment to dump .env files for - e.g. "prod".'), + new InputArgument('env', null === $this->defaultEnv ? InputArgument::REQUIRED : InputArgument::OPTIONAL, 'The application environment to dump .env files for - e.g. "prod".'), ]) ->addOption('empty', null, InputOption::VALUE_NONE, 'Ignore the content of .env files') ->setHelp(<<<'EOT' -The %command.name% command compiles the contents of the .env files into a PHP-optimized file called .env.local.php. +The %command.name% command compiles .env files into a PHP-optimized file called .env.local.php. %command.full_name% EOT @@ -54,23 +65,13 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output): int { - /** @var Application $application */ - $application = $this->getApplication(); - $kernel = $application->getKernel(); - - if ($env = $input->getArgument('env')) { - $_SERVER['APP_ENV'] = $env; - } - - $path = $kernel->getProjectDir().'/.env'; - - if (!$env || !$input->getOption('empty')) { - $vars = $this->loadEnv($path, $env, $kernel->getProjectDir().'/composer.json'); - $env = $vars['APP_ENV']; - } + $env = $input->getArgument('env') ?? $this->defaultEnv; if ($input->getOption('empty')) { $vars = ['APP_ENV' => $env]; + } else { + $vars = $this->loadEnv($env); + $env = $vars['APP_ENV']; } $vars = var_export($vars, true); @@ -82,47 +83,31 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $vars; EOF; - file_put_contents($path.'.local.php', $vars, \LOCK_EX); + file_put_contents($this->dotenvPath.'.local.php', $vars, \LOCK_EX); - $output->writeln('Successfully dumped .env files in .env.local.php'); + $output->writeln(sprintf('Successfully dumped .env files in .env.local.php for the %s environment.', $env)); - return Command::SUCCESS; + return 0; } - private function loadEnv(string $path, ?string $env, $composerFilePath): array + private function loadEnv(string $env): array { + $dotenv = new Dotenv(); + $composerFile = \dirname($this->dotenvPath).'/composer.json'; + $testEnvs = (is_file($composerFile) ? json_decode(file_get_contents($composerFile), true) : [])['extra']['runtime']['test_envs'] ?? ['test']; + $globalsBackup = [$_SERVER, $_ENV]; unset($_SERVER['APP_ENV']); $_ENV = ['APP_ENV' => $env]; $_SERVER['SYMFONY_DOTENV_VARS'] = implode(',', array_keys($_SERVER)); - putenv('SYMFONY_DOTENV_VARS='.$_SERVER['SYMFONY_DOTENV_VARS']); try { - $dotenv = new Dotenv(); - - if (!$env && file_exists($p = "$path.local")) { - $env = $_ENV['APP_ENV'] = $dotenv->parse(file_get_contents($p), $p)['APP_ENV'] ?? null; - } - - if (!$env) { - throw new \RuntimeException('Please provide the name of the environment either by passing it as command line argument or by defining the "APP_ENV" variable in the ".env.local" file.'); - } - - $dotenv->loadEnv( - $path, - null, - 'dev', - json_decode(file_get_contents($composerFilePath), true)['extra']['runtime']['test_envs'] ?? ['test'] - ); - - if (isset($_ENV['SYMFONY_DOTENV_VARS'])) { - unset($_ENV['SYMFONY_DOTENV_VARS']); - } - $env = $_ENV; + $dotenv->loadEnv($this->dotenvPath, null, 'dev', $testEnvs); + unset($_ENV['SYMFONY_DOTENV_VARS']); + + return $_ENV; } finally { [$_SERVER, $_ENV] = $globalsBackup; } - - return $env; } } diff --git a/src/Symfony/Component/Dotenv/Tests/Command/DotenvDumpCommandTest.php b/src/Symfony/Component/Dotenv/Tests/Command/DotenvDumpCommandTest.php new file mode 100644 index 0000000000000..c1f3e959f9199 --- /dev/null +++ b/src/Symfony/Component/Dotenv/Tests/Command/DotenvDumpCommandTest.php @@ -0,0 +1,102 @@ + + * + * 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\Application; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Dotenv\Command\DotenvDumpCommand; + +class DotenvDumpCommandTest extends TestCase +{ + protected function setUp(): void + { + file_put_contents(__DIR__.'/.env', <<createCommand(); + $command->execute([ + 'env' => 'test', + ]); + + $this->assertFileExists(__DIR__.'/.env.local.php'); + + $vars = require __DIR__.'/.env.local.php'; + $this->assertSame([ + 'APP_ENV' => 'test', + 'APP_SECRET' => 'abc123', + ], $vars); + } + + public function testExecuteEmpty() + { + $command = $this->createCommand(); + $command->execute([ + 'env' => 'test', + '--empty' => true, + ]); + + $this->assertFileExists(__DIR__.'/.env.local.php'); + + $vars = require __DIR__.'/.env.local.php'; + $this->assertSame(['APP_ENV' => 'test'], $vars); + } + + public function testExecuteTestEnvs() + { + file_put_contents(__DIR__.'/composer.json', <<createCommand(); + $command->execute([ + 'env' => 'test', + ]); + + $this->assertFileExists(__DIR__.'/.env.local.php'); + + $vars = require __DIR__.'/.env.local.php'; + $this->assertSame([ + 'APP_ENV' => 'test', + 'APP_SECRET' => 'abc123', + 'APP_LOCAL' => 'yes', + ], $vars); + } + + private function createCommand(): CommandTester + { + $application = new Application(); + $application->add(new DotenvDumpCommand(__DIR__.'/.env')); + + return new CommandTester($application->find('dotenv:dump')); + } +} diff --git a/src/Symfony/Component/Dotenv/composer.json b/src/Symfony/Component/Dotenv/composer.json index 89de2c5a4fffe..8ef8831988bee 100644 --- a/src/Symfony/Component/Dotenv/composer.json +++ b/src/Symfony/Component/Dotenv/composer.json @@ -20,8 +20,7 @@ "symfony/deprecation-contracts": "^2.1" }, "require-dev": { - "symfony/console": "^5.4|^6.0", - "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/console": "^4.4|^5.0|^6.0", "symfony/process": "^4.4|^5.0|^6.0" }, "autoload": {