bug #29129 [Dotenv] add loadEnv(), a smoother alternative to loadForEnv() (nicolas-grekas)

This PR was merged into the 4.2-dev branch.

Discussion
----------

[Dotenv] add loadEnv(), a smoother alternative to loadForEnv()

| Q             | A
| ------------- | ---
| Branch?       | 4.2
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | yes (4.2-only)
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

This PR replaces the `loadForEnv()` method introduced in #28533 by a new `loadEnv()` method.
- It accepts only one mandatory argument: `$path`, which is the path to the `.env` file.
- The 2nd argument is optional and defines the name of the environment variable that defines the Symfony env. This plays better with the current practice of defining the env in `.env` (`loadForEnv()` requires knowing the env before being called, leading to a chicken-n-egg situation that `loadEnv()` avoids.)
- the possibility to load several files at once is removed. We don't have a use case for it and those who do can call `loadEnv()` in a loop anyway.

In addition to $path (.env), the following files are loaded, the latter taking precedence in this order:
.env < env.local < .env.$env < .env.$env.local

Note that `loadForEnv()` used to give higher precedence to .env.local vs .env.$env.
The new behavior is aligned with [the order used by create-react-app](https://github.com/facebook/create-react-app/blob/master/docusaurus/docs/adding-custom-environment-variables.md#what-other-env-files-can-be-used). It also allows overriding the env in .env.local, which should be convenient for DX.

Last but not least, the "test" env has this special behaviors:
- `.env.local` file is skipped for the "test" env (same as before and as in create-react-app)
- ~vars defined in .env files **override** real env vars (similar to what Rails' dotenv does: you don't want your tests to randomly fail because of some real env vars)~.

Commits
-------

0cf9acb70f [Dotenv] add loadEnv(), a smoother alternative to loadForEnv()
This commit is contained in:
Nicolas Grekas 2018-11-09 09:14:35 +01:00
commit 664a032940
2 changed files with 51 additions and 48 deletions

View File

@ -48,34 +48,41 @@ final class Dotenv
*/
public function load(string $path, string ...$extraPaths): void
{
$this->doLoad(false, false, \func_get_args());
$this->doLoad(false, \func_get_args());
}
/**
* Loads one or several .env and the corresponding .env.$env, .env.local and .env.$env.local files if they exist.
* Loads a .env file and the corresponding .env.local, .env.$env and .env.$env.local files if they exist.
*
* .env.local is always ignored in test env because tests should produce the same results for everyone.
*
* @param string $path A file to load
* @param ...string $extraPaths A list of additional files to load
* @param string $path A file to load
* @param string $varName The name of the env vars that defines the app env
* @param string $defaultEnv The app env to use when none is defined
* @param array $testEnvs A list of app envs for which .env.local should be ignored
*
* @throws FormatException when a file has a syntax error
* @throws PathException when a file does not exist or is not readable
*
* @see https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
*/
public function loadForEnv(string $env, string $path, string ...$extraPaths): void
public function loadEnv(string $path, string $varName = 'APP_ENV', string $defaultEnv = 'dev', array $testEnvs = array('test')): void
{
$paths = \func_get_args();
for ($i = 1; $i < \func_num_args(); ++$i) {
$path = $paths[$i];
$pathList = array($path, "$path.$env");
if ('test' !== $env) {
$pathList[] = "$path.local";
}
$pathList[] = "$path.$env.local";
$this->load($path);
$this->doLoad(false, true, $pathList);
if (null === $env = $_SERVER[$varName] ?? $_ENV[$varName] ?? null) {
$this->populate(array($varName => $env = $defaultEnv));
}
if (!\in_array($env, $testEnvs, true) && file_exists($p = "$path.local")) {
$this->load($p);
$env = $_SERVER[$varName] ?? $_ENV[$varName] ?? $env;
}
if (file_exists($p = "$path.$env")) {
$this->load($p);
}
if (file_exists($p = "$path.$env.local")) {
$this->load($p);
}
}
@ -90,7 +97,7 @@ final class Dotenv
*/
public function overload(string $path, string ...$extraPaths): void
{
$this->doLoad(true, false, \func_get_args());
$this->doLoad(true, \func_get_args());
}
/**
@ -435,14 +442,14 @@ final class Dotenv
return new FormatException($message, new FormatExceptionContext($this->data, $this->path, $this->lineno, $this->cursor));
}
private function doLoad(bool $overrideExistingVars, bool $ignoreMissingExtraPaths, array $paths): void
private function doLoad(bool $overrideExistingVars, array $paths): void
{
foreach ($paths as $i => $path) {
if (is_readable($path) && !is_dir($path)) {
$this->populate($this->parse(file_get_contents($path), $path), $overrideExistingVars);
} elseif (!$ignoreMissingExtraPaths || 0 === $i) {
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);
}
}
}

View File

@ -186,7 +186,7 @@ class DotenvTest extends TestCase
$this->assertSame('BAZ', $bar);
}
public function testLoadForEnv()
public function testLoadEnv()
{
unset($_ENV['FOO']);
unset($_ENV['BAR']);
@ -197,50 +197,46 @@ class DotenvTest extends TestCase
@mkdir($tmpdir = sys_get_temp_dir().'/dotenv');
$path1 = tempnam($tmpdir, 'sf-');
$path2 = tempnam($tmpdir, 'sf-');
file_put_contents($path1, 'FOO=BAR');
file_put_contents($path2, 'BAR=BAZ');
$path = tempnam($tmpdir, 'sf-');
// .env
(new DotEnv())->loadForEnv('dev', $path1, $path2);
file_put_contents($path, 'FOO=BAR');
(new DotEnv())->loadEnv($path, 'TEST_APP_ENV');
$this->assertSame('BAR', getenv('FOO'));
$this->assertSame('BAZ', getenv('BAR'));
// .env.dev
file_put_contents("$path1.dev", 'FOO=devBAR');
(new DotEnv())->loadForEnv('dev', $path1, $path2);
$this->assertSame('devBAR', getenv('FOO'));
$this->assertSame('dev', getenv('TEST_APP_ENV'));
// .env.local
file_put_contents("$path1.local", 'FOO=localBAR');
(new DotEnv())->loadForEnv('dev', $path1, $path2);
file_put_contents("$path.local", 'FOO=localBAR');
(new DotEnv())->loadEnv($path, 'TEST_APP_ENV');
$this->assertSame('localBAR', getenv('FOO'));
// special case for test
file_put_contents("$path1.local", 'FOO=testBAR');
(new DotEnv())->loadForEnv('test', $path1, $path2);
$_SERVER['TEST_APP_ENV'] = 'test';
(new DotEnv())->loadEnv($path, 'TEST_APP_ENV');
$this->assertSame('BAR', getenv('FOO'));
// .env.dev
unset($_SERVER['TEST_APP_ENV']);
file_put_contents("$path.dev", 'FOO=devBAR');
(new DotEnv())->loadEnv($path, 'TEST_APP_ENV');
$this->assertSame('devBAR', getenv('FOO'));
// .env.dev.local
file_put_contents("$path1.dev.local", 'FOO=devlocalBAR');
(new DotEnv())->loadForEnv('dev', $path1, $path2);
file_put_contents("$path.dev.local", 'FOO=devlocalBAR');
(new DotEnv())->loadEnv($path, 'TEST_APP_ENV');
$this->assertSame('devlocalBAR', getenv('FOO'));
putenv('FOO');
putenv('BAR');
unlink($path1);
unlink("$path1.dev");
unlink("$path1.local");
unlink("$path1.dev.local");
unlink($path2);
unlink($path);
unlink("$path.dev");
unlink("$path.local");
unlink("$path.dev.local");
rmdir($tmpdir);
}