Skip to content

Latest commit

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

ordinary/config

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.

Installation

composer require ordinary/config

Core concepts

The Config interface

All 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.

Layered resolution

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 defaults

Pipeline resolution with per-key handlers

PipelineConfig 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, missing

Memoising cache layer

Because 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 again

Multi-level cache chain

String 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 touched

Chain ordering and fluent API

define() 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));

Sources

RuntimeConfig

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'); // 42

JsonConfig

Config 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"

JsonFileConfig

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: '/');

CachingConfig

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');

Composition example

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,
]),
);

About

Flat key/value configuration management with layered resolution for OrdinaryPHP.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages