Part of Jardis — the Domain-Driven Design platform for PHP. You model your domain; Jardis generates the production-ready hexagonal code (DTOs, Command/Query handlers, repositories, persistence). This package is part of the open-source foundation that generated code runs on.
A .env loader for PHP with cascading overrides, variable interpolation, type casting, and include directives. Goes beyond simple .env parsing — supports public and private loading modes, nested variable references, and an extensible cast chain.
- Public/Private Loading —
loadPublic()writes to$_ENV/$_SERVER;loadPrivate()returns an isolated array without touching global state - Cascading Overrides — two-stage loading: base
.envfirst, thenAPP_ENV-specific files (e.g..env.production) override selectively - Variable Interpolation —
${VAR}references are resolved against already-loaded values in the same file - Type Casting Chain — automatically converts strings to
bool, numeric, JSON, andarrayvia a chainable handler pipeline - Home Path Expansion —
~/is expanded to the OS home directory in both loading modes - Include Directives —
load(.env.database)andload?(.env.optional)split configuration across multiple files - Circular Include Detection — prevents infinite include loops with a typed
CircularEnvIncludeException - Docker
_FILESecret Resolution —DB_PASSWORD_FILE=/run/secrets/db_passwordreads the file and exposes the content asDB_PASSWORD. Works with Docker Swarm, Kubernetes mounted secrets, and any file-based secret store. Combines withjardissupport/secret— a_FILEthat containssecret(aes:...)is decrypted automatically through the cast chain - Extensible via
addHandler()— prepend or append custom cast handlers; remove built-in ones viaremoveHandler() - Raw-Key Cast Exemption —
addRawKeys()marks keys/suffixes (e.g._PASSWORD) whose values skip the built-in casts, so a credential likeDB_PASSWORD=falsesurvives as the string'false'instead ofbool(false). Handlers registered viaaddHandler()(e.g. secret decryption) still run for these keys - String Input —
loadPublicFromString()/loadPrivateFromString()parse.env-formatted content that never touched disk (e.g. a secrets manager payload), reusing the same cast chain, variable substitution and_FILEresolution as file loading
composer require jardissupport/dotenvuseJardisSupport\DotEnv\DotEnv;
$dotEnv = newDotEnv();
// Write into $_ENV / $_SERVER / putenv — suitable for application bootstrap$dotEnv->loadPublic('/path/to/app');
// Return an isolated array — no global state, suitable for bounded contexts$config = $dotEnv->loadPrivate('/path/to/domain');
echo$config['DB_HOST']; // 'localhost'echo$config['DEBUG']; // bool(true) — automatically castuseJardisSupport\DotEnv\DotEnv;
useJardisSupport\DotEnv\Handler\CastStringToBool;
useJardisSupport\Secret\Handler\SecretHandler;
useJardisSupport\Secret\KeyProvider\FileKeyProvider;
// .env example://// APP_ENV=production// load(.env.database) <- required include// load?(.env.local) <- optional include, silently skipped if absent// DB_URL=mysql://${DB_HOST}/${DB_NAME} <- variable interpolation// LOG_PATH=~/logs/app.log <- home path expansion// PORTS=[80,443] <- cast to array [80, 443]// DEBUG=true <- cast to bool(true)$dotEnv = newDotEnv();
// Prepend a custom handler — runs before all built-in casters$dotEnv->addHandler($myCustomHandler, prepend: true);
// Remove a built-in handler when its behaviour is not needed$dotEnv->removeHandler(CastStringToBool::class);
// Integrate secret decryption (requires jardissupport/secret)// Values like DB_PASSWORD=secret(...) are decrypted transparently$dotEnv->addHandler(
newSecretHandler(newFileKeyProvider('support/secret.key')),
prepend: true,
);
// Two-stage cascade:// Stage 1 → .env + .env.local// Stage 2 → .env.production + .env.production.local (driven by APP_ENV)$config = $dotEnv->loadPrivate('/path/to/app');Read secrets from mounted files — the industry-standard pattern for Docker Swarm and Kubernetes:
# .envAPP_NAME=MyAppDB_HOST=localhostDB_PASSWORD_FILE=/run/secrets/db_passwordREDIS_TOKEN_FILE=/run/secrets/redis_token$config = (newDotEnv())->loadPrivate('/path/to/app');
echo$config['DB_PASSWORD']; // content of /run/secrets/db_passwordecho$config['REDIS_TOKEN']; // content of /run/secrets/redis_token// DB_PASSWORD_FILE / REDIS_TOKEN_FILE are NOT in the resultThe _FILE suffix is stripped, the file content is read and passed through the full cast chain — variable substitution, type casting, and even secret decryption all work:
# _FILE + secret() combined: file contains encrypted value# /run/secrets/db_password contains: secret(aes:base64encodedValue)DB_PASSWORD_FILE=/run/secrets/db_password$dotEnv = newDotEnv();
$dotEnv->addHandler(
newSecretHandler(newFileKeyProvider('support/secret.key')),
prepend: true,
);
$config = $dotEnv->loadPrivate('/path/to/app');
// DB_PASSWORD → file read → secret() decrypted → plaintextfalse, 0 or 123456 as a password are the cast chain's blind spot: it turns them into
bool/int before the caller ever sees the intended string. addRawKeys() registers
keys/suffixes (case-insensitive, exact or suffix match, no substring match) whose values skip
the built-in casts and stay strings. Raw means cast-free, not handler-free: handlers registered
via addHandler() (e.g. the SecretHandler decrypting secret(...) values) still run for raw
keys, in their chain order — only their string result is used:
$dotEnv = newDotEnv();
$dotEnv->addRawKeys(['_PASSWORD', '_TOKEN']); // suffixes ...$dotEnv->addRawKeys(['API_KEY']); // ... or an exact key name// .env: DB_PASSWORD=false$config = $dotEnv->loadPrivate('/path/to/app');
var_dump($config['DB_PASSWORD']); // string(5) "false" — not bool(false)The check applies at both cast sites: a plain KEY=value line and a KEY_FILE=... secret file —
for the latter the resolved key is checked (DB_PASSWORD_FILE → rule matches DB_PASSWORD).
Registrations accumulate and de-duplicate; there is no removeRawKeys().
removeHandler() also detaches a value handler from the raw path.
loadPublicFromString()/loadPrivateFromString() accept .env-formatted content directly,
useful when the content comes from somewhere other than a file — AWS Secrets Manager, for example:
$secretsManagerPayload = "DB_HOST=prod-db\nDB_PASSWORD=s3cret!\n";
$config = (newDotEnv())->loadPrivateFromString($secretsManagerPayload);Behaves like file loading (same cast chain, ${VAR} substitution, _FILE resolution, raw-key
exemption) with two differences dictated by having no file on disk:
- There is no
APP_ENVcascade — a string is a single source, not a directory of variants. - A
load()/load?()directive throwsIncludeNotSupportedException— a string has no file-system context to resolve an include against.
A relative KEY_FILE=... path needs an explicit base directory to resolve against; pass it as
the second argument. An absolute path never needs one:
$dotEnv->loadPrivateFromString($content, baseDir: '/path/to/secrets-dir');Full documentation, guides, and API reference:
docs.jardis.io/en/support/dotenv
This package is licensed under the MIT License.
Jardis · Documentation · Headgent
This package ships with a skill for Claude Code, Cursor, Continue, and Aider. Install it in your consuming project:
composer require --dev jardis/dev-skillsMore details: https://docs.jardis.io/en/skills