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..e044199433f0f --- /dev/null +++ b/src/Symfony/Component/Dotenv/Command/DotenvDumpCommand.php @@ -0,0 +1,113 @@ + + * + * 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\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 .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 + ->setDefinition([ + 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 .env files into a PHP-optimized file called .env.local.php. + + %command.full_name% +EOT + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $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); + $vars = <<dotenvPath.'.local.php', $vars, \LOCK_EX); + + $output->writeln(sprintf('Successfully dumped .env files in .env.local.php for the %s environment.', $env)); + + return 0; + } + + 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)); + + try { + $dotenv->loadEnv($this->dotenvPath, null, 'dev', $testEnvs); + unset($_ENV['SYMFONY_DOTENV_VARS']); + + return $_ENV; + } finally { + [$_SERVER, $_ENV] = $globalsBackup; + } + } +} 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 9bd6cda177f1c..8ef8831988bee 100644 --- a/src/Symfony/Component/Dotenv/composer.json +++ b/src/Symfony/Component/Dotenv/composer.json @@ -20,6 +20,7 @@ "symfony/deprecation-contracts": "^2.1" }, "require-dev": { + "symfony/console": "^4.4|^5.0|^6.0", "symfony/process": "^4.4|^5.0|^6.0" }, "autoload": {