typed-registry-laravel maintained by alexkart
typed-registry-laravel
Laravel integration for typed-registry following Laravel best practices for environment variable and configuration access.
Why?
Laravel's env() and config() helpers return mixed values, making strict type checking difficult. This package provides:
- Type-safe config access -
TypedConfigfacade andtypedConfig()helper with strict return types - Type-safe env access -
typedEnv()helper for use in config files only (following Laravel best practices) - String-preserving env access -
typedEnvString()helper for values that must stay strings (passwords, tokens) - Intelligent casting - Environment variables automatically cast from strings (
"8080"→8080,"1e3"→1000.0) - PHPStan ready - Works seamlessly with static analysis at max level
- Zero runtime overhead - Simple wrappers around Laravel's existing systems
Installation
composer require alexkart/typed-registry-laravel
Requires:
- PHP 8.3+
- Laravel 11+ or 12+ or 13+
The package uses Laravel's auto-discovery feature. The service provider and facade are registered automatically.
Quick Start
Environment Variables in Config Files
Following Laravel Best Practices: Environment variables should ONLY be accessed in config files, never directly in controllers or services.
// config/app.php
return [
'name' => typedEnv()->getStringOr('APP_NAME', 'Laravel'),
'debug' => typedEnv()->getBoolOr('APP_DEBUG', false),
'port' => typedEnv()->getIntOr('APP_PORT', 8080), // "8080" → 8080
'timeout' => typedEnv()->getFloatOr('TIMEOUT', 2.5), // "2.5" → 2.5
'max_items' => typedEnv()->getInt('MAX_ITEMS'), // Throws if missing
];
Configuration Access Everywhere
Use the TypedConfig facade or typedConfig() helper in controllers, services, and anywhere else:
use TypedRegistry\Laravel\Facades\TypedConfig;
class UserController
{
public function index()
{
$perPage = TypedConfig::getInt('app.pagination.per_page');
$appName = TypedConfig::getString('app.name');
$features = TypedConfig::getStringList('app.enabled_features');
// Or use the helper
$timeout = typedConfig()->getFloat('app.timeout');
}
}
Features
typedEnv() Helper - For Config Files Only
Wraps Illuminate\Support\Env with intelligent type casting for numeric strings:
// config/app.php
return [
// Automatic type casting from .env strings:
'port' => typedEnv()->getInt('PORT'), // "8080" → int(8080)
'rate' => typedEnv()->getFloat('RATE'), // "2.5" → float(2.5)
'limit' => typedEnv()->getFloat('LIMIT'), // "1e3" → float(1000.0)
'debug' => typedEnv()->getBool('APP_DEBUG'), // "true" → bool(true)
// With defaults (never throws):
'name' => typedEnv()->getStringOr('APP_NAME', 'Laravel'),
'timeout' => typedEnv()->getFloatOr('TIMEOUT', 30.0),
];
Casting Rules:
- Numeric strings →
intorfloatbased on format (handles scientific notation, leading zeros, whitespace, overflow) - Boolean strings (
"true","false") →bool(handled by Laravel'sEnv) - Null strings (
"null","(null)") →null(handled by Laravel'sEnv) - All other strings remain unchanged
typedEnvString() Helper - For String Values
Use this helper when you have environment variables that are semantically strings but may contain only digits (passwords, tokens, API keys):
// config/auth.php
return [
// Problem: typedEnv() casts "123456" to int, getStringOr returns ''
// 'password' => typedEnv()->getStringOr('API_PASSWORD', ''),
// Solution: typedEnvString() keeps all values as strings
'password' => typedEnvString()->getStringOr('API_PASSWORD', ''),
'token' => typedEnvString()->getString('API_TOKEN'),
];
Casting Rules:
- All scalar values (string, int, float, bool) →
string - Booleans:
true→"1",false→""(PHP's native casting) - Null remains
null
TypedConfig Facade - Use Anywhere
Wraps Laravel's Config facade with strict typing, no casting:
use TypedRegistry\Laravel\Facades\TypedConfig;
// In controllers, services, jobs, etc.
$driver = TypedConfig::getString('database.default');
$port = TypedConfig::getInt('database.connections.mysql.port');
$options = TypedConfig::getStringMap('database.connections.mysql.options');
// With defaults:
$perPage = TypedConfig::getIntOr('app.pagination.per_page', 15);
Or use the helper function:
$driver = typedConfig()->getString('database.default');
Full API
All three helpers (typedEnv(), typedEnvString(), and TypedConfig) expose the same 20 methods from TypedRegistry:
Primitive Getters
->getString('KEY'); // string - throws if missing/wrong type
->getInt('KEY'); // int
->getBool('KEY'); // bool
->getFloat('KEY'); // float
Nullable Variants
->getNullableString('KEY'); // string|null
->getNullableInt('KEY'); // int|null
->getNullableBool('KEY'); // bool|null
->getNullableFloat('KEY'); // float|null
With Defaults (Never Throws)
->getStringOr('KEY', 'default'); // Returns default if missing/wrong type
->getIntOr('KEY', 8080);
->getBoolOr('KEY', false);
->getFloatOr('KEY', 1.5);
Lists (Sequential Arrays)
->getStringList('KEY'); // list<string>
->getIntList('KEY'); // list<int>
->getBoolList('KEY'); // list<bool>
->getFloatList('KEY'); // list<float>
Maps (Associative Arrays with String Keys)
->getStringMap('KEY'); // array<string, string>
->getIntMap('KEY'); // array<string, int>
->getBoolMap('KEY'); // array<string, bool>
->getFloatMap('KEY'); // array<string, float>
Type Casting Behavior
EnvProvider - Intelligent Casting
The EnvProvider (used by typedEnv()) intelligently casts numeric environment variable strings:
// Integer casting (handles edge cases)
"123" → int(123)
"-456" → int(-456)
"0" → int(0)
"042" → int(42) // Leading zeros removed
"-042" → int(-42) // Negative with leading zeros
"+42" → int(42) // Leading plus removed
" 042 " → int(42) // Whitespace trimmed
// Integer overflow protection (values exceeding PHP_INT_MAX/MIN)
"9223372036854775808" → float(9.223372036854776E+18) // Too large for int
"-9223372036854775809" → float(-9.223372036854776E+18) // Too small for int
// Float casting (decimal point or scientific notation)
"3.14" → float(3.14)
"0.0" → float(0.0)
"1e3" → float(1000.0) // Scientific notation
"2.5e-4" → float(0.00025) // Scientific with decimal
"1E10" → float(10000000000.0) // Uppercase E
"042.5" → float(42.5)
// No casting
"Laravel" → "Laravel" // Non-numeric
"123abc" → "123abc" // Mixed alphanumeric
"" → "" // Empty string
// Laravel's Env handles these:
"true" → bool(true)
"false" → bool(false)
"null" → null
"(null)" → null
EnvStringProvider - Cast to String
The EnvStringProvider (used by typedEnvString()) casts all scalar values to strings:
// All values become strings
"123456" → "123456" // Numeric string preserved
"3.14" → "3.14" // Float string preserved
"1e3" → "1e3" // Scientific notation preserved
"042" → "042" // Leading zeros preserved
"Laravel" → "Laravel" // Non-numeric unchanged
// Laravel's Env converts these first, then we cast to string:
"true" → "1" // bool(true) → string
"false" → "" // bool(false) → string
"null" → null // Stays null (not scalar)
ConfigProvider - No Casting
ConfigProvider performs zero type coercion. Values must be stored with the correct type:
// config/app.php
return [
'port' => 8080, // ✅ int - TypedConfig::getInt() works
'port_str' => '8080', // ❌ string - TypedConfig::getInt() throws
];
Error Handling
Strict Getters Throw on Type Mismatch
use TypedRegistry\RegistryTypeError;
try {
$port = TypedConfig::getInt('app.name'); // If 'app.name' is a string
} catch (RegistryTypeError $e) {
// "[typed-registry] key 'app.name' must be int, got 'Laravel'"
}
Default Getters Never Throw
// Returns default value on missing key OR type mismatch
$port = typedEnv()->getIntOr('NONEXISTENT_PORT', 8080); // 8080
$timeout = TypedConfig::getFloatOr('cache.timeout', 3.0); // 3.0
Real-World Example
// config/app.php
return [
'name' => typedEnv()->getStringOr('APP_NAME', 'Laravel'),
'env' => typedEnv()->getStringOr('APP_ENV', 'production'),
'debug' => typedEnv()->getBoolOr('APP_DEBUG', false),
'url' => typedEnv()->getStringOr('APP_URL', 'http://localhost'),
'timezone' => 'UTC',
'locale' => typedEnv()->getStringOr('APP_LOCALE', 'en'),
'providers' => [
// Service providers...
],
];
// app/Http/Controllers/DashboardController.php
use TypedRegistry\Laravel\Facades\TypedConfig;
class DashboardController extends Controller
{
public function index()
{
$appName = TypedConfig::getString('app.name');
$isDebug = TypedConfig::getBool('app.debug');
$locale = TypedConfig::getString('app.locale');
return view('dashboard', compact('appName', 'isDebug', 'locale'));
}
}
PHPStan Integration
The package works seamlessly with PHPStan at max level:
/** @var int $port */
$port = TypedConfig::getInt('app.port'); // PHPStan knows this is int
/** @var list<string> $hosts */
$hosts = TypedConfig::getStringList('app.hosts'); // PHPStan knows the shape
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
- Test Coverage: All providers and facades
- PHP Version: 8.3+
- Laravel Version: 11+, 12+, 13+
Comparison with Core Package
| Feature | alexkart/typed-registry |
alexkart/typed-registry-laravel |
|---|---|---|
| Framework | Framework-agnostic | Laravel-specific |
| Type Casting | None (strict only) | EnvProvider casts numeric strings |
| String Preservation | N/A | EnvStringProvider keeps all as strings |
| Facades | No | TypedConfig |
| Helper Functions | No | typedEnv(), typedEnvString(), typedConfig() |
| Auto-discovery | N/A | Yes |
| Laravel Best Practices | N/A | Enforced (env only in config) |
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, final classes)
License
MIT License. See LICENSE for details.
Credits
- Built on alexkart/typed-registry
- Maintained by the TypedRegistry contributors
Questions? Open an issue on GitHub.