Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[Dotenv] Handle dynamic variables in multiple .env files #48636

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

Open
wants to merge 6 commits into
base: 7.3
Choose a base branch
Loading
from
Open
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
86 changes: 70 additions & 16 deletions 86 src/Symfony/Component/Dotenv/Dotenv.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,13 @@ public function usePutenv(bool $usePutenv = true): static
*/
public function load(string $path, string ...$extraPaths): void
{
$this->doLoad(false, \func_get_args());
$needsAutoValueResolution = 1 === \func_num_args();

$this->doLoad(false, $needsAutoValueResolution, \func_get_args());

if (!$needsAutoValueResolution) {
$this->resolveAllVariables();
}
}

/**
Expand All @@ -99,33 +105,45 @@ 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
{
// FIXME - Keep 'false' for loadEnv() or try to know if we will have multiple files to load?
// -> The environment is grabbed on the first doLoad() call so we cannot check conditions (except by loading the files multiple times..)
$needsAutoValueResolution = false;

$k = $envKey ?? $this->envKey;

if (is_file($path) || !is_file($p = "$path.dist")) {
$this->doLoad($overrideExistingVars, [$path]);
$this->doLoad($overrideExistingVars, $needsAutoValueResolution, [$path]);
} else {
$this->doLoad($overrideExistingVars, [$p]);
$this->doLoad($overrideExistingVars, $needsAutoValueResolution, [$p]);
}

if (null === $env = $_SERVER[$k] ?? $_ENV[$k] ?? null) {
$this->populate([$k => $env = $defaultEnv], $overrideExistingVars);
}

if (!\in_array($env, $testEnvs, true) && is_file($p = "$path.local")) {
$this->doLoad($overrideExistingVars, [$p]);
$this->doLoad($overrideExistingVars, $needsAutoValueResolution, [$p]);
$env = $_SERVER[$k] ?? $_ENV[$k] ?? $env;
}

if ('local' === $env) {
if (!$needsAutoValueResolution) {
$this->resolveAllVariables();
}

return;
}

if (is_file($p = "$path.$env")) {
$this->doLoad($overrideExistingVars, [$p]);
$this->doLoad($overrideExistingVars, $needsAutoValueResolution, [$p]);
}

if (is_file($p = "$path.$env.local")) {
$this->doLoad($overrideExistingVars, [$p]);
$this->doLoad($overrideExistingVars, $needsAutoValueResolution, [$p]);
}

if (!$needsAutoValueResolution) {
$this->resolveAllVariables();
}
}

Expand Down Expand Up @@ -166,7 +184,13 @@ public function bootEnv(string $path, string $defaultEnv = 'dev', array $testEnv
*/
public function overload(string $path, string ...$extraPaths): void
{
$this->doLoad(true, \func_get_args());
$needsAutoValueResolution = 1 === \func_num_args();

$this->doLoad(true, $needsAutoValueResolution, \func_get_args());

if (!$needsAutoValueResolution) {
$this->resolveAllVariables();
}
}

/**
Expand Down Expand Up @@ -219,12 +243,13 @@ public function populate(array $values, bool $overrideExistingVars = false): voi
/**
* Parses the contents of an .env file.
*
* @param string $data The data to be parsed
* @param string $path The original file name where data where stored (used for more meaningful error messages)
* @param string $data The data to be parsed
* @param string $path The original file name where data where stored (used for more meaningful error messages)
* @param bool $needsValueResolution true when the value resolution needs to be done automatically
*
* @throws FormatException when a file has a syntax error
*/
public function parse(string $data, string $path = '.env'): array
public function parse(string $data, string $path = '.env', bool $needsValueResolution = true): array
{
$this->path = $path;
$this->data = str_replace(["\r\n", "\r"], "\n", $data);
Expand All @@ -245,7 +270,7 @@ public function parse(string $data, string $path = '.env'): array
break;

case self::STATE_VALUE:
$this->values[$name] = $this->lexValue();
$this->values[$name] = $this->lexValue($needsValueResolution);
$state = self::STATE_VARNAME;
break;
}
Expand Down Expand Up @@ -291,7 +316,7 @@ private function lexVarname(): string
return $matches[2];
}

private function lexValue(): string
private function lexValue(bool $needsValueResolution): string
{
if (preg_match('/[ \t]*+(?:#.*)?$/Am', $this->data, $matches, 0, $this->cursor)) {
$this->moveCursor($matches[0]);
Expand Down Expand Up @@ -340,7 +365,11 @@ private function lexValue(): string
++$this->cursor;
$value = str_replace(['\\"', '\r', '\n'], ['"', "\r", "\n"], $value);
$resolvedValue = $value;
$resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars);

if ($needsValueResolution) {
$resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars);
}

$resolvedValue = $this->resolveCommands($resolvedValue, $loadedVars);
$resolvedValue = str_replace('\\\\', '\\', $resolvedValue);
$v .= $resolvedValue;
Expand All @@ -363,7 +392,11 @@ private function lexValue(): string
}
$value = rtrim($value);
$resolvedValue = $value;
$resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars);

if ($needsValueResolution) {
$resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars);
}

$resolvedValue = $this->resolveCommands($resolvedValue, $loadedVars);
$resolvedValue = str_replace('\\\\', '\\', $resolvedValue);

Expand Down Expand Up @@ -545,14 +578,35 @@ private function createFormatException(string $message): FormatException
return new FormatException($message, new FormatExceptionContext($this->data, $this->path, $this->lineno, $this->cursor));
}

private function doLoad(bool $overrideExistingVars, array $paths): void
private function doLoad(bool $overrideExistingVars, bool $needsValueResolution, array $paths): void
{
foreach ($paths as $path) {
if (!is_readable($path) || is_dir($path)) {
throw new PathException($path);
}

$this->populate($this->parse(file_get_contents($path), $path), $overrideExistingVars);
$this->populate($this->parse(file_get_contents($path), $path, $needsValueResolution), $overrideExistingVars);
}
}

private function resolveAllVariables(): void
{
$resolvedVars = [];
$loadedVars = array_flip(explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? ''));
unset($loadedVars['']);

foreach ($_ENV as $name => $value) {
if ('SYMFONY_DOTENV_VARS' === $name) {
continue;
}

$resolvedValue = $this->resolveVariables($value, $loadedVars);

if ($resolvedValue !== $value) {
$resolvedVars[$name] = $resolvedValue;
}
}

$this->populate($resolvedVars, true);
}
}
126 changes: 126 additions & 0 deletions 126 src/Symfony/Component/Dotenv/Tests/DotenvTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,53 @@ public function testLoad()
$this->assertSame('BAZ', $bar);
}

public function testLoadDynamicVar()
{
unset($_ENV['VAR_STATIC1']);
unset($_ENV['VAR_STATIC2']);
unset($_ENV['VAR_DYNAMIC1']);
unset($_ENV['VAR_DYNAMIC2']);
unset($_ENV['VAR_DYNAMIC3']);
unset($_ENV['VAR_DYNAMIC_FAIL']);
unset($_SERVER['VAR_STATIC1']);
unset($_SERVER['VAR_STATIC2']);
unset($_SERVER['VAR_DYNAMIC1']);
unset($_SERVER['VAR_DYNAMIC2']);
unset($_SERVER['VAR_DYNAMIC3']);
unset($_SERVER['VAR_DYNAMIC_FAIL']);

@mkdir($tmpdir = sys_get_temp_dir().'/dotenv');

$path1 = tempnam($tmpdir, 'sf-');
$path2 = tempnam($tmpdir, 'sf-');
$path3 = tempnam($tmpdir, 'sf-');

file_put_contents($path1, "VAR_STATIC1=env\nVAR_DYNAMIC1=profile:\${VAR_STATIC1}\nVAR_DYNAMIC_FAIL=\$VAR_DYNAMIC1");
file_put_contents($path2, 'VAR_STATIC1=local');
file_put_contents($path3, "VAR_STATIC2=file3\nVAR_DYNAMIC2=\$VAR_STATIC1\nVAR_DYNAMIC3=\$VAR_STATIC2");

(new Dotenv())->usePutenv()->load($path1, $path2, $path3);

$static1 = getenv('VAR_STATIC1');
$static2 = getenv('VAR_STATIC2');
$dynamic1 = getenv('VAR_DYNAMIC1');
$dynamic2 = getenv('VAR_DYNAMIC2');
$dynamic3 = getenv('VAR_DYNAMIC3');
$dynamicFail = getenv('VAR_DYNAMIC_FAIL');

unlink($path1);
unlink($path2);
unlink($path3);
rmdir($tmpdir);

$this->assertSame('local', $static1);
$this->assertSame('file3', $static2);
$this->assertSame('profile:local', $dynamic1);
$this->assertSame('local', $dynamic2);
$this->assertSame('file3', $dynamic3);
$this->assertSame('profile:${VAR_STATIC1}', $dynamicFail);
}

public function testLoadEnv()
{
$resetContext = static function (): void {
Expand Down Expand Up @@ -350,6 +397,38 @@ public function testLoadEnv()
rmdir($tmpdir);
}

public function testLoadEnvDynamicVar()
{
unset($_ENV['VAR_STATIC1']);
unset($_ENV['VAR_DYNAMIC1']);
unset($_ENV['VAR_DYNAMIC_FAIL']);
unset($_SERVER['VAR_STATIC1']);
unset($_SERVER['VAR_DYNAMIC1']);
unset($_SERVER['VAR_DYNAMIC_FAIL']);

@mkdir($tmpdir = sys_get_temp_dir().'/dotenv');

$pathEnv = tempnam($tmpdir, 'sf-');
$pathLocalEnv = "$pathEnv.local";

file_put_contents($pathEnv, "VAR_STATIC1=env\nVAR_DYNAMIC1=profile:\${VAR_STATIC1}\nVAR_DYNAMIC_FAIL=\$VAR_DYNAMIC1");
file_put_contents($pathLocalEnv, 'VAR_STATIC1=local');

(new Dotenv())->usePutenv()->loadEnv($pathEnv, 'TEST_APP_ENV');

$static1 = getenv('VAR_STATIC1');
$dynamic1 = getenv('VAR_DYNAMIC1');
$dynamicFail = getenv('VAR_DYNAMIC_FAIL');

unlink($pathEnv);
unlink($pathLocalEnv);
rmdir($tmpdir);

$this->assertSame('local', $static1);
$this->assertSame('profile:local', $dynamic1);
$this->assertSame('profile:${VAR_STATIC1}', $dynamicFail);
}

public function testOverload()
{
unset($_ENV['FOO']);
Expand Down Expand Up @@ -385,6 +464,53 @@ public function testOverload()
$this->assertSame('BAZ', $bar);
}

public function testOverloadDynamicVar()
{
unset($_ENV['VAR_STATIC1']);
unset($_ENV['VAR_STATIC2']);
unset($_ENV['VAR_DYNAMIC1']);
unset($_ENV['VAR_DYNAMIC2']);
unset($_ENV['VAR_DYNAMIC3']);
unset($_ENV['VAR_DYNAMIC_FAIL']);
unset($_SERVER['VAR_STATIC1']);
unset($_SERVER['VAR_STATIC2']);
unset($_SERVER['VAR_DYNAMIC1']);
unset($_SERVER['VAR_DYNAMIC2']);
unset($_SERVER['VAR_DYNAMIC3']);
unset($_SERVER['VAR_DYNAMIC_FAIL']);

@mkdir($tmpdir = sys_get_temp_dir().'/dotenv');

$path1 = tempnam($tmpdir, 'sf-');
$path2 = tempnam($tmpdir, 'sf-');
$path3 = tempnam($tmpdir, 'sf-');

file_put_contents($path1, "VAR_STATIC1=env\nVAR_DYNAMIC1=profile:\${VAR_STATIC1}\nVAR_DYNAMIC_FAIL=\$VAR_DYNAMIC1");
file_put_contents($path2, 'VAR_STATIC1=local');
file_put_contents($path3, "VAR_STATIC2=file3\nVAR_DYNAMIC2=\$VAR_STATIC1\nVAR_DYNAMIC3=\$VAR_STATIC2");

(new Dotenv())->usePutenv()->overload($path1, $path2, $path3);

$static1 = getenv('VAR_STATIC1');
$static2 = getenv('VAR_STATIC2');
$dynamic1 = getenv('VAR_DYNAMIC1');
$dynamic2 = getenv('VAR_DYNAMIC2');
$dynamic3 = getenv('VAR_DYNAMIC3');
$dynamicFail = getenv('VAR_DYNAMIC_FAIL');

unlink($path1);
unlink($path2);
unlink($path3);
rmdir($tmpdir);

$this->assertSame('local', $static1);
$this->assertSame('file3', $static2);
$this->assertSame('profile:local', $dynamic1);
$this->assertSame('local', $dynamic2);
$this->assertSame('file3', $dynamic3);
$this->assertSame('profile:${VAR_STATIC1}', $dynamicFail);
}

public function testLoadDirectory()
{
$this->expectException(PathException::class);
Expand Down
Morty Proxy This is a proxified and sanitized view of the page, visit original site.