Repository: symfony/flex (php)

Issue:
dump-env always looks for the .env file in the project-root
**Problem**

Given the situation that I relocated the env files lookup from root to root/my-custom-folder. It works just fine with configuring runtime variables (dotenv_path) in the composer.json. 

```json
"runtime": {
   "dotenv_path": "env/.env",
}
```

However when I want to dump-env the variables into a php file I run into the problem that the symfony:dump-env command always looks for the project root instead of using the newly configured env folder.

In the DumpEnvCommand class the path is hardcoded.

`class Symfony\Flex\Command\DumpEnvCommand`

```php
protected function execute(InputInterface $input, OutputInterface $output): int
{
        if ($env = $input->getArgument('env')) {
            $_SERVER['APP_ENV'] = $env;
        }

        $path = $this->options->get('root-dir').'/.env';
```

**Proposed solutions:**

- Either add an optional argument to the command (path) where the lookup path for .env could be overridden (this seems to be the easiest solution)

- Get the location of the env files from runtime somehow? (haven't done any proof of concept about this yet)

Symfony version: v.6.0.0
Flex version: v1.18.1


Pull request:
Fix reading config for symfony/runtime
Fix #855

Patch:
diff --git a/src/Command/DumpEnvCommand.php b/src/Command/DumpEnvCommand.php
index d2e837c..6f7ed6f 100644
--- a/src/Command/DumpEnvCommand.php
+++ b/src/Command/DumpEnvCommand.php
@@ -13,7 +13,6 @@ namespace Symfony\Flex\Command;
 
 use Composer\Command\BaseCommand;
 use Composer\Config;
-use Composer\Factory;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -48,19 +47,22 @@ class DumpEnvCommand extends BaseCommand
 
     protected function execute(InputInterface $input, OutputInterface $output): int
     {
-        if ($env = $input->getArgument('env')) {
-            $_SERVER['APP_ENV'] = $env;
+        $runtime = $this->options->get('runtime') ?? [];
+        $envKey = $runtime['env_var_name'] ?? 'APP_ENV';
+
+        if ($env = $input->getArgument('env') ?? $runtime['env'] ?? null) {
+            $_SERVER[$envKey] = $env;
         }
 
-        $path = $this->options->get('root-dir').'/.env';
+        $path = $this->options->get('root-dir').'/'.($runtime['dotenv_path'] ?? '.env');
 
         if (!$env || !$input->getOption('empty')) {
-            $vars = $this->loadEnv($path, $env);
-            $env = $vars['APP_ENV'];
+            $vars = $this->loadEnv($path, $env, $runtime);
+            $env = $vars[$envKey];
         }
 
         if ($input->getOption('empty')) {
-            $vars = ['APP_ENV' => $env];
+            $vars = [$envKey => $env];
         }
 
         $vars = var_export($vars, true);
@@ -79,7 +81,7 @@ EOF;
         return 0;
     }
 
-    private function loadEnv(string $path, ?string $env): array
+    private function loadEnv(string $path, ?string $env, array $runtime): array
     {
         if (!file_exists($autoloadFile = $this->config->get('vendor-dir').'/autoload.php')) {
             throw new \RuntimeException(sprintf('Please run "composer install" before running this command: "%s" not found.', $autoloadFile));
@@ -91,9 +93,10 @@ EOF;
             throw new \RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
         }
 
+        $envKey = $runtime['env_var_name'] ?? 'APP_ENV';
         $globalsBackup = [$_SERVER, $_ENV];
-        unset($_SERVER['APP_ENV']);
-        $_ENV = ['APP_ENV' => $env];
+        unset($_SERVER[$envKey]);
+        $_ENV = [$envKey => $env];
         $_SERVER['SYMFONY_DOTENV_VARS'] = implode(',', array_keys($_SERVER));
         putenv('SYMFONY_DOTENV_VARS='.$_SERVER['SYMFONY_DOTENV_VARS']);
 
@@ -105,18 +108,17 @@ EOF;
             }
 
             if (!$env && file_exists($p = "$path.local")) {
-                $env = $_ENV['APP_ENV'] = $dotenv->parse(file_get_contents($p), $p)['APP_ENV'] ?? null;
+                $env = $_ENV[$envKey] = $dotenv->parse(file_get_contents($p), $p)[$envKey] ?? 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.');
+                throw new \RuntimeException(sprintf('Please provide the name of the environment either by passing it as command line argument or by defining the "%s" variable in the ".env.local" file.', $envKey));
             }
 
-            $config = @json_decode(file_get_contents(Factory::getComposerFile()), true);
-            $testEnvs = $config['extra']['runtime']['test_envs'] ?? ['test'];
+            $testEnvs = $runtime['test_envs'] ?? ['test'];
 
             if (method_exists($dotenv, 'loadEnv')) {
-                $dotenv->loadEnv($path, 'APP_ENV', 'dev', $testEnvs);
+                $dotenv->loadEnv($path, $envKey, 'dev', $testEnvs);
             } else {
                 // fallback code in case your Dotenv component is not 4.2 or higher (when loadEnv() was added)
                 $dotenv->load(file_exists($path) || !file_exists($p = "$path.dist") ? $path : $p);
diff --git a/src/Command/InstallRecipesCommand.php b/src/Command/InstallRecipesCommand.php
index de6e36f..d1ae1a4 100644
--- a/src/Command/InstallRecipesCommand.php
+++ b/src/Command/InstallRecipesCommand.php
@@ -13,6 +13,7 @@ namespace Symfony\Flex\Command;
 
 use Composer\Command\BaseCommand;
 use Composer\DependencyResolver\Operation\InstallOperation;
+use Composer\Util\ProcessExecutor;
 use Symfony\Component\Console\Exception\RuntimeException;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
@@ -26,11 +27,13 @@ class InstallRecipesCommand extends BaseCommand
     /** @var Flex */
     private $flex;
     private $rootDir;
+    private $dotenvPath;
 
-    public function __construct(/* cannot be type-hinted */ $flex, string $rootDir)
+    public function __construct(/* cannot be type-hinted */ $flex, string $rootDir, string $dotenvPath = '.env')
     {
         $this->flex = $flex;
         $this->rootDir = $rootDir;
+        $this->dotenvPath = $dotenvPath;
 
         parent::__construct();
     }
@@ -120,12 +123,15 @@ class InstallRecipesCommand extends BaseCommand
             $operations[] = new InstallOperation($pkg);
         }
 
-        if ($createEnvLocal = $force && file_exists($this->rootDir.'/.env') && file_exists($this->rootDir.'/.env.dist') && !file_exists($this->rootDir.'/.env.local')) {
-            rename($this->rootDir.'/.env', $this->rootDir.'/.env.local');
+        $dotenvFile = $this->dotenvPath;
+        $dotenvPath = $this->rootDir.'/'.$dotenvFile;
+
+        if ($createEnvLocal = $force && file_exists($dotenvPath) && file_exists($dotenvPath.'.dist') && !file_exists($dotenvPath.'.local')) {
+            rename($dotenvPath, $dotenvPath.'.local');
             $pipes = [];
-            proc_close(proc_open(sprintf('git mv .env.dist .env > %s 2>&1 || %s .env.dist .env', $win ? 'NUL' : '/dev/null', $win ? 'rename' : 'mv'), $pipes, $pipes, $this->rootDir));
+            proc_close(proc_open(sprintf('git mv %s %s > %s 2>&1 || %s %1$s %2$s', ProcessExecutor::escape($dotenvFile.'.dist'), ProcessExecutor::escape($dotenvFile), $win ? 'NUL' : '/dev/null', $win ? 'rename' : 'mv'), $pipes, $pipes, $this->rootDir));
             if (file_exists($this->rootDir.'/phpunit.xml.dist')) {
-                touch($this->rootDir.'/.env.test');
+                touch($dotenvPath.'.test');
             }
         }
 
@@ -156,8 +162,9 @@ class InstallRecipesCommand extends BaseCommand
             $output[] = '';
 
             if ($createEnvLocal) {
+                $root = '.' !== $this->rootDir ? $this->rootDir.'/' : '';
                 $output[] = '    To revert the changes made to .env files, run';
-                $output[] = sprintf('    <comment>git mv %s.env %1$s.env.dist</> && <comment>%s %1$s.env.local %1$s.env</>', '.' !== $this->rootDir ? $this->rootDir.'/' : '', $win ? 'rename' : 'mv');
+                $output[] = sprintf('    <comment>git mv %s %s</> && <comment>%s %s %1$s</>', ProcessExecutor::escape($root.$dotenvFile), ProcessExecutor::escape($root.$dotenvFile.'.dist'), $win ? 'rename' : 'mv', ProcessExecutor::escape($root.$dotenvFile.'.local'));
                 $output[] = '';
             }
 
diff --git a/src/Configurator/EnvConfigurator.php b/src/Configurator/EnvConfigurator.php
index 32f8abc..9fd81bd 100644
--- a/src/Configurator/EnvConfigurator.php
+++ b/src/Configurator/EnvConfigurator.php
@@ -25,7 +25,7 @@ class EnvConfigurator extends AbstractConfigurator
         $this->write('Adding environment variable defaults');
 
         $this->configureEnvDist($recipe, $vars, $options['force'] ?? false);
-        if (!file_exists($this->options->get('root-dir').'/.env.test')) {
+        if (!file_exists($this->options->get('root-dir').'/'.($this->options->get('runtime')['dotenv_path'] ?? '.env').'.test')) {
             $this->configurePhpUnit($recipe, $vars, $options['force'] ?? false);
         }
     }
@@ -49,7 +49,9 @@ class EnvConfigurator extends AbstractConfigurator
 
     private function configureEnvDist(Recipe $recipe, $vars, bool $update)
     {
-        foreach (['.env.dist', '.env'] as $file) {
+        $dotenvPath = $this->options->get('runtime')['dotenv_path'] ?? '.env';
+
+        foreach ([$dotenvPath.'.dist', $dotenvPath] as $file) {
             $env = $this->options->get('root-dir').'/'.$file;
             if (!is_file($env)) {
                 continue;
@@ -133,7 +135,9 @@ class EnvConfigurator extends AbstractConfigurator
 
     private function unconfigureEnvFiles(Recipe $recipe, $vars)
     {
-        foreach (['.env', '.env.dist'] as $file) {
+        $dotenvPath = $this->options->get('runtime')['dotenv_path'] ?? '.env';
+
+        foreach ([$dotenvPath, $dotenvPath.'.dist'] as $file) {
             $env = $this->options->get('root-dir').'/'.$file;
             if (!file_exists($env)) {
                 continue;
@@ -200,7 +204,8 @@ class EnvConfigurator extends AbstractConfigurator
 
     private function getContentsAfterApplyingRecipe(string $rootDir, Recipe $recipe, array $vars): array
     {
-        $files = ['.env', '.env.dist', 'phpunit.xml.dist', 'phpunit.xml'];
+        $dotenvPath = $this->options->get('runtime')['dotenv_path'] ?? '.env';
+        $files = [$dotenvPath, $dotenvPath.'.dist', 'phpunit.xml.dist', 'phpunit.xml'];
 
         if (0 === \count($vars)) {
             return array_fill_keys($files, null);
@@ -217,7 +222,7 @@ class EnvConfigurator extends AbstractConfigurator
             true
         );
 
-        if (!file_exists($rootDir.'/.env.test')) {
+        if (!file_exists($rootDir.'/'.$dotenvPath.'.test')) {
             $this->configurePhpUnit(
                 $recipe,
                 $vars,
diff --git a/src/Configurator/MakefileConfigurator.php b/src/Configurator/MakefileConfigurator.php
index 5ab400c..5b5abe4 100644
--- a/src/Configurator/MakefileConfigurator.php
+++ b/src/Configurator/MakefileConfigurator.php
@@ -71,11 +71,13 @@ class MakefileConfigurator extends AbstractConfigurator
         $data = "\n".ltrim($data, "\r\n");
 
         if (!file_exists($makefile)) {
+            $envKey = $this->options->get('runtime')['env_var_name'] ?? 'APP_ENV';
+            $dotenvPath = $this->options->get('runtime')['dotenv_path'] ?? '.env';
             file_put_contents(
                 $this->options->get('root-dir').'/Makefile',
                 <<<EOF
-ifndef APP_ENV
-	include .env
+ifndef {$envKey}
+	include {$dotenvPath}
 endif
 
 .DEFAULT_GOAL := help
diff --git a/src/Flex.php b/src/Flex.php
index 128ebda..29b5769 100644
--- a/src/Flex.php
+++ b/src/Flex.php
@@ -276,7 +276,7 @@ class Flex implements PluginInterface, EventSubscriberInterface
             $app->add(new Command\RemoveCommand($resolver));
             $app->add(new Command\UnpackCommand($resolver));
             $app->add(new Command\RecipesCommand($this, $this->lock, $rfs));
-            $app->add(new Command\InstallRecipesCommand($this, $this->options->get('root-dir')));
+            $app->add(new Command\InstallRecipesCommand($this, $this->options->get('root-dir'), $this->options->get('runtime')['dotenv_path'] ?? '.env'));
             $app->add(new Command\UpdateRecipesCommand($this, $this->downloader, $rfs, $this->configurator, $this->options->get('root-dir')));
             if (class_exists(Command\GenerateIdCommand::class)) {
                 $app->add(new Command\GenerateIdCommand(null));
@@ -391,9 +391,11 @@ class Flex implements PluginInterface, EventSubscriberInterface
     public function install(Event $event)
     {
         $rootDir = $this->options->get('root-dir');
+        $runtime = $this->options->get('runtime');
+        $dotenvPath = $rootDir.'/'.($runtime['dotenv_path'] ?? '.env');
 
-        if (!file_exists("$rootDir/.env") && !file_exists("$rootDir/.env.local") && file_exists("$rootDir/.env.dist") && false === strpos(file_get_contents("$rootDir/.env.dist"), '.env.local')) {
-            copy($rootDir.'/.env.dist', $rootDir.'/.env');
+        if (!file_exists($dotenvPath) && !file_exists($dotenvPath.'.local') && file_exists($dotenvPath.'.dist') && false === strpos(file_get_contents($dotenvPath.'.dist'), '.env.local')) {
+            copy($dotenvPath.'.dist', $dotenvPath);
         }
 
         // Execute missing recipes
@@ -829,6 +831,7 @@ class Flex implements PluginInterface, EventSubscriberInterface
             'var-dir' => 'var',
             'public-dir' => 'public',
             'root-dir' => $extra['symfony']['root-dir'] ?? '.',
+            'runtime' => $extra['runtime'] ?? [],
         ], $extra);
 
         return new Options($options, $this->io);


Test patch:
diff --git a/tests/Command/DumpEnvCommandTest.php b/tests/Command/DumpEnvCommandTest.php
index aed761b..0127455 100644
--- a/tests/Command/DumpEnvCommandTest.php
+++ b/tests/Command/DumpEnvCommandTest.php
@@ -174,7 +174,6 @@ EOF;
         $env = FLEX_TEST_DIR.'/.env';
         $envLocal = FLEX_TEST_DIR.'/.env.local';
         $envLocalPhp = FLEX_TEST_DIR.'/.env.local.php';
-        $composer = FLEX_TEST_DIR.'/composer.json';
 
         @unlink($envLocalPhp);
 
@@ -184,9 +183,8 @@ APP_ENV=test
 APP_SECRET=abcdefgh123456789
 EOF
         );
-        file_put_contents(FLEX_TEST_DIR.'/composer.json', '{"extra":{"runtime":{"test_envs":[]}}}');
 
-        $command = $this->createCommandDumpEnv();
+        $command = $this->createCommandDumpEnv(['runtime' => ['test_envs' => []]]);
         $command->execute([
             'env' => 'test',
         ]);
@@ -202,14 +200,13 @@ EOF
         unlink($env);
         unlink($envLocal);
         unlink($envLocalPhp);
-        unlink($composer);
     }
 
-    private function createCommandDumpEnv()
+    private function createCommandDumpEnv(array $options = [])
     {
         $command = new DumpEnvCommand(
             new Config(false, __DIR__.'/../..'),
-            new Options(['root-dir' => FLEX_TEST_DIR])
+            new Options($options + ['root-dir' => FLEX_TEST_DIR])
         );
 
         $application = new Application();