A configuration component in which each configuration is an instance of a class unique to it, registered against a
module and a name, held in an immutable catalogue and injected by its class. Environment variables are available
during bootstrap through a static Env class, read from the process environment or from an env file, with typed
accessors that cast their values.
The panel needs a configuration system to configure the engine’s components, and to provide additional configuration for modules.
| Term | Meaning |
|---|---|
| Configuration object | An instance of a class unique to one configuration, holding that configuration’s values. |
| Module | The owner a configuration is registered against. The engine’s own configuration is registered against a module like any other. |
| Configuration name | The name a configuration is registered under within its module. |
| Configuration catalogue | The immutable collection of every configuration object, looked up by module and name, or by class. |
| Environment variable | A value supplied through the process environment or an env file, read during bootstrap. |
| Component | Responsibility |
|---|---|
ConfigObject |
Contract every configuration object implements. |
ConfigCatalogue |
Immutable. Holds the configuration objects and looks them up by module and name, or by class. |
Env |
Static. Holds the environment variables during bootstrap and reads them, with typed accessors. |
ConfigException |
Marker contract implemented by every exception the component throws. |
Each configuration is an instance of a class unique to that configuration, per ADR-0005. Its values are typed properties, and it may contain child objects, but the configuration itself is contained within its class. Each configuration is registered against a module and a name, and its class maps to exactly one module and name.
A configuration object implements ConfigObject, which requires __set_state(). PHP calls __set_state() when an
object exported with var_export() is restored, so configuration objects can be written to a cache and read back.
final readonly class MailConfig implements ConfigObject
{
public function __construct(
public string $host,
public int $port,
) {}
public static function __set_state(array $data): static
{
return new static($data['host'], $data['port']);
}
}
ConfigCatalogue is immutable. It is constructed with every configuration object, keyed by module and then by name,
and builds a map from each object’s class to its module and name as it is constructed. Nothing collects
registrations: the registry that seals into it arrives with RFC-0004.
| Method | Effect |
|---|---|
get(string $module, string $config): ?ConfigObject |
Returns the configuration object registered under the module and name, or null. |
has(string $module, string $config): bool |
Returns whether a configuration object is registered under the module and name. |
for(string $class): ?ConfigObject |
Returns the configuration object of the given class, or null. |
$mail = $catalogue->get('engine', 'mail');
$mail = $catalogue->for(MailConfig::class);
Because each configuration object has a class of its own, a component receives the configuration it needs by type. The config component binds every configuration object into the container as a shared instance under its own class name, so a parameter typed as a configuration class is resolved to that object through an ordinary binding, as described in RFC-0001.
public function __construct(
private MailConfig $mail,
) {}
Env holds environment variables for the length of bootstrap, per
ADR-0006. It is a static singleton, initialised
once, from one of two sources:
| Method | Effect |
|---|---|
Env::createFromSuperglobal() |
Initialises Env with the values in the $_ENV superglobal. |
Env::createFromFile(string $path) |
Initialises Env with the values parsed from the .env file in the directory at the path. The file’s values are held by Env alone and are not written into $_ENV. |
Env::destroy() |
Removes the instance, and with it the values read from an env file. |
Initialising Env a second time throws, and so does reading from it before it has been initialised. Once bootstrap
is over, Env is destroyed and nothing reads environment variables directly.
Values are read with these accessors. Each typed accessor returns the default when the variable is missing or
null, and throws when the value cannot be cast to its type.
| Accessor | Behaviour |
|---|---|
get(string $key, $default = null) |
Returns the value, or the default when the variable is missing or null. |
has(string $key) |
Returns whether the variable is present, including when its value is null. |
string(string $key, ?string $default = null) |
Casts a string, number or boolean to a string. |
int(string $key, ?int $default = null) |
Casts an integer, numeric string or boolean to an integer. |
float(string $key, ?float $default = null) |
Casts a float, numeric string or boolean to a float. |
bool(string $key, ?bool $default = null) |
Casts a boolean or integer to a boolean. The strings true, 1 and yes are true, and false, 0 and no are false. Any other string throws. |
Env::createFromFile('/path/to/directory');
$port = Env::int('MAIL_PORT', 25);
$debug = Env::bool('APP_DEBUG', false);
Env::destroy();
Every exception implements ConfigException.
| Exception | Thrown when |
|---|---|
EnvInitialisationException |
Env is read before it has been initialised, or initialised a second time. |
InvalidEnvException |
A typed accessor’s value cannot be cast to its type. |
The decisions this design rests on are recorded separately, with the alternatives each one rejected:
No other alternatives were weighed.
Nothing breaks. No configuration component exists before this change.
$_ENV or a local env file, and injection because each
configuration is a unique class. This is created.decided. ConfigObject, ConfigCatalogue, Env and the exceptions are described from
src/Config at 180794a and
tests/Unit/Config at 180794a.Env accessor throwing when a value cannot be
cast, and Env existing only during bootstrap: first written down on 2026-09-14. The code at 180794a differs on
three of these: nothing binds configuration objects into the container, Env::string() returns the default
instead of throwing, and nothing confines Env to bootstrap. It also defines a
MissingEnvVariableException that is not part of the design.