phpdot/env

Typed, schema-validated, immutable .env configuration for modern PHP.

Maintainers

Package info

github.com/phpdot/env

pkg:composer/phpdot/env

Statistics

Installs: 8

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.3 2026-04-28 09:16 UTC

This package is auto-updated.

Last update: 2026-04-28 09:16:55 UTC


README

Typed, schema-validated, immutable .env configuration for modern PHP.

Install

composer require phpdot/env

Zero dependencies. Pure PHP 8.3+.

Quick Start

Two access patterns are supported — pick whichever fits your bootstrap.

Instance-based

use PHPdot\Env\Env;

$env = Env::create(
    schema: __DIR__ . '/env.schema.php',
    paths: __DIR__ . '/.env',
);

$env->get('APP_PORT');    // int(8080)
$env->get('APP_DEBUG');   // bool(false)
$env->get('APP_ENV');     // AppEnv::PRODUCTION
$env->get('DB_HOST');     // string("localhost")
$env->get('ORIGINS');     // ['http://localhost', 'https://example.com']

Global facade (recommended for app bootstrap)

use PHPdot\Env\Env;

// Once, at the top of your bootstrap
Env::init(
    schema: __DIR__ . '/env.schema.php',
    paths: __DIR__ . '/.env',
);

// Anywhere in your app — pure array lookup
env('APP_PORT');                // int(8080)
env('APP_DEBUG', false);        // bool — default returned if key missing or no Env initialized
env('APP_ENV');                 // AppEnv::PRODUCTION

Env::init() is a thin wrapper over safeCreate() — missing .env files are silently skipped, schema defaults are used. The global env() helper reads from the singleton.

Every value is typed. Every key is validated. Every access is a pure array lookup.

Architecture

graph TD
    FILES[".env file(s)<br/><br/>One or more sources,<br/>loaded in order — later<br/>files override earlier ones"]

    subgraph Parser
        direction TB
        LEX["Lexer<br/><br/>Character-by-character tokenizer.<br/>Handles quotes, escapes, multiline,<br/>BOM, export prefix, comments"]
        RES["Resolver<br/><br/>Variable interpolation,<br/>cross-file references,<br/>circular reference detection"]
        LEX --> RES
    end

    subgraph Schema
        direction TB
        SCH["EnvSchema<br/><br/>Type casting + constraint validation:<br/>STRING, INT, FLOAT, BOOL,<br/>ENUM, LIST, JSON.<br/>required, min/max, allowed, pattern"]
    end

    ENV["Env<br/><br/>Immutable. readonly arrays.<br/>All values eagerly cast at boot.<br/>get / env helper = pure array lookup.<br/>Zero computation per request"]

    FILES --> Parser
    Parser --> Schema
    Schema --> ENV
Loading

Schema

The schema is the source of truth. Every env var must be declared.

// env.schema.php
use PHPdot\Env\Enum\EnvType;
use PHPdot\Env\Enum\AppEnv;

return [
    'APP_ENV' => [
        'enum'     => AppEnv::class,
        'required' => true,
        'default'  => AppEnv::DEVELOPMENT,
    ],
    'APP_DEBUG' => [
        'type'    => EnvType::BOOL,
        'default' => false,
    ],
    'APP_PORT' => [
        'type'    => EnvType::INT,
        'default' => 8080,
        'min'     => 1,
        'max'     => 65535,
    ],
    'APP_KEY' => [
        'type'      => EnvType::STRING,
        'required'  => true,
        'not_empty' => true,
        'sensitive' => true,
    ],
    'ALLOWED_ORIGINS' => [
        'type'    => EnvType::LIST,
        'default' => [],
    ],
    'FEATURE_CONFIG' => [
        'type'    => EnvType::JSON,
        'default' => [],
    ],
    'LOG_LEVEL' => [
        'default' => 'info',
        'allowed' => ['debug', 'info', 'warning', 'error'],
    ],
];

Type System

Type PHP return Example
STRING string APP_NAME=MyApp"MyApp"
INT int PORT=80808080
FLOAT float RATE=1.51.5
BOOL bool DEBUG=truetrue
ENUM BackedEnum ENV=productionAppEnv::PRODUCTION
LIST list<string> IPS=a,b,c["a","b","c"]
JSON mixed CFG={"a":1}["a" => 1]

Bool recognizes (case-insensitive): true/false, 1/0, yes/no, on/off.

Constraints

Constraint Applies to Example
required All Key must exist or have default
not_empty All '' after trim fails
min INT, FLOAT 'min' => 1
max INT, FLOAT 'max' => 65535
allowed STRING 'allowed' => ['debug', 'info']
pattern STRING 'pattern' => '/^https?:\/\//'
sensitive All Masked in allMasked()

Multi-File Loading

Files load in order. Later files override earlier ones.

$env = Env::create(
    schema: __DIR__ . '/env.schema.php',
    paths: [
        __DIR__ . '/.env',        // base
        __DIR__ . '/.env.local',  // overrides (gitignored)
    ],
);

Cross-file interpolation works:

# .env
BASE_URL=https://example.com

# .env.local
API_URL=${BASE_URL}/api    → https://example.com/api

.env Syntax

Values

SIMPLE=value
DOUBLE="value with spaces"
SINGLE='literal ${no-interpolation}'
EMPTY=

Escapes (double-quoted only)

NEWLINE="hello\nworld"
TAB="col1\tcol2"
BACKSLASH="back\\slash"
QUOTE="say\"hi\""
DOLLAR="cost\$5"

Comments

# Full line comment
KEY=value # inline comment
HASH=color#fff           # no space before # = part of value
QUOTED="value # kept"    # inside quotes = part of value

Multiline

RSA_KEY="-----BEGIN RSA KEY-----
MIIBogIBAAJBALRiMLAH
-----END RSA KEY-----"

Interpolation

BASE=/app
DATA=${BASE}/data        # /app/data
LOGS=$BASE/logs          # /app/logs
NESTED=${DATA}/cache     # /app/data/cache
LITERAL='${BASE}/raw'   # ${BASE}/raw (no interpolation)

Export Prefix

export FOO=bar           # FOO=bar (export stripped)

Safe Loading

For Docker/k8s where .env may not exist:

$env = Env::safeCreate(
    schema: __DIR__ . '/env.schema.php',
    paths: __DIR__ . '/.env',
);

Missing files are silently skipped. Schema defaults are used.

Testing

$env = Env::createForTesting(
    schema: [
        'DB_HOST' => ['required' => true],
        'DB_PORT' => ['type' => EnvType::INT, 'default' => 5432],
    ],
    values: ['DB_HOST' => 'localhost'],
);

$env->get('DB_HOST');  // 'localhost'
$env->get('DB_PORT');  // 5432

Instance API

Beyond get(), the Env instance exposes:

Method Returns Purpose
$env->get($key) mixed (typed) Throws SchemaException on unknown key
$env->has($key) bool True if explicitly set in a .env file (not just defaulted)
$env->all() array<string, mixed> All typed values, including defaults
$env->allMasked() array<string, mixed> Same, but sensitive keys replaced with ***
$env->getRaw($key) string|null Raw string before type cast
$env->getSchema() EnvSchema The compiled schema
$env->getLoadedFiles() list<string> Paths of .env files actually parsed
$env->compile($path) void Write a cache file for fast worker boot

The static facade exposes a parallel surface: Env::env($key, $default), Env::getInstance(), Env::resetInstance() (testing).

Sensitive Values

$env->get('API_KEY');     // "actual-secret-key"
$env->allMasked();        // ['API_KEY' => '***', 'DB_HOST' => 'localhost', ...]

allMasked() is safe for logging and error reports.

Config Caching

For production — skip parsing on every worker boot:

// Deploy script (run once)
$env = Env::create(schema: ..., paths: ...);
$env->compile(__DIR__ . '/cache/env.php');

// Application boot (every worker)
$env = Env::createFromCache(
    schema: __DIR__ . '/env.schema.php',
    cachePath: __DIR__ . '/cache/env.php',
);

Opcache caches the compiled file. Zero disk I/O, zero parsing per worker.

EnvEditor (CLI Only)

Write tool for setup wizards and deployment scripts.

use PHPdot\Env\EnvEditor;
use PHPdot\Env\Schema\EnvSchema;
use PHPdot\Env\Enum\AppEnv;

$editor = new EnvEditor(__DIR__ . '/.env', new EnvSchema($schema));

$editor->set('DB_HOST', 'new-host.example.com');
$editor->set('APP_ENV', AppEnv::STAGING);
$editor->remove('LOG_LEVEL');
$editor->save();

Preserves comments, blank lines, and key order.

Parsing a String

$values = Env::parseString("FOO=bar\nBAZ=\"\${FOO}/qux\"");
// ['FOO' => 'bar', 'BAZ' => 'bar/qux']

Swoole Safety

Env is immutable. readonly arrays. Zero mutation methods. Two safe patterns:

// Static facade — call Env::init() once per worker boot (recommended)
Env::init(
    schema: __DIR__ . '/env.schema.php',
    paths: __DIR__ . '/.env',
);
// Anywhere afterwards: env('APP_KEY') — shared by every coroutine, no per-request cost.

// Or register as a DI singleton if you prefer instance access
Env::class => singleton(fn() => Env::create(
    schema: __DIR__ . '/env.schema.php',
    paths: __DIR__ . '/.env',
)),

Package Structure

src/
├── Env.php                    Main read-only facade
├── EnvEditor.php              CLI-only write tool
├── Schema/
│   ├── EnvSchema.php          Type casting + validation
│   └── Definition.php         Variable definition value object
├── Parser/
│   ├── Parser.php             Orchestrator
│   ├── Lexer.php              Character-by-character tokenizer
│   ├── Entry.php              Parsed entry value object
│   └── Resolver.php           Variable interpolation
├── Enum/
│   ├── EnvType.php            STRING, INT, FLOAT, BOOL, ENUM, LIST, JSON
│   └── AppEnv.php             DEVELOPMENT, STAGING, PRODUCTION
└── Exception/
    ├── EnvException.php       Base exception
    ├── FileNotFoundException.php
    ├── EncodingException.php
    ├── ParseException.php
    ├── SchemaException.php
    ├── ValidationException.php
    └── WriteException.php

Development

composer test        # PHPUnit (147 tests)
composer analyse     # PHPStan level 10
composer cs-fix      # PHP-CS-Fixer
composer check       # All three

License

MIT