PHP gives you three ways to read an environment variable, and they do not always agree. The getenv() function reads directly from the process; the $_ENV superglobal contains whatever the SAPI populated at startup; and $_SERVER mixes web-server CGI variables with the same env data. Which you should use depends on your variables_order directive in php.ini (default "EGPCS" — note: capital E means env-vars are populated). The ecosystem standard for loading .env files is vlucas/phpdotenv, which Laravel ships by default. Symfony has its own symfony/dotenv with slightly different precedence semantics. This guide covers all three APIs, the framework idioms, and the FPM thread-safety gotcha that surprises OPcache-heavy deployments.
getenv vs $_ENV vs $_SERVER — which should you use?
Three sources, subtly different behaviour:
getenv('KEY')reads the live process environment via the C library. Returnsfalsefor unset keys (notnull).$_ENV['KEY']reads from a snapshot captured at request start — but only if yourphp.inivariables_orderdirective includesE. With the production-recommended"GPCS"(no E), this superglobal is empty.$_SERVER['KEY']mixes web-server CGI variables with environment variables. In FPM, this is the most reliably populated source.
$dbUrl = getenv('DATABASE_URL'); // false if unset
$port = getenv('PORT') ?: '3000';
$apiKey = $_ENV['API_KEY'] ?? null;
$debug = $_SERVER['APP_DEBUG'] ?? 'false';
// Defensive helper — covers all three
function env(string $key, $default = null) {
$value = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key);
return $value === false ? $default : $value;
}How do you set an environment variable at runtime?
putenv('MY_VAR=value');
$_ENV['MY_VAR'] = 'value';
$_SERVER['MY_VAR'] = 'value';
// Unset
putenv('MY_VAR');
unset($_ENV['MY_VAR']);putenv mutates the C-library environment seen by getenv; the superglobal assignments are independent. Most apps want all three to stay in sync, which is exactly what dotenv loaders do for you.
How do you load a .env file with phpdotenv?
Vance Lucas' vlucas/phpdotenv is the reference .env loader. Laravel ships it preconfigured. Outside Laravel:
composer require vlucas/phpdotenvrequire_once __DIR__ . '/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
// Or with required + format validation
$dotenv->required(['DATABASE_URL', 'APP_KEY'])->notEmpty();
$dotenv->required('APP_DEBUG')->isBoolean();createImmutable refuses to overwrite variables already set in the environment — the safe default for production where real env vars should always win. createMutable overwrites — useful in tests. For a complete reference on .env file syntax, see the .env file guide.
How does Laravel use environment variables?
Laravel calls phpdotenv during bootstrap and exposes values through two helpers. Critically, you should only use env() inside config files; everywhere else use config() against the resolved configuration array. This matters in production: php artisan config:cache bakes the configs into a single PHP file, and at that point env() calls outside config files return null because the .env file is never read again.
// config/database.php — env() is fine here
return [
'connections' => [
'pgsql' => [
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE'),
],
],
];
// Anywhere else — use config(), not env()
$db = config('database.connections.pgsql.database');How does Symfony's Dotenv differ?
Symfony has its own symfony/dotenv component (Symfony 4+). Loading order is configurable but the default flex setup loads .env, then .env.local, then .env.<env>, then .env.<env>.local — later files override earlier ones (opposite of phpdotenv's "first wins" with createImmutable):
composer require symfony/dotenv
# Production builds: bake variables into .env.local.php for performance
composer dump-env prodPractical patterns and gotchas
- PHP-FPM thread-safety:
getenv()is not thread-safe in ZTS builds with multiple FPM workers. Prefer$_SERVERor$_ENVin those configurations. - getenv returns false, not null. Use
!== falsefor the existence check, or passgetenv('KEY', true)with the second argument set totrueto limit the lookup to the local scope (PHP 7.1+). - Don't put .env in your web root. With Laravel and Symfony this is automatic, but if you assemble a stack manually, ensure
.envsits one level abovepublic/so a misconfigured server cannot serve the file. - php artisan config:cache caches env() to null. After caching,
env()outside config files returns null. Always go throughconfig()in application code. - Validate your .env file before deploying with the .env validator.
For language-agnostic security, validation, and rotation rules, see environment variable best practices.