DotEnv
Konfiguration, die mitdenkt, typisiert, modular und sicher.
Einführung
Die meisten .env-Libraries liefern Strings. DB_PORT=3306 bleibt "3306", DEBUG=true bleibt "true", und im Code folgen endlose (int) Casts und === 'true' Vergleiche. Konfigurationsdateien wachsen in eine einzige, unübersichtliche Datei, und Secrets landen im Klartext neben dem Hostnamen.
jardissupport/dotenv löst das grundlegend anders. Das Package versteht .env-Dateien nicht als flache Key-Value-Listen, sondern als typisierte, modulare Konfigurationsquelle:
- Automatisches Type-Casting —
truewirdbool,42wirdint,[a,b,c]wirdarray. Keine manuellen Casts mehr im Anwendungscode. - Variablen-Interpolation —
DATABASE_URL=mysql://${DB_HOST}/${DB_NAME}wird zur Ladezeit aufgelöst. Eine Wahrheitsquelle, keine Duplikate. - Modulare Includes —
load(.env.database)teilt Konfiguration in fachliche Einheiten auf. Schluss mit 200-Zeilen.env-Dateien. - Umgebungs-Cascading —
.env→.env.local→.env.staging→.env.staging.local. Automatisch, basierend aufAPP_ENV. - Sichere Secrets —
_FILE-Pattern für Docker Secrets und Vault-Mounts. Verschlüsselte Werte mitjardissupport/secret. Klartext-Passwörter in.envgehören der Vergangenheit an. - Erweiterbare Pipeline — eigene Cast-Handler registrieren, bestehende entfernen. Die Verarbeitung passt sich dem Projekt an, nicht umgekehrt.
Installation
composer require jardissupport/dotenvGitHub: jardisSupport/dotenv
Optionale Abhängigkeit für verschlüsselte Werte:
composer require jardissupport/secretGrundlegende Nutzung
Zwei Zeilen Code, und die gesamte Konfiguration steht typisiert bereit.
Öffentlich laden — in globalen State schreiben
use JardisSupport\DotEnv\DotEnv;
$dotEnv = new DotEnv();
$dotEnv->loadPublic(__DIR__);Die Werte stehen danach in $_ENV, $_SERVER und via getenv() zur Verfügung:
$debug = $_ENV['APP_DEBUG']; // bool: true
$port = $_ENV['DB_PORT']; // int: 3306
$host = getenv('DB_HOST'); // string: "localhost" (putenv speichert immer Strings)putenv() vs. $_ENV
putenv() / getenv() speichern immer Strings (POSIX-Constraint). $_ENV und $_SERVER erhalten die typisierten Werte (bool, int, float). Arrays landen nur in $_ENV, da sie nicht POSIX-serialisierbar sind.
Privat laden — isoliertes Array
$config = $dotEnv->loadPrivate(__DIR__);
// $config = [
// 'APP_ENV' => 'production',
// 'DB_HOST' => 'localhost',
// 'DB_PORT' => 3306,
// 'DEBUG' => false,
// ]Kein globaler State wird verändert. Ideal für Tests oder Multi-Tenant-Szenarien.
Dateilade-Reihenfolge
Ein häufiges Problem: Die gleiche .env-Datei soll in Development, Staging und Production funktionieren, mit unterschiedlichen Werten. DotEnv löst das mit einem intelligenten Zwei-Stufen-Loading. Spätere Dateien überschreiben frühere Werte, ohne dass Logik im Code nötig ist.
Stufe 1 — Base-Dateien (immer)
| Datei | Zweck |
|---|---|
.env | Standardwerte für alle Umgebungen |
.env.local | Lokale Overrides (nicht ins VCS) |
Stufe 2 — Umgebungsspezifisch (wenn APP_ENV gesetzt)
| Datei | Zweck |
|---|---|
.env.{APP_ENV} | Umgebungsspezifische Werte (z.B. .env.staging) |
.env.{APP_ENV}.local | Lokale Overrides für diese Umgebung |
APP_ENV wird aus Stufe 1 oder aus dem OS-Environment gelesen.
Beispiel mit APP_ENV=staging:
.env ← Basis
.env.local ← Lokale Overrides
.env.staging ← Staging-spezifisch
.env.staging.local ← Lokale Staging-OverridesNicht existierende Dateien werden stillschweigend übersprungen.
Type-Casting
Das Herzstück des Packages. Statt im Anwendungscode (int) $_ENV['PORT'] oder $_ENV['DEBUG'] === 'true' zu schreiben, liefert DotEnv direkt die richtigen PHP-Typen. Eine Pipeline aus spezialisierten Handlern verarbeitet jeden Wert sequenziell, sobald ein Handler einen Nicht-String-Typ zurückgibt, stoppt die Pipeline.
Cast-Reihenfolge
| # | Handler | Eingabe | Ausgabe |
|---|---|---|---|
| 1 | CastStringToValue | ${DB_HOST} | aufgelöster String |
| 2 | CastUserHome | ~/logs | /home/user/logs |
| 3 | CastStringToNumeric | "3306" | 3306 (int) |
| 4 | CastStringToBool | "true" | true (bool) |
| 5 | CastStringToJson | '{"a":1}' | ['a' => 1] (array) |
| 6 | CastStringToArray | [a=>1,b=>2] | ['a' => 1, 'b' => 2] (array) |
Variablen-Interpolation
Bereits geladene Variablen können mit ${VAR} referenziert werden:
DB_HOST=localhost
DB_PORT=3306
DB_NAME=myapp
DATABASE_URL=mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}Ergebnis: DATABASE_URL = "mysql://localhost:3306/myapp"
Nicht aufgelöste Referenzen bleiben als ${VAR} im String stehen.
Home-Verzeichnis
~/ am Anfang eines Wertes wird zum Home-Verzeichnis expandiert:
LOG_PATH=~/logs
CACHE_DIR=~/cache/myappFunktioniert auf Unix ($HOME) und Windows (HOMEDRIVE + HOMEPATH). Ein in .env definiertes HOME=/custom/path wird respektiert.
Numerische Werte
PORT=8080 # int: 8080
RATE=0.75 # float: 0.75
VERSION=2 # int: 2
NOT_NUMERIC=v2.1 # string: "v2.1"Boolean-Werte
Erkannt werden (case-insensitive): true, false, yes, no, on, off, 1, 0
DEBUG=true # bool: true
CACHE_ENABLED=yes # bool: true
VERBOSE=off # bool: falseJSON-Werte
Werte die mit { oder [ beginnen und gültiges JSON sind:
ALLOWED_ORIGINS=["https://app.example.com","https://api.example.com"]
SMTP_CONFIG={"host":"mail.example.com","port":587,"tls":true}String-Werte innerhalb des JSON werden rekursiv durch die Cast-Pipeline geschickt.
Array-Syntax
Jardis bringt eine eigene, kompakte Syntax für strukturierte Werte mit, lesbarer als JSON und natürlicher in .env-Dateien:
# Einfache Liste
CACHE_LAYERS=[memory,redis]
# Assoziatives Array
DB_OPTIONS=[charset=>utf8mb4,collation=>utf8mb4_unicode_ci]
# Gemischt mit Type-Casting
MIXED=[a=>1,2,b=>true,4.1,test=>[1,2,3]]Das letzte Beispiel ergibt:
[
'a' => 1, // int (gecastet)
0 => 2, // int
'b' => true, // bool (gecastet)
1 => 4.1, // float
'test' => [1, 2, 3] // verschachteltes Array
]Include-Direktiven
Ab einer gewissen Projektgröße wird eine einzelne .env-Datei unübersichtlich. Datenbankzugänge, Cache-Konfiguration, API-Keys, Feature-Flags, alles in einer Datei. DotEnv bietet load()-Direktiven, die Konfiguration in fachliche Module aufteilen:
APP_NAME=MyApp
APP_ENV=production
load(.env.database)
load(.env.logger)
load?(.env.optional)
APP_DEBUG=falseload() vs. load?()
| Direktive | Verhalten bei fehlender Datei |
|---|---|
load(.env.database) | Wirft EnvFileNotFoundException |
load?(.env.optional) | Wird stillschweigend übersprungen |
Include-Cascade
Jede load()-Direktive löst automatisch eine Cascade aus, analog zum Zwei-Stufen-Loading:
load(.env.database)Lädt (falls existent):
.env.database.env.database.local.env.database.{APP_ENV}, wennAPP_ENVbekannt.env.database.{APP_ENV}.local
APP_ENV muss vor load() definiert sein
Die Cascade nutzt APP_ENV aus der Variable-Registry. Definiere APP_ENV vor der ersten load()-Direktive, damit umgebungsspezifische Include-Varianten geladen werden.
Pfad-Auflösung
- Relative Pfade werden relativ zum Verzeichnis der includierenden Datei aufgelöst
- Absolute Pfade werden direkt verwendet
- Gequotete Pfade sind erlaubt:
load("config/my service.env")
Zirkuläre Includes
Zirkuläre Include-Ketten werden erkannt und werfen eine CircularEnvIncludeException:
Circular include detected: .env → .env.database → .env → (Abbruch)File-Secrets (_FILE-Pattern)
In containerisierten Umgebungen werden Secrets oft als Dateien gemounted, Docker Secrets, Kubernetes Volumes, HashiCorp Vault. DotEnv unterstützt dieses Pattern nativ:
DB_PASSWORD_FILE=secrets/db_password
REDIS_TOKEN_FILE=secrets/redis_tokenVerhalten:
DB_PASSWORD_FILEwird aufgelöst: Inhalt vonsecrets/db_passwordwird gelesen und getrimmt- Der Schlüssel im Ergebnis ist
DB_PASSWORD(ohne_FILE) DB_PASSWORD_FILEselbst erscheint nicht im Ergebnis- Der gelesene Wert durchläuft die gesamte Cast-Pipeline (inkl. Secret-Entschlüsselung)
Eigene Handler
Die Cast-Pipeline ist offen für projektspezifische Erweiterungen. Braucht das Projekt Enum-Mapping, Base64-Dekodierung oder eine eigene DSL? Ein Handler ist ein Invokable mit der Signatur __invoke(?string $value): mixed:
use JardisSupport\DotEnv\DotEnv;
// Handler als Klasse
final class CastStringToEnum
{
public function __invoke(?string $value = null): mixed
{
if ($value === null) {
return null;
}
return match ($value) {
'low' => Priority::Low,
'medium' => Priority::Medium,
'high' => Priority::High,
default => $value, // Nicht erkannt → String zurückgeben
};
}
}
$dotEnv = new DotEnv();
$dotEnv->addHandler(new CastStringToEnum());Handler-Position
// Am Ende der Pipeline anhängen (Standard)
$dotEnv->addHandler(new MyHandler());
// Am Anfang der Pipeline einfügen (vor allen anderen)
$dotEnv->addHandler(new MyHandler(), prepend: true);prepend: true ist wichtig für Handler die vor dem Type-Casting laufen müssen, z.B. Secret-Entschlüsselung.
Handler entfernen
$dotEnv->removeHandler(CastStringToBool::class);Integration mit Secret
Passwörter und API-Keys im Klartext in .env-Dateien (auch wenn sie nicht ins VCS gehören) sind ein Sicherheitsrisiko. Mit jardissupport/secret können Werte verschlüsselt in der .env stehen und werden erst zur Ladezeit entschlüsselt:
DB_PASSWORD=secret(aes:base64encodedEncryptedValue)
API_KEY=secret(sodium:base64encodedEncryptedValue)use JardisSupport\DotEnv\DotEnv;
use JardisSupport\Secret\Handler\SecretHandler;
use JardisSupport\Secret\Provider\FileKeyProvider;
$dotEnv = new DotEnv();
$dotEnv->addHandler(
new SecretHandler(new FileKeyProvider('path/to/secret.key')),
prepend: true // Muss VOR dem Type-Casting laufen
);
$dotEnv->loadPublic(__DIR__);Kombinierbar mit _FILE-Pattern: Die Secret-Datei kann einen secret(...)-String enthalten.
Fehlerbehandlung
| Exception | Ursache |
|---|---|
EnvFileNotFoundException | load() referenziert eine nicht existierende Datei |
EnvFileNotReadableException | Datei existiert, ist aber nicht lesbar |
CircularEnvIncludeException | Zirkuläre Include-Kette erkannt |
Alle Exceptions erben von DotEnvException.
use JardisSupport\DotEnv\Exception\EnvFileNotFoundException;
use JardisSupport\DotEnv\Exception\CircularEnvIncludeException;
try {
$dotEnv->loadPublic(__DIR__);
} catch (EnvFileNotFoundException $e) {
echo "Datei nicht gefunden: " . $e->getFilePath();
} catch (CircularEnvIncludeException $e) {
echo "Zirkulärer Include: " . implode(' → ', $e->getIncludeStack());
}Architektur
Unter der Haube folgt DotEnv dem Closure-Orchestrator-Pattern, dem architektonischen Grundprinzip aller Jardis-Packages. Jede Verarbeitungsstufe ist eine eigenständige, testbare Klasse mit __invoke() als einzigem Einstiegspunkt:
DotEnv ← Orchestrator (Public API)
├── LoadFilesFromPath ← Dateinamen auflösen
├── LoadValuesFromFiles ← Dateien lesen + verarbeiten
│ ├── ParseLoadDirective ← load()-Direktiven parsen
│ └── CastTypeHandler ← Cast-Pipeline orchestrieren
│ ├── VariableRegistry ← ${VAR}-Werte speichern
│ ├── CastStringToValue ← ${VAR}-Interpolation
│ ├── CastUserHome ← ~/Expansion
│ ├── CastStringToNumeric ← int/float
│ ├── CastStringToBool ← bool
│ ├── CastStringToJson ← JSON → array
│ └── CastStringToArray ← [key=>val] → arrayVerzeichnisstruktur
src/
├── DotEnv.php ← Orchestrator
├── Reader/
│ ├── LoadFilesFromPath.php ← Handler
│ ├── LoadValuesFromFiles.php ← Handler
│ └── ParseLoadDirective.php ← Handler
├── Handler/
│ ├── CastTypeHandler.php ← Pipeline-Orchestrator
│ ├── VariableRegistry.php ← Registry
│ ├── CastStringToValue.php ← Handler
│ ├── CastUserHome.php ← Handler
│ ├── CastStringToNumeric.php ← Handler
│ ├── CastStringToBool.php ← Handler
│ ├── CastStringToJson.php ← Handler
│ └── CastStringToArray.php ← Handler
└── Exception/
├── DotEnvException.php
├── CircularEnvIncludeException.php
├── EnvFileNotFoundException.php
└── EnvFileNotReadableException.phpAPI-Referenz
DotEnv
| Methode | Signatur | Beschreibung |
|---|---|---|
loadPublic | loadPublic(string $path): void | Lädt in $_ENV, $_SERVER, putenv() |
loadPrivate | loadPrivate(string $path): array<string, mixed> | Gibt isoliertes Array zurück |
addHandler | addHandler(object $handler, bool $prepend = false): void | Handler zur Cast-Pipeline hinzufügen |
removeHandler | removeHandler(string $handlerClass): void | Handler per FQCN entfernen |
Interface
DotEnv implementiert JardisSupport\Contract\DotEnv\DotEnvInterface mit loadPublic() und loadPrivate(). Die Methoden addHandler() und removeHandler() sind Erweiterungen der konkreten Klasse.
Vollständiges Beispiel
Alles zusammen, modulare Konfiguration mit Includes, Variablen-Interpolation, File-Secrets und Type-Casting:
# .env
APP_NAME=MyApp
APP_ENV=production
load(.env.database)
load(.env.cache)
load?(.env.local.overrides)
LOG_PATH=~/logs/${APP_NAME}
APP_DEBUG=false# .env.database
DB_HOST=db.example.com
DB_PORT=3306
DB_NAME=myapp
DB_PASSWORD_FILE=secrets/db_password
DATABASE_URL=mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}# .env.cache
CACHE_LAYERS=[memory,redis]
REDIS_HOST=redis.example.com
REDIS_PORT=6379use JardisSupport\DotEnv\DotEnv;
$dotEnv = new DotEnv();
$config = $dotEnv->loadPrivate(__DIR__);
// $config = [
// 'APP_NAME' => 'MyApp',
// 'APP_ENV' => 'production',
// 'DB_HOST' => 'db.example.com',
// 'DB_PORT' => 3306,
// 'DB_NAME' => 'myapp',
// 'DB_PASSWORD' => 's3cret!Pass',
// 'DATABASE_URL' => 'mysql://db.example.com:3306/myapp',
// 'CACHE_LAYERS' => ['memory', 'redis'],
// 'REDIS_HOST' => 'redis.example.com',
// 'REDIS_PORT' => 6379,
// 'LOG_PATH' => '/home/deploy/logs/MyApp',
// 'APP_DEBUG' => false,
// ]