Reads a .env file into the environment. Values keep their types, and anything it cannot finish
reading it refuses rather than guessing at.
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:
| Language | Library | Expands ${…} |
|---|---|---|
| JavaScript | dotenv |
no — dotenv-expand does |
| Java | dotenv-java |
no |
| PHP | quillstack/dotenv |
no — quillstack/dotenv-expand does |
| PHP | symfony/dotenv |
yes |
| PHP | vlucas/phpdotenv |
yes |
| PHP | josegonzalez/dotenv |
yes |
| Python | python-dotenv |
yes |
| Ruby | dotenv |
yes |
| Dart | dotenv |
yes |
| C | dotenv-c |
yes |
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:
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.
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.
- PHP 8.1 or newer
composer require quillstack/dotenvAPP_DEBUG=true
APP_NAME=quillstack
DB_PORT=5432
use Quillstack\Dotenv\Dotenv;
(new Dotenv('.env'))->load();env('APP_DEBUG'); // true, a boolean
env('APP_NAME'); // 'quillstack'
env('DB_PORT'); // 5432, a numberValues keep the type they plainly have, so if (env('APP_DEBUG')) means what it reads as.
env('MISSING', 'a default'); // 'a default'Where there is no sensible default, say so and find out at boot rather than at midnight:
$host = required('DATABASE_HOST');DotenvValueNotSetException:
Value not set for key: DATABASE_HOST
A # starts a comment where a shell would treat it as one — after whitespace, and outside
quotes:
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
env('DB_PORT'); // 5432, still a number
env('PASSWORD'); // 'hunter2#7'
env('QUOTED'); // 'a # inside quotes'The same file read by source is written with export, and it is understood here too:
export DB_PORT=5432
env('DB_PORT'); // 5432Write \n rather than a real line break:
PRIVATE_KEY="line1\nline2\nline3"
BASE=https://example.org
URL=${BASE}/v1
Refused, as above. Install
quillstack/dotenv-expand and it resolves. Where a
${ means only itself, escape it:
PRICE=\${9.99}
env('PRICE'); // '${9.99}'parse() hands back what the file holds and touches nothing:
$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}.
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/dotenv | v0.7.1 |
| quillstack/dotenv-expand | v0.6.1 |
| symfony/dotenv | v7.4.15 |
| josegonzalez/dotenv | 4.0.0 (on m1/env 2.2.0) |
| vlucas/phpdotenv | v5.6.4 |
Reading that file, once:
| Per load | Relative | Files loaded | Memory | |
|---|---|---|---|---|
| quillstack/dotenv | 146 µs | — | 5 | 70 kB |
| quillstack/dotenv + dotenv-expand | 176 µs | 1.20× | 6 | 87 kB |
| symfony/dotenv | 233 µs | 1.60× | 1 | 149 kB |
| josegonzalez/dotenv | 301 µs | 2.06× | 7 | 153 kB |
| vlucas/phpdotenv | 479 µs | 3.27× | 34 | 336 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.
composer test
composer stanThis is one component of Quillstack, a PHP framework which is as simple to use as it is strict about what it does.
- quillstack/dotenv-expand — values built from other values
- quillstack/config — settings on top of these
- quillstack/framework — where both are wired in
- quillstack/local-storage — reads the file underneath
MIT — see LICENSE.