[Dotenv] Add Dotenv::bootEnv() to check for .env.local.php before calling Dotenv::loadEnv()

This commit is contained in:
Nicolas Grekas 2020-01-11 15:48:32 +01:00
parent 3945a5c80e
commit 98c7d3027b
8 changed files with 141 additions and 40 deletions

View File

@ -6,6 +6,11 @@ Console
* `Command::setHidden()` is final since Symfony 5.1
Dotenv
------
* Deprecated passing `$usePutenv` argument to Dotenv's constructor, use `Dotenv::usePutenv()` instead.
EventDispatcher
---------------

View File

@ -6,6 +6,11 @@ Console
* `Command::setHidden()` has a default value (`true`) for `$hidden` parameter
Dotenv
------
* Removed argument `$usePutenv` from Dotenv's constructor, use `Dotenv::usePutenv()` instead.
EventDispatcher
---------------

View File

@ -38,7 +38,7 @@ class DotenvVaultTest extends TestCase
$vault->seal('foo', $plain);
unset($_SERVER['foo'], $_ENV['foo']);
(new Dotenv(false))->load($this->envFile);
(new Dotenv())->load($this->envFile);
$decrypted = $vault->reveal('foo');
$this->assertSame($plain, $decrypted);
@ -50,7 +50,7 @@ class DotenvVaultTest extends TestCase
$this->assertFalse($vault->remove('foo'));
unset($_SERVER['foo'], $_ENV['foo']);
(new Dotenv(false))->load($this->envFile);
(new Dotenv())->load($this->envFile);
$this->assertArrayNotHasKey('foo', $vault->list());
}

View File

@ -37,7 +37,7 @@
"symfony/console": "^4.4|^5.0",
"symfony/css-selector": "^4.4|^5.0",
"symfony/dom-crawler": "^4.4|^5.0",
"symfony/dotenv": "^4.4|^5.0",
"symfony/dotenv": "^5.1",
"symfony/polyfill-intl-icu": "~1.0",
"symfony/form": "^4.4|^5.0",
"symfony/expression-language": "^4.4|^5.0",
@ -71,7 +71,7 @@
"symfony/asset": "<4.4",
"symfony/browser-kit": "<4.4",
"symfony/console": "<4.4",
"symfony/dotenv": "<4.4",
"symfony/dotenv": "<5.1",
"symfony/dom-crawler": "<4.4",
"symfony/http-client": "<4.4",
"symfony/form": "<4.4",

View File

@ -41,6 +41,7 @@
},
"conflict": {
"symfony/dependency-injection": "<4.4",
"symfony/dotenv": "<5.1",
"symfony/event-dispatcher": "<4.4",
"symfony/lock": "<4.4",
"symfony/process": "<4.4"

View File

@ -1,6 +1,15 @@
CHANGELOG
=========
5.1.0
-----
* added `Dotenv::bootEnv()` to check for `.env.local.php` before calling `Dotenv::loadEnv()`
* added `Dotenv::setProdEnvs()` and `Dotenv::usePutenv()`
* made Dotenv's constructor accept `$envKey` and `$debugKey` arguments, to define
the name of the env vars that configure the env name and debug settings
* deprecated passing `$usePutenv` argument to Dotenv's constructor
5.0.0
-----

View File

@ -35,15 +35,47 @@ final class Dotenv
private $data;
private $end;
private $values;
private $usePutenv;
private $envKey;
private $debugKey;
private $prodEnvs = ['prod'];
private $usePutenv = false;
/**
* @var bool If `putenv()` should be used to define environment variables or not.
* Beware that `putenv()` is not thread safe, that's why this setting defaults to false
* @param string $envKey
*/
public function __construct(bool $usePutenv = false)
public function __construct($envKey = 'APP_ENV', string $debugKey = 'APP_DEBUG')
{
if (\in_array($envKey = (string) $envKey, ['1', ''], true)) {
@trigger_error(sprintf('Passing a boolean to the constructor of "%s" is deprecated since Symfony 5.1, use "Dotenv::usePutenv()".', __CLASS__), E_USER_DEPRECATED);
$this->usePutenv = (bool) $envKey;
$envKey = 'APP_ENV';
}
$this->envKey = $envKey;
$this->debugKey = $debugKey;
}
/**
* @return $this
*/
public function setProdEnvs(array $prodEnvs): self
{
$this->prodEnvs = $prodEnvs;
return $this;
}
/**
* @param bool $usePutenv If `putenv()` should be used to define environment variables or not.
* Beware that `putenv()` is not thread safe, that's why this setting defaults to false
*
* @return $this
*/
public function usePutenv($usePutenv = true): self
{
$this->usePutenv = $usePutenv;
return $this;
}
/**
@ -66,29 +98,31 @@ final class Dotenv
* .env.local is always ignored in test env because tests should produce the same results for everyone.
* .env.dist is loaded when it exists and .env is not found.
*
* @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
* @param string $path A file to load
* @param string $envKey|null 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
*/
public function loadEnv(string $path, string $varName = 'APP_ENV', string $defaultEnv = 'dev', array $testEnvs = ['test']): void
public function loadEnv(string $path, string $envKey = null, string $defaultEnv = 'dev', array $testEnvs = ['test']): void
{
$k = $envKey ?? $this->envKey;
if (file_exists($path) || !file_exists($p = "$path.dist")) {
$this->load($path);
} else {
$this->load($p);
}
if (null === $env = $_SERVER[$varName] ?? $_ENV[$varName] ?? null) {
$this->populate([$varName => $env = $defaultEnv]);
if (null === $env = $_SERVER[$k] ?? $_ENV[$k] ?? null) {
$this->populate([$k => $env = $defaultEnv]);
}
if (!\in_array($env, $testEnvs, true) && file_exists($p = "$path.local")) {
$this->load($p);
$env = $_SERVER[$varName] ?? $_ENV[$varName] ?? $env;
$env = $_SERVER[$k] ?? $_ENV[$k] ?? $env;
}
if ('local' === $env) {
@ -104,6 +138,32 @@ final class Dotenv
}
}
/**
* Loads env vars from .env.local.php if the file exists or from the other .env files otherwise.
*
* This method also configures the APP_DEBUG env var according to the current APP_ENV.
*
* See method loadEnv() for rules related to .env files.
*/
public function bootEnv(string $path, string $defaultEnv = 'dev', array $testEnvs = ['test']): void
{
$p = $path.'.local.php';
$env = (\function_exists('opcache_is_script_cached') && @opcache_is_script_cached($p)) || file_exists($p) ? include $p : null;
$k = $this->envKey;
if (\is_array($env) && (!isset($env[$k]) || ($_SERVER[$k] ?? $_ENV[$k] ?? $env[$k]) === $env[$k])) {
$this->populate($env);
} else {
$this->loadEnv($path, $k, $defaultEnv, $testEnvs);
}
$_SERVER += $_ENV;
$k = $this->debugKey;
$debug = $_SERVER[$k] ?? !\in_array($_SERVER[$this->envKey], $this->prodEnvs, true);
$_SERVER[$k] = $_ENV[$k] = (int) $debug || (!\is_bool($debug) && filter_var($debug, FILTER_VALIDATE_BOOLEAN)) ? '1' : '0';
}
/**
* Loads one or several .env files and enables override existing vars.
*

View File

@ -22,7 +22,7 @@ class DotenvTest extends TestCase
*/
public function testParseWithFormatError($data, $error)
{
$dotenv = new Dotenv(true);
$dotenv = new Dotenv();
try {
$dotenv->parse($data);
@ -66,7 +66,7 @@ class DotenvTest extends TestCase
*/
public function testParse($data, $expected)
{
$dotenv = new Dotenv(true);
$dotenv = new Dotenv();
$this->assertSame($expected, $dotenv->parse($data));
}
@ -208,7 +208,7 @@ class DotenvTest extends TestCase
file_put_contents($path1, 'FOO=BAR');
file_put_contents($path2, 'BAR=BAZ');
(new Dotenv(true))->load($path1, $path2);
(new Dotenv())->usePutenv()->load($path1, $path2);
$foo = getenv('FOO');
$bar = getenv('BAR');
@ -239,7 +239,7 @@ class DotenvTest extends TestCase
// .env
file_put_contents($path, 'FOO=BAR');
(new Dotenv(true))->loadEnv($path, 'TEST_APP_ENV');
(new Dotenv())->usePutenv()->loadEnv($path, 'TEST_APP_ENV');
$this->assertSame('BAR', getenv('FOO'));
$this->assertSame('dev', getenv('TEST_APP_ENV'));
@ -247,33 +247,33 @@ class DotenvTest extends TestCase
$_SERVER['TEST_APP_ENV'] = 'local';
file_put_contents("$path.local", 'FOO=localBAR');
(new Dotenv(true))->loadEnv($path, 'TEST_APP_ENV');
(new Dotenv())->usePutenv()->loadEnv($path, 'TEST_APP_ENV');
$this->assertSame('localBAR', getenv('FOO'));
// special case for test
$_SERVER['TEST_APP_ENV'] = 'test';
(new Dotenv(true))->loadEnv($path, 'TEST_APP_ENV');
(new Dotenv())->usePutenv()->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(true))->loadEnv($path, 'TEST_APP_ENV');
(new Dotenv())->usePutenv()->loadEnv($path, 'TEST_APP_ENV');
$this->assertSame('devBAR', getenv('FOO'));
// .env.dev.local
file_put_contents("$path.dev.local", 'FOO=devlocalBAR');
(new Dotenv(true))->loadEnv($path, 'TEST_APP_ENV');
(new Dotenv())->usePutenv()->loadEnv($path, 'TEST_APP_ENV');
$this->assertSame('devlocalBAR', getenv('FOO'));
// .env.dist
unlink($path);
file_put_contents("$path.dist", 'BAR=distBAR');
(new Dotenv(true))->loadEnv($path, 'TEST_APP_ENV');
(new Dotenv())->usePutenv()->loadEnv($path, 'TEST_APP_ENV');
$this->assertSame('distBAR', getenv('BAR'));
putenv('FOO');
@ -305,7 +305,7 @@ class DotenvTest extends TestCase
file_put_contents($path1, 'FOO=BAR');
file_put_contents($path2, 'BAR=BAZ');
(new Dotenv(true))->overload($path1, $path2);
(new Dotenv())->usePutenv()->overload($path1, $path2);
$foo = getenv('FOO');
$bar = getenv('BAR');
@ -323,7 +323,7 @@ class DotenvTest extends TestCase
public function testLoadDirectory()
{
$this->expectException('Symfony\Component\Dotenv\Exception\PathException');
$dotenv = new Dotenv(true);
$dotenv = new Dotenv();
$dotenv->load(__DIR__);
}
@ -331,7 +331,7 @@ class DotenvTest extends TestCase
{
$originalValue = $_SERVER['argc'];
$dotenv = new Dotenv(true);
$dotenv = new Dotenv();
$dotenv->populate(['argc' => 'new_value']);
$this->assertSame($originalValue, $_SERVER['argc']);
@ -342,7 +342,7 @@ class DotenvTest extends TestCase
putenv('TEST_ENV_VAR=original_value');
$_SERVER['TEST_ENV_VAR'] = 'original_value';
$dotenv = new Dotenv(true);
$dotenv = (new Dotenv())->usePutenv();
$dotenv->populate(['TEST_ENV_VAR' => 'new_value']);
$this->assertSame('original_value', getenv('TEST_ENV_VAR'));
@ -352,7 +352,7 @@ class DotenvTest extends TestCase
{
$_SERVER['HTTP_TEST_ENV_VAR'] = 'http_value';
$dotenv = new Dotenv(true);
$dotenv = (new Dotenv())->usePutenv();
$dotenv->populate(['HTTP_TEST_ENV_VAR' => 'env_value']);
$this->assertSame('env_value', getenv('HTTP_TEST_ENV_VAR'));
@ -364,7 +364,7 @@ class DotenvTest extends TestCase
{
putenv('TEST_ENV_VAR_OVERRIDEN=original_value');
$dotenv = new Dotenv(true);
$dotenv = (new Dotenv())->usePutenv();
$dotenv->populate(['TEST_ENV_VAR_OVERRIDEN' => 'new_value'], true);
$this->assertSame('new_value', getenv('TEST_ENV_VAR_OVERRIDEN'));
@ -386,7 +386,7 @@ class DotenvTest extends TestCase
unset($_SERVER['DATABASE_URL']);
putenv('DATABASE_URL');
$dotenv = new Dotenv(true);
$dotenv = (new Dotenv())->usePutenv();
$dotenv->populate(['APP_DEBUG' => '1', 'DATABASE_URL' => 'mysql://root@localhost/db']);
$this->assertSame('APP_DEBUG,DATABASE_URL', getenv('SYMFONY_DOTENV_VARS'));
@ -403,7 +403,7 @@ class DotenvTest extends TestCase
unset($_SERVER['DATABASE_URL']);
putenv('DATABASE_URL');
$dotenv = new Dotenv(true);
$dotenv = (new Dotenv())->usePutenv();
$dotenv->populate(['APP_DEBUG' => '0', 'DATABASE_URL' => 'mysql://root@localhost/db']);
$dotenv->populate(['DATABASE_URL' => 'sqlite:///somedb.sqlite']);
@ -419,7 +419,7 @@ class DotenvTest extends TestCase
putenv('BAZ=baz');
putenv('DOCUMENT_ROOT=/var/www');
$dotenv = new Dotenv(true);
$dotenv = (new Dotenv())->usePutenv();
$dotenv->populate(['FOO' => 'foo1', 'BAR' => 'bar1', 'BAZ' => 'baz1', 'DOCUMENT_ROOT' => '/boot']);
$this->assertSame('foo1', getenv('FOO'));
@ -431,7 +431,7 @@ class DotenvTest extends TestCase
public function testGetVariablesValueFromEnvFirst()
{
$_ENV['APP_ENV'] = 'prod';
$dotenv = new Dotenv(true);
$dotenv = new Dotenv();
$test = "APP_ENV=dev\nTEST1=foo1_\${APP_ENV}";
$values = $dotenv->parse($test);
@ -448,7 +448,7 @@ class DotenvTest extends TestCase
{
putenv('Foo=Bar');
$dotenv = new Dotenv(true);
$dotenv = new Dotenv();
try {
$values = $dotenv->parse('Foo=${Foo}');
@ -460,19 +460,40 @@ class DotenvTest extends TestCase
public function testNoDeprecationWarning()
{
$dotenv = new Dotenv(true);
$this->assertInstanceOf(Dotenv::class, $dotenv);
$dotenv = new Dotenv(false);
$dotenv = new Dotenv();
$this->assertInstanceOf(Dotenv::class, $dotenv);
}
public function testDoNotUsePutenv()
{
$dotenv = new Dotenv(false);
$dotenv = new Dotenv();
$dotenv->populate(['TEST_USE_PUTENV' => 'no']);
$this->assertSame('no', $_SERVER['TEST_USE_PUTENV']);
$this->assertSame('no', $_ENV['TEST_USE_PUTENV']);
$this->assertFalse(getenv('TEST_USE_PUTENV'));
}
public function testBootEnv()
{
@mkdir($tmpdir = sys_get_temp_dir().'/dotenv');
$path = tempnam($tmpdir, 'sf-');
file_put_contents($path, 'FOO=BAR');
(new Dotenv('TEST_APP_ENV', 'TEST_APP_DEBUG'))->bootEnv($path);
$this->assertSame('BAR', $_SERVER['FOO']);
unset($_SERVER['FOO'], $_ENV['FOO']);
unlink($path);
file_put_contents($path.'.local.php', '<?php return ["TEST_APP_ENV" => "dev", "FOO" => "BAR"];');
(new Dotenv('TEST_APP_ENV', 'TEST_APP_DEBUG'))->bootEnv($path);
$this->assertSame('BAR', $_SERVER['FOO']);
$this->assertSame('1', $_SERVER['TEST_APP_DEBUG']);
unset($_SERVER['FOO'], $_ENV['FOO']);
unlink($path.'.local.php');
rmdir($tmpdir);
}
}