componenta/config composes application configuration for PHP 8.4+, keeps
environment access explicit, and separates runtime settings from the raw
dependency definitions consumed by Componenta DI.
The package has one runtime path. Providers are executed for every application start in every environment; Config does not generate or load a persistent cache. This keeps development and production behavior equivalent.
Config owns:
- ordered provider execution and deterministic structural merging;
- an immutable application
Configwith oneEnvironment; - raw
DependencyDefinitionsas a separate composition result; - runtime descriptors, dotenv loading, and PHP/JSON file readers.
Config does not validate whether DI can build a service, normalize definitions, construct a container, write cache files, or choose application entry points. Those responsibilities belong to the DI and application packages.
composer require componenta/configRequirements:
- PHP 8.4 or newer;
componenta/arrayablefor the shared array conversion contract;psr/containerforContainerValueand the container helper.
A configuration provider is a callable. It returns either one contribution as
an array or an iterable of array contributions. ConfigFactory calls every
provider occurrence exactly once, folds all contributions in order, and returns
a ConfigComposition:
configcontains only application sections;dependenciescontains raw container definition sections.
DependencyDefinitions is a transport object, not a built dependency graph.
It does not normalize class names, trigger autoload, or decide whether a service
is constructible. Pass it to the DI composition root; do not expose it as an
application configuration section.
<?php
use Componenta\Config\ConfigFactory;
use Componenta\Config\ConfigProvider;
use Componenta\Config\FileProvider;
use Componenta\Config\Loader\EnvLoader;
use function Componenta\Config\path;
final class AppConfigProvider extends ConfigProvider
{
protected function getConfig(): array
{
return [
'app' => ['name' => 'Example'],
];
}
protected function getFactories(): array
{
return [
App::class => AppFactory::class,
];
}
}
$environment = (new EnvLoader(__DIR__))->load();
$composition = (new ConfigFactory())->create(
$environment,
new AppConfigProvider(),
new FileProvider(__DIR__ . '/config/*.{php,json}'),
);
$config = $composition->config;
$dependencies = $composition->dependencies;
echo $config->string(path('app.name'));The same Environment object passed to ConfigFactory::create() is retained
by identity in the resulting Config.
ConfigProvider produces one contribution. Application code prepares the
complete ordered provider list; providers do not recursively discover or invoke
other providers.
The available dependency hooks are:
getFactories()
getInvokables()
getAliases()
getDelegators()
getServices()
getParameterResolvers()
shouldReplaceParameterResolvers()
getAttributeDefinitions()
shouldReplaceAttributeDefinitions()
getAttributeCapabilities()
getConfig() returns application sections and must not define the reserved
dependencies root. Replacement hooks are tri-state: true and false
are explicit values; null leaves the previously composed value unchanged.
- list plus list appends in provider order;
- map plus map merges recursively by exact integer or string key;
- a list/map shape change replaces the earlier value;
- scalars, objects, and different value shapes are replaced by the later value;
ConfigOverride::replace($value)replaces a subtree explicitly;ConfigOverride::remove()removes a key.
Override markers are map values. Lists are append-only and reject markers.
PHP array references and nesting deeper than 64 levels are rejected with
ConfigMergeException.
factories,aliases,services, andparameter_resolversmerge by key; the later entry replaces the earlier entry atomically;- numeric
invokablesappend, while keyed invokables replace the same key; - delegator pipelines, attribute definitions, and attribute capabilities append in provider order;
- the last explicitly supplied replacement flag wins.
Application values and dependency sections are folded independently. Adding or omitting the reserved dependency root does not change the application list/map shape, and replacing the application root does not discard previously composed dependency definitions.
Config validates only the shapes needed to merge these sections safely. DI owns semantic validation of factories, callables, aliases, types, and service constructibility.
A string or integer passed to Config::get() is a literal top-level key.
ConfigPath performs nested lookup:
use function Componenta\Config\path;
$config->get('database.host'); // literal key
$config->get(path('database.host')); // nested pathTyped accessors are available for common values:
$config->string('name');
$config->int('port');
$config->float('ratio');
$config->bool('enabled');
$config->array('hosts');Missing values throw unless a default is supplied. Stored null, false,
an empty string, and an empty array are existing values. Integer conversion is
lossless; fractional, overflowing, non-finite, or ambiguous input is rejected.
toArray(), iteration, only(), and except() expose the stored
application sections. They do not add dependency definitions or eagerly resolve
the whole configuration graph.
The helper functions create explicit runtime values:
use Componenta\Config\Config;
use function Componenta\Config\config_entry;
use function Componenta\Config\env;
use function Componenta\Config\lazy;
use function Componenta\Config\path;
return [
'host' => env('DATABASE_HOST'),
'port' => env('DATABASE_PORT', '3306'),
'display_name' => config_entry(path('app.name')),
'dsn' => lazy(
static fn(Config $config): string =>
'mysql:host=' . $config->string('host'),
),
];EnvironmentEntryreads theEnvironmentattached to the sameConfig;ConfigEntryreads another key from thatConfig;LazyValueexecutes only when read;- ordinary callables remain ordinary values and are not executed automatically.
When the selected value is an array, descriptors are resolved throughout that
subtree while keys are preserved. Descriptor results are also resolved
recursively. Cycles fail with
ConfigResolutionException and include the resolution path. A cached
LazyValue stores one successful result per Config or ContainerValue
context. Exceptions are not cached. Same-Fiber re-entry is a cycle; concurrent
resolution from another Fiber is allowed, and the first successful completion
becomes the cached value.
EnvLoader reads .env and then .env.local by default:
use Componenta\Config\Loader\EnvLoader;
$environment = (new EnvLoader(
paths: [__DIR__, '/etc/example'],
required: ['APP_ENV'],
))->load();The loader is pure: it never writes to $_ENV, $_SERVER, or the process
environment. Existing process values win by default. Pass override: true to
load() when dotenv file values should win in the returned snapshot; globals
still remain unchanged.
Only explicitly listed basenames are loaded. Sample and backup files are not
picked up automatically. read() returns only parsed dotenv values (or
null when no file exists), and parse diagnostics never include secret values.
Environment::fromGlobals() creates an independent snapshot with this
precedence:
process environment < $_SERVER < $_ENV
FileProvider supports PHP and JSON and returns one contribution per matched
file in lexical path order. It does not merge files itself:
$provider = new FileProvider(__DIR__ . '/config/*.{php,json}');
foreach ($provider() as $contribution) {
// ConfigFactory performs the merge.
}PHP files are included in an isolated static scope and must return an array.
JSON must contain an object or array at the root; integers outside the platform
range are preserved as strings. Implement FileReaderInterface to add another
format.
ContainerValue wraps a PSR-11 container and carries the same runtime
Config. Both values are required constructor arguments and are retained by
identity; ContainerValue never discovers, rebuilds, or clones Config. Its
optional fallbacks support ContainerEntry, ConfigEntry, EnvironmentEntry,
and LazyValue.
All expected package failures implement ConfigExceptionInterface. Important
types include:
InvalidConfigArgumentExceptionfor invalid public arguments;ConfigMergeExceptionfor unsafe or malformed composition;ConfigResolutionExceptionfor descriptor cycles;InvalidConfigValueExceptionandInvalidContainerValueExceptionfor failed type contracts;EnvLoaderExceptionfor dotenv failures;ConfigExceptionfor missing configuration and reader failures.
Provider exceptions are propagated unchanged. Wrapped reader failures preserve
the source exception in getPrevious().
componenta/di consumes DependencyDefinitions, validates
their meaning, and constructs the application container.