From 68cd3f91d2a6a12066556bfeaff9e48c1ca1dce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 17 Nov 2023 23:14:35 +0100 Subject: [PATCH 1/4] DotEnv debug command aware of custom dotenv_path Introduce SYMFONY_DOTENV_PATH set by DotEnv class and read by debug:dotenv command to contextualize the debug info with the file that was actually parsed. --- CHANGELOG.md | 5 +++ Command/DebugCommand.php | 50 ++++++++++++++++-------------- Command/DotenvDumpCommand.php | 1 + Dotenv.php | 12 +++++++ Tests/Command/DebugCommandTest.php | 42 +++++++++++++++++++++++-- 5 files changed, 85 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3b7b7c..a587fba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `SYMFONY_DOTENV_PATH` variable with the path to the `.env` file loaded by `Dotenv::loadEnv()` or `Dotenv::bootEnv()` + 6.2 --- diff --git a/Command/DebugCommand.php b/Command/DebugCommand.php index 1353969..b544530 100644 --- a/Command/DebugCommand.php +++ b/Command/DebugCommand.php @@ -70,21 +70,23 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } - $envFiles = $this->getEnvFiles(); - $availableFiles = array_filter($envFiles, fn (string $file) => is_file($this->getFilePath($file))); + $filePath = $_SERVER['SYMFONY_DOTENV_PATH'] ?? $this->projectDirectory.\DIRECTORY_SEPARATOR.'.env'; - if (\in_array('.env.local.php', $availableFiles, true)) { - $io->warning('Due to existing dump file (.env.local.php) all other dotenv files are skipped.'); + $envFiles = $this->getEnvFiles($filePath); + $availableFiles = array_filter($envFiles, fn (string $file) => is_file($file)); + + if (\in_array(sprintf('%s.local.php', $filePath), $availableFiles, true)) { + $io->warning(sprintf('Due to existing dump file (%s.local.php) all other dotenv files are skipped.', $this->getRelativeName($filePath))); } - 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.'); + if (is_file($filePath) && is_file(sprintf('%s.dist', $filePath))) { + $io->warning(sprintf('The file %s.dist gets skipped due to the existence of %1$s.', $this->getRelativeName($filePath))); } $io->section('Scanned Files (in descending priority)'); - $io->listing(array_map(static fn (string $envFile) => \in_array($envFile, $availableFiles, true) - ? sprintf('✓ %s', $envFile) - : sprintf('⨯ %s', $envFile), $envFiles)); + $io->listing(array_map(fn (string $envFile) => \in_array($envFile, $availableFiles, true) + ? sprintf('✓ %s', $this->getRelativeName($envFile)) + : sprintf('⨯ %s', $this->getRelativeName($envFile)), $envFiles)); $nameFilter = $input->getArgument('filter'); $variables = $this->getVariables($availableFiles, $nameFilter); @@ -93,7 +95,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($variables || null === $nameFilter) { $io->table( - array_merge(['Variable', 'Value'], $availableFiles), + array_merge(['Variable', 'Value'], array_map($this->getRelativeName(...), $availableFiles)), $this->getVariables($availableFiles, $nameFilter) ); @@ -147,36 +149,38 @@ private function getAvailableVars(): array return $vars; } - private function getEnvFiles(): array + private function getEnvFiles(string $filePath): array { $files = [ - '.env.local.php', - sprintf('.env.%s.local', $this->kernelEnvironment), - sprintf('.env.%s', $this->kernelEnvironment), + sprintf('%s.local.php', $filePath), + sprintf('%s.%s.local', $filePath, $this->kernelEnvironment), + sprintf('%s.%s', $filePath, $this->kernelEnvironment), ]; if ('test' !== $this->kernelEnvironment) { - $files[] = '.env.local'; + $files[] = sprintf('%s.local', $filePath); } - if (!is_file($this->getFilePath('.env')) && is_file($this->getFilePath('.env.dist'))) { - $files[] = '.env.dist'; + if (!is_file($filePath) && is_file(sprintf('%s.dist', $filePath))) { + $files[] = sprintf('%s.dist', $filePath); } else { - $files[] = '.env'; + $files[] = $filePath; } return $files; } - private function getFilePath(string $file): string + private function getRelativeName(string $filePath): string { - return $this->projectDirectory.\DIRECTORY_SEPARATOR.$file; + if (str_starts_with($filePath, $this->projectDirectory)) { + return substr($filePath, \strlen($this->projectDirectory) + 1); + } + + return basename($filePath); } - private function loadValues(string $file): array + private function loadValues(string $filePath): array { - $filePath = $this->getFilePath($file); - if (str_ends_with($filePath, '.php')) { return include $filePath; } diff --git a/Command/DotenvDumpCommand.php b/Command/DotenvDumpCommand.php index 051f4b0..20f35c9 100644 --- a/Command/DotenvDumpCommand.php +++ b/Command/DotenvDumpCommand.php @@ -107,6 +107,7 @@ private function loadEnv(string $dotenvPath, string $env, array $config): array try { $dotenv->loadEnv($dotenvPath, null, 'dev', $testEnvs); unset($_ENV['SYMFONY_DOTENV_VARS']); + unset($_ENV['SYMFONY_DOTENV_PATH']); return $_ENV; } finally { diff --git a/Dotenv.php b/Dotenv.php index 6e693ac..3c27ec6 100644 --- a/Dotenv.php +++ b/Dotenv.php @@ -100,6 +100,8 @@ public function load(string $path, string ...$extraPaths): void */ public function loadEnv(string $path, string $envKey = null, string $defaultEnv = 'dev', array $testEnvs = ['test'], bool $overrideExistingVars = false): void { + $this->populatePath($path); + $k = $envKey ?? $this->envKey; if (is_file($path) || !is_file($p = "$path.dist")) { @@ -144,6 +146,7 @@ public function bootEnv(string $path, string $defaultEnv = 'dev', array $testEnv $k = $this->envKey; if (\is_array($env) && ($overrideExistingVars || !isset($env[$k]) || ($_SERVER[$k] ?? $_ENV[$k] ?? $env[$k]) === $env[$k])) { + $this->populatePath($path); $this->populate($env, $overrideExistingVars); } else { $this->loadEnv($path, $k, $defaultEnv, $testEnvs, $overrideExistingVars); @@ -556,4 +559,13 @@ private function doLoad(bool $overrideExistingVars, array $paths): void $this->populate($this->parse(file_get_contents($path), $path), $overrideExistingVars); } } + + private function populatePath(string $path): void + { + $_ENV['SYMFONY_DOTENV_PATH'] = $_SERVER['SYMFONY_DOTENV_PATH'] = $path; + + if ($this->usePutenv) { + putenv('SYMFONY_DOTENV_PATH='.$path); + } + } } diff --git a/Tests/Command/DebugCommandTest.php b/Tests/Command/DebugCommandTest.php index 8bf7873..da5a567 100644 --- a/Tests/Command/DebugCommandTest.php +++ b/Tests/Command/DebugCommandTest.php @@ -124,6 +124,28 @@ public function testScenario2InProdEnv() $this->assertStringContainsString('TEST 1234 1234 1234 0000', $output); } + public function testScenario2WithCustomPath() + { + $output = $this->executeCommand(__DIR__.'/Fixtures', 'prod', [], __DIR__.'/Fixtures/Scenario2/.env'); + + // Scanned Files + $this->assertStringContainsString('✓ Scenario2/.env.local.php', $output); + $this->assertStringContainsString('⨯ Scenario2/.env.prod.local', $output); + $this->assertStringContainsString('✓ Scenario2/.env.prod', $output); + $this->assertStringContainsString('⨯ Scenario2/.env.local', $output); + $this->assertStringContainsString('✓ Scenario2/.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 Scenario2/.env.local.php Scenario2/.env.prod Scenario2/.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'); @@ -132,6 +154,14 @@ public function testWarningOnEnvAndEnvDistFile() $this->assertStringContainsString('[WARNING] The file .env.dist gets skipped', $output); } + public function testWarningOnEnvAndEnvDistFileWithCustomPath() + { + $output = $this->executeCommand(__DIR__.'/Fixtures', 'dev', dotenvPath: __DIR__.'/Fixtures/Scenario3/.env'); + + // Warning + $this->assertStringContainsString('[WARNING] The file Scenario3/.env.dist gets skipped', $output); + } + public function testWarningOnPhpEnvFile() { $output = $this->executeCommand(__DIR__.'/Fixtures/Scenario2', 'prod'); @@ -140,6 +170,14 @@ public function testWarningOnPhpEnvFile() $this->assertStringContainsString('[WARNING] Due to existing dump file (.env.local.php)', $output); } + public function testWarningOnPhpEnvFileWithCustomPath() + { + $output = $this->executeCommand(__DIR__.'/Fixtures', 'prod', dotenvPath: __DIR__.'/Fixtures/Scenario2/.env'); + + // Warning + $this->assertStringContainsString('[WARNING] Due to existing dump file (Scenario2/.env.local.php)', $output); + } + public function testScenario1InDevEnvWithNameFilter() { $output = $this->executeCommand(__DIR__.'/Fixtures/Scenario1', 'dev', ['filter' => 'FoO']); @@ -226,10 +264,10 @@ public function testCompletion() $this->assertSame(['FOO', 'TEST'], $tester->complete([''])); } - private function executeCommand(string $projectDirectory, string $env, array $input = []): string + private function executeCommand(string $projectDirectory, string $env, array $input = [], string $dotenvPath = null): string { $_SERVER['TEST_ENV_KEY'] = $env; - (new Dotenv('TEST_ENV_KEY'))->bootEnv($projectDirectory.'/.env'); + (new Dotenv('TEST_ENV_KEY'))->bootEnv($dotenvPath ?? $projectDirectory.'/.env'); $command = new DebugCommand($env, $projectDirectory); $command->setHelperSet(new HelperSet([new FormatterHelper()])); From 850329b1c3171eb3bc36859709bb740db9ec2b05 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 8 Dec 2023 15:23:08 +0100 Subject: [PATCH 2/4] Fx README files --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 08c90fc..2a1cc02 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ accessible via `$_SERVER` or `$_ENV`. Getting Started --------------- -``` -$ composer require symfony/dotenv +```bash +composer require symfony/dotenv ``` ```php From 0106cf412c61c210a9476f36d98743de9a2c34d0 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Mon, 18 Dec 2023 08:46:12 +0100 Subject: [PATCH 3/4] Code updates --- Dotenv.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dotenv.php b/Dotenv.php index 6e693ac..5fb8fdd 100644 --- a/Dotenv.php +++ b/Dotenv.php @@ -463,7 +463,7 @@ private function resolveCommands(string $value, array $loadedVars): string throw $this->createFormatException(sprintf('Issue expanding a command (%s)', $process->getErrorOutput())); } - return preg_replace('/[\r\n]+$/', '', $process->getOutput()); + return rtrim($process->getOutput(), "\n\r"); }, $value); } From f3e6a5e66a52313cb5405f3995726bd871eb6068 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 8 Jan 2024 08:27:24 +0100 Subject: [PATCH 4/4] [Dotenv][ErrorHandler][EventDispatcher] Use CPP --- Command/DebugCommand.php | 12 ++++-------- Command/DotenvDumpCommand.php | 12 ++++-------- Dotenv.php | 10 ++++------ Exception/FormatException.php | 12 ++++++------ Exception/FormatExceptionContext.php | 17 ++++++----------- 5 files changed, 24 insertions(+), 39 deletions(-) diff --git a/Command/DebugCommand.php b/Command/DebugCommand.php index 309d40e..0156838 100644 --- a/Command/DebugCommand.php +++ b/Command/DebugCommand.php @@ -29,14 +29,10 @@ #[AsCommand(name: 'debug:dotenv', description: 'List all dotenv files with variables and values')] final class DebugCommand extends Command { - private string $kernelEnvironment; - private string $projectDirectory; - - public function __construct(string $kernelEnvironment, string $projectDirectory) - { - $this->kernelEnvironment = $kernelEnvironment; - $this->projectDirectory = $projectDirectory; - + public function __construct( + private string $kernelEnvironment, + private string $projectDirectory, + ) { parent::__construct(); } diff --git a/Command/DotenvDumpCommand.php b/Command/DotenvDumpCommand.php index 20f35c9..afa8f56 100644 --- a/Command/DotenvDumpCommand.php +++ b/Command/DotenvDumpCommand.php @@ -29,14 +29,10 @@ #[AsCommand(name: 'dotenv:dump', description: 'Compile .env files to .env.local.php')] final class DotenvDumpCommand extends Command { - private string $projectDir; - private ?string $defaultEnv; - - public function __construct(string $projectDir, string $defaultEnv = null) - { - $this->projectDir = $projectDir; - $this->defaultEnv = $defaultEnv; - + public function __construct( + private string $projectDir, + private ?string $defaultEnv = null, + ) { parent::__construct(); } diff --git a/Dotenv.php b/Dotenv.php index 6c9db53..9908d63 100644 --- a/Dotenv.php +++ b/Dotenv.php @@ -35,15 +35,13 @@ final class Dotenv private string $data; private int $end; private array $values = []; - private string $envKey; - private string $debugKey; private array $prodEnvs = ['prod']; private bool $usePutenv = false; - public function __construct(string $envKey = 'APP_ENV', string $debugKey = 'APP_DEBUG') - { - $this->envKey = $envKey; - $this->debugKey = $debugKey; + public function __construct( + private string $envKey = 'APP_ENV', + private string $debugKey = 'APP_DEBUG', + ) { } /** diff --git a/Exception/FormatException.php b/Exception/FormatException.php index 8f1aa84..7d8ec8f 100644 --- a/Exception/FormatException.php +++ b/Exception/FormatException.php @@ -18,12 +18,12 @@ */ final class FormatException extends \LogicException implements ExceptionInterface { - private FormatExceptionContext $context; - - public function __construct(string $message, FormatExceptionContext $context, int $code = 0, \Throwable $previous = null) - { - $this->context = $context; - + public function __construct( + string $message, + private FormatExceptionContext $context, + int $code = 0, + \Throwable $previous = null, + ) { parent::__construct(sprintf("%s in \"%s\" at line %d.\n%s", $message, $context->getPath(), $context->getLineno(), $context->getDetails()), $code, $previous); } diff --git a/Exception/FormatExceptionContext.php b/Exception/FormatExceptionContext.php index 11db3ab..86ce7e5 100644 --- a/Exception/FormatExceptionContext.php +++ b/Exception/FormatExceptionContext.php @@ -16,17 +16,12 @@ */ final class FormatExceptionContext { - private string $data; - private string $path; - private int $lineno; - private int $cursor; - - public function __construct(string $data, string $path, int $lineno, int $cursor) - { - $this->data = $data; - $this->path = $path; - $this->lineno = $lineno; - $this->cursor = $cursor; + public function __construct( + private string $data, + private string $path, + private int $lineno, + private int $cursor, + ) { } public function getPath(): string