# Symfony OptionsResolver Symfony OptionsResolver is a PHP component that provides an enhanced replacement for PHP's `array_replace` function. It enables developers to create robust options systems with required options, default values, type validation, value validation, normalization, and deprecation notices. The component is ideal for configuring objects, handling method parameters, and processing user input where strict validation and sensible defaults are needed. The OptionsResolver component uses a fluent interface for configuration and supports lazy option evaluation via closures. It handles complex scenarios like nested options, prototype arrays, and option dependencies while providing clear error messages when validation fails. The component ensures type safety and data integrity by validating options before they reach your application logic. ## Setting Default Values Use `setDefault()` to define default values for options. When a closure with `Options` type-hint is passed, it becomes a lazy option evaluated during resolution. Lazy options can access other resolved options. ```php use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; $resolver = new OptionsResolver(); // Simple default value $resolver->setDefault('host', 'localhost'); $resolver->setDefault('port', 3306); // Set multiple defaults at once $resolver->setDefaults([ 'charset' => 'utf8mb4', 'timeout' => 30, ]); // Lazy default value based on other options $resolver->setDefault('dsn', function (Options $options) { return sprintf('mysql:host=%s;port=%d;charset=%s', $options['host'], $options['port'], $options['charset'] ); }); // Access previous default value in subclass/extension $resolver->setDefault('timeout', function (Options $options, $previousValue) { // $previousValue is 30 from previous setDefault return $previousValue * 2; // 60 }); $resolved = $resolver->resolve(['host' => 'db.example.com']); // Result: ['host' => 'db.example.com', 'port' => 3306, 'charset' => 'utf8mb4', // 'timeout' => 60, 'dsn' => 'mysql:host=db.example.com;port=3306;charset=utf8mb4'] ``` ## Required Options Use `setRequired()` to mark options that must be provided by the user. Required options without defaults will throw `MissingOptionsException` if not supplied. ```php use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; $resolver = new OptionsResolver(); // Mark single option as required $resolver->setRequired('api_key'); // Mark multiple options as required $resolver->setRequired(['username', 'password']); // Required option with a default (must be defined but has fallback) $resolver->setRequired('environment'); $resolver->setDefault('environment', 'production'); // Check option status var_dump($resolver->isRequired('api_key')); // true var_dump($resolver->isMissing('api_key')); // true (no default set) var_dump($resolver->isMissing('environment')); // false (has default) // Get all required/missing options $resolver->getRequiredOptions(); // ['api_key', 'username', 'password', 'environment'] $resolver->getMissingOptions(); // ['api_key', 'username', 'password'] // This throws MissingOptionsException try { $resolver->resolve([]); } catch (MissingOptionsException $e) { echo $e->getMessage(); // 'The required options "api_key", "password", "username" are missing.' } // Successful resolution $resolved = $resolver->resolve([ 'api_key' => 'secret123', 'username' => 'admin', 'password' => 'pass123', ]); ``` ## Defining Options Without Defaults Use `setDefined()` to declare valid option names without setting default values. Defined options are accepted but not included in results unless provided. ```php use Symfony\Component\OptionsResolver\OptionsResolver; $resolver = new OptionsResolver(); // Define options that can be passed but have no default $resolver->setDefined('debug'); $resolver->setDefined(['cache_dir', 'log_level']); // Set some defaults for other options $resolver->setDefault('environment', 'production'); // Check if option is defined var_dump($resolver->isDefined('debug')); // true var_dump($resolver->isDefined('environment')); // true var_dump($resolver->isDefined('undefined')); // false // Get all defined options $resolver->getDefinedOptions(); // ['debug', 'cache_dir', 'log_level', 'environment'] // Resolve without optional options $resolved = $resolver->resolve([]); // Result: ['environment' => 'production'] // Resolve with optional options provided $resolved = $resolver->resolve([ 'debug' => true, 'cache_dir' => '/tmp/cache', ]); // Result: ['environment' => 'production', 'debug' => true, 'cache_dir' => '/tmp/cache'] ``` ## Type Validation Use `setAllowedTypes()` to restrict option values to specific types. Supports PHP type names, class names, array notation (`string[]`), union types, and nullable types. ```php use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; $resolver = new OptionsResolver(); // Basic types $resolver->setDefault('name', 'default'); $resolver->setAllowedTypes('name', 'string'); $resolver->setDefault('count', 0); $resolver->setAllowedTypes('count', 'int'); // Multiple allowed types $resolver->setDefault('id', null); $resolver->setAllowedTypes('id', ['int', 'string', 'null']); // Union type syntax $resolver->setDefault('value', 0); $resolver->setAllowedTypes('value', 'string|int'); // Typed arrays $resolver->setDefault('tags', []); $resolver->setAllowedTypes('tags', 'string[]'); // Union types in arrays $resolver->setDefault('items', []); $resolver->setAllowedTypes('items', '(string|int)[]'); // Class/interface types $resolver->setDefault('date', null); $resolver->setAllowedTypes('date', ['null', \DateTimeInterface::class]); // Add additional allowed types $resolver->addAllowedTypes('date', \DateTimeImmutable::class); // Type validation error try { $resolver->resolve(['name' => 123]); } catch (InvalidOptionsException $e) { echo $e->getMessage(); // 'The option "name" with value 123 is expected to be of type "string", but is of type "int".' } // Successful resolution $resolved = $resolver->resolve([ 'name' => 'Product', 'count' => 5, 'id' => 'abc-123', 'tags' => ['php', 'symfony'], 'items' => ['foo', 42, 'bar'], 'date' => new \DateTime(), ]); ``` ## Value Validation Use `setAllowedValues()` to restrict options to specific values or validation closures. Closures should return `true` for valid values. ```php use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; $resolver = new OptionsResolver(); // Whitelist of allowed values $resolver->setDefault('transport', 'smtp'); $resolver->setAllowedValues('transport', ['smtp', 'sendmail', 'mail', 'null']); // Closure validation $resolver->setDefault('port', 25); $resolver->setAllowedTypes('port', 'int'); $resolver->setAllowedValues('port', function ($value) { return $value > 0 && $value <= 65535; }); // Multiple validation options (any can match) $resolver->setDefault('priority', 'normal'); $resolver->setAllowedValues('priority', [ 'low', 'normal', 'high', function ($value) { return is_int($value) && $value >= 1 && $value <= 10; }, ]); // Add more allowed values $resolver->addAllowedValues('transport', 'ses'); // Validation error with whitelist try { $resolver->resolve(['transport' => 'invalid']); } catch (InvalidOptionsException $e) { echo $e->getMessage(); // 'The option "transport" with value "invalid" is invalid. // Accepted values are: "smtp", "sendmail", "mail", "null", "ses".' } // Validation error with closure try { $resolver->resolve(['port' => -1]); } catch (InvalidOptionsException $e) { echo $e->getMessage(); // 'The option "port" with value -1 is invalid.' } // Successful resolution $resolved = $resolver->resolve([ 'transport' => 'ses', 'port' => 587, 'priority' => 5, // matches the closure validator ]); ``` ## Normalizers Use `setNormalizer()` to transform option values after validation. Normalizers can convert formats, sanitize input, or compute derived values. ```php use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; $resolver = new OptionsResolver(); // Normalize string to lowercase $resolver->setDefault('format', 'json'); $resolver->setAllowedTypes('format', 'string'); $resolver->setNormalizer('format', function (Options $options, $value) { return strtolower(trim($value)); }); // Convert to specific type $resolver->setDefault('date', null); $resolver->setAllowedTypes('date', ['null', 'string', \DateTimeInterface::class]); $resolver->setNormalizer('date', function (Options $options, $value) { if (is_string($value)) { return new \DateTimeImmutable($value); } return $value; }); // Normalizer with access to other options $resolver->setDefault('filename', null); $resolver->setDefault('extension', 'txt'); $resolver->setNormalizer('filename', function (Options $options, $value) { if ($value && !pathinfo($value, PATHINFO_EXTENSION)) { return $value . '.' . $options['extension']; } return $value; }); // Add multiple normalizers (executed in order) $resolver->addNormalizer('format', function (Options $options, $value) { return $value === 'yml' ? 'yaml' : $value; }); // Prepend a normalizer (executed first) $resolver->addNormalizer('format', function (Options $options, $value) { return str_replace('-', '_', $value); }, forcePrepend: true); $resolved = $resolver->resolve([ 'format' => ' JSON-LD ', 'date' => '2024-01-15', 'filename' => 'report', ]); // Result: ['format' => 'json_ld', 'date' => DateTimeImmutable, 'filename' => 'report.txt', 'extension' => 'txt'] ``` ## Fluent Configuration with define() Use `define()` for a fluent API to configure options. Returns an `OptionConfigurator` that chains configuration methods. ```php use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; $resolver = new OptionsResolver(); // Fluent definition of a required option with type and allowed values $resolver ->define('log_level') ->required() ->allowedTypes('string') ->allowedValues('debug', 'info', 'warning', 'error', 'critical') ->info('The minimum log level to capture'); // Fluent definition with default, type, and normalizer $resolver ->define('timeout') ->default(30) ->allowedTypes('int', 'float') ->allowedValues(function ($value) { return $value > 0; }) ->normalize(function (Options $options, $value) { return (int) ceil($value); }) ->info('Connection timeout in seconds'); // Chain multiple option definitions $resolver ->define('host') ->default('localhost') ->allowedTypes('string') ->info('Database host address') ->define('port') ->default(3306) ->allowedTypes('int') ->info('Database port number') ->define('ssl') ->default(false) ->allowedTypes('bool') ->info('Whether to use SSL connection'); // Deprecated option with custom message $resolver ->define('legacy_mode') ->default(false) ->allowedTypes('bool') ->deprecated('vendor/package', '2.0', 'The "%name%" option is deprecated, use "compatibility_mode" instead.'); $resolved = $resolver->resolve([ 'log_level' => 'warning', 'timeout' => 45.7, 'ssl' => true, ]); // Result: ['log_level' => 'warning', 'timeout' => 46, 'host' => 'localhost', // 'port' => 3306, 'ssl' => true, 'legacy_mode' => false] ``` ## Nested Options Use `setOptions()` to define nested option structures. The closure receives a new resolver for the nested options. ```php use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; $resolver = new OptionsResolver(); // Define nested options for database configuration $resolver->setOptions('database', function (OptionsResolver $nested, Options $parent) { $nested->setDefault('driver', 'mysql'); $nested->setAllowedValues('driver', ['mysql', 'pgsql', 'sqlite']); $nested->setRequired('name'); $nested->setAllowedTypes('name', 'string'); $nested->setDefault('host', 'localhost'); $nested->setDefault('port', function (Options $options) { return $options['driver'] === 'pgsql' ? 5432 : 3306; }); $nested->setDefined(['username', 'password']); $nested->setAllowedTypes('username', 'string'); $nested->setAllowedTypes('password', 'string'); }); // Define nested options for cache $resolver->setOptions('cache', function (OptionsResolver $nested) { $nested->setDefault('enabled', true); $nested->setAllowedTypes('enabled', 'bool'); $nested->setDefault('ttl', 3600); $nested->setAllowedTypes('ttl', 'int'); $nested->setDefault('driver', 'file'); $nested->setAllowedValues('driver', ['file', 'redis', 'memcached']); }); // Using fluent API for nested options $resolver ->define('logging') ->options(function (OptionsResolver $nested) { $nested ->define('enabled') ->default(true) ->allowedTypes('bool'); $nested ->define('path') ->default('/var/log/app.log') ->allowedTypes('string'); }); $resolved = $resolver->resolve([ 'database' => [ 'name' => 'myapp', 'username' => 'admin', 'password' => 'secret', ], 'cache' => [ 'driver' => 'redis', 'ttl' => 7200, ], ]); // Result: [ // 'database' => ['driver' => 'mysql', 'name' => 'myapp', 'host' => 'localhost', // 'port' => 3306, 'username' => 'admin', 'password' => 'secret'], // 'cache' => ['enabled' => true, 'ttl' => 7200, 'driver' => 'redis'], // 'logging' => ['enabled' => true, 'path' => '/var/log/app.log'] // ] ``` ## Prototype Arrays Use `setPrototype(true)` in nested options to allow arrays of identically structured items. ```php use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; $resolver = new OptionsResolver(); // Define prototype for connection pool $resolver->setOptions('connections', function (OptionsResolver $nested) { // Mark as prototype - each array element follows this structure $nested->setPrototype(true); $nested->setRequired('name'); $nested->setAllowedTypes('name', 'string'); $nested->setRequired('dsn'); $nested->setAllowedTypes('dsn', 'string'); $nested->setDefault('timeout', 30); $nested->setAllowedTypes('timeout', 'int'); $nested->setDefault('persistent', false); $nested->setAllowedTypes('persistent', 'bool'); }); // Define servers as prototype array $resolver->setOptions('servers', function (OptionsResolver $nested) { $nested->setPrototype(true); $nested ->define('host') ->required() ->allowedTypes('string'); $nested ->define('port') ->default(80) ->allowedTypes('int'); $nested ->define('weight') ->default(1) ->allowedTypes('int') ->allowedValues(function ($v) { return $v >= 1 && $v <= 100; }); }); $resolved = $resolver->resolve([ 'connections' => [ 'primary' => [ 'name' => 'Primary DB', 'dsn' => 'mysql:host=db1.example.com;dbname=app', 'timeout' => 60, ], 'replica' => [ 'name' => 'Read Replica', 'dsn' => 'mysql:host=db2.example.com;dbname=app', 'persistent' => true, ], ], 'servers' => [ ['host' => 'server1.example.com', 'port' => 8080, 'weight' => 10], ['host' => 'server2.example.com', 'weight' => 5], ['host' => 'server3.example.com', 'port' => 9000], ], ]); // Result: [ // 'connections' => [ // 'primary' => ['name' => 'Primary DB', 'dsn' => '...', 'timeout' => 60, 'persistent' => false], // 'replica' => ['name' => 'Read Replica', 'dsn' => '...', 'timeout' => 30, 'persistent' => true], // ], // 'servers' => [ // ['host' => 'server1.example.com', 'port' => 8080, 'weight' => 10], // ['host' => 'server2.example.com', 'port' => 80, 'weight' => 5], // ['host' => 'server3.example.com', 'port' => 9000, 'weight' => 1], // ] // ] ``` ## Deprecating Options Use `setDeprecated()` to mark options as deprecated. Triggers E_USER_DEPRECATED when the option is used. ```php use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; $resolver = new OptionsResolver(); // Simple deprecation with default message $resolver->setDefault('legacy_option', false); $resolver->setAllowedTypes('legacy_option', 'bool'); $resolver->setDeprecated('legacy_option', 'vendor/package', '2.0'); // Triggers: "Since vendor/package 2.0: The option "legacy_option" is deprecated." // Custom deprecation message $resolver->setDefault('old_name', null); $resolver->setDeprecated( 'old_name', 'vendor/package', '2.1', 'The option "%name%" is deprecated, use "new_name" instead.' ); // Conditional deprecation based on value $resolver->setDefault('format', 'json'); $resolver->setAllowedTypes('format', 'string'); $resolver->setAllowedValues('format', ['json', 'xml', 'yaml', 'csv']); $resolver->setDeprecated('format', 'vendor/package', '3.0', function (Options $options, $value) { if ($value === 'xml') { return 'The XML format is deprecated, use JSON instead.'; } if ($value === 'csv') { return 'The CSV format is deprecated and will be removed in 4.0.'; } return ''; // Empty string = no deprecation triggered }); // Deprecation based on other options $resolver->setDefault('use_cache', true); $resolver->setDefault('cache_ttl', 3600); $resolver->setDeprecated('cache_ttl', 'vendor/package', '2.5', function (Options $options, $value) { if (!$options['use_cache']) { return 'Setting "cache_ttl" has no effect when "use_cache" is false.'; } return ''; }); // Using fluent API $resolver ->define('deprecated_feature') ->default(false) ->allowedTypes('bool') ->deprecated('vendor/package', '2.0', 'The "%name%" option is deprecated.'); // Check if option is deprecated var_dump($resolver->isDeprecated('legacy_option')); // true var_dump($resolver->isDeprecated('format')); // true $resolved = $resolver->resolve([ 'legacy_option' => true, // Triggers deprecation notice 'format' => 'xml', // Triggers: "The XML format is deprecated..." ]); ``` ## Ignoring Undefined Options Use `setIgnoreUndefined()` to silently ignore unknown options instead of throwing exceptions. ```php use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; $resolver = new OptionsResolver(); $resolver->setDefault('known_option', 'value'); $resolver->setDefault('another_option', true); // By default, undefined options throw an exception try { $resolver->resolve([ 'known_option' => 'custom', 'undefined_option' => 'ignored', ]); } catch (UndefinedOptionsException $e) { echo $e->getMessage(); // 'The option "undefined_option" does not exist. Defined options are: "another_option", "known_option".' } // Enable ignoring undefined options $resolver->setIgnoreUndefined(true); // Now undefined options are silently ignored $resolved = $resolver->resolve([ 'known_option' => 'custom', 'undefined_option' => 'this will be ignored', 'extra_data' => ['also', 'ignored'], ]); // Result: ['known_option' => 'custom', 'another_option' => true] // Useful when extending configurations class ExtendedConfig { public function configure(OptionsResolver $resolver): void { $resolver->setIgnoreUndefined(true); // Allow subclasses to add options $resolver->setDefault('base_option', 'value'); } } // Using fluent API $resolver = new OptionsResolver(); $resolver ->define('option') ->default('value') ->ignoreUndefined(); // Enable for entire resolver $resolved = $resolver->resolve(['option' => 'test', 'unknown' => 'ignored']); // Result: ['option' => 'test'] ``` ## Option Information Use `setInfo()` and `getInfo()` to attach and retrieve descriptive information about options. ```php use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector; $resolver = new OptionsResolver(); // Set info for documentation/error messages $resolver->setDefault('api_key', null); $resolver->setRequired('api_key'); $resolver->setAllowedTypes('api_key', 'string'); $resolver->setInfo('api_key', 'Your API authentication key from the dashboard'); $resolver->setDefault('timeout', 30); $resolver->setAllowedTypes('timeout', 'int'); $resolver->setAllowedValues('timeout', function ($value) { return $value > 0 && $value <= 300; }); $resolver->setInfo('timeout', 'Request timeout in seconds (1-300)'); // Using fluent API $resolver ->define('retry_count') ->default(3) ->allowedTypes('int') ->allowedValues(function ($v) { return $v >= 0 && $v <= 10; }) ->info('Number of retry attempts (0-10)'); // Retrieve info echo $resolver->getInfo('api_key'); // 'Your API authentication key from the dashboard' echo $resolver->getInfo('timeout'); // 'Request timeout in seconds (1-300)' echo $resolver->getInfo('retry_count'); // 'Number of retry attempts (0-10)' // Info is included in validation error messages try { $resolver->resolve([ 'api_key' => 'valid-key', 'retry_count' => 99, // Invalid value ]); } catch (\Symfony\Component\OptionsResolver\Exception\InvalidOptionsException $e) { echo $e->getMessage(); // 'The option "retry_count" with value 99 is invalid. Info: Number of retry attempts (0-10).' } ``` ## Removing and Clearing Options Use `remove()` to delete specific options and `clear()` to reset the resolver completely. ```php use Symfony\Component\OptionsResolver\OptionsResolver; $resolver = new OptionsResolver(); // Setup initial options $resolver->setDefaults([ 'option_a' => 'value_a', 'option_b' => 'value_b', 'option_c' => 'value_c', ]); $resolver->setRequired('option_d'); $resolver->setAllowedTypes('option_a', 'string'); $resolver->setAllowedValues('option_b', ['value_b', 'other']); // Remove single option (removes all configuration for that option) $resolver->remove('option_a'); var_dump($resolver->isDefined('option_a')); // false var_dump($resolver->hasDefault('option_a')); // false // Remove multiple options $resolver->remove(['option_b', 'option_d']); var_dump($resolver->isDefined('option_b')); // false var_dump($resolver->isRequired('option_d')); // false // Only option_c remains $resolved = $resolver->resolve([]); // Result: ['option_c' => 'value_c'] // Clear all options $resolver->setDefaults(['x' => 1, 'y' => 2, 'z' => 3]); $resolver->clear(); var_dump($resolver->getDefinedOptions()); // [] var_dump($resolver->hasDefault('x')); // false $resolved = $resolver->resolve([]); // Result: [] ``` ## Debugging with OptionsResolverIntrospector Use `OptionsResolverIntrospector` to inspect the internal configuration of an `OptionsResolver` for debugging. ```php use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector; use Symfony\Component\OptionsResolver\Exception\NoConfigurationException; $resolver = new OptionsResolver(); $resolver->setDefault('name', 'default_name'); $resolver->setAllowedTypes('name', 'string'); $resolver->setAllowedValues('name', function ($v) { return strlen($v) > 0; }); $resolver->setInfo('name', 'The display name'); $resolver->setDefault('count', function (Options $options) { return 10; }); $resolver->setAllowedTypes('count', 'int'); $resolver->setNormalizer('count', function (Options $options, $value) { return max(0, $value); }); $resolver->setDefault('deprecated_opt', false); $resolver->setDeprecated('deprecated_opt', 'vendor/pkg', '1.0', 'Option %name% is deprecated.'); $resolver->setOptions('nested', function (OptionsResolver $nested) { $nested->setDefault('key', 'value'); }); // Create introspector $introspector = new OptionsResolverIntrospector($resolver); // Get default value $default = $introspector->getDefault('name'); echo $default; // 'default_name' // Get lazy closures $lazies = $introspector->getLazyClosures('count'); var_dump(count($lazies)); // 1 (array of closures) // Get allowed types $types = $introspector->getAllowedTypes('name'); var_dump($types); // ['string'] // Get allowed values (includes closures) $values = $introspector->getAllowedValues('name'); var_dump(count($values)); // 1 (closure validator) // Get normalizers $normalizers = $introspector->getNormalizers('count'); var_dump(count($normalizers)); // 1 (array of normalizer closures) // Get first normalizer only $normalizer = $introspector->getNormalizer('count'); var_dump($normalizer instanceof \Closure); // true // Get deprecation info $deprecation = $introspector->getDeprecation('deprecated_opt'); var_dump($deprecation); // ['package' => 'vendor/pkg', 'version' => '1.0', 'message' => 'Option %name% is deprecated.'] // Get nested option closures $nested = $introspector->getNestedOptions('nested'); var_dump(count($nested)); // 1 (array of configuration closures) // Handle missing configuration try { $introspector->getAllowedTypes('deprecated_opt'); // No types configured } catch (NoConfigurationException $e) { echo $e->getMessage(); // 'No allowed types were set for the "deprecated_opt" option.' } ``` ## Complete Configuration Example This example demonstrates a real-world configuration class using OptionsResolver for a mailer service. ```php use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class MailerConfiguration { private array $config; public function __construct(array $options = []) { $resolver = new OptionsResolver(); $this->configureOptions($resolver); $this->config = $resolver->resolve($options); } private function configureOptions(OptionsResolver $resolver): void { // Transport configuration $resolver ->define('transport') ->default('smtp') ->allowedTypes('string') ->allowedValues('smtp', 'sendmail', 'ses', 'mailgun') ->info('Mail transport driver'); // SMTP-specific nested options $resolver->setOptions('smtp', function (OptionsResolver $smtp, Options $parent) { $smtp ->define('host') ->default('localhost') ->allowedTypes('string'); $smtp ->define('port') ->default(function (Options $options) { return $options['encryption'] === 'ssl' ? 465 : 587; }) ->allowedTypes('int') ->allowedValues(function ($v) { return $v > 0 && $v <= 65535; }); $smtp ->define('encryption') ->default('tls') ->allowedTypes('string', 'null') ->allowedValues('tls', 'ssl', null); $smtp->setDefined(['username', 'password']); $smtp->setAllowedTypes('username', 'string'); $smtp->setAllowedTypes('password', 'string'); }); // Sender configuration $resolver ->define('from') ->required() ->allowedTypes('string', 'array') ->normalize(function (Options $options, $value) { if (is_string($value)) { return ['address' => $value, 'name' => '']; } return $value; }) ->info('Default sender email address'); // Retry configuration $resolver->setOptions('retry', function (OptionsResolver $retry) { $retry->setDefault('enabled', true); $retry->setAllowedTypes('enabled', 'bool'); $retry->setDefault('max_attempts', 3); $retry->setAllowedTypes('max_attempts', 'int'); $retry->setAllowedValues('max_attempts', function ($v) { return $v >= 1 && $v <= 10; }); $retry->setDefault('delay', 1000); $retry->setAllowedTypes('delay', 'int'); $retry->setInfo('delay', 'Delay between retries in milliseconds'); }); // Logging $resolver->setDefault('logging', true); $resolver->setAllowedTypes('logging', 'bool'); // Debug mode (deprecated) $resolver ->define('debug') ->default(false) ->allowedTypes('bool') ->deprecated('app/mailer', '2.0', 'Use "logging" option instead of "%name%".'); } public function get(string $key): mixed { return $this->config[$key] ?? null; } public function getSmtpDsn(): string { $smtp = $this->config['smtp']; $auth = ''; if (isset($smtp['username'])) { $auth = urlencode($smtp['username']); if (isset($smtp['password'])) { $auth .= ':' . urlencode($smtp['password']); } $auth .= '@'; } return sprintf('smtp://%s%s:%d', $auth, $smtp['host'], $smtp['port']); } } // Usage $mailer = new MailerConfiguration([ 'transport' => 'smtp', 'smtp' => [ 'host' => 'mail.example.com', 'encryption' => 'ssl', 'username' => 'user@example.com', 'password' => 'secret', ], 'from' => 'noreply@example.com', 'retry' => [ 'max_attempts' => 5, ], ]); echo $mailer->getSmtpDsn(); // 'smtp://user%40example.com:secret@mail.example.com:465' var_dump($mailer->get('from')); // ['address' => 'noreply@example.com', 'name' => ''] ``` ## Summary The Symfony OptionsResolver component excels at creating robust configuration systems for PHP applications. It's commonly used in Symfony forms, bundle configuration, service definitions, and any scenario requiring validated options with sensible defaults. The component integrates seamlessly with Symfony's dependency injection container and can be used standalone in any PHP project. Key integration patterns include extending base configurations in class hierarchies using lazy defaults and `setIgnoreUndefined()`, building form type options with type validation and normalization, and creating service factories that validate constructor parameters. The fluent API via `define()` provides a clean, readable syntax for option definitions, while the `OptionsResolverIntrospector` enables debugging and documentation generation from resolver configurations.