alexkart / typed-registry
Dependency-free typed facade over mixed registries/config sources for PHPStan-level codebases.
Installs: 21
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/alexkart/typed-registry
Requires
- php: >=8.3
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^12.0
Suggests
- alexkart/typed-registry-psl: Optional adapter for shape/union types & coercion (future).
- alexkart/typed-registry-webmozart: Optional adapter for assertion-based guards (future).
This package is auto-updated.
Last update: 2025-10-16 16:42:16 UTC
README
A dependency-free, strict facade that turns mixed
config/registry values into real PHP types.
No magic. No coercion. PHPStan-ready.
Why?
Modern PHP codebases use strict types and static analysis (PHPStan, Psalm), but configuration systems often return mixed
values. This package provides a strict, non-coercive boundary between your config sources and typed code:
- No implicit coercion -
"123"
stays a string, won't becomeint(123)
- Deep validation - Lists and maps validate every element
- Explicit defaults -
getIntOr($key, 8080)
makes fallback behavior grep-able - Pluggable sources - Wrap any config system via a simple
Provider
interface - PHPStan Level 10 - Zero errors, precise return types
Installation
composer require alexkart/typed-registry
Requires PHP 8.3 or later.
Quick Start
use TypedRegistry\TypedRegistry; use TypedRegistry\ArrayProvider; $registry = new TypedRegistry(new ArrayProvider([ 'app.debug' => true, 'app.port' => 8080, 'app.hosts' => ['web1.local', 'web2.local'], ])); $debug = $registry->getBool('app.debug'); // bool(true) $port = $registry->getInt('app.port'); // int(8080) $hosts = $registry->getStringList('app.hosts'); // list<string> // With defaults (no exception on missing/wrong type) $timeout = $registry->getIntOr('app.timeout', 30); // int(30)
Core Concepts
1. Provider Interface
Any config source can be wrapped by implementing the Provider
interface:
interface Provider { public function get(string $key): mixed; }
Built-in providers:
ArrayProvider
- Array-backed (great for tests or preloaded config)CallbackProvider
- Wrap any callableCompositeProvider
- Fallback chain (env → config → defaults)
2. Strict Type Checking
All getXxx()
methods validate types without coercion:
$registry = new TypedRegistry(new ArrayProvider(['port' => '8080'])); $registry->getInt('port'); // ❌ Throws RegistryTypeError // "[typed-registry] key 'port' must be int, got '8080'"
To handle this, either:
- Store the correct type:
['port' => 8080]
- Use a default:
$registry->getIntOr('port', 8080)
3. Collections
Lists and maps are validated deeply:
// Lists (sequential arrays) $registry->getStringList('app.hosts'); // ✅ ['a', 'b', 'c'] $registry->getIntList('app.ids'); // ✅ [1, 2, 3] // Maps (associative arrays with string keys) $registry->getStringMap('app.labels'); // ✅ ['env' => 'prod', 'tier' => 'web'] $registry->getIntMap('app.limits'); // ✅ ['max' => 100, 'min' => 10] // Invalid examples $registry->getStringList('key'); // ❌ If value is ['a', 123, 'c'] // "[typed-registry] key 'key[1]' must be string, got 123" $registry->getStringMap('key'); // ❌ If value is [0 => 'value'] // "[typed-registry] key 'key' must be map<string,string>, got array"
API Reference
Primitive Getters
Method | Return Type | Throws on Type Mismatch |
---|---|---|
getString(string $key) |
string |
✅ |
getInt(string $key) |
int |
✅ |
getBool(string $key) |
bool |
✅ |
getFloat(string $key) |
float |
✅ |
Nullable Variants
Accept null
as a legitimate value:
Method | Return Type | Throws on Type Mismatch |
---|---|---|
getNullableString(string $key) |
?string |
✅ (unless null or string) |
getNullableInt(string $key) |
?int |
✅ (unless null or int) |
getNullableBool(string $key) |
?bool |
✅ (unless null or bool) |
getNullableFloat(string $key) |
?float |
✅ (unless null or float) |
Getters with Defaults
Return the default value if key is missing or type mismatches (no exception):
Method | Return Type | Throws |
---|---|---|
getStringOr(string $key, string $default) |
string |
❌ |
getIntOr(string $key, int $default) |
int |
❌ |
getBoolOr(string $key, bool $default) |
bool |
❌ |
getFloatOr(string $key, float $default) |
float |
❌ |
List Getters
Return sequential arrays (validated with array_is_list()
):
Method | Return Type |
---|---|
getStringList(string $key) |
list<string> |
getIntList(string $key) |
list<int> |
getBoolList(string $key) |
list<bool> |
getFloatList(string $key) |
list<float> |
Map Getters
Return associative arrays with string keys:
Method | Return Type |
---|---|
getStringMap(string $key) |
array<string, string> |
getIntMap(string $key) |
array<string, int> |
getBoolMap(string $key) |
array<string, bool> |
getFloatMap(string $key) |
array<string, float> |
Usage Examples
Example 1: Wrap Any Config System
use TypedRegistry\TypedRegistry; use TypedRegistry\Provider; // Wrap your existing config library/registry final class SomeExternalLibraryConfigProvider implements Provider { public function get(string $key): mixed { // Adapt any existing config/registry system return \Some\Library\Config::get($key); } } $registry = new TypedRegistry(new SomeExternalLibraryConfigProvider()); $debug = $registry->getBool('app.debug'); $hosts = $registry->getStringList('app.allowed_hosts');
Example 2: Composite Provider (Fallback Chain)
Environment variables → Config file → Defaults:
use TypedRegistry\TypedRegistry; use TypedRegistry\ArrayProvider; use TypedRegistry\CallbackProvider; use TypedRegistry\CompositeProvider; $registry = new TypedRegistry(new CompositeProvider([ new CallbackProvider(fn($k) => $_ENV[$k] ?? null), // Environment new ArrayProvider(['app.port' => 8080, 'app.debug' => false]), // Config new ArrayProvider(['app.timeout' => 30]), // Defaults ])); // Will use $_ENV['app.port'] if set, otherwise 8080 from config $port = $registry->getInt('app.port');
Example 3: Testing with ArrayProvider
use PHPUnit\Framework\TestCase; use TypedRegistry\TypedRegistry; use TypedRegistry\ArrayProvider; final class MyServiceTest extends TestCase { public function testServiceUsesConfiguredPort(): void { $registry = new TypedRegistry(new ArrayProvider([ 'service.host' => 'localhost', 'service.port' => 9000, 'service.ssl' => true, ])); $service = new MyService($registry); self::assertSame('https://localhost:9000', $service->getBaseUrl()); } }
Error Handling
When type validation fails, RegistryTypeError
(extends RuntimeException
) is thrown:
use TypedRegistry\RegistryTypeError; try { $registry->getInt('app.port'); } catch (RegistryTypeError $e) { // Message format: "[typed-registry] key 'app.port' must be int, got '8080'" logger()->error($e->getMessage()); }
For graceful degradation, use the getXxxOr()
variants:
$timeout = $registry->getIntOr('app.timeout', 30); // Never throws
Design Philosophy
What This Library Does
- Provides strict type boundaries around
mixed
config sources - Validates primitives, lists, and maps without coercion
- Enables PHPStan Level 10 compliance in config-heavy code
- Keeps implementation dependency-free (~250 LOC)
What This Library Doesn't Do
- Coercion - Use a dedicated validation library if you need
"123"
→123
- Schema validation - For DTOs/shapes, see future
typed-registry-psl
adapter - Config file parsing - This library consumes already-loaded config
- PSR container - Not a service locator, strictly config/registry access
Development
# Install dependencies composer install # Run tests composer test # or: vendor/bin/phpunit # Run static analysis composer phpstan # or: vendor/bin/phpstan analyse
Quality Standards:
- PHPStan Level: Max (10) with strict rules + bleeding edge
- Test Coverage: 100% (75 tests, 98 assertions)
- PHP Version: ≥8.3
- Dependencies: Zero (core package)
Roadmap
Future optional packages (not required for core usage):
alexkart/typed-registry-psl
- Shape/union types via PHP Standard Library Typesalexkart/typed-registry-schema
- Schema validation and DTO mapping
Contributing
Contributions are welcome! Please ensure:
- All tests pass (
vendor/bin/phpunit
) - PHPStan Level 10 passes (
vendor/bin/phpstan analyse
) - Code follows existing style (strict types, explicit return types)
License
MIT License. See LICENSE for details.
Credits
Maintained by the TypedRegistry contributors.
Questions? Open an issue on GitHub. Need coercion? Check out webmozart/assert or azjezz/psl.