Flat key/value configuration management with layered and pipeline resolution.
Config sets are flat — every key is a string and every value is mixed. Nested
data structures are a matter of convention (e.g. "db.host") or of JSON
flattening at load time. Sources are composable: combine them in a LayeredConfig
for priority-based resolution, or in a PipelineConfig for per-key resolver
chains that can transform, short-circuit, or delegate to underlying sources.
composer require ordinary/configAll sources implement the same contract:
interface Config
{
publicfunctionhas(string$key): bool;
publicfunctionget(string$key, mixed$default = null): mixed;
}A key that exists with a null value satisfies has(). A missing key does not.
LayeredConfig accepts any number of Config sources. Sources are checked in
registration order; the first source that owns the key wins. Register
high-priority sources (overrides, environment) before low-priority ones
(defaults, files). No write-back occurs — sources remain independent.
useOrdinary\Config\LayeredConfig;
useOrdinary\Config\RuntimeConfig;
useOrdinary\Config\JsonFileConfig;
useOrdinary\Config\CachingConfig;
$config = newLayeredConfig(
newRuntimeConfig(['debug' => true]), // highest prioritynewCachingConfig(newJsonFileConfig('/etc/app/config.json')), // cached filenewRuntimeConfig(['debug' => false, 'timeout' => 30]), // defaults
);
$config->get('debug'); // true — from first source$config->get('timeout'); // 30 — falls through to defaultsPipelineConfig wraps any number of Config sources and allows you to register
per-key resolver chains via define(). Each handler in the chain receives the key
and a $next callable. Calling $next($key) passes control to the next handler;
when the chain is exhausted $next consults the sources in registration order.
Returning without calling $next short-circuits further resolution.
Keys with no registered handlers fall through to sources directly, identical to
LayeredConfig behaviour.
useOrdinary\Config\PipelineConfig;
useOrdinary\Config\RuntimeConfig;
$source = newRuntimeConfig(['db.host' => 'localhost']);
$config = newPipelineConfig($source);
// Short-circuit — ignore all sources$config->define('app.env', fn(string$k, \Closure$next) => 'production');
// Transform — upper-case whatever the source holds$config->define('db.host', function (string$k, \Closure$next): mixed {
$val = $next($k);
return\is_string($val) ? \strtoupper($val) : $val;
});
$config->get('app.env'); // 'production' — source never consulted$config->get('db.host'); // 'LOCALHOST' — source value transformed$config->get('db.port'); // null — falls through to source, missingBecause handlers close over arbitrary state, a handler can act as an in-process cache for its key — no extra class required:
$config = newPipelineConfig($expensiveSource);
$cache = [];
$config->define(
'db.dsn',
staticfunction (string$k, \Closure$next) use (&$cache): mixed {
return$cache[$k] ??= $next($k);
},
);
$config->get('db.dsn'); // resolved from source, stored in $cache$config->get('db.dsn'); // served from $cache — source not consulted againString a chain of handlers to model layered caches in front of a source of truth:
useOrdinary\Config\PipelineConfig;
useOrdinary\Config\RuntimeConfig;
$runtime = []; // hot in-process cache$redis = []; // simulated Redis cache (use your real adapter here)$db = newRuntimeConfig(['api.key' => 'secret']); // source of truth$config = newPipelineConfig($db);
$config->define(
'api.key',
// Layer 1 — runtimestaticfunction (string$k, \Closure$next) use (&$runtime): mixed {
return$runtime[$k] ??= $next($k);
},
// Layer 2 — Redisstaticfunction (string$k, \Closure$next) use (&$redis): mixed {
return$redis[$k] ??= $next($k);
},
);
$config->get('api.key'); // misses runtime → misses Redis → hits DB; warms both caches$config->get('api.key'); // served from runtime; Redis and DB not toucheddefine() accepts multiple handlers in a single call (run left-to-right) and
returns static for fluent chaining. Calling define() for the same key a
second time replaces the previous chain entirely.
$config = (newPipelineConfig($source))
->define('a', fn(string$k, \Closure$next) => 'override-a')
->define('b', fn(string$k, \Closure$next) => $next($k));Mutable in-memory store. Useful for overrides, environment injection, and test
fixtures. Implements StackLayer, so it participates in StackConfig write-back
automatically.
$config = newRuntimeConfig(['key' => 'value']);
$config->set('another', 42);
$config->remove('key');
$config->has('another'); // true$config->get('another'); // 42Config from a JSON string. By default only root-level keys are visible. Pass
flatten: true to recursively expand nested objects into dot-separated keys.
Indexed JSON arrays are never flattened — they remain as their decoded value.
useOrdinary\Config\JsonConfig;
// Root-level only (default)$config = newJsonConfig('{"host": "localhost", "port": 3306}');
$config->get('host'); // "localhost"// Flatten nested objects$config = newJsonConfig(
'{"db": {"host": "localhost", "port": 3306}, "tags": ["php"]}',
flatten: true,
);
$config->get('db.host'); // "localhost"$config->get('db.port'); // 3306$config->get('tags'); // ["php"] — indexed arrays are not flattened// Custom separator$config = newJsonConfig('{"db": {"host": "localhost"}}', flatten: true, separator: '/');
$config->get('db/host'); // "localhost"Reads a JSON file at construction time and delegates to JsonConfig. Accepts
the same flatten and separator options.
useOrdinary\Config\JsonFileConfig;
$config = newJsonFileConfig('/path/to/config.json');
$config = newJsonFileConfig('/path/to/config.json', flatten: true);
$config = newJsonFileConfig('/path/to/config.json', flatten: true, separator: '/');Decorator that wraps any Config source with an in-memory read-through cache.
Each key is resolved from the inner source at most once — both resolved values
(including null) and confirmed absences are cached.
useOrdinary\Config\CachingConfig;
useOrdinary\Config\JsonFileConfig;
$config = newCachingConfig(newJsonFileConfig('/path/to/config.json'));
// File is read only once; all subsequent accesses hit the cache.$config->get('key');
$config->get('key');useOrdinary\Config\CachingConfig;
useOrdinary\Config\JsonFileConfig;
useOrdinary\Config\LayeredConfig;
useOrdinary\Config\RuntimeConfig;
$config = newLayeredConfig(
// Runtime overrides — highest prioritynewRuntimeConfig([
'app.env' => $_SERVER['APP_ENV'] ?? 'production',
]),
// Cached file — read once, served many timesnewCachingConfig(newJsonFileConfig('/etc/app/config.json', flatten: true)),
// Compiled-in defaults — lowest prioritynewRuntimeConfig([
'app.env' => 'production',
'app.debug' => false,
'db.port' => 3306,
'cache.ttl' => 3600,
]),
);