symfony/options-resolver
Symfony OptionsResolver is array_replace on steroids: define required options, defaults, allowed types/values, normalizers, and validation for robust option/config handling in your PHP code. Great for APIs, components, and reusable libraries.
composer require symfony/options-resolver
array_replace_recursive):
use Symfony\Component\OptionsResolver\OptionsResolver;
$resolver = new OptionsResolver();
$resolver->setDefaults([
'timeout' => 30,
'retries' => 3,
]);
$config = $resolver->resolve([
'timeout' => 60, // Overrides default
'retries' => 0, // Invalid (will be normalized)
]);
// Returns: ['timeout' => 60, 'retries' => 3] (retries normalized to min(3))
// app/Services/QueueService.php
use Symfony\Component\OptionsResolver\OptionsResolver;
class QueueService
{
public function __construct(array $config)
{
$resolver = new OptionsResolver();
$resolver->setRequired(['driver'])
->setAllowedValues('driver', ['database', 'beanstalkd', 'redis'])
->setDefaults(['timeout' => 60])
->setNormalizer('retries', fn($val) => max(0, $val));
$this->config = $resolver->resolve($config);
}
}
Call it:
$service = new QueueService([
'driver' => 'redis',
'retries' => -5, // Normalized to 0
]);
OptionsResolver class (core methods: setDefaults(), setRequired(), setAllowedTypes(), resolve()).Options class (for nested configurations, e.g., setOptions() in Symfony 8+).Pattern: Centralize validation for Laravel services (e.g., Mailer, Logger).
// app/Providers/AppServiceProvider.php
use Symfony\Component\OptionsResolver\OptionsResolver;
public function register()
{
$this->app->singleton(QueueService::class, function ($app) {
$config = $app['config']['queue'];
$resolver = new OptionsResolver();
$resolver->setRequired(['driver'])
->setAllowedTypes('timeout', ['int', 'null'])
->setDefaults(['timeout' => 60]);
return new QueueService($resolver->resolve($config));
});
}
Pattern: Validate complex structures (e.g., database connections).
// app/Services/DatabaseService.php
$resolver = new OptionsResolver();
$dbResolver = new OptionsResolver();
$dbResolver->setRequired(['host', 'port'])
->setAllowedTypes('port', 'int');
$resolver->setDefaults([
'default' => $dbResolver->resolve([
'host' => 'localhost',
'port' => 3306,
]),
'replicas' => [],
]);
Pattern: Runtime defaults (e.g., environment-aware configs).
$resolver->setDefaults([
'debug' => fn() => app()->environment('local'),
'api_key' => fn() => config('services.stripe.key'),
]);
Pattern: Phase out legacy configs with warnings.
$resolver->setDeprecated('old_driver', '2.0', 'Use `new_driver` instead.');
$resolver->setDefault('new_driver', fn() => $config['old_driver'] ?? 'default');
Pattern: Export resolvers in custom Laravel packages.
// vendor/package/src/Resolver/NotifierResolver.php
namespace Package\Resolver;
use Symfony\Component\OptionsResolver\OptionsResolver;
class NotifierResolver
{
public static function configure(OptionsResolver $resolver): void
{
$resolver->setRequired(['driver'])
->setAllowedValues('driver', ['mail', 'slack', 'sms']);
}
}
Usage:
$resolver = new OptionsResolver();
NotifierResolver::configure($resolver);
$config = $resolver->resolve(['driver' => 'slack']);
Pattern: Bind resolvers to container for dependency injection.
// app/Providers/AppServiceProvider.php
$this->app->bind(QueueService::class, function ($app) {
$config = $app['config']['queue'];
$resolver = $app->make(QueueResolver::class);
return new QueueService($resolver->resolve($config));
});
Pattern: Validate API request payloads.
// app/Http/Requests/StoreQueueJobRequest.php
use Symfony\Component\OptionsResolver\OptionsResolver;
public function rules()
{
$resolver = new OptionsResolver();
$resolver->setRequired(['driver', 'payload'])
->setAllowedValues('driver', ['database', 'redis']);
$this->resolvedConfig = $resolver->resolve($this->all());
return [];
}
Pattern: Test resolvers in isolation.
// tests/Unit/Services/QueueServiceTest.php
public function testResolver()
{
$resolver = new OptionsResolver();
$resolver->setRequired(['driver'])
->setAllowedValues('driver', ['database']);
$this->assertFalse($resolver->isValid(['driver' => 'redis']));
$this->assertTrue($resolver->isValid(['driver' => 'database']));
}
Nested Options in Symfony 8+:
setDefault() for nested options was deprecated in v7.3.0 and removed in v8.0.0.setOptions() for nested structures:
$resolver->setOptions([
'database' => new OptionsResolver(),
]);
Closure Evaluation Timing:
fn() => ... for dynamic values (e.g., environment vars):
$resolver->setDefaults(['timeout' => fn() => env('QUEUE_TIMEOUT', 60)]);
Error Paths in Nested Resolvers:
try {
$resolver->resolve($input);
} catch (\InvalidArgumentException $e) {
$path = $e->getPath(); // e.g., 'database.ssl.cert'
}
Performance with Large Configs:
private $resolvedConfig;
public function __construct(array $config)
{
$this->resolvedConfig = $this->resolver->resolve($config);
}
Deprecation Warnings:
$resolver = new OptionsResolver();
$resolver->setDeprecated('old_key', '1.0', 'Use `new_key`');
$resolver->resolve($config); // Warning triggered
Type Validation Quirks:
setAllowedTypes() rejects null unless explicitly allowed:
$resolver->setAllowedTypes('timeout', ['int', 'null']);
null for optional nullable fields.Normalizer Side Effects:
setNormalizer() for transformations, not validation:
$resolver->setNormalizer('retries', fn($val) => max(0, $val));
Inspect Resolved Configs:
$resolver->resolve($input); // Returns final config
$resolver->getDefinedOptions(); // List of defined options
$resolver->getDefinedOptionNames(); // Array of option names
Validate Without Resolving:
if (!$resolver->isValid($input)) {
$errors = $resolver->getErrors($input);
// $errors = ['timeout' => ['This value should be of type int.']]
}
Enable Deprecation Warnings:
How can I help you explore Laravel packages today?