phpdotenv: variable precedence decides which value your code actually reads
Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.
At a glance
- What is it?
- PHP dotenv reads a plain text file and publishes its variables through $_ENV and $_SERVER, with getenv() left out unless you ask for it. Most surprises come from two decisions it makes quietly: the process environment outranks the file under the immutable constructors, and an undefined ${REFERENCE} stays in the value instead of collapsing to nothing.
- Who is it for?
- Adopt phpdotenv when your PHP application needs per-environment settings and your host does not let you edit Apache or Nginx virtual hosts. Skip the getenv() integration unless a dependency forces it, since the project calls putenv() and getenv() not thread safe.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 19 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- vlucas/phpdotenv13,553 ★ · -2
| Repository | 2026-09-22 | 2026-11-13 |
|---|---|---|
| vlucas/phpdotenv | 13.6k | 13.6k |
-2 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
The constructor name decides which store your values land in
The job is narrow: read a file and publish the variables. Where they land is decided by which factory you call. `Dotenv\Dotenv::createImmutable(__DIR__)` fills the `$_ENV` and `$_SERVER` super-globals, and reading them afterwards is the documented pattern:
$s3_bucket = $_ENV['S3_BUCKET'];
$s3_bucket = $_SERVER['S3_BUCKET'];Filling super-globals rather than the process environment is what lets one file work the same under Apache, Nginx, the CLI runner and PHP's built-in web server, with no virtual host edits and no `php_value` flags added to .htaccess. The library is a PHP port of the Ruby dotenv, and it assumes the twelve-factor position that credentials do not belong in code.
`getenv()` is a separate decision. The project states plainly that `getenv()` and `putenv()` are strongly discouraged because they are not thread safe, yet the escape hatch exists: `Dotenv::createUnsafeImmutable` adds a `PutenvAdapter` behind the scenes, and `Dotenv::createUnsafeMutable` is the counterpart to `Dotenv::createMutable`. With that adapter in place the same three read paths open up:
$s3_bucket = getenv('S3_BUCKET');
$s3_bucket = $_ENV['S3_BUCKET'];
$s3_bucket = $_SERVER['S3_BUCKET'];Code that already calls `getenv('S3_BUCKET')` sees nothing at all until you pick an unsafe constructor. That is the most common migration surprise: adding the library does not make a `getenv()` call start working.
createImmutable lets the process environment outrank your file
The word immutable in that factory name is about precedence, not about the file. Resolution reads from the repository being loaded into. Under `createImmutable`, a variable already present in `$_SERVER` or `$_ENV` takes precedence over the value written in your `.env`, and under `createUnsafeImmutable` the same rule extends to values visible through `getenv()` and `putenv()`.
For a developer on a laptop this is invisible, because nothing else defines those names. For an operator whose process manager exports `S3_BUCKET` into the environment, the file is still read, the value is still discarded, and the configuration appears to ignore every edit you make. No warning is emitted, because precedence is the designed behaviour rather than an error condition. Anyone chasing a value that refuses to change should inspect what the process already has before editing the file a second time. The mutable constructors exist for the opposite arrangement, where your file wins.
The distinction matters when several components read configuration. One library reading `$_ENV` and another calling `getenv()` can disagree about which value is current. The immutable constructors give every reader the same answer for the life of the process.
A reference to an undefined variable is left in the value
Interpolation is opt-in and spelled one way. Wrapping a variable in `${…}` nests it; a bare `$BASE_DIR` is never interpolated. The README's own example shows the pattern for cutting repetition:
BASE_DIR="/var/webroot/project-root"
CACHE_DIR="${BASE_DIR}/cache"
TMP_DIR="${BASE_DIR}/tmp"Interpolation happens in unquoted and double-quoted values but never inside single-quoted ones, and inside double quotes a reference can be escaped by writing `\${BASE_DIR}` to keep it as literal text. References resolve right to left, so a resolved inner reference can form part of an outer one.
The failure mode is quiet and specific. If the referenced variable is not defined, the reference is left in place verbatim rather than replaced by an empty string. A typo in `BASE_DIR` does not yield `/cache`. It yields the literal path `${BASE_DIR}/cache`, which is not where the application will look.
Single quotes freeze the value, double quotes recognise eight escapes
Values may be unquoted, single-quoted or double-quoted, and the three are not interchangeable. A single-quoted value is treated completely literally: no escape sequences are recognised and no variables are interpolated. Backslashes in unquoted values are also treated literally, which is the detail that catches anyone pasting a Windows path or a regular expression into a `.env` file.
Double quotes are where escapes are processed, and the recognised set is deliberately short. Exactly `\"`, `\\`, `\$`, `\f`, `\n`, `\r`, `\t` and `\v` are recognised, with the character escapes producing their real control characters. A `\n` inside double quotes therefore lands in your string as an actual line break, which is what you want for a PEM key block and not what you want if you meant two characters.
This is a fixed rule set rather than clever parsing, which pays off when six people edit the same file and bites when you expect shell-compatible behaviour nobody wrote down.
composer require, then load() where a missing file is a hard stop
Installing is a single Composer line, or a hand edit to your own `composer.json`:
composer require vlucas/phpdotenvThe file itself is plain text in the project root, and two keys are enough to show the format:
S3_BUCKET="dotenv"
SECRET_KEY="souper_seekret_key"Then two lines load it:
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();`load()` throws when there is no `.env` file, which is the right default in production and the wrong one for a test bootstrap running in a directory without secrets. `safeLoad()` is the variant that does not:
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();The filename is the second parameter when `.env` is not the name you want. Given arrays, only the first readable file is loaded; passing `false` as the third parameter merges every readable file with later files overriding earlier ones, and the fourth parameter sets the encoding:
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__, ['.env', '.env.local'], false, 'UTF-8');
$dotenv->load();That array form is how a project layers a shared `.env` underneath a developer `.env.local` that nobody commits.
The .env.example split is the whole secret-protection mechanism
The arrangement the project recommends is two files. `.env` stays out of version control because it can hold API keys and passwords. A separate `.env.example` is committed, carrying the variables your application requires with dummy or blank values:
S3_BUCKET="devbucket"
SECRET_KEY="abc123"Collaborators copy the example to a local `.env`, fill in their own secrets, and add the real file to the `.gitignore` file that sits at the repository root beside it. The stated benefit is that no password or API key ever reaches the version control history, and that production values never have to be handed to everyone who can clone the repository.
The consequence of skipping that gitignore line is worth stating plainly, because the library cannot undo it. Once a `.env` holding real credentials is pushed, the values stay in the history for as long as the repository exists, and deleting the file in a later commit does not remove them. Prevention is the entire mechanism.
Contributors install inside a PHP 8.5 container and commit a PHPStan baseline
Working on the library itself has its own shape. The repository root holds composer.json, phpunit.xml.dist, phpstan.neon.dist and phpstan-baseline.neon, and the Makefile drives everything through Docker instead of a local PHP install. The install target runs Composer inside registry.gitlab.com/grahamcampbell/php:8.5-base:
install:
@docker run -it -w /data -v ${PWD}:/data:delegated -v ~/.composer:/root/.composer:delegated --entrypoint composer --rm registry.gitlab.com/grahamcampbell/php:8.5-base update
@docker run -it -w /data -v ${PWD}:/data:delegated -v ~/.composer:/root/.composer:delegated --entrypoint composer --rm registry.gitlab.com/grahamcampbell/php:8.5-base bin all update`test` runs phpunit and phpstan-analyze, and `clean` deletes composer.lock alongside vendor and vendor-bin contents, so a fresh checkout starts without a lock file. The Makefile gives no local phpunit or phpstan invocation outside Docker.
For users, the versioning note matters more: the project follows semantic versioning, so breaking changes land between major releases, and UPGRADING.md carries guides for V2 to V3, V3 to V4 and V4 to V5. v5.8.0 shipped on 2026-10-25, the same day as the last push to master, following v5.7.0 on 2026-08-24 and v5.6.4 on 2026-07-06. The licence is BSD-3-Clause.
Editorial conclusion
Adopt phpdotenv when your PHP application needs per-environment settings and your host does not let you edit Apache or Nginx virtual hosts. Skip the getenv() integration unless a dependency forces it, since the project calls putenv() and getenv() not thread safe. Before the first run, decide whether your process manager exports any of these variable names, because under createImmutable an existing $_SERVER or $_ENV entry silently beats whatever your .env file says.
Frequently asked questions
how to install phpdotenv
Via Composer with `composer require vlucas/phpdotenv`, or by adding it by hand to your own `composer.json` file. The library also runs on PHP's built-in web server and the CLI runner, since no virtual host changes are needed.
how to use php dotenv
Call `Dotenv\Dotenv::createImmutable(__DIR__)` and then `load()`, after which every defined variable is available in the `$_ENV` and `$_SERVER` super-globals. Use `safeLoad()` instead of `load()` if the absence of a `.env` file should not throw.
What exactly is a .env file?
A plain text file in the project root holding key value pairs such as `S3_BUCKET="dotenv"`, used to load custom configuration variables without editing .htaccess files or Apache and Nginx virtual hosts. A `.env.example` file with blank or dummy values is committed so collaborators know which variables are required.
What happens if we push a .env file?
The credentials stay in the version control history, and removing the file in a later commit does not remove them. The recommended arrangement is to add `.env` to the project's `.gitignore` file so it is never committed, and to commit only `.env.example`.
Is dotenv needed anymore?
The project's position is that you should never store sensitive credentials in your code, and that anything likely to change between deployment environments belongs in environment variables. If your variables are already exported into `$_SERVER` or `$_ENV`, the file adds nothing, because under `createImmutable` those existing values take precedence.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/vlucas-phpdotenv)Star history chart for your README
[](https://hysenlabs.com/projects/vlucas-phpdotenv)Was this useful?