symfony/options-resolver
Symfony OptionsResolver enhances array_replace with a robust options system: define required options, set defaults, validate types and values, and normalize inputs. Ideal for building configurable APIs, form components, and reusable libraries with strict option handling.
composer require symfony/options-resolver
use Symfony\Component\OptionsResolver\OptionsResolver;
$resolver = new OptionsResolver();
$resolver->setDefaults([
'timeout' => 30,
'retry' => false,
]);
$resolved = $resolver->resolve([
'timeout' => 45,
'retry' => true,
]);
// Returns: ['timeout' => 45, 'retry' => true]
use Symfony\Component\OptionsResolver\OptionsResolver;
class StripeClientConfigurator
{
public function __construct()
{
$this->resolver = new OptionsResolver();
$this->configureResolver();
}
protected function configureResolver(): void
{
$this->resolver
->setRequired(['api_key'])
->setDefaults([
'timeout' => 30,
'endpoint' => 'https://api.stripe.com/v1',
])
->setAllowedTypes('timeout', ['int', 'null'])
->setNormalizer('endpoint', fn($value) => rtrim($value, '/'));
}
public function resolve(array $config): array
{
return $this->resolver->resolve($config);
}
}
OptionsResolver class (core class for defining rules).Options class (for nested option definitions in Symfony 7.4+).app(OptionsResolver::class) or bind it in AppServiceProvider.Pattern: Validate service-specific configurations (e.g., queues, caches, APIs). Example:
use Symfony\Component\OptionsResolver\OptionsResolver;
class QueueConfigurator
{
public function __invoke(OptionsResolver $resolver): void
{
$resolver
->setDefaults([
'driver' => 'sync',
'options' => [],
'retry_after' => 90,
])
->setAllowedValues('driver', ['sync', 'database', 'redis'])
->setAllowedTypes('retry_after', ['int', 'null'])
->setNormalizer('options', fn($options) => is_array($options) ? $options : []);
}
}
// Usage in a service provider:
$resolver = app(OptionsResolver::class);
$resolver->configure([$this, 'configureQueue']);
$config = $resolver->resolve(config('queue.default'));
Pattern: Define hierarchical configurations (e.g., database connections, API endpoints). Example:
use Symfony\Component\OptionsResolver\Options;
$resolver = new OptionsResolver();
$resolver->setDefaults([
'database' => new Options([
'host' => 'localhost',
'port' => 3306,
'ssl' => false,
]),
'timeout' => 30,
]);
$resolved = $resolver->resolve([
'database' => [
'host' => 'db.example.com',
'ssl' => true,
],
]);
// Returns:
// [
// 'database' => [
// 'host' => 'db.example.com',
// 'port' => 3306,
// 'ssl' => true,
// ],
// 'timeout' => 30,
// ]
Pattern: Use closures for computed defaults (e.g., fetch from config or environment). Example:
$resolver = new OptionsResolver();
$resolver->setDefaults([
'cache_ttl' => fn() => config('app.cache_ttl', 60),
'log_path' => fn() => storage_path('logs/app.log'),
]);
Pattern: Bind OptionsResolver as a singleton and reuse it across services.
Example (in AppServiceProvider):
public function register(): void
{
$this->app->singleton(OptionsResolver::class, fn() => new OptionsResolver());
}
public function boot(): void
{
$this->app->resolving(QueueWorker::class, function ($worker, $app) {
$resolver = $app->make(OptionsResolver::class);
$resolver->configure([$this, 'configureQueueWorker']);
$worker->setConfig($resolver->resolve(config('queue.worker')));
});
}
Pattern: Use setValidator for custom logic (e.g., validate retry only if max_retries is set).
Example:
$resolver->setValidator('retry', fn($value, $options) => [
$options['max_retries'] ?? null,
$value >= 0 && $value <= ($options['max_retries'] ?? 5),
]);
Pattern: Mark legacy options for deprecation (Symfony 7.3+). Example:
$resolver->setDeprecated('old_option', 'Use `new_option` instead.');
Pattern: Leverage modern PHP types for stricter validation. Example:
$resolver->setAllowedTypes('status', ['string', 'int', 'null']);
$resolver->setAllowedTypes('roles', ['array', 'string']);
Nested Options in Symfony <7.4:
setDefault() hacks.setOptions() for nested structures in newer versions:
$resolver->setOptions([
'database' => new Options([
'host' => 'localhost',
]),
]);
Closure Scope Issues:
setDefaults or setNormalizer lose scope (e.g., $this).fn() or bind the closure:
$resolver->setDefaults([
'path' => fn() => $this->getDefaultPath(),
]);
Overwriting vs. Merging:
resolve() replaces existing options with defaults, not merges.resolveWithDefaults() for partial overrides:
$resolver->resolveWithDefaults(['timeout' => 60]); // Merges with defaults
Error Paths in Nested Structures:
database.host).setPrototypeValue() or setPrototypeNormalizer() for nested validation:
$resolver->setPrototypeValue('database', new Options([
'host' => 'localhost',
]));
Performance with Large Configs:
resolve() sparingly in hot paths.Deprecation Warnings:
setValidator to enforce deprecation:
$resolver->setDeprecated('old_key', 'Use `new_key` instead.');
$resolver->setValidator('old_key', fn($v) => throw new \RuntimeException('Deprecated!'));
Enable Debug Mode:
$resolver->setDebug(true); // Shows detailed error paths
Example error:
Invalid configuration for path "database.ssl": The option "ssl" with value "yes" is expected to be of type "bool", but is of type "string".
Inspect Resolver State:
$resolver->getDefinedOptions(); // List all defined options
$resolver->getDefaults(); // Show current defaults
Test Edge Cases:
null, empty arrays, and invalid types.resolve() with partial configs to simulate real-world usage.Custom Normalizers:
$resolver->setNormalizer('enum_value', fn($value) => YourEnum::from($value));
Dynamic Option Definitions:
$resolver->setDefaults(fn() => [
'timeout' => config('app.timeout'),
]);
Integration with Laravel Validation:
use Illuminate\Support\Facades\Validator;
$resolver = new
How can I help you explore Laravel packages today?