camoo / config-interop
Common interface for immutable configuration access and interoperability.
Requires
- php: >=8.2
Requires (Dev)
- phpstan/phpstan: ^1.12
- phpstan/phpstan-strict-rules: ^1.6
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-05-11 10:40:24 UTC
README
⚠️ Draft Proposal — This is a pre-submission draft for the PHP-FIG PSR process. The interfaces and specification are subject to change.
What Is This?
This repository houses a PHP-FIG draft proposal for a minimal, immutable, framework-agnostic configuration interface: Config Proposal.
The goal is to let libraries, packages, and frameworks accept configuration through a single shared interface — without coupling either side to a specific framework or implementation.
The Problem
Every major PHP framework ships its own configuration container:
| Framework | Class | Mutable? | Framework-coupled? |
|---|---|---|---|
| Symfony | ParameterBag |
✅ | ✅ |
| Laravel | Illuminate\Config\Repository |
✅ | ✅ |
| Laminas | Laminas\Config\Config |
Optional | Partially |
| Nette | ArrayHash |
✅ | Partially |
A library author who needs to read a database.host value must today either:
- Accept a raw
array(no type safety, no IDE support, no contract), - Couple to a specific framework's container, or
- Invent their own interface (fragmentation).
This proposal solves this by defining the smallest possible read-only interface that every framework can implement without sacrificing its own model.
The Interfaces
namespace ConfigInterop; interface ConfigInterface { /** Implementations MUST return false when the stored value is null. */ public function has(string $key): bool; public function get(string $key, mixed $default = null): mixed; /** @return array<string, mixed> */ public function all(): array; public function withPrefix(string $prefix): static; } interface ConfigProviderInterface { public function getConfig(): ConfigInterface; }
That's it. Four methods, zero runtime dependencies, PHP 8.2+.
has() checks key existence, even when the stored value is null.
Key Design Principles
| Principle | What it means |
|---|---|
| Immutable | No set(), merge(), or push() — the interface is read-only |
| Framework-neutral | Zero dependencies; works in any PHP project |
| Static-analysis-friendly | PHPDoc on every member; static return preserves concrete types |
| Minimal surface | Four methods cover all practical read use-cases |
| Prefix scoping | withPrefix('db') scopes keys: get('host') → db.host |
Repository Layout
psr-config/
├── src/
│ ├── ConfigInterface.php # Primary read interface
│ └── ConfigProviderInterface.php # Factory / provider interface
├── docs/
│ └── config-interop-draft.md # Full PSR draft document
├── examples/
│ ├── ArrayConfig.php # Array-backed reference implementation
│ ├── EnvConfig.php # Environment-variable-backed implementation
│ ├── CachedConfigDecorator.php # Caching decorator
│ ├── PrefixedConfig.php # withPrefix() delegation example
│ └── LazyConfigProvider.php # Deferred-instantiation provider
├── tests/
│ └── ConfigInterfaceComplianceTest.php
├── composer.json
├── CONTRIBUTING.md
└── LICENSE
Quick Start
composer require camoo/config-interop
Array-backed config
use ConfigInterop\Example\ArrayConfig; $config = new ArrayConfig([ 'database' => [ 'host' => 'localhost', 'port' => 5432, ], 'cache' => [ 'enabled' => true, 'ttl' => 3600, ], ]); $config->get('database.host'); // 'localhost' $config->has('database.port'); // true $db = $config->withPrefix('database'); $db->get('host'); // 'localhost' $db->get('missing', 'default'); // 'default'
Env-backed config
use ConfigInterop\Example\EnvConfig; // Reads APP_DATABASE_HOST, APP_DATABASE_PORT, etc. $config = new EnvConfig(envPrefix: 'APP_'); $config->get('database.host');
Caching decorator
use ConfigInterop\Example\CachedConfigDecorator; $config = new CachedConfigDecorator($expensiveConfig); // Each key resolved at most once per instance lifetime.
Lazy provider
use ConfigInterop\Example\LazyConfigProvider; use ConfigInterop\Example\ArrayConfig; $provider = new LazyConfigProvider( static fn () => new ArrayConfig(require 'config.php') ); // Config file is not loaded until this call: $config = $provider->getConfig();
Library Authors — How to Accept Config
Accept ConfigInterface directly — do not depend on specific implementations:
use ConfigInterop\ConfigInterface; class DatabaseConnection { public function __construct(private readonly ConfigInterface $config) {} public function connect(): \PDO { $db = $this->config->withPrefix('database'); return new \PDO( dsn: sprintf('pgsql:host=%s;dbname=%s', $db->get('host'), $db->get('name')), username: $db->get('user'), password: $db->get('password'), ); } }
Running Tests & Static Analysis
composer install composer test # PHPUnit 11 composer analyse # PHPStan level 9 composer check # both
The Full PSR Proposal
See docs/config-interop-draft.md for the complete specification, including:
- Behavioral requirements (MUST / SHOULD / MAY)
- Immutability guarantees
- Prefix scoping semantics
- Error handling recommendations
- Security & performance considerations
- Prior art & ecosystem comparison
- FAQ
Contributing
Please read CONTRIBUTING.md before opening a pull request or issue.
License
MIT © Camoo SARL. See LICENSE for details.