diff --git a/UPGRADE-5.1.md b/UPGRADE-5.1.md index c2f4910726..fed8562658 100644 --- a/UPGRADE-5.1.md +++ b/UPGRADE-5.1.md @@ -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 --------------- diff --git a/UPGRADE-6.0.md b/UPGRADE-6.0.md index 8ff5ab2491..d5908b9084 100644 --- a/UPGRADE-6.0.md +++ b/UPGRADE-6.0.md @@ -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 --------------- diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.php index d494c82e68..62ca08b07c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.php @@ -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()); } diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 896d149b10..ac3ce7572b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -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", diff --git a/src/Symfony/Component/Console/composer.json b/src/Symfony/Component/Console/composer.json index 34cb029543..ee1d76f6c2 100644 --- a/src/Symfony/Component/Console/composer.json +++ b/src/Symfony/Component/Console/composer.json @@ -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" diff --git a/src/Symfony/Component/Dotenv/CHANGELOG.md b/src/Symfony/Component/Dotenv/CHANGELOG.md index ef1df04aee..e004ed8d75 100644 --- a/src/Symfony/Component/Dotenv/CHANGELOG.md +++ b/src/Symfony/Component/Dotenv/CHANGELOG.md @@ -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 ----- diff --git a/src/Symfony/Component/Dotenv/Dotenv.php b/src/Symfony/Component/Dotenv/Dotenv.php index 8851514451..596117b90b 100644 --- a/src/Symfony/Component/Dotenv/Dotenv.php +++ b/src/Symfony/Component/Dotenv/Dotenv.php @@ -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. * diff --git a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php index 1ae3eefa94..f43ebac6ac 100644 --- a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php +++ b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php @@ -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', ' "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); + } }