Search by

eurym3d0n / elephenv

Eurym3d0n

A strictly typed PHP environment loader with validation, type inference, integrity checks, and structured error rendering.

v2.0.3 2026-11-06 16:53 UTC

This package is auto-updated.

Last update: 2026-11-09 06:46:38 UTC


README

Latest Version PHP Version License

Elephenv is a strictly typed PHP library for loading, parsing, validating, and accessing environment configuration. It provides type inference, variable interpolation, nested array notation, integrity checks, and configurable error rendering for HTTP and CLI applications.

Contents

Requirements

  • PHP 8.4.27 or later in the 8.4 series, or 8.5.12 or later in the 8.5 series.
  • Composer 2.

The runtime library does not require an additional PHP extension. Development tools have their own platform requirements. PHPUnit requires DOM, mbstring, and XMLWriter; PHP_CodeSniffer requires SimpleXML and XMLWriter in addition to the tokenizer and libxml extensions.

PHP 8.6 is not currently included in the supported version range.

Installation

composer require eurym3d0n/elephenv

Quick start

Load an environment file and retrieve typed values through the main facade:

<?php

declare(strict_types=1);

use Elephenv\Elephenv;

Elephenv::load(__DIR__ . '/.env');

$debug = Elephenv::get('APP_DEBUG'); // bool
$port = Elephenv::get('DB_PORT');   // int, except the literal "0" which maps to false
$name = Elephenv::get('APP_NAME');  // string

The Elephenv\Facade\Env facade exposes the common loading and repository operations if a shorter class name is preferred:

use Elephenv\Facade\Env;

Env::load(__DIR__ . '/.env');
$url = Env::get('DATABASE_URL', 'sqlite::memory:');

Environment file syntax

Assignments

APP_NAME=Acme
APP_ENV=production

Variable names must start with an ASCII letter or underscore. Subsequent characters may be ASCII letters, digits, or underscores. Bracket notation is also accepted for nested values, for example DATABASE[HOST].

A UTF-8 byte order mark at the beginning of a file is ignored. Blank lines and lines whose first non-whitespace character is # are ignored.

Quoted values

Double-quoted values remove their surrounding quotes and expand these escape sequences: \n, \t, \r, \\, and \". An escaped dollar sign (\$) remains literal and is not treated as an interpolation placeholder.

Single-quoted values are literal: their content is not unescaped or interpolated.

GREETING="Hello\nWorld"
REGEX='^\d+$'                 # A PCRE pattern matching one or more digits
LITERAL='${NOT_EXPANDED}'
PRICE="\$19.99"

Empty values and comments

An assignment with no value is resolved to null. Use the empty sentinel for an explicit empty string. Sentinel keywords are case-insensitive.

UNSET_VALUE=
EMPTY_VALUE=empty

An inline comment starts at an unquoted # only when it is the first character of the value or follows a space or tab. A # embedded in a token is preserved.

APP_NAME=Acme                 # Comment
APP_URL=https://example.test/#section
PASSWORD=abc#def

The optional export prefix is accepted and removed before parsing:

export APP_ENV=production

Loading sources

Load one file

$values = Elephenv::load(__DIR__ . '/.env');

A missing file raises FileNotFoundException. An existing but unreadable file raises UnreadableFileException. File-size and permission checks are applied before the content is parsed.

Load an optional file

$values = Elephenv::loadIfExists(__DIR__ . '/.env.local');

If the path is not a regular file, this method returns an empty array.

Load multiple files

$values = Elephenv::loadMany([
    __DIR__ . '/.env',
    __DIR__ . '/.env.local',
]);

Files are processed in the supplied order, and later files can reference values from earlier files. Later files replace earlier values for duplicate top-level keys. File contents are staged as one batch: a read, parsing, or validation failure before commit leaves the repository unchanged. With the default EnvironmentRepository, a failed commit also restores the previous $_ENV, $_SERVER, and process-environment values for every affected key. Custom repositories can provide the optional RuntimeStateAwareRepositoryInterface capability for the same external-state rollback guarantee; otherwise rollback covers their internal repository state only. Missing files are skipped by default; set skipMissing to false to make a missing path an error:

Elephenv::loadMany(['.env', '.env.local'], [
    'skipMissing' => false,
]);

Load from a string

$values = Elephenv::loadString("APP_ENV=testing\nDB_PORT=5432");

This is useful for tests and for configuration content obtained from another trusted source. loadString() does not apply filesystem permission or file-size checks because it does not read a file.

Loader options

Options are passed as the second argument to load(), loadIfExists(), loadMany(), or loadString():

OptionTypeDefaultDescription
castbooltrueInfer native scalar types from string values.
interpolatearray<string, mixed>[]Additional placeholder values; these take precedence over the repository.
interpolateCallbackcallable\|nullnullTransform each resolved placeholder before substitution.
valueCallbackcallable\|nullnullTransform each fully resolved value before it is stored. Receives the variable name and value.
rulesarray<string, RuleSet>[]Validation rules indexed by variable name.
skipMissingbooltrueSkip missing paths in loadMany(); ignored by other loading methods.
strictPermissionsboolfalseThrow SecurityException instead of emitting a warning for group/world-readable files.

The default maximum file size is 1 MiB. It can be changed by constructing an EnvLoader with a different maxBytes value and injecting it with Elephenv::swap().

Type inference and casting

Casting is enabled by default. Matching for keywords is case-insensitive, but input is not trimmed by the caster; whitespace inside quoted values is significant.

Input valueResult
true, yes, on, 1true
false, no, off, 0false
null, nil, nonenull
empty''
42, -7Integer, when within the platform integer range
3.14, 1.5e3Finite float
01234, overflowing integers, other valuesString

Integer inference accepts canonical decimal notation only. Leading-zero identifiers and integers outside the platform range remain strings to avoid silently changing their value. Float inference requires a fractional part and rejects non-finite results.

Boolean inference takes precedence over integer inference, so the literal 0 becomes false, not integer 0. Disable casting when the original string representation must be preserved:

Elephenv::load('.env', ['cast' => false]);

The caster is replaceable through CasterInterface:

use Elephenv\Contracts\CasterInterface;
use Elephenv\Elephenv;
use Elephenv\Enum\CastType;

final readonly class StringOnlyCaster implements CasterInterface
{
    public function detect(string $raw): CastType
    {
        return CastType::String;
    }

    public function cast(mixed $value): mixed
    {
        return $value;
    }
}

Elephenv::swap(caster: new StringOnlyCaster());

Variable interpolation

Both ${NAME} and $NAME placeholders are supported. During file loading, values can reference variables already available in the repository, including values declared earlier in the same file. A missing repository reference is resolved to an empty string by the loader's interpolation stage.

APP_SCHEME=https
APP_HOST=example.test
APP_URL=${APP_SCHEME}://${APP_HOST}

Custom values supplied through the interpolate option take precedence over repository values. Custom values can themselves contain placeholders and are resolved recursively, with cycle detection and a default maximum depth of ten. The configurable depth is bounded to the range 1–100 to prevent unbounded recursive expansion. Boolean values are interpolated as the strings true and false, matching their process-environment representation.

Elephenv::loadString('APP_URL=${SCHEME}://${HOST}', [
    'interpolate' => [
        'SCHEME' => 'https',
        'HOST' => 'example.test',
    ],
]);

An interpolateCallback can transform each resolved placeholder before it is inserted:

Elephenv::loadString('APP_NAME=${NAME}', [
    'interpolate' => ['NAME' => 'acme'],
    'interpolateCallback' => static fn(string $value): string => strtoupper($value),
]);

Single-quoted values bypass interpolation. A dollar sign preceded by a backslash is treated literally.

Array notation

Bracket-notation keys are inflated into nested arrays after parsing:

DATABASE[HOST]=localhost
DATABASE[PORT]=5432
DATABASE[NAME]=application
$values = Elephenv::load('.env');

$values['DATABASE']['HOST']; // 'localhost'
$values['DATABASE']['PORT']; // 5432
Elephenv::get('DATABASE');   // The nested array
Elephenv::has('DATABASE[HOST]'); // false after inflation

When a scalar key and a bracket-notation key conflict, the last processed entry wins. The array inflation strategy can be replaced through ArrayFlattenerInterface. Every bracket segment must be explicit and non-empty; append notation such as ITEMS[] is rejected so that array indexes remain deterministic and can be checked by the integrity validator.

Validation

Validation is composed with RuleSet and passed through the rules loader option. All violations from the loading pass are collected before a ValidationException is thrown.

Built-in rules

use Elephenv\Elephenv;
use Elephenv\Validation\RuleSet;

$urlRules = RuleSet::make()
    ->isRequired()
    ->notEmptyString()
    ->match('/^https?:\/\//');

Elephenv::load('.env', [
    'rules' => ['APP_URL' => $urlRules],
]);

allowEmpty() is a modifier for a RuleSet. When present, it causes notEmptyString() to be skipped, regardless of the order in which those two rules were added:

$rules = RuleSet::make()
    ->isRequired()
    ->notEmptyString()
    ->allowEmpty();

Callback validation

A callback receives the variable name and resolved value. Return true when the value is valid, or a non-empty error message when validation fails. Avoid including raw values in custom messages because environment values may contain credentials or other secrets.

$emailRules = RuleSet::make()->callback(
    static fn(string $name, mixed $value): bool|string =>
        filter_var($value, FILTER_VALIDATE_EMAIL) !== false
            ? true
            : sprintf('%s must be a valid email address.', $name),
);

Custom validator

Implement ValidatorInterface to add a reusable rule:

use Elephenv\Contracts\ValidatorInterface;
use Elephenv\Exception\ValidationException;

final class PortRangeRule implements ValidatorInterface
{
    public function validate(string $name, mixed $value): void
    {
        if (is_int($value) && $value >= 1 && $value <= 65535) {
            return;
        }

        throw new ValidationException([[
            'variable' => $name,
            'rule' => 'port_range',
            'message' => sprintf('%s must be an integer between 1 and 65535.', $name),
        ]]);
    }
}

$rules = RuleSet::make()->isRequired()->add(new PortRangeRule());

Rule sets can be composed with merge():

$required = RuleSet::make()->isRequired()->notEmptyString();
$urlRules = RuleSet::make()->merge($required)->match('/^https?:\/\//');

Handling validation failures

use Elephenv\Exception\ValidationException;

try {
    Elephenv::load('.env', ['rules' => ['APP_URL' => $urlRules]]);
} catch (ValidationException $exception) {
    foreach ($exception->violations() as $violation) {
        printf("%s: %s\n", $violation['variable'], $violation['message']);
    }
}

Do not expose validation messages directly to untrusted clients without reviewing their content. Custom validators control their own messages, and those messages must not contain secrets.

Fluent value API

Elephenv::value() returns an EnvValue wrapper. It supports validation, default values, transformations, explicit casts, type inspection, and assignment helpers.

$dsn = Elephenv::value('DATABASE_URL')
    ->required()
    ->notEmptyString()
    ->match('/^postgres:\/\//')
    ->toString();

Available operations include:

  • raw() — return the value currently held by the wrapper. Automatic casting performed by Elephenv::get() has already taken place.
  • required(), notEmptyString(), match(), validate(), applyRules() — validate the value.
  • defaults($value) — use a fallback when the current value is null or an empty string.
  • transform($callback) — transform the current value.
  • toString(), toBool(), toArray(), toInt(), toFloat() — convert the current value. Unsupported compound values become '' in toString(); unrecognised boolean values become false; non-array values are wrapped in a one-element array (including null); invalid integer and float conversions return 0 and 0.0 respectively. These methods are convenience conversions, not strict validators.
  • isString(), isBool(), isArray(), isInt(), isFloat(), isNull() — inspect the current type.
  • assign($name, $value), assignMany($values), context() — write related values and inspect assignments made by the chain. The context is updated only after a successful repository write. assignMany() stages the complete batch and preserves the previous repository state and context if a write fails.

Example:

$host = Elephenv::value('APP_URL')
    ->required()
    ->validate(static fn(string $name, mixed $value): bool|string =>
        is_string($value) ? true : sprintf('%s must be a string.', $name)
    )
    ->transform(static fn(string $url): string => (string) parse_url($url, PHP_URL_HOST))
    ->toString();

Integrity checking

The integrity checker compares the variable names declared in a reference file (such as .env.example) with the active repository. It raises IntegrityException when required names are missing.

# .env.example
APP_NAME=
APP_ENV=
DATABASE_URL=
MAIL_HOST=
Elephenv::load('.env');
Elephenv::checkIntegrity(__DIR__ . '/.env.example');

To retrieve the required names without running the comparison:

use Elephenv\Integrity\IntegrityChecker;

$requiredNames = (new IntegrityChecker())->listRequired(__DIR__ . '/.env.example');

Security controls

File size

The default maximum file size is 1 MiB (1,048,576 bytes). Files larger than the configured limit are rejected with SecurityException. Reads are bounded on the opened file descriptor, so a file growing after the initial metadata check cannot trigger an unbounded allocation. The IntegrityChecker also applies the 1 MiB default limit to reference files. The loader limit can be changed through the EnvLoader constructor:

use Elephenv\Elephenv;
use Elephenv\Loader\EnvLoader;
use Elephenv\Repository\EnvironmentRepository;

$repository = new EnvironmentRepository();
$loader = new EnvLoader($repository, maxBytes: 512 * 1024);
Elephenv::swap(loader: $loader, repository: $repository);

File permissions

On non-Windows systems, permissions are inspected on the opened file. Files readable by the group or by other users trigger a PHP warning by default. Pass strictPermissions => true to raise SecurityException instead:

Elephenv::load('.env', ['strictPermissions' => true]);

Operational guidance

  • Keep real .env files out of version control; commit only sanitized examples.
  • Restrict filesystem permissions on files containing credentials.
  • Do not enable debug error rendering in production.
  • Do not serialize raw exception context or environment values into public error responses.
  • Prefer a secret manager or the deployment platform's secret store for production credentials.

Error rendering

Elephenv propagates exceptions to the calling application by default. It does not render an error page, write to STDERR, or terminate the process unless terminal error rendering is explicitly enabled. This keeps framework and application exception handlers in control of failures.

For a standalone application that deliberately wants Elephenv to render an HTTP error page or CLI message and terminate, enable the bundled renderer before loading configuration:

use Elephenv\Elephenv;

Elephenv::enableDefaultErrorRendering(debug: false);
Elephenv::load(__DIR__ . '/.env');

To restore normal exception propagation:

Elephenv::disableErrorRendering();

A custom terminal renderer can be registered with setErrorRenderer():

use Elephenv\Elephenv;
use Elephenv\Renderer\ErrorRendererFactory;

$renderer = ErrorRendererFactory::make(
    viewsPath: __DIR__ . '/resources/views',
    debug: false,
    version: Elephenv::VERSION,
);

Elephenv::setErrorRenderer($renderer);

A registered renderer implements ErrorRendererInterface::render(): never: it must render the failure and terminate execution. Framework integrations such as Laravel and Symfony should normally leave terminal rendering disabled and allow the original exception to reach the framework's exception handler.

In non-debug mode, the bundled renderer replaces exception messages with stable, public-safe summaries. Structured context is sanitized: sensitive assignments and credential-like context keys are masked, while paths, raw values, and arbitrary context messages are omitted. Custom validation messages are replaced with a generic failure description. Stack traces and throw-site details are not included. Debug mode exposes unredacted messages, context, and stack information; it is intended for local development only and must remain disabled in production.

MatchRule compares scalar values using their string representation, with booleans normalized to true or false; non-scalar values fail validation. It never places the rejected value or configured pattern in its validation message. Redaction cannot reliably identify every secret embedded in arbitrary free text, so custom callbacks must never put raw values into error messages. The bundled error page is self-contained, loads no remote JavaScript or fonts, and sends a restrictive Content Security Policy.

Repository API

The default EnvironmentRepository stores values exactly as supplied in its own in-memory map and synchronizes managed values with $_ENV, $_SERVER, and the process environment. It never performs implicit interpolation; this is handled by the loader, which can distinguish quoted and escaped values. Lookup order is:

  1. Elephenv's internal repository.
  2. $_ENV.
  3. $_SERVER.
  4. getenv().
$repository = Elephenv::repository();

$repository->set('APP_NAME', 'Acme');
$repository->get('APP_NAME');
$repository->has('APP_NAME');
$repository->forget('APP_NAME');
$repository->all();
$repository->clear();

Elephenv::reset() clears values owned by the active repository before releasing the singleton and its error renderer. External values that were never managed by that repository remain untouched.

all() returns values explicitly stored in the current repository instance. clear() removes values managed by that instance; it does not clear unrelated operating-system environment variables. Similarly, forget() only removes a variable when it is owned by the repository; values discovered solely through $_ENV, $_SERVER, or getenv() are left untouched.

Environment variables are strings at the process level. Complex values such as arrays are stored in the repository and mirrored to PHP superglobals, but are not passed to putenv() by default. Elephenv::setComplexExportMode('json') can enable JSON encoding for complex values exported to the process environment; the default mode is skip.

The PHP runtime environment is process-global, even when multiple repository instances exist. If two instances write the same key, the latest successful write owns the synchronized runtime value. Clearing an older instance will not remove a value most recently written by another instance. This ownership rule prevents stale repository instances from deleting newer runtime configuration; it does not provide per-instance isolation of $_ENV, $_SERVER, or getenv(). Use separate PHP processes when true environment isolation is required.

Extending Elephenv

The main services are defined by contracts under Elephenv\Contracts and can be replaced where supported by Elephenv::swap():

ContractDefault implementationResponsibility
LoaderInterfaceEnvLoaderReads and processes environment sources.
RepositoryInterfaceEnvironmentRepositoryStores and synchronizes values without interpreting them.
RuntimeStateAwareRepositoryInterfaceEnvironmentRepositorySnapshots external state for failed staged commits.
CasterInterfaceInferrerDetects and casts scalar types.
IntegrityCheckerInterfaceIntegrityCheckerCompares loaded variables with a reference file.
ArrayFlattenerInterfaceArrayFlattenerConverts bracket notation into nested arrays.
ParserInterfaceLineParserParses individual environment-file lines.
InterpolatorInterfaceInterpolatorResolves placeholders.
ValidatorInterfaceValidation rule classesValidates a variable value.
ErrorRendererInterfaceOptional ErrorRendererRenders an exception and terminates execution only when explicitly enabled.

Replace one or more services without rebuilding the entire runtime manually. When a custom loader is supplied, its repository() instance becomes the active repository unless the same instance is explicitly supplied. Passing a different repository alongside that loader is rejected to prevent split runtime state:

Elephenv::swap(
    repository: $repository,
    caster: new StringOnlyCaster(),
);

Helper functions

Composer loads the helper file automatically. The functions are declared in the Elephenv namespace; import them explicitly when using them from another namespace:

use function Elephenv\env;
use function Elephenv\env_all;
use function Elephenv\env_forget;
use function Elephenv\env_has;
use function Elephenv\env_set;
use function Elephenv\env_value;

$port = env('DB_PORT');
$debug = env('APP_DEBUG', false);

if (env_has('STRIPE_SECRET')) {
    // Configure the integration.
}

$dsn = env_value('DATABASE_URL')->required()->toString();
env_set('MAINTENANCE_MODE', true);
env_forget('MAINTENANCE_MODE');
$all = env_all();

Available helpers are env(), env_has(), env_value(), env_set(), env_forget(), and env_all(). Their declarations are guarded against pre-existing global functions with the same names; if a framework already provides one of these helpers, use the Elephenv facade instead. env_set() uses the same runtime synchronization behavior as Elephenv::set(): it updates the current process environment through the configured PHP runtime targets, but does not modify persistent operating-system or deployment configuration. Bootstrap and infrastructure operations remain on the Elephenv facade.

Development

The GitLab CI pipeline (.gitlab-ci.yml) is the canonical quality workflow for this repository. It runs on PHP 8.4.27 and 8.5.12, installs the current compatible development dependencies, and executes strict Composer validation, coding-standard, static-analysis, PHPUnit, and exported-package consumer checks on both supported PHP baselines. The GitHub Actions workflow provides the same checks when the repository is mirrored to GitHub.

Install development dependencies and run the quality checks:

composer install
composer cs
composer analyse
composer test

Apply automatic coding-standard fixes with:

composer cs:fix

Run the complete quality gate with:

composer check

Verify the actual Composer distribution archive (requires the PHP zip extension and unzip):

composer package:check

This creates a temporary Composer archive, rejects missing runtime files, development-only content, local environment files, private-key material, duplicate entries, and unsafe paths. It then installs the exported archive into an isolated Composer consumer project and executes a public-API smoke test. The same check runs in both CI workflows. Run the complete release gate with:

composer check:release

The project coding standard follows PSR-12 conventions with exactly two spaces per indentation level. PHPUnit configuration is maintained in phpunit.xml. Run composer test:coverage to generate the text summary in build/coverage.txt and the HTML report in build/coverage/; this optional command requires a coverage driver such as Xdebug or PCOV. PHPStan analyses both src/ and tests/ at its maximum rule level.

Contributions should include focused regression tests for behavior changes and must pass the configured coding standard, static analysis, and test suite.

Application integration example

See INTEGRATION.md and the framework-neutral B2B example for process-wide configuration, tenant-scoped settings, and worker boundaries.

License

Elephenv is open-source software released under the MIT License. See LICENSE for the full license text.