env.dev

Environment Variables in PHP: getenv, $_ENV & phpdotenv

Three ways PHP reads env vars (getenv, $_ENV, $_SERVER) and when each is reliable. Plus vlucas/phpdotenv, Laravel's env() vs config(), and the FPM thread-safety gotcha.

Last updated:

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. Returns false for unset keys (not null).
  • $_ENV['KEY'] reads from a snapshot captured at request start — but only if your php.ini variables_order directive includes E. 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.
php
$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?

php
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:

bash
composer require vlucas/phpdotenv
php
require_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.

php
// 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):

bash
composer require symfony/dotenv

# Production builds: bake variables into .env.local.php for performance
composer dump-env prod

Practical patterns and gotchas

  • PHP-FPM thread-safety: getenv() is not thread-safe in ZTS builds with multiple FPM workers. Prefer $_SERVER or $_ENV in those configurations.
  • getenv returns false, not null. Use !== false for the existence check, or pass getenv('KEY', true) with the second argument set to true to 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 .env sits one level above public/ 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 through config() 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.

Was this helpful?

Read next

Environment Variables in Rust: std::env, dotenvy & envy

Read env vars in Rust with std::env::var, load .env files via dotenvy, and deserialize typed config with envy. Plus the Rust 2024 unsafe-set_var change.

Continue →

Frequently Asked Questions

getenv, $_ENV, or $_SERVER — which should I use?

$_SERVER is the most reliably populated source under PHP-FPM. $_ENV is empty unless variables_order in php.ini includes E. getenv() reads the live process environment but is not thread-safe in ZTS builds. A defensive helper that checks all three in order is the safe pattern.

Why does Laravel say to never use env() outside config files?

php artisan config:cache bakes the configs into a single PHP file at deploy time. After caching, the .env file is never read again, so env() outside config files returns null. Always go through config() in application code; reserve env() for config files only.

phpdotenv createImmutable vs createMutable?

createImmutable refuses to overwrite variables already set in the environment — the right default for production where real env vars should win. createMutable overwrites — useful in tests where you want the .env file to take priority.

How do Symfony and Laravel differ on .env precedence?

Symfony loads .env, then .env.local, then .env.<env>, then .env.<env>.local with later files winning. Laravel via phpdotenv createImmutable uses first-wins. Symfony also bakes variables into .env.local.php at deploy time for performance.

Stay up to date

Get notified about new guides, tools, and cheatsheets.