Skip to content

Dotenv

composer require quillstack/dotenv

Reads a .env file into the environment. Values keep their types, and anything it cannot finish reading it refuses rather than guessing at.

Why this exists

It does not expand ${SOMETHING}, and that is on purpose

Most PHP .env libraries resolve one value from another out of the box. This one does not, and the model it follows is JavaScript's.

dotenv for Node is the most installed .env library anywhere, and it does not interpolate. Nor does dotenv-java. In that world, building values from other values is a second package — dotenv-expand — because it is a second decision: it turns a list of pairs into a small language, with escaping and ordering and undefined names to settle. Here that package is quillstack/dotenv-expand.

Running the same file through eight implementations across six languages, this is where they stand:

LanguageLibraryExpands ${…}
JavaScriptdotenvno — dotenv-expand does
Javadotenv-javano
PHPquillstack/dotenvno — quillstack/dotenv-expand does
PHPsymfony/dotenvyes
PHPvlucas/phpdotenvyes
PHPjosegonzalez/dotenvyes
Pythonpython-dotenvyes
Rubydotenvyes
Dartdotenvyes
Cdotenv-cyes

The part JavaScript does not do

Node's dotenv leaves URL=${BASE}/v1 as the literal text ${BASE}/v1, and hands it over without comment. An application then holds a string that looks like an address and is not one.

This refuses instead:

text
DotenvInterpolationNotSupportedException:
The value of `URL` uses `${...}`, which this package does not expand. Install
quillstack/dotenv-expand to resolve it, or write `\${` for a literal `${`.

Which is what makes leaving the second package out safe rather than merely cheap. Nothing is quietly half-read, in either configuration.

The same care everywhere else

A # after a value is a comment, an export prefix is understood, and hunter2#7 is still a password — the details a file written for a shell gets right and a naive parser does not. Each of those was a wrong value handed over without a word until it was fixed.

Requirements

  • PHP 8.1 or newer

Installation

shell
composer require quillstack/dotenv

Usage

text
APP_DEBUG=true
APP_NAME=quillstack
DB_PORT=5432
php
use Quillstack\Dotenv\Dotenv;

(new Dotenv('.env'))->load();
php
env('APP_DEBUG');   // true, a boolean
env('APP_NAME');    // 'quillstack'
env('DB_PORT');     // 5432, a number

Values keep the type they plainly have, so if (env('APP_DEBUG')) means what it reads as.

Default values

php
env('MISSING', 'a default');   // 'a default'

Required keys

Where there is no sensible default, say so and find out at boot rather than at midnight:

php
$host = required('DATABASE_HOST');
text
DotenvValueNotSetException:
Value not set for key: DATABASE_HOST

Comments after a value

A # starts a comment where a shell would treat it as one — after whitespace, and outside quotes:

text
DB_PORT=5432 # the default
PASSWORD=hunter2#7            # not a comment: no space before the hash
QUOTED="a # inside quotes"    # the hash inside stays, the one out here goes
php
env('DB_PORT');    // 5432, still a number
env('PASSWORD');   // 'hunter2#7'
env('QUOTED');     // 'a # inside quotes'

The export keyword

The same file read by source is written with export, and it is understood here too:

text
export DB_PORT=5432
php
env('DB_PORT');   // 5432

Multi-line values

Write \n rather than a real line break:

text
PRIVATE_KEY="line1\nline2\nline3"

Values built from other values

text
BASE=https://example.org
URL=${BASE}/v1

Refused, as above. Install quillstack/dotenv-expand and it resolves. Where a ${ means only itself, escape it:

text
PRICE=\${9.99}
php
env('PRICE');   // '${9.99}'

Reading a file without loading it

parse() hands back what the file holds and touches nothing:

php
$values = (new Dotenv('.env'))->parse();
// ['BASE' => 'https://example.org', 'URL' => '${BASE}/v1']

References are left exactly as written, escapes included — which is what lets quillstack/dotenv-expand tell ${BASE} from \${BASE}.

Benchmark

Measured with quillstack/benchmark on one file of 34 keys with no interpolation in it, which all five read identically. Runs are interleaved, each figure is the median of five, and PHP is 8.5.7.

Version
quillstack/dotenvv0.7.1
quillstack/dotenv-expandv0.6.1
symfony/dotenvv7.4.15
josegonzalez/dotenv4.0.0 (on m1/env 2.2.0)
vlucas/phpdotenvv5.6.4

Reading that file, once:

Per loadRelativeFiles loadedMemory
quillstack/dotenv146 µs570 kB
quillstack/dotenv + dotenv-expand176 µs1.20×687 kB
symfony/dotenv233 µs1.60×1149 kB
josegonzalez/dotenv301 µs2.06×7153 kB
vlucas/phpdotenv479 µs3.27×34336 kB

The second row is this package with quillstack/dotenv-expand on top: adding it costs about a fifth of the reading time, and it is still the fastest way in this table to resolve a .env at all. That package's README has the same comparison on a file which does use ${…} — one this package refuses outright, so it has no row there.

The files-loaded column is where the cold-start difference comes from: vlucas/phpdotenv reads 34 files and four packages into memory before parsing anything. Starting a process and loading those dominates the first read by an order of magnitude more than parsing does, which is why the per-load figure above is measured warm — it is the part this package controls.

What the numbers do not say: the other three expand ${…} and this one does not, and symfony/dotenv also reads .env.local layering and shell command substitution. Being faster because you do less is not being faster; the row above with dotenv-expand added is the like-for -like one.

Tests

shell
composer test
composer stan

The rest of Quillstack

This is one component of Quillstack, a PHP framework which is as simple to use as it is strict about what it does.

License

MIT — see LICENSE.

Released under the MIT License.