OptionsResolver — le contrat d'entrée de ta classe
Valider, normaliser et documenter les options d'une classe en un seul endroit — c'est le rôle d'OptionsResolver.
Imagine une classe Mailer qui accepte un tableau d'options : host, port,
timeout, encryption… Sans outillage, tu te retrouves à écrire à la main
une dizaine de lignes if (!isset($options['host'])) throw …,
puis des vérifications de type, puis des valeurs par défaut — du code répétitif, fragile et sans documentation.
Composant Symfony (
symfony/options-resolver) qui centralise la définition,
la validation et la normalisation des options d'une classe.
Un seul appel à resolve(array $options) valide tout et retourne un tableau propre.
Le pattern consiste à déclarer les options dans une méthode configureOptions()
(ou similaire), puis à appeler resolve() dans le constructeur ou dans une factory.
Le composant est utilisé en interne par tous les composants Symfony qui acceptent des options :
Form, HttpClient, Messenger, etc.
Visualise le pipeline — essaie différentes valeurs pour observer les étapes de validation et normalisation :
Lance resolve() pour voir les étapes.
Les avantages clés par rapport à une validation manuelle :
| Aspect | Validation manuelle | OptionsResolver |
|---|---|---|
| Options inconnues | Silencieuses | UndefinedOptionsException immédiate |
| Options manquantes | Notice / undefined key | MissingOptionsException explicite |
| Type erroné | À coder à la main | setAllowedTypes() — une ligne |
| Documentation | Dans un commentaire | setInfo() — incluse dans les erreurs |
| Déprécation | À coder manuellement | setDeprecated() — un appel |
Définir et résoudre — les fondations
setDefault, setRequired, setDefined — définir les options. resolve() — les valider toutes d'un coup.
Tout commence par un objet OptionsResolver dans lequel tu déclares tes options
avant d'appeler resolve(). Pense-y comme à un contrat signé avant que des données
n'entrent dans ta classe.
setDefined() — l'option est acceptée si fournie, mais pas obligatoire et sans valeur par défaut.setRequired() — l'option est obligatoire ; resolve() lève MissingOptionsException si absente.setDefault() — l'option est facultative avec une valeur de secours si non fournie.
use Symfony\Component\OptionsResolver\OptionsResolver; class HttpClient { public function __construct(array $options = []) { $resolver = new OptionsResolver(); $this->configureOptions($resolver); $this->options = $resolver->resolve($options); } private function configureOptions(OptionsResolver $resolver): void { // Obligatoire — MissingOptionsException si absent $resolver->setRequired('host'); // Facultatif avec valeur par défaut $resolver->setDefault('port', 80); $resolver->setDefault('timeout', 30); // Défini mais sans valeur — acceptable si fourni, pas obligatoire $resolver->setDefined(['proxy', 'auth_bearer']); } } // Utilisation new HttpClient(['host' => 'api.example.com', 'port' => 443]); // MissingOptionsException — 'host' est required new HttpClient(['port' => 443]); // UndefinedOptionsException — 'verify_ssl' n'est pas déclaré new HttpClient(['host' => 'api.example.com', 'verify_ssl' => true]);
setDefaults() est un raccourci pour définir plusieurs options par défaut en une seule fois —
exactement comme appeler setDefault() en boucle.
$resolver->setDefaults([ 'port' => 80, 'timeout' => 30, 'retries' => 3, ]);
setDefined() sans
setDefault(), l'option n'apparaît pas dans le tableau résolu quand elle n'est pas fournie.
Avec setDefault(), elle est toujours présente avec sa valeur de secours.
La méthode setIgnoreUndefined(true) permet d'assouplir le contrat : les options inconnues
sont silencieusement ignorées plutôt que de lever une exception.
À utiliser avec parcimonie — en général pour des sous-composants qui acceptent un tableau d'options
plus large que ce qu'ils consomment.
Validation — types et valeurs autorisées
setAllowedTypes filtre par type PHP ou FQCN. setAllowedValues accepte des valeurs statiques ou une closure.
Une option définie mais sans contrainte accepte n'importe quelle valeur — ce qui n'est souvent
pas ce que tu veux. setAllowedTypes() et setAllowedValues() verrouillent
le contrat.
Valide le type PHP de la valeur. Accepte les types scalaires natifs (
'string', 'int', 'bool', 'float', 'array', 'null'),
'callable', 'object', ou un FQCN (DateTimeInterface::class).
Pour un tableau de strings : 'string[]'.
Valide la valeur elle-même. Accepte une liste statique (enum de chaînes) ou une closure
function(mixed $value): bool. Retourner false déclenche
InvalidOptionsException.
$resolver // Types : string ou null (option nullable) ->setAllowedTypes('host', ['string', 'null']) // Type entier uniquement ->setAllowedTypes('port', 'int') // Valeur dans une liste finie ->setAllowedValues('encryption', ['tls', 'ssl', null]) // Validation custom : port dans la plage TCP valide ->setAllowedValues('port', function(int $port): bool { return $port >= 1 && $port <= 65535; }) // Tableau de strings : FQCN[] ->setAllowedTypes('middlewares', 'string[]') // Instance d'une interface ->setAllowedTypes('logger', ['null', \Psr\Log\LoggerInterface::class]);
L'ordre de validation est fixe : les types sont vérifiés avant les valeurs.
Si port reçoit "abc" (string), la validation de type échoue
avant même d'atteindre la closure de plage.
Cela signifie que dans ta closure setAllowedValues, tu peux supposer que le type est correct.
// FQCN — accepte une instance ou null $resolver->setDefault('transport', null); $resolver->setAllowedTypes('transport', ['null', TransportInterface::class]); // Enum PHP natif : passe directement l'instance $resolver->setAllowedValues('level', LogLevel::cases());
| Type string | Correspondance PHP |
|---|---|
'string' | is_string() |
'int' | is_int() |
'bool' | is_bool() |
'float' | is_float() |
'array' | is_array() |
'null' | is_null() |
'callable' | is_callable() |
'object' | is_object() |
'string[]' | array de strings |
| FQCN | instanceof |
Normalizers — transformer les valeurs après validation
Les normalizers s'exécutent après la validation — ils transforment sans valider. setNormalizer remplace, addNormalizer enchaîne.
La validation décide si une valeur est acceptable. La normalisation décide de sa forme définitive. Les deux étapes sont séparées : les normalizers s'exécutent après la validation des types et des valeurs.
Remplace tous les normalizers précédents pour cette option. La closure reçoit
Options $options (le resolver en lecture) et $value.
Elle retourne la valeur transformée.
Ajoute un normalizer à la chaîne sans effacer les précédents. Le paramètre
$forcePrepend = true insère en tête plutôt qu'en queue.
$resolver->setDefault('host', 'localhost'); // Premier normalizer : trim des espaces $resolver->setNormalizer('host', function(\Symfony\Options $options, string $value): string { return trim($value); }); // Deuxième normalizer (enchaîné) : lowercase $resolver->addNormalizer('host', function(\Symfony\Options $options, string $value): string { return strtolower($value); }); // ' MyHost ' → 'myhost' $resolved = $resolver->resolve(['host' => ' MyHost ']);
Le paramètre Options $options donne accès à d'autres options déjà résolues —
pratique pour construire une valeur composite.
// Normalizer qui lit une autre option $resolver->addNormalizer('base_url', function(\Symfony\Options $options, string $value): string { if ($options['port'] !== 80) { return $value . ':' . $options['port']; } return $value; });
setNormalizer() après
addNormalizer() sur la même option efface les normalizers ajoutés.
Si tu étends un resolver existant, préfère toujours addNormalizer().
Chaîne de normalizers — clique sur un normalizer pour l'ajouter, puis lance :
Lazy options — valeurs calculées à la demande
Une closure dans setDefault n'est évaluée qu'à la résolution — elle peut lire d'autres options déjà résolues.
Imagine que ta valeur par défaut n'est pas une constante, mais dépend d'une autre option — ou d'un calcul coûteux que tu ne veux exécuter que si l'option n'est pas fournie. C'est exactement le rôle des lazy options.
Passer une
Closure à setDefault(). La closure est évaluée
seulement lors de la résolution — jamais à la déclaration. Signature minimale :
function(Options $options): mixed.
use Symfony\Component\OptionsResolver\Options; $resolver->setDefault('host', 'localhost'); $resolver->setDefault('port', 3306); // dsn est calculé à partir de host et port déjà résolus $resolver->setDefault('dsn', function(Options $options): string { return 'mysql://' . $options['host'] . ':' . $options['port']; }); // Si port n'est pas fourni, dsn utilise 3306 // Si port est fourni (ex. 5432), dsn le reflète automatiquement
La signature longue — avec un second paramètre $previousValue — permet de
chaîner avec la valeur fournie par l'appelant : la closure reçoit la valeur passée
dans resolve() et peut la modifier ou la remplacer.
// Signature longue : accès à la valeur fournie par l'appelant $resolver->setDefault('tags', function(Options $options, ?array $previous): array { $base = ['app']; return array_merge($base, $previous ?? []); }); // Sans fournir 'tags' → ['app'] // Avec ['tags' => ['feature']] → ['app', 'feature']
OptionDefinitionException au moment de la résolution.
Le message indique la chaîne de dépendances impliquée.
resolve(). Ce qui signifie qu'un calcul coûteux (requête base
de données, lecture de fichier) dans un lazy default est court-circuité dès que l'appelant
passe la valeur.
Les lazy options peuvent accéder à d'autres lazy options — le resolver gère automatiquement
l'ordre d'évaluation en construisant le graphe de dépendances. En revanche, modifier le resolver
depuis l'intérieur d'une lazy closure est interdit et lève AccessException.
Options imbriquées et prototypes
setOptions() crée un sous-resolver pour une option tableau. setPrototype(true) applique le même schéma à chaque élément d'un array.
Pour des options qui sont elles-mêmes des tableaux structurés — configuration d'une connexion,
d'un transport, d'un serveur — setOptions() crée un sous-resolver
dédié plutôt que d'imbriquer des if/isset en cascade.
La closure reçoit un
OptionsResolver vierge pour le sous-tableau,
plus un objet Options $parent donnant accès aux options du niveau supérieur.
Le sous-resolver est configuré exactement comme un resolver racine.
$resolver->setOptions('database', function(OptionsResolver $sub, Options $parent): void { $sub->setDefault('host', 'localhost'); $sub->setRequired('name'); $sub->setDefault('port', function(Options $subOpts): int { // Accède au parent pour choisir le port par défaut return $parent['env'] === 'test' ? 5432 : 3306; }); $sub->setAllowedTypes('port', 'int'); }); // Utilisation $resolver->resolve([ 'env' => 'prod', 'database' => ['name' => 'my_db', 'host' => 'db.example.com'], ]);
Quand l'option est un tableau dont chaque élément doit respecter le même schéma, activer le mode prototype. Le sous-resolver est appliqué à chaque entrée du tableau, avec son index comme clé de contexte dans les messages d'erreur.
$resolver->setOptions('servers', function(OptionsResolver $sub): void { $sub->setPrototype(true); // active le mode prototype $sub->setRequired('hostname'); $sub->setDefault('port', 80); $sub->setAllowedTypes('port', 'int'); }); // Chaque élément est résolu séparément $resolver->resolve([ 'servers' => [ ['hostname' => 'web1.example.com'], ['hostname' => 'web2.example.com', 'port' => 8080], ], ]);
setOptions() seul
L'option est un unique tableau structuré. Exemple : database, cache, transport.
setOptions() + setPrototype(true)
L'option est une liste de tableaux ayant le même schéma. Exemple : servers[], middlewares[].
servers[1].port reçoit
une chaîne, l'exception indiquera The option "servers[1][port]" … — très pratique
pour déboguer des listes longues.
Déprecation et documentation des options
setDeprecated() émet un warning à l'utilisation. setInfo() documente l'option dans les messages d'erreur.
Au fil du temps, certaines options deviennent obsolètes ou changent de nom.
setDeprecated() émet un E_USER_DEPRECATED dès que l'option
est utilisée — sans casser le code existant.
setInfo() ajoute une phrase d'explication dans les messages d'erreur.
Signature :
setDeprecated(string $option, string $package, string $version, string|Closure $message).Le
$package est le nom Composer du paquet qui déprécie l'option.
Le $version est la version où la déprécation a été introduite.
// Déprecation simple — message statique $resolver->setDeprecated( 'timeout', 'acme/http-client', '2.0', 'L\'option "timeout" est dépréciée depuis 2.0, utilise "connect_timeout" et "read_timeout".' ); // Déprecation conditionnelle — closure // Retourner '' désactive la déprecation pour cette valeur $resolver->setDeprecated( 'encryption', 'acme/mailer', '3.0', function(Options $options, mixed $value): string { if ($value === 'ssl') { return 'La valeur "ssl" est obsolète, utilise "tls".'; } return ''; // pas de warning pour les autres valeurs } );
Ajoute une ligne de documentation affichée dans le message de l'exception quand l'option est invalide. Idéal pour guider l'utilisateur vers la bonne valeur.
$resolver->setInfo('log_level', 'Niveau PSR-3. Valeurs : debug, info, notice, warning, error, critical, alert, emergency.'); // Si log_level reçoit 'verbose', l'exception inclura : // Hint: Niveau PSR-3. Valeurs : debug, info, notice, …
isDeprecated(string $option) permet de vérifier depuis l'extérieur si une option
est marquée comme dépréciée — utile dans les outils de debug ou les tests qui
veulent s'assurer qu'aucune option dépréciée n'est utilisée.
setDeprecated() + un normalizer qui recopie sa valeur dans la nouvelle option.
L'ancien code continue de fonctionner, mais reçoit un warning clair.
// Ancienne option 'timeout' renommée en 'connect_timeout' $resolver->setDefined('timeout'); $resolver->setDefault('connect_timeout', 30); $resolver->setDeprecated('timeout', 'acme/http', '2.0', 'Utilise "connect_timeout".'); // Recopie automatique dans la nouvelle option $resolver->setNormalizer('connect_timeout', function(Options $options, int $value): int { if (isset($options['timeout'])) { return $options['timeout']; } return $value; });
define() — l'interface fluente
define() retourne un OptionConfigurator qui chaîne toutes les méthodes de configuration en une seule expression.
Toutes les méthodes vues jusqu'ici (setDefault(), setAllowedTypes(),
setAllowedValues(), setNormalizer(), setDeprecated(),
setInfo()) peuvent être remplacées par une interface fluente via define().
Retourne un
OptionConfigurator qui expose en méthodes chaînables toutes les
configurations d'une option. L'option est déclarée implicitement — pas besoin d'appeler
setDefined() avant.
// Style classique $resolver->setRequired('host'); $resolver->setAllowedTypes('host', 'string'); $resolver->setInfo('host', 'Nom d\'hôte ou IP du serveur cible.'); // Style fluent — équivalent exact $resolver->define('host') ->required() ->allowedTypes('string') ->info('Nom d\'hôte ou IP du serveur cible.');
L'interface fluente est particulièrement lisible pour les options avec plusieurs contraintes.
Voici un exemple complet de configureOptions() utilisant uniquement define() :
private function configureOptions(OptionsResolver $resolver): void { $resolver->define('host') ->required() ->allowedTypes('string') ->normalize(fn(Options $o, string $v) => strtolower(trim($v))) ->info('Adresse DNS ou IP du serveur.'); $resolver->define('port') ->default(80) ->allowedTypes('int') ->allowedValues(fn(int $p) => $p >= 1 && $p <= 65535) ->info('Port TCP (1-65535).'); $resolver->define('encryption') ->default(null) ->allowedValues([null, 'tls', 'ssl']) ->deprecated('ssl', 'acme/mailer', '3.0', 'Utilise "tls".') ->info('Protocole de chiffrement. null = pas de chiffrement.'); }
| Méthode standalone | Méthode OptionConfigurator |
|---|---|
setDefault($opt, $val) | ->default($val) |
setRequired($opt) | ->required() |
setAllowedTypes($opt, $types) | ->allowedTypes($types) |
setAllowedValues($opt, $vals) | ->allowedValues($vals) |
setNormalizer($opt, $fn) | ->normalize($fn) |
setDeprecated($opt, ...) | ->deprecated(...) |
setInfo($opt, $msg) | ->info($msg) |
setOptions($opt, $fn) | ->options($fn) |
define('host') deux fois sur le même
resolver lève OptionDefinitionException. Si tu veux reconfigurer une option
déjà définie (dans une sous-classe), utilise les méthodes standalone qui permettent
l'overriding partiel.
Exceptions — comprendre chaque erreur
Six exceptions, six situations — comprendre laquelle arrive quand permet de diagnostiquer instantanément.
OptionsResolver lève six types d'exceptions. Chacune correspond à une situation précise — les identifier instantanément évite de fouiller dans la stack trace.
| Exception | Quand | Exemple de déclencheur |
|---|---|---|
UndefinedOptionsException |
Option inconnue passée à resolve() |
resolve(['verify_ssl' => true]) alors que verify_ssl n'est pas déclarée |
MissingOptionsException |
Option required absente de resolve() |
setRequired('host') + resolve([]) |
InvalidOptionsException |
Type ou valeur invalide | setAllowedTypes('port','int') + resolve(['port' => 'abc']) |
OptionDefinitionException |
Dépendance circulaire entre lazy ou define() doublon |
Option A lazy lit B, option B lazy lit A |
AccessException |
Modification du resolver après verrouillage (dans une lazy/normalizer) | $resolver->setDefault(…) depuis l'intérieur d'un normalizer |
NoSuchOptionException |
Accès lazy à une option non définie via $options['x'] |
Faute de frappe dans le nom d'option lu dans une lazy closure |
Tu peux attraper
ExceptionInterface pour gérer toutes les erreurs
OptionsResolver en un seul catch, ou attraper chaque exception
individuellement pour une réponse différenciée.
use Symfony\Component\OptionsResolver\Exception\ExceptionInterface; use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; try { $resolved = $resolver->resolve($options); } catch (MissingOptionsException $e) { // Champ obligatoire absent — feedback utilisateur throw new \InvalidArgumentException('Configuration incomplète : ' . $e->getMessage()); } catch (InvalidOptionsException $e) { // Valeur incorrecte — logguer et remonter $this->logger->error('Options invalides', ['error' => $e->getMessage()]); throw $e; } catch (ExceptionInterface $e) { // Toutes les autres erreurs OptionsResolver throw $e; }
InvalidOptionsException liste les
valeurs acceptées et, si setInfo() a été appelé, ajoute le hint.
UndefinedOptionsException suggère les options proches via la distance de Levenshtein —
une faute de frappe donne un message du type "Did you mean …?".
Sous le capot — la mécanique de resolve()
Un verrou, un clone, et des closures évaluées dans le désordre : la vraie mécanique derrière resolve().
Les sections précédentes t'ont montré l'API : quelles méthodes appeler pour
déclarer, valider, normaliser. Cette section montre la machine — ce qui se passe
réellement à l'intérieur de resolve() et pourquoi l'ordre de résolution
n'est jamais celui que tu déclares, mais celui que tes options s'imposent entre elles.
La première ligne utile de
resolve() est $clone = clone $this.
Toute la résolution se joue sur ce clone : les valeurs fournies écrasent les défauts,
le clone est marqué locked, puis chaque option est résolue.
L'objet d'origine, lui, reste intact et déverrouillé — c'est pour ça que tu peux appeler
resolve() plusieurs fois sur le même resolver, avec des entrées différentes.
// Dans OptionsResolver::resolve() if ($this->locked) { throw new AccessException('Options cannot be resolved from a lazy option or normalizer.'); } $clone = clone $this; // l'original reste réutilisable // ... injection des valeurs fournies, contrôle des required ... $clone->locked = true; // à partir d'ici, plus aucune mutation foreach ($clone->defaults as $option => $_) { $clone->offsetGet($option); // résout l'option ET ses dépendances }
Une fois locked à true, tous les mutateurs
(setDefault, setRequired, setNormalizer,
setOptions…) lèvent une AccessException. Le but est de garantir
qu'une closure lazy ne puisse pas redéfinir le contrat pendant qu'on l'applique —
ce qui donnerait des résultats dépendant de l'ordre d'évaluation, donc imprévisibles.
class OptionsResolver implements Options. L'objet Options que reçoit
une closure lazy ou un normalizer n'est pas un conteneur séparé : c'est le clone verrouillé
lui-même. Quand tu écris $options['port'] dans une lazy option, tu appelles
offsetGet('port') sur ce clone.
C'est la clé de la résolution paresseuse dirigée par les dépendances.
La boucle de resolve() parcourt les options dans leur ordre de déclaration,
mais offsetGet() mémoïse chaque résultat dans un tableau resolved :
une option n'est calculée qu'une seule fois. Si une lazy option lit
$options['host'] avant que la boucle n'atteigne host,
alors host est résolu à cet instant et mis en cache. L'ordre réel
d'évaluation émerge donc du graphe de dépendances, pas de l'ordre où tu as appelé
setDefault().
Ce même graphe est protégé contre les boucles. Avant d'évaluer une closure (lazy, nested
ou normalizer), offsetGet() pose un drapeau calling[$option] = true
dans un bloc try/finally. Si la résolution ré-entre la même option alors que son
drapeau est encore posé, c'est qu'il y a un cycle : OptionDefinitionException,
avec la liste exacte des options impliquées.
À l'intérieur d'
offsetGet(), une option traverse toujours ces étapes dans cet ordre :
setAllowedTypes() et setAllowedValues()
voient la valeur produite par la lazy closure, mais jamais la valeur normalisée.
Un normalizer qui renverrait une valeur hors contrat ne serait pas rattrapé — la validation
est déjà passée. Valide dans la lazy option, formate dans le normalizer, pas l'inverse.
setDefault() ne traite une closure comme
lazy que si son premier paramètre est typé exactement Options. La détection
passe par la réflexion : un type absent, un type natif, ou un type union
(Options|Foo) ne matchent pas. Dans ce cas la closure est stockée telle quelle,
comme valeur littérale, et n'est jamais évaluée — $options['x'] te renvoie alors
l'objet Closure au lieu du résultat. Bug silencieux classique.
// LAZY — 1er paramètre typé Options → évaluée à la résolution $resolver->setDefault('dsn', function (Options $o): string { return 'mysql://' . $o['host']; }); // PAS lazy — pas de type → la Closure est stockée telle quelle $resolver->setDefault('dsn', function ($o): string { return 'mysql://' . $o['host']; // jamais appelée }); // $options['dsn'] renvoie ici l'objet Closure, pas la chaîne
Enfin, les options imbriquées suivent la même logique paresseuse. Le sous-resolver n'est pas
construit à la déclaration : c'est offsetGet() qui fait new self()
au moment où l'option parent est résolue, puis exécute la closure de setOptions()
et résout le sous-tableau. Les options enfants n'existent donc que lorsque leur parent est
lui-même atteint dans le graphe.
Traceur de résolution
Lance la résolution et observe l'ordre réel des appels à offsetGet() :
la mémoïsation, les dépendances qui se résolvent avant leur tour, et la détection de cycle.
Synthèse — guide de construction d'un resolver
Ordre de construction, guide de choix entre setNormalizer/setAllowedValues/lazy — tout en un tableau.
Voici un récapitulatif de tout ce que tu peux faire avec OptionsResolver, présenté dans l'ordre logique de construction d'un resolver robuste.
| Étape | Méthode(s) | Quand l'utiliser |
|---|---|---|
| 1. Déclarer les options | setDefined() / setRequired() / setDefault() |
Toujours — c'est la base du contrat |
| 2. Valider les types | setAllowedTypes() |
Dès qu'une option a un type précis (toujours, en pratique) |
| 3. Valider les valeurs | setAllowedValues() |
Enum, plages, expressions régulières, validation métier |
| 4. Normaliser | setNormalizer() / addNormalizer() |
Trim, lowercase, cast, construction d'objet à partir de scalaires |
| 5. Valeurs calculées | setDefault(Closure) — lazy |
Valeur dérivée d'une autre option, calcul coûteux optionnel |
| 6. Structure profonde | setOptions() + setPrototype() |
Option qui est elle-même un tableau structuré ou une liste |
| 7. Documenter | setInfo() |
Toujours — enrichit les messages d'erreur pour les utilisateurs |
| 8. Déprécier | setDeprecated() |
Renommage ou suppression progressive d'une option |
setAllowedValues — pour rejeter une valeur invalide.setNormalizer — pour transformer une valeur valide en une forme canonique.setDefault(Closure) — pour calculer une valeur par défaut dépendant du contexte.
Un pattern courant dans les composants Symfony est d'utiliser configureOptions()
comme seul point de configuration, et de l'appeler dans le constructeur.
Les sous-classes peuvent appeler parent::configureOptions($resolver) puis
ajouter leurs propres options avec addAllowedTypes() ou addNormalizer()
sans remplacer ce qu'a fait le parent.
class BaseTransport { protected function configureOptions(OptionsResolver $resolver): void { $resolver->define('timeout')->default(30)->allowedTypes('int'); } } class SslTransport extends BaseTransport { protected function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); $resolver->define('verify_peer')->default(true)->allowedTypes('bool'); $resolver->define('ca_file')->default(null)->allowedTypes(['null', 'string']); } }
composer require symfony/options-resolver.
Le composant est autonome — pas besoin du framework complet.
Il est inclus automatiquement dans les projets Symfony via symfony/symfony.
Applique OptionsResolver dans tes propres classes de configuration — remplace tes tableaux d'options validés à la main par un resolver dédié.