eurym3d0n / elephenv
A strictly typed PHP environment loader with validation, type inference, integrity checks, and structured error rendering.
Requires
- php: >=8.4.27 <8.5 || >=8.5.12 <8.6
Requires (Dev)
- phpstan/phpstan: ^2.3
- phpunit/phpunit: ^13.4
- squizlabs/php_codesniffer: ^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-09 06:46:38 UTC
README
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
- Installation
- Quick start
- Environment file syntax
- Loading sources
- Type inference and casting
- Variable interpolation
- Array notation
- Validation
- Fluent value API
- Integrity checking
- Security controls
- Error rendering
- Repository API
- Extending Elephenv
- Helper functions
- Development
- Integration guide
- Public API contracts
- License
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():
| Option | Type | Default | Description |
|---|---|---|---|
cast | bool | true | Infer native scalar types from string values. |
interpolate | array<string, mixed> | [] | Additional placeholder values; these take precedence over the repository. |
interpolateCallback | callable\|null | null | Transform each resolved placeholder before substitution. |
valueCallback | callable\|null | null | Transform each fully resolved value before it is stored. Receives the variable name and value. |
rules | array<string, RuleSet> | [] | Validation rules indexed by variable name. |
skipMissing | bool | true | Skip missing paths in loadMany(); ignored by other loading methods. |
strictPermissions | bool | false | Throw 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 value | Result |
|---|---|
true, yes, on, 1 | true |
false, no, off, 0 | false |
null, nil, none | null |
empty | '' |
42, -7 | Integer, when within the platform integer range |
3.14, 1.5e3 | Finite float |
01234, overflowing integers, other values | String |
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 byElephenv::get()has already taken place.required(),notEmptyString(),match(),validate(),applyRules()— validate the value.defaults($value)— use a fallback when the current value isnullor an empty string.transform($callback)— transform the current value.toString(),toBool(),toArray(),toInt(),toFloat()— convert the current value. Unsupported compound values become''intoString(); unrecognised boolean values becomefalse; non-array values are wrapped in a one-element array (includingnull); invalid integer and float conversions return0and0.0respectively. 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
.envfiles 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:
- Elephenv's internal repository.
$_ENV.$_SERVER.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():
| Contract | Default implementation | Responsibility |
|---|---|---|
LoaderInterface | EnvLoader | Reads and processes environment sources. |
RepositoryInterface | EnvironmentRepository | Stores and synchronizes values without interpreting them. |
RuntimeStateAwareRepositoryInterface | EnvironmentRepository | Snapshots external state for failed staged commits. |
CasterInterface | Inferrer | Detects and casts scalar types. |
IntegrityCheckerInterface | IntegrityChecker | Compares loaded variables with a reference file. |
ArrayFlattenerInterface | ArrayFlattener | Converts bracket notation into nested arrays. |
ParserInterface | LineParser | Parses individual environment-file lines. |
InterpolatorInterface | Interpolator | Resolves placeholders. |
ValidatorInterface | Validation rule classes | Validates a variable value. |
ErrorRendererInterface | Optional ErrorRenderer | Renders 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.