Skip to content

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-Castingtrue wird bool, 42 wird int, [a,b,c] wird array. Keine manuellen Casts mehr im Anwendungscode.
  • Variablen-InterpolationDATABASE_URL=mysql://${DB_HOST}/${DB_NAME} wird zur Ladezeit aufgelöst. Eine Wahrheitsquelle, keine Duplikate.
  • Modulare Includesload(.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 auf APP_ENV.
  • Sichere Secrets_FILE-Pattern für Docker Secrets und Vault-Mounts. Verschlüsselte Werte mit jardissupport/secret. Klartext-Passwörter in .env gehören der Vergangenheit an.
  • Erweiterbare Pipeline — eigene Cast-Handler registrieren, bestehende entfernen. Die Verarbeitung passt sich dem Projekt an, nicht umgekehrt.

Installation

bash
composer require jardissupport/dotenv

GitHub: jardisSupport/dotenv

Optionale Abhängigkeit für verschlüsselte Werte:

bash
composer require jardissupport/secret

Grundlegende Nutzung

Zwei Zeilen Code, und die gesamte Konfiguration steht typisiert bereit.

Öffentlich laden — in globalen State schreiben

php
use JardisSupport\DotEnv\DotEnv;

$dotEnv = new DotEnv();
$dotEnv->loadPublic(__DIR__);

Die Werte stehen danach in $_ENV, $_SERVER und via getenv() zur Verfügung:

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

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

DateiZweck
.envStandardwerte für alle Umgebungen
.env.localLokale Overrides (nicht ins VCS)

Stufe 2 — Umgebungsspezifisch (wenn APP_ENV gesetzt)

DateiZweck
.env.{APP_ENV}Umgebungsspezifische Werte (z.B. .env.staging)
.env.{APP_ENV}.localLokale 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-Overrides

Nicht 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

#HandlerEingabeAusgabe
1CastStringToValue${DB_HOST}aufgelöster String
2CastUserHome~/logs/home/user/logs
3CastStringToNumeric"3306"3306 (int)
4CastStringToBool"true"true (bool)
5CastStringToJson'{"a":1}'['a' => 1] (array)
6CastStringToArray[a=>1,b=>2]['a' => 1, 'b' => 2] (array)

Variablen-Interpolation

Bereits geladene Variablen können mit ${VAR} referenziert werden:

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

ini
LOG_PATH=~/logs
CACHE_DIR=~/cache/myapp

Funktioniert auf Unix ($HOME) und Windows (HOMEDRIVE + HOMEPATH). Ein in .env definiertes HOME=/custom/path wird respektiert.

Numerische Werte

ini
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

ini
DEBUG=true          # bool: true
CACHE_ENABLED=yes   # bool: true
VERBOSE=off         # bool: false

JSON-Werte

Werte die mit { oder [ beginnen und gültiges JSON sind:

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

ini
# 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:

php
[
    '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:

ini
APP_NAME=MyApp
APP_ENV=production

load(.env.database)
load(.env.logger)
load?(.env.optional)

APP_DEBUG=false

load() vs. load?()

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

ini
load(.env.database)

Lädt (falls existent):

  1. .env.database
  2. .env.database.local
  3. .env.database.{APP_ENV}, wenn APP_ENV bekannt
  4. .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:

ini
DB_PASSWORD_FILE=secrets/db_password
REDIS_TOKEN_FILE=secrets/redis_token

Verhalten:

  • DB_PASSWORD_FILE wird aufgelöst: Inhalt von secrets/db_password wird gelesen und getrimmt
  • Der Schlüssel im Ergebnis ist DB_PASSWORD (ohne _FILE)
  • DB_PASSWORD_FILE selbst 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:

php
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

php
// 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

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

ini
DB_PASSWORD=secret(aes:base64encodedEncryptedValue)
API_KEY=secret(sodium:base64encodedEncryptedValue)
php
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

ExceptionUrsache
EnvFileNotFoundExceptionload() referenziert eine nicht existierende Datei
EnvFileNotReadableExceptionDatei existiert, ist aber nicht lesbar
CircularEnvIncludeExceptionZirkuläre Include-Kette erkannt

Alle Exceptions erben von DotEnvException.

php
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] → array

Verzeichnisstruktur

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.php

API-Referenz

DotEnv

MethodeSignaturBeschreibung
loadPublicloadPublic(string $path): voidLädt in $_ENV, $_SERVER, putenv()
loadPrivateloadPrivate(string $path): array<string, mixed>Gibt isoliertes Array zurück
addHandleraddHandler(object $handler, bool $prepend = false): voidHandler zur Cast-Pipeline hinzufügen
removeHandlerremoveHandler(string $handlerClass): voidHandler 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:

ini
# .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
ini
# .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}
ini
# .env.cache
CACHE_LAYERS=[memory,redis]
REDIS_HOST=redis.example.com
REDIS_PORT=6379
php
use 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,
// ]