A lightweight, data-only configuration loading library for PHP 8.3+.
ConfigLoader provides a deterministic and strict pipeline for loading application configuration from a single declarative format, applying layered overrides and environment interpolation, and returning a final normalized array.
No executable configuration. No framework coupling. No hidden magic.
- Overview
- Architecture
- Components
- Usage
- Development
Config is data, not code.
ConfigLoader enforces:
- Single format per project (no YAML + JSON chaos)
- Declarative configuration only (no PHP execution)
- Deterministic behavior (no surprises, no implicit merging tricks)
- Strict failure on invalid syntax or unresolved values
This keeps configuration:
- predictable
- portable
- inspectable
- safe to evolve
- YAML support (default)
- JSON support (explicit opt-in)
- Config root resolution
- Layered configuration merging
- Environment variable interpolation
- Strict error handling
- Array output only
ConfigLoader deliberately does not provide:
- Schema validation (handled by a separate future library)
- Executable config (PHP files are not supported)
- Application-specific logic
- Framework integration
- File system discovery beyond config root
composer require liquidrazor/config-loader$loader = new ConfigLoader(
new LoaderOptions(
configRoot: __DIR__ . '/config'
)
);
$config = $loader->load('services');By default, configuration is expected in:
<project-root>/config
You can override this:
new LoaderOptions(
configRoot: '/custom/path/to/config'
);- Preferred format
- Uses
ext-yamlif available - Falls back to internal parser otherwise
Supported extensions:
.yaml
.yml
Must be explicitly enabled:
new LoaderOptions(
configRoot: __DIR__ . '/config',
format: ConfigFormat::JSON
);Supported extension:
.json
JSON requires the PHP ext-json extension. If ext-json is unavailable, install it or switch the loader format to YAML.
A loader instance supports only one format.
Mixing formats is not allowed.
Config is loaded by logical name:
$loader->load('services');Resolves to:
config/services.yaml
(or .json depending on format)
ConfigLoader supports layered overrides.
Example:
$loader->loadLayered('services', ['prod', 'local']);Resolves and merges in order:
services.yaml
services.prod.yaml
services.local.yaml
Default merge rules:
- Associative arrays → recursive merge
- Scalar values → overridden by later layers
- Indexed arrays → fully replaced (not appended)
This ensures predictable behavior and avoids duplication issues.
Environment variables can be interpolated inside config values.
${VAR_NAME}
${VAR_NAME:-default}
database:
host: ${DB_HOST:-localhost}
port: ${DB_PORT}- Interpolation is applied after merging
- Missing variables without defaults throw an exception
- Only string values are interpolated
- No expression evaluation or casting is performed
ConfigLoader fails fast and loudly:
- Invalid syntax → exception
- Missing config file → exception
- Unsupported format → exception
- Missing env variable → exception
No silent fallbacks. No guessing.
The configuration loading process follows a strict pipeline:
resolve files → parse → merge → interpolate → return array
Each stage is isolated and deterministic.
Project structure follows:
include/ → contracts, enums, value objects, exceptions
lib/ → core logic (loader, parsers, interpolation, merge)
src/ → optional bootstrap/factory (minimal)
- ConfigLoader
- YamlParser
- JsonParser
- EnvInterpolator
- LayeredConfigMerger
- No hidden behavior
- No implicit format mixing
- No execution in config
- No framework dependencies
- Minimal, composable components
Planned but intentionally excluded from this library:
- Schema validation (separate library)
- DSN parsing
- Advanced config composition
- Environment profiles abstraction
ConfigLoader is a strict, minimal, and predictable configuration pipeline.
It does one job:
Load configuration as data, correctly.
Nothing more. Nothing less.