CLI tool
vlucas/phpdotenv avatar
vlucas/phpdotenv

phpdotenv: variable precedence decides which value your code actually reads

Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.

13,553 stars668 forksPHPBSD-3-Clause

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.

From daily snapshots

Star history

  • vlucas/phpdotenv13,553 ★ · -2
Star history
Repository2026-09-222026-11-13
vlucas/phpdotenv13.6k13.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:

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

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

shell
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`:

bash
composer require vlucas/phpdotenv

The file itself is plain text in the project root, and two keys are enough to show the format:

shell
S3_BUCKET="dotenv"
SECRET_KEY="souper_seekret_key"

Then two lines load it:

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

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

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

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

bash
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

  1. Issues
  2. License: BSD-3-Clause
  3. README
  4. Releases
  5. vlucas/phpdotenv on GitHub
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/vlucas-phpdotenv.svg)](https://hysenlabs.com/projects/vlucas-phpdotenv)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/vlucas-phpdotenv.svg)](https://hysenlabs.com/projects/vlucas-phpdotenv)

Was this useful?