Skip to content
Merged
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"symfony/process": "^7.4",
"symfony/routing": "^7.4",
"symfony/translation": "^7.4",
"vlucas/phpdotenv": "^5.6.1",
"voku/portable-ascii": "2.0.3"
},
"replace": {
Expand Down
70 changes: 70 additions & 0 deletions src/Illuminate/Foundation/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ class Application extends Container implements HttpKernelInterface, TerminableIn
*/
protected $hasBeenBootstrapped = false;

/**
* The custom environment path defined by the developer.
*
* @var string|null
*/
protected $environmentPath;

/**
* The environment file to load during bootstrapping.
*
* @var string
*/
protected $environmentFile = '.env';

/**
* The array of booting callbacks.
*
Expand Down Expand Up @@ -265,6 +279,62 @@ public function useStoragePath($path)
return $this;
}

/**
* Get the path to the environment file directory.
*
* @return string
*/
public function environmentPath()
{
return $this->environmentPath ?: $this['path.base'];
}

/**
* Set the directory for the environment file.
*
* @param string $path
* @return $this
*/
public function useEnvironmentPath($path)
{
$this->environmentPath = $path;

return $this;
}

/**
* Set the environment file to be loaded during bootstrapping.
*
* @param string $file
* @return $this
*/
public function loadEnvironmentFrom($file)
{
$this->environmentFile = $file;

return $this;
}

/**
* Get the environment file the application is using.
*
* @return string
*/
public function environmentFile()
{
return $this->environmentFile ?: '.env';
}

/**
* Get the fully qualified path to the environment file.
*
* @return string
*/
public function environmentFilePath()
{
return $this->environmentPath().DIRECTORY_SEPARATOR.$this->environmentFile();
}

/**
* Get the path to the application configuration files.
*
Expand Down
80 changes: 80 additions & 0 deletions src/Illuminate/Foundation/Bootstrap/LoadEnvironmentVariables.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php namespace Illuminate\Foundation\Bootstrap;

use Dotenv\Dotenv;
use Illuminate\Foundation\Application;
use Illuminate\Support\Env;
use Symfony\Component\Console\Input\ArgvInput;

/**
* ponytail: v13's LoadEnvironmentVariables bootstrapper, without the config cache check
* and the invalid-file message. The start script runs it before the L4.2 .env.{env}.php
* file, which still wins on the keys both define. Remove at the flip.
*/
class LoadEnvironmentVariables {

/**
* Bootstrap the given application.
*
* @param \Illuminate\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
$this->checkForSpecificEnvironmentFile($app);

$this->createDotenv($app)->safeLoad();
}

/**
* Detect if a custom environment file matching the APP_ENV exists.
*
* @param \Illuminate\Foundation\Application $app
* @return void
*/
protected function checkForSpecificEnvironmentFile($app)
{
if ($app->runningInConsole() &&
($input = new ArgvInput)->hasParameterOption('--env') &&
$this->setEnvironmentFilePath($app, $app->environmentFile().'.'.$input->getParameterOption('--env')))
{
return;
}

$environment = Env::get('APP_ENV');

if ( ! $environment) return;

$this->setEnvironmentFilePath($app, $app->environmentFile().'.'.$environment);
}

/**
* Load a custom environment file.
*
* @param \Illuminate\Foundation\Application $app
* @param string $file
* @return bool
*/
protected function setEnvironmentFilePath($app, $file)
{
if (is_file($app->environmentPath().'/'.$file))
{
$app->loadEnvironmentFrom($file);

return true;
}

return false;
}

/**
* Create a Dotenv instance.
*
* @param \Illuminate\Foundation\Application $app
* @return \Dotenv\Dotenv
*/
protected function createDotenv($app)
{
return Dotenv::create(Env::getRepository(), $app->environmentPath(), $app->environmentFile());
}

}
9 changes: 6 additions & 3 deletions src/Illuminate/Foundation/start.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
use Illuminate\Foundation\AliasLoader;
use Illuminate\Config\EnvironmentVariables;
use Illuminate\Foundation\Bootstrap\LoadConfiguration;
use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;

/*
|--------------------------------------------------------------------------
Expand Down Expand Up @@ -94,12 +95,14 @@
| Register The Environment Variables
|--------------------------------------------------------------------------
|
| Here we will register all of the $_ENV and $_SERVER variables into the
| process so that they're globally available configuration options so
| sensitive configuration information can be swept out of the code.
| v13's LoadEnvironmentVariables loads the .env file first. The L4.2
| .env.{env}.php file loads after it, so its values win on the keys both
| files define.
|
*/

$app->make(LoadEnvironmentVariables::class)->bootstrap($app);

with($envVariables = new EnvironmentVariables(
$app->getEnvironmentVariablesLoader()))->load($env);

Expand Down
17 changes: 17 additions & 0 deletions tests/Foundation/FoundationApplicationBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ protected function tearDown(): void
error_reporting($this->errorReporting);
ini_set('display_errors', $this->displayErrors);
date_default_timezone_set($this->timezone);
foreach (array('FORK_START_SHARED', 'FORK_START_DOTENV') as $key)
{
putenv($key);
unset($_ENV[$key], $_SERVER[$key]);
}
Facade::clearResolvedInstances();
Facade::setFacadeApplication(null);

Expand Down Expand Up @@ -112,6 +117,18 @@ public function everyConfigurationFileLoadsWithoutAnEnvironmentCascade()
$this->assertSame('http://builder.test', $config['app.url']);
}

#[Test]
public function theEnvFileLoadsFirstAndTheEnvironmentsPhpFileWins()
{
file_put_contents($this->base.'/.env', "FORK_START_SHARED=dotenv\nFORK_START_DOTENV=dotenv-only\n");
file_put_contents($this->base.'/.env.testing.php', '<?php return array("FORK_START_SHARED" => "php");');

$this->bootstrapped('testing');

$this->assertSame('php', getenv('FORK_START_SHARED'));
$this->assertSame('dotenv-only', getenv('FORK_START_DOTENV'));
}

#[Test]
public function theConfigurationLoadsFromTheConfiguredPath()
{
Expand Down
130 changes: 130 additions & 0 deletions tests/Foundation/FoundationLoadEnvironmentVariablesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

class FoundationLoadEnvironmentVariablesTest extends TestCase
{
private string $base;
private array $argv;

protected function setUp(): void
{
$this->argv = $_SERVER['argv'];
$this->base = sys_get_temp_dir().'/fork-dotenv-'.uniqid();
mkdir($this->base.'/elsewhere', 0777, true);
}

protected function tearDown(): void
{
$_SERVER['argv'] = $this->argv;
foreach (array('FORK_DOTENV_VALUE', 'FORK_DOTENV_PROCESS', 'FORK_DOTENV_OTHER', 'APP_ENV') as $key)
{
putenv($key);
unset($_ENV[$key], $_SERVER[$key]);
}

$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->base, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file) $file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname());
rmdir($this->base);
}

#[Test]
public function theEnvFileLoadsIntoTheEnvironment()
{
file_put_contents($this->base.'/.env', "FORK_DOTENV_VALUE=from-dotenv\n");

$app = $this->bootstrapped();

$this->assertSame('.env', $app->environmentFile());
$this->assertSame($this->base.'/.env', $app->environmentFilePath());
$this->assertSame('from-dotenv', getenv('FORK_DOTENV_VALUE'));
$this->assertSame('from-dotenv', $_ENV['FORK_DOTENV_VALUE']);
$this->assertSame('from-dotenv', $_SERVER['FORK_DOTENV_VALUE']);
$this->assertSame('from-dotenv', env('FORK_DOTENV_VALUE'));
}

#[Test]
public function theEnvOptionPicksTheMatchingEnvFile()
{
file_put_contents($this->base.'/.env', "FORK_DOTENV_VALUE=base\n");
file_put_contents($this->base.'/.env.staging', "FORK_DOTENV_VALUE=staging\n");
$_SERVER['argv'] = array('artisan', 'list', '--env=staging');

$app = $this->bootstrapped();

$this->assertSame('.env.staging', $app->environmentFile());
$this->assertSame('staging', getenv('FORK_DOTENV_VALUE'));
}

#[Test]
public function appEnvPicksTheMatchingEnvFile()
{
file_put_contents($this->base.'/.env', "FORK_DOTENV_VALUE=base\n");
file_put_contents($this->base.'/.env.staging', "FORK_DOTENV_VALUE=staging\n");
putenv('APP_ENV=staging');

$app = $this->bootstrapped();

$this->assertSame('.env.staging', $app->environmentFile());
$this->assertSame('staging', getenv('FORK_DOTENV_VALUE'));
}

#[Test]
public function anEnvironmentWithoutItsOwnFileFallsBackToTheEnvFile()
{
file_put_contents($this->base.'/.env', "FORK_DOTENV_VALUE=base\n");
$_SERVER['argv'] = array('artisan', 'list', '--env=missing');

$app = $this->bootstrapped();

$this->assertSame('.env', $app->environmentFile());
$this->assertSame('base', getenv('FORK_DOTENV_VALUE'));
}

#[Test]
public function variablesAlreadyInTheEnvironmentAreKept()
{
file_put_contents($this->base.'/.env', "FORK_DOTENV_PROCESS=from-dotenv\nFORK_DOTENV_OTHER=added\n");
putenv('FORK_DOTENV_PROCESS=from-the-process');

$this->bootstrapped();

$this->assertSame('from-the-process', getenv('FORK_DOTENV_PROCESS'));
$this->assertSame('added', getenv('FORK_DOTENV_OTHER'));
}

#[Test]
public function aMissingEnvFileLoadsNothing()
{
$this->bootstrapped();

$this->assertFalse(getenv('FORK_DOTENV_VALUE'));
}

#[Test]
public function theEnvFileLoadsFromTheConfiguredEnvironmentPath()
{
file_put_contents($this->base.'/.env', "FORK_DOTENV_VALUE=base\n");
file_put_contents($this->base.'/elsewhere/.env', "FORK_DOTENV_VALUE=elsewhere\n");

$app = new Application;
$app->setBasePath($this->base);
$this->assertSame($app, $app->useEnvironmentPath($this->base.'/elsewhere'));
(new LoadEnvironmentVariables)->bootstrap($app);

$this->assertSame($this->base.'/elsewhere', $app->environmentPath());
$this->assertSame('elsewhere', getenv('FORK_DOTENV_VALUE'));
}

private function bootstrapped(): Application
{
$app = new Application;
$app->setBasePath($this->base);
(new LoadEnvironmentVariables)->bootstrap($app);

return $app;
}
}
Loading