pilotphp / config
Deterministic compiled configuration for the PilotPHP Agent-First framework.
Requires
- php: ^8.5
- pilotphp/contracts: ^0.1.4
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.88
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^11.5
README
Deterministic compiled configuration for the PilotPHP Agent-First framework.
Configuration is assembled and compiled before requests are served. In the worker, reading a value is one hash lookup — no file access, no environment access, no merging.
Installation
composer require pilotphp/config
Requires PHP 8.5 and pilotphp/contracts (0.1.4 Build SPI line).
What it does
- Registers
ConfigBuildStagethroughConfigPackage - Loads configuration from trusted PHP files, directories, and typed declarations
- Merges ordered layers deterministically, refusing ambiguity
- Keeps external values (secrets, environment) as unresolved references
- Compiles to a byte-reproducible PHP artifact with a SHA-256 fingerprint
- Serves an immutable, typed
ConfigInterfaceat runtime
What it does not do
No DI container, kernel, runtime loop, RoadRunner, gRPC, HTTP, ORM,
Blueprint, console commands, .env parsing, YAML/XML, secret-provider
implementations, or Composer package scanning. Each belongs to its own
package — see docs/architecture.md.
It also does not own build orchestration, the shared build fingerprint, the
build cache, locks, or atomic publication. Those belong to pilotphp/build.
Lifecycle
active ConfigPackage → registration → immutable replay
→ ConfigBuildStage → config artifact → runtime loader
Production pipeline
Build tooling activates pilotphp/config and runs the universal build.
ConfigBuildStage prepares from replayed declarations and writes
config/compiled.php through ArtifactWriterInterface.
Worker boot
use PilotPHP\Config\Compiler\CompiledConfigLoader;
use PilotPHP\Config\Runtime\ImmutableConfig;
use PilotPHP\Config\Value\{ConfigValueResolver, EnvironmentValueResolver};
$payload = new CompiledConfigLoader()->load($explicitArtifactPath);
$resolved = new ConfigValueResolver([
new EnvironmentValueResolver($environment),
])->resolve($payload->data());
$config = new ImmutableConfig($resolved);
The loader reads only that artifact. It does not compile, scan source directories, or resolve environment placeholders.
Development / standalone usage
use PilotPHP\Config\Build\{ConfigBuilder, ConfigLayer, ConfigLayerKind};
use PilotPHP\Config\Compiler\PhpConfigCompiler;
use PilotPHP\Config\Source\PhpFileConfigSource;
$result = new ConfigBuilder()
->addLayer(new ConfigLayer(
id: 'framework',
kind: ConfigLayerKind::FrameworkDefaults,
data: ['app' => ['name' => 'unnamed', 'workers' => 1]],
))
->addSource(new PhpFileConfigSource(
path: __DIR__ . '/config/app.php',
id: 'application',
kind: ConfigLayerKind::Application,
))
->build();
new PhpConfigCompiler()->compile($result, __DIR__ . '/var/cache/config.php');
Layers
| Kind | Value | Purpose |
|---|---|---|
FrameworkDefaults | 100 | framework baseline |
PackageDefaults | 200 | installed packages' defaults |
Application | 300 | the application's own config |
Environment | 400 | per-deployment overrides |
Runtime | 500 | programmatic overrides, applied last |
Sorted by kind, then order, then layer id — a total ordering, so the
result never depends on the order sources were added.
Merging
Maps merge recursively; lists are replaced wholesale; a scalar replaces a
scalar of the same type. Anything ambiguous is refused and must be
stated explicitly via ConfigMerge::{append,prepend,replace,remove}().
Full table: docs/merge-semantics.md.
Environment references
use PilotPHP\Config\Value\{DeferredValue, DeferredValueType};
return [
'database' => [
'host' => DeferredValue::environment('DB_HOST', default: 'localhost'),
'password' => DeferredValue::environment('DB_PASSWORD', required: true),
],
];
These stay unresolved through merging, fingerprinting, and compilation.
Values obtained by a DeferredValueResolver never reach the build
result, fingerprint, or compiled artifact. Application boot injects an
environment map into EnvironmentValueResolver — the package never calls
getenv() / $_ENV / $_SERVER.
Package defaults
$registration->add(new ConfigDefaultsDeclaration(
prefix: 'grpc',
path: 'config/default.php',
));
Composition also supplies ConfigPackageRootsDeclaration so the stage can
resolve those paths without discovering packages. In-memory overlays use
ConfigFragmentDeclaration. See
docs/package-integration.md.
Documentation
| Document | Contents |
|---|---|
| architecture.md | Pipeline, boundaries, trust model |
| public-api.md | Public vs internal types |
| compatibility.md | SemVer and machine-readable contracts |
| merge-semantics.md | Complete merge table |
| deferred-values.md | Providers and secret safety |
| compilation.md | Deterministic artifacts |
| package-integration.md | Package defaults |
| security.md | Threat model and secret policy |
| build-integration.md | ConfigBuildStage and Build SPI |
| migration.md | Consumer migration notes |
| AGENTS.md | Rules for changing this package |
Repository
A standalone Git repository and Composer package. PilotPHP has no monorepo; every package is its own repository.