feature #35308 [Dotenv] Add Dotenv::bootEnv() to check for .env.local.php before calling Dotenv::loadEnv() (nicolas-grekas)

This PR was merged into the 5.1-dev branch.

Discussion
----------

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

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | yes
| Tickets       | -
| License       | MIT
| Doc PR        | -

The goal of this PR is to eventually get rid of the `config/bootstrap.php` file in Symfony 5.1 apps.
I think we've done enough iterations on that piece of bootstrapping logic to put it inside the `Dotenv` component.
This fully replaces https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/4.2/config/bootstrap.php

It doesn't conflict with current apps so they'll be fine keeping the `config/bootstrap.php` file until they're upgraded.

The new bootstrapping logic will require adding this line in `bin/console` and `public/index.php`:
```php
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
```

Recipes updated at https://github.com/symfony/recipes/pull/724

Commits
-------

98c7d3027b [Dotenv] Add Dotenv::bootEnv() to check for .env.local.php before calling Dotenv::loadEnv()
This commit is contained in:
Fabien Potencier 2020-01-30 13:04:33 +01:00
commit 8b337fc94a
8 changed files with 141 additions and 40 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,15 @@
CHANGELOG 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 5.0.0
----- -----

View File

@ -35,15 +35,47 @@ final class Dotenv
private $data; private $data;
private $end; private $end;
private $values; 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. * @param string $envKey
* Beware that `putenv()` is not thread safe, that's why this setting defaults to false
*/ */
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; $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.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. * .env.dist is loaded when it exists and .env is not found.
* *
* @param string $path A file 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 $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 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 array $testEnvs A list of app envs for which .env.local should be ignored
* *
* @throws FormatException when a file has a syntax error * @throws FormatException when a file has a syntax error
* @throws PathException when a file does not exist or is not readable * @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")) { if (file_exists($path) || !file_exists($p = "$path.dist")) {
$this->load($path); $this->load($path);
} else { } else {
$this->load($p); $this->load($p);
} }
if (null === $env = $_SERVER[$varName] ?? $_ENV[$varName] ?? null) { if (null === $env = $_SERVER[$k] ?? $_ENV[$k] ?? null) {
$this->populate([$varName => $env = $defaultEnv]); $this->populate([$k => $env = $defaultEnv]);
} }
if (!\in_array($env, $testEnvs, true) && file_exists($p = "$path.local")) { if (!\in_array($env, $testEnvs, true) && file_exists($p = "$path.local")) {
$this->load($p); $this->load($p);
$env = $_SERVER[$varName] ?? $_ENV[$varName] ?? $env; $env = $_SERVER[$k] ?? $_ENV[$k] ?? $env;
} }
if ('local' === $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. * 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) public function testParseWithFormatError($data, $error)
{ {
$dotenv = new Dotenv(true); $dotenv = new Dotenv();
try { try {
$dotenv->parse($data); $dotenv->parse($data);
@ -66,7 +66,7 @@ class DotenvTest extends TestCase
*/ */
public function testParse($data, $expected) public function testParse($data, $expected)
{ {
$dotenv = new Dotenv(true); $dotenv = new Dotenv();
$this->assertSame($expected, $dotenv->parse($data)); $this->assertSame($expected, $dotenv->parse($data));
} }
@ -208,7 +208,7 @@ class DotenvTest extends TestCase
file_put_contents($path1, 'FOO=BAR'); file_put_contents($path1, 'FOO=BAR');
file_put_contents($path2, 'BAR=BAZ'); file_put_contents($path2, 'BAR=BAZ');
(new Dotenv(true))->load($path1, $path2); (new Dotenv())->usePutenv()->load($path1, $path2);
$foo = getenv('FOO'); $foo = getenv('FOO');
$bar = getenv('BAR'); $bar = getenv('BAR');
@ -239,7 +239,7 @@ class DotenvTest extends TestCase
// .env // .env
file_put_contents($path, 'FOO=BAR'); 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('BAR', getenv('FOO'));
$this->assertSame('dev', getenv('TEST_APP_ENV')); $this->assertSame('dev', getenv('TEST_APP_ENV'));
@ -247,33 +247,33 @@ class DotenvTest extends TestCase
$_SERVER['TEST_APP_ENV'] = 'local'; $_SERVER['TEST_APP_ENV'] = 'local';
file_put_contents("$path.local", 'FOO=localBAR'); 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')); $this->assertSame('localBAR', getenv('FOO'));
// special case for test // special case for test
$_SERVER['TEST_APP_ENV'] = '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')); $this->assertSame('BAR', getenv('FOO'));
// .env.dev // .env.dev
unset($_SERVER['TEST_APP_ENV']); unset($_SERVER['TEST_APP_ENV']);
file_put_contents("$path.dev", 'FOO=devBAR'); 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')); $this->assertSame('devBAR', getenv('FOO'));
// .env.dev.local // .env.dev.local
file_put_contents("$path.dev.local", 'FOO=devlocalBAR'); 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')); $this->assertSame('devlocalBAR', getenv('FOO'));
// .env.dist // .env.dist
unlink($path); unlink($path);
file_put_contents("$path.dist", 'BAR=distBAR'); 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')); $this->assertSame('distBAR', getenv('BAR'));
putenv('FOO'); putenv('FOO');
@ -305,7 +305,7 @@ class DotenvTest extends TestCase
file_put_contents($path1, 'FOO=BAR'); file_put_contents($path1, 'FOO=BAR');
file_put_contents($path2, 'BAR=BAZ'); file_put_contents($path2, 'BAR=BAZ');
(new Dotenv(true))->overload($path1, $path2); (new Dotenv())->usePutenv()->overload($path1, $path2);
$foo = getenv('FOO'); $foo = getenv('FOO');
$bar = getenv('BAR'); $bar = getenv('BAR');
@ -323,7 +323,7 @@ class DotenvTest extends TestCase
public function testLoadDirectory() public function testLoadDirectory()
{ {
$this->expectException('Symfony\Component\Dotenv\Exception\PathException'); $this->expectException('Symfony\Component\Dotenv\Exception\PathException');
$dotenv = new Dotenv(true); $dotenv = new Dotenv();
$dotenv->load(__DIR__); $dotenv->load(__DIR__);
} }
@ -331,7 +331,7 @@ class DotenvTest extends TestCase
{ {
$originalValue = $_SERVER['argc']; $originalValue = $_SERVER['argc'];
$dotenv = new Dotenv(true); $dotenv = new Dotenv();
$dotenv->populate(['argc' => 'new_value']); $dotenv->populate(['argc' => 'new_value']);
$this->assertSame($originalValue, $_SERVER['argc']); $this->assertSame($originalValue, $_SERVER['argc']);
@ -342,7 +342,7 @@ class DotenvTest extends TestCase
putenv('TEST_ENV_VAR=original_value'); putenv('TEST_ENV_VAR=original_value');
$_SERVER['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']); $dotenv->populate(['TEST_ENV_VAR' => 'new_value']);
$this->assertSame('original_value', getenv('TEST_ENV_VAR')); $this->assertSame('original_value', getenv('TEST_ENV_VAR'));
@ -352,7 +352,7 @@ class DotenvTest extends TestCase
{ {
$_SERVER['HTTP_TEST_ENV_VAR'] = 'http_value'; $_SERVER['HTTP_TEST_ENV_VAR'] = 'http_value';
$dotenv = new Dotenv(true); $dotenv = (new Dotenv())->usePutenv();
$dotenv->populate(['HTTP_TEST_ENV_VAR' => 'env_value']); $dotenv->populate(['HTTP_TEST_ENV_VAR' => 'env_value']);
$this->assertSame('env_value', getenv('HTTP_TEST_ENV_VAR')); $this->assertSame('env_value', getenv('HTTP_TEST_ENV_VAR'));
@ -364,7 +364,7 @@ class DotenvTest extends TestCase
{ {
putenv('TEST_ENV_VAR_OVERRIDEN=original_value'); putenv('TEST_ENV_VAR_OVERRIDEN=original_value');
$dotenv = new Dotenv(true); $dotenv = (new Dotenv())->usePutenv();
$dotenv->populate(['TEST_ENV_VAR_OVERRIDEN' => 'new_value'], true); $dotenv->populate(['TEST_ENV_VAR_OVERRIDEN' => 'new_value'], true);
$this->assertSame('new_value', getenv('TEST_ENV_VAR_OVERRIDEN')); $this->assertSame('new_value', getenv('TEST_ENV_VAR_OVERRIDEN'));
@ -386,7 +386,7 @@ class DotenvTest extends TestCase
unset($_SERVER['DATABASE_URL']); unset($_SERVER['DATABASE_URL']);
putenv('DATABASE_URL'); putenv('DATABASE_URL');
$dotenv = new Dotenv(true); $dotenv = (new Dotenv())->usePutenv();
$dotenv->populate(['APP_DEBUG' => '1', 'DATABASE_URL' => 'mysql://root@localhost/db']); $dotenv->populate(['APP_DEBUG' => '1', 'DATABASE_URL' => 'mysql://root@localhost/db']);
$this->assertSame('APP_DEBUG,DATABASE_URL', getenv('SYMFONY_DOTENV_VARS')); $this->assertSame('APP_DEBUG,DATABASE_URL', getenv('SYMFONY_DOTENV_VARS'));
@ -403,7 +403,7 @@ class DotenvTest extends TestCase
unset($_SERVER['DATABASE_URL']); unset($_SERVER['DATABASE_URL']);
putenv('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(['APP_DEBUG' => '0', 'DATABASE_URL' => 'mysql://root@localhost/db']);
$dotenv->populate(['DATABASE_URL' => 'sqlite:///somedb.sqlite']); $dotenv->populate(['DATABASE_URL' => 'sqlite:///somedb.sqlite']);
@ -419,7 +419,7 @@ class DotenvTest extends TestCase
putenv('BAZ=baz'); putenv('BAZ=baz');
putenv('DOCUMENT_ROOT=/var/www'); putenv('DOCUMENT_ROOT=/var/www');
$dotenv = new Dotenv(true); $dotenv = (new Dotenv())->usePutenv();
$dotenv->populate(['FOO' => 'foo1', 'BAR' => 'bar1', 'BAZ' => 'baz1', 'DOCUMENT_ROOT' => '/boot']); $dotenv->populate(['FOO' => 'foo1', 'BAR' => 'bar1', 'BAZ' => 'baz1', 'DOCUMENT_ROOT' => '/boot']);
$this->assertSame('foo1', getenv('FOO')); $this->assertSame('foo1', getenv('FOO'));
@ -431,7 +431,7 @@ class DotenvTest extends TestCase
public function testGetVariablesValueFromEnvFirst() public function testGetVariablesValueFromEnvFirst()
{ {
$_ENV['APP_ENV'] = 'prod'; $_ENV['APP_ENV'] = 'prod';
$dotenv = new Dotenv(true); $dotenv = new Dotenv();
$test = "APP_ENV=dev\nTEST1=foo1_\${APP_ENV}"; $test = "APP_ENV=dev\nTEST1=foo1_\${APP_ENV}";
$values = $dotenv->parse($test); $values = $dotenv->parse($test);
@ -448,7 +448,7 @@ class DotenvTest extends TestCase
{ {
putenv('Foo=Bar'); putenv('Foo=Bar');
$dotenv = new Dotenv(true); $dotenv = new Dotenv();
try { try {
$values = $dotenv->parse('Foo=${Foo}'); $values = $dotenv->parse('Foo=${Foo}');
@ -460,19 +460,40 @@ class DotenvTest extends TestCase
public function testNoDeprecationWarning() public function testNoDeprecationWarning()
{ {
$dotenv = new Dotenv(true); $dotenv = new Dotenv();
$this->assertInstanceOf(Dotenv::class, $dotenv);
$dotenv = new Dotenv(false);
$this->assertInstanceOf(Dotenv::class, $dotenv); $this->assertInstanceOf(Dotenv::class, $dotenv);
} }
public function testDoNotUsePutenv() public function testDoNotUsePutenv()
{ {
$dotenv = new Dotenv(false); $dotenv = new Dotenv();
$dotenv->populate(['TEST_USE_PUTENV' => 'no']); $dotenv->populate(['TEST_USE_PUTENV' => 'no']);
$this->assertSame('no', $_SERVER['TEST_USE_PUTENV']); $this->assertSame('no', $_SERVER['TEST_USE_PUTENV']);
$this->assertSame('no', $_ENV['TEST_USE_PUTENV']); $this->assertSame('no', $_ENV['TEST_USE_PUTENV']);
$this->assertFalse(getenv('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);
}
} }