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.
Install the package:
composer require symfony/options-resolver
Basic usage:
use Symfony\Component\OptionsResolver\OptionsResolver;
$resolver = new OptionsResolver();
$resolver->setDefaults([
'driver' => 'mysql',
'timeout' => 30,
]);
$resolver->setRequired(['host']);
$resolver->setAllowedTypes('timeout', ['int', 'null']);
$config = $resolver->resolve([
'host' => 'localhost',
'timeout' => 'invalid', // Will throw an exception
]);
First use case:
Validate and normalize configuration for a Laravel service (e.g., App\Services\PaymentGateway):
$resolver = new OptionsResolver();
$resolver->setDefaults([
'api_key' => null,
'timeout' => 10,
'retry' => 3,
]);
$resolver->setRequired(['api_key']);
$resolver->setAllowedTypes('timeout', 'int');
$resolver->setNormalizer('retry', fn($val) => max(0, $val));
$config = $resolver->resolve(config('services.payment'));
OptionsResolver class (core methods: setDefaults(), setRequired(), setAllowedTypes(), resolve()).Options class (for nested configurations, e.g., setOptions() in Symfony 8+).config() or service containers (e.g., bind resolved configs to interfaces).// In a Laravel service provider
$resolver = new OptionsResolver();
$resolver->setDefaults([
'queue' => 'default',
'timeout' => 60,
'retry_after' => 30,
]);
$resolver->setAllowedValues('queue', ['default', 'high', 'low']);
$resolver->setAllowedTypes('timeout', 'int');
$config = $resolver->resolve(config('app.job_processor'));
$this->app->singleton(JobProcessor::class, fn() => new JobProcessor($config));
$dbResolver = new OptionsResolver();
$dbResolver->setRequired(['host', 'port']);
$dbResolver->setAllowedTypes('port', 'int');
$resolver = new OptionsResolver();
$resolver->setDefaults([
'database' => $dbResolver->resolve([]), // Nested resolver
'api' => [
'base_uri' => 'https://api.example.com',
'timeout' => 30,
],
]);
$resolver->setAllowedTypes('api.timeout', 'int');
$resolver = new OptionsResolver();
$resolver->setDefaults([
'debug' => fn() => app()->environment('local'),
'cache_ttl' => fn() => env('CACHE_TTL', 3600),
]);
$resolver = new OptionsResolver();
$resolver->setDeprecated('old_driver', '2.0', 'Use `new_driver` instead.');
$resolver->setDefault('new_driver', fn() => config('old_driver', 'default'));
// In a custom package (e.g., `laravel-notifier`)
class NotifierResolver {
public static function getResolver(): OptionsResolver {
$resolver = new OptionsResolver();
$resolver->setRequired(['driver']);
$resolver->setAllowedValues('driver', ['mail', 'slack', 'sms']);
$resolver->setDefaults(['timeout' => 10]);
return $resolver;
}
}
// Usage in Laravel
$config = NotifierResolver::getResolver()->resolve(config('services.notifier'));
$this->app->bind(PaymentGatewayInterface::class, function ($app) {
$resolver = new OptionsResolver();
$resolver->setRequired(['api_key']);
$resolver->setAllowedTypes('timeout', 'int');
$config = $resolver->resolve(config('services.stripe'));
return new StripeGateway($config);
});
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
public function handle($request, Closure $next) {
try {
$resolver = new OptionsResolver();
$resolver->setRequired(['api_token']);
$resolver->resolve($request->input('config'));
} catch (InvalidOptionsException $e) {
abort(422, $e->getMessage());
}
return $next($request);
}
protected $resolver;
public function __construct() {
$this->resolver = new OptionsResolver();
$this->resolver->setDefaults(['limit' => 100]);
$this->resolver->setAllowedTypes('limit', 'int');
}
protected function getConfig(): array {
return $this->resolver->resolve([
'limit' => $this->option('limit'),
'offset' => $this->option('offset') ?? 0,
]);
}
Nested Options in Symfony <8.0:
setDefault() for nested options (e.g., setDefault('database.host', 'localhost')) is deprecated in Symfony 8+. Use setOptions() instead.// Symfony 7.x (deprecated in 8.0)
$resolver->setDefault('database.host', 'localhost');
// Symfony 8.0+
$resolver->setOptions([
'database' => new OptionsResolver(),
]);
$resolver->setDefault('database', ['host' => 'localhost']);
Closure Evaluation Timing:
setDefaults() are evaluated once (when the resolver is created), not per-resolve.fn() => ... for dynamic values:
$resolver->setDefaults([
'cache_ttl' => fn() => env('CACHE_TTL', 3600), // Evaluated per-resolve
]);
Error Paths in Nested Resolvers:
try {
$resolver->resolve($input);
} catch (InvalidOptionsException $e) {
throw new \RuntimeException("Invalid 'database' config: " . $e->getMessage());
}
Performance with Large Configs:
OptionsResolver::class in Laravel’s container:
$this->app->singleton(OptionsResolver::class, fn() => new OptionsResolver());
Type Safety with PHP 8.0+:
int|string) may not work as expected in older PHP versions.setAllowedTypes() with arrays:
$resolver->setAllowedTypes('timeout', ['int', 'null']);
Inspect Resolver State:
$resolver->resolve([]); // Throws if required options are missing
$resolver->getDefinedOptions(); // List all defined options
$resolver->getDefinedOptionNames(); // List option names
Enable Strict Mode:
$resolver->setStrict(true); // Throws on unknown options
Custom Error Messages:
$resolver->setErrorMessage('timeout', 'Timeout must be a positive integer.');
Log Resolved Configs:
$config = $resolver->resolve($input);
\Log::debug('Resolved config', ['config' => $config]);
$resolver->setNormalizer('slug', fn($val) => Str::slug($val));
How can I help you explore Laravel packages today?