The Game Panel specification

RFC-0002: Configuration objects

Abstract

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.

Motivation

The panel needs a configuration system to configure the engine’s components, and to provide additional configuration for modules.

Proposal

Concepts

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.

Components

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.

Configuration objects

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

Catalogue

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

Injection

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,
) {}

Environment variables

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

Errors

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.

Out of scope

Alternatives considered

The decisions this design rests on are recorded separately, with the alternatives each one rejected:

No other alternatives were weighed.

Backwards compatibility

Nothing breaks. No configuration component exists before this change.

Open questions

Changelog

Sources