diff --git a/components/options_resolver.rst b/components/options_resolver.rst index eaad4bc7eb1..ea1372f4ea6 100644 --- a/components/options_resolver.rst +++ b/components/options_resolver.rst @@ -5,8 +5,7 @@ The OptionsResolver Component ============================= - The OptionsResolver component helps you configure objects with option - arrays. It supports default values, option constraints and lazy options. + The OptionsResolver component is `array_replace()` on steroids. Installation ------------ @@ -16,14 +15,19 @@ You can install the component in 2 different ways: * :doc:`Install it via Composer ` (``symfony/options-resolver`` on `Packagist`_); * Use the official Git repository (https://github.com/symfony/OptionsResolver). +Notes on Previous Versions +-------------------------- + +.. versionadded:: 2.6 + This documentation was written for Symfony 2.6 and later. If you use an older + version, please read the corresponding documentation using the version + drop-down on the upper right. + Usage ----- -Imagine you have a ``Mailer`` class which has 2 options: ``host`` and -``password``. These options are going to be handled by the OptionsResolver -Component. - -First, create the ``Mailer`` class:: +Imagine you have a ``Mailer`` class which has four options: ``host``, +``username``, ``password`` and ``port``:: class Mailer { @@ -31,72 +35,124 @@ First, create the ``Mailer`` class:: public function __construct(array $options = array()) { + $this->options = $options; } } -You could of course set the ``$options`` value directly on the property. Instead, -use the :class:`Symfony\\Component\\OptionsResolver\\OptionsResolver` class -and let it resolve the options by calling -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::resolve`. -The advantages of doing this will become more obvious as you continue:: +When accessing the ``$options``, you need to add a lot of boilerplate code to +check which options are set:: - use Symfony\Component\OptionsResolver\OptionsResolver; - - // ... - public function __construct(array $options = array()) + class Mailer { - $resolver = new OptionsResolver(); - - $this->options = $resolver->resolve($options); + // ... + public function sendMail($from, $to) + { + $mail = ...; + $mail->setHost(isset($this->options['host']) + ? $this->options['host'] + : 'smtp.example.org'); + $mail->setUsername(isset($this->options['username']) + ? $this->options['username'] + : 'user'); + $mail->setPassword(isset($this->options['password']) + ? $this->options['password'] + : 'pa$$word'); + $mail->setPort(isset($this->options['port']) + ? $this->options['port'] + : 25); + // ... + } } -The options property now is a well defined array with all resolved options -readily available:: +This boilerplate is hard to read and repetitive. Also, the default values of the +options are buried in the business logic of your code. We can use +:phpfunction:`array_replace` to fix that:: - // ... - public function sendMail($from, $to) + class Mailer { - $mail = ...; - $mail->setHost($this->options['host']); - $mail->setUsername($this->options['username']); - $mail->setPassword($this->options['password']); // ... + public function __construct(array $options = array()) + { + $this->options = array_replace(array( + 'host' => 'smtp.example.org', + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, + ), $options); + } } -Configuring the OptionsResolver -------------------------------- +Now all four options are guaranteed to be set. But what happens if the user of +the ``Mailer`` class does a mistake? -Now, try to actually use the class:: +.. code-block:: php $mailer = new Mailer(array( - 'host' => 'smtp.example.org', - 'username' => 'user', - 'password' => 'pa$$word', + 'usernme' => 'johndoe', )); -Right now, you'll receive a -:class:`Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException`, -which tells you that the options ``host`` and ``password`` do not exist. -This is because you need to configure the ``OptionsResolver`` first, so it -knows which options should be resolved. +No error will be shown. In the best case, the bug will be appear during testing. +The developer will possibly spend a lot of time looking for the problem. In the +worst case, however, the bug won't even appear and will be deployed to the live +system. + +Let's use the :class:`Symfony\\Component\\OptionsResolver\\OptionsResolver` +class to fix this problem:: + + use Symfony\Component\OptionsResolver\Options; + + class Mailer + { + // ... + public function __construct(array $options = array()) + { + $resolver = new OptionsResolver(); + $resolver->setDefaults(array( + 'host' => 'smtp.example.org', + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, + )); + + $this->options = $resolver->resolve($options); + } + } -.. tip:: +Like before, all options will be guaranteed to be set. Additionally, an +:class:`Symfony\\Component\\OptionsResolver\\Exception\\UndefinedOptionsException` +is thrown if an unknown option is passed:: - To check if an option exists, you can use the - :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isKnown` - function. + $mailer = new Mailer(array( + 'usernme' => 'johndoe', + )); -A best practice is to put the configuration in a method (e.g. -``configureOptions``). You call this method in the constructor to configure -the ``OptionsResolver`` class:: + // UndefinedOptionsException: The option "usernme" does not exist. Known + // options are: "host", "password", "port", "username" - use Symfony\Component\OptionsResolver\OptionsResolver; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; +The rest of your code can access the values of the options without boilerplate +code:: + // ... class Mailer { - protected $options; + // ... + public function sendMail($from, $to) + { + $mail = ...; + $mail->setHost($this->options['host']); + $mail->setUsername($this->options['username']); + $mail->setPassword($this->options['password']); + $mail->setPort($this->options['port']); + // ... + } + } +It's a good practice to split the option configuration into a separate method:: + + // ... + class Mailer + { + // ... public function __construct(array $options = array()) { $resolver = new OptionsResolver(); @@ -105,322 +161,555 @@ the ``OptionsResolver`` class:: $this->options = $resolver->resolve($options); } - protected function configureOptions(OptionsResolverInterface $resolver) + protected function configureOptions(OptionsResolver $resolver) { - // ... configure the resolver, you will learn this - // in the sections below + $resolver->setDefaults(array( + 'host' => 'smtp.example.org', + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, + 'encryption' => null, + )); } } -Set default Values -~~~~~~~~~~~~~~~~~~ +First, your code becomes easier to read, especially if the constructor does more +than processing options. Second, sub-classes may now override the +``configureOptions()`` method to adjust the configuration of the options:: + + // ... + class GoogleMailer extends Mailer + { + protected function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + $resolver->setDefaults(array( + 'host' => 'smtp.google.com', + 'encryption' => 'ssl', + )); + } + } + +Required Options +~~~~~~~~~~~~~~~~ + +If an option must be set by the caller, pass that option to +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setRequired`. +For example, let's make the ``host`` option required:: + + // ... + class Mailer + { + // ... + protected function configureOptions(OptionsResolver $resolver) + { + // ... + $resolver->setRequired('host'); + } + } -Most of the options have a default value. You can configure these options by -calling :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setDefaults`:: +.. versionadded:: 2.6 + Before Symfony 2.6, `setRequired()` accepted only arrays. Since then, single + option names can be passed as well. + +If you omit a required option, a +:class:`Symfony\\Component\\OptionsResolver\\Exception\\MissingOptionsException` +will be thrown:: + + $mailer = new Mailer(); + + // MissingOptionsException: The required option "host" is missing. + +The :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setRequired` +method accepts a single name or an array of option names if you have more than +one required option:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { // ... + protected function configureOptions(OptionsResolver $resolver) + { + // ... + $resolver->setRequired(array('host', 'username', 'password')); + } + } - $resolver->setDefaults(array( - 'username' => 'root', - )); +.. versionadded:: 2.6 + The methods :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isRequired` + and :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::getRequiredOptions` + were introduced in Symfony 2.6. + +Use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isRequired` to find +out if an option is required. You can use +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::getRequiredOptions` to +retrieve the names of all required options:: + + // ... + class GoogleMailer extends Mailer + { + protected function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + if ($resolver->isRequired('host')) { + // ... + } + + $requiredOptions = $resolver->getRequiredOptions(); + } } -This would add an option - ``username`` - and give it a default value of -``root``. If the user passes in a ``username`` option, that value will -override this default. You don't need to configure ``username`` as an optional -option. +.. versionadded:: 2.6 + The methods :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isMissing` + and :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::getMissingOptions` + were introduced in Symfony 2.6. -Required Options -~~~~~~~~~~~~~~~~ +If you want to check whether a required option is still missing from the default +options, you can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isMissing`. +The difference to :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isRequired` +is that this method will return false for required options that have already +been set:: + + // ... + class Mailer + { + // ... + protected function configureOptions(OptionsResolver $resolver) + { + // ... + $resolver->setRequired('host'); + } + } + + // ... + class GoogleMailer extends Mailer + { + protected function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + $resolver->isRequired('host'); + // => true + + $resolver->isMissing('host'); + // => true -The ``host`` option is required: the class can't work without it. You can set -the required options by calling -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setRequired`:: + $resolver->setDefault('host', 'smtp.google.com'); + + $resolver->isRequired('host'); + // => true + + $resolver->isMissing('host'); + // => false + } + } + +The method :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::getMissingOptions` +lets you access the names of all missing options. + +Type Validation +~~~~~~~~~~~~~~~ + +You can run additional checks on the options to make sure they were passed +correctly. To validate the types of the options, call +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setAllowedTypes`:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { - $resolver->setRequired(array('host')); + // ... + protected function configureOptions(OptionsResolver $resolver) + { + // ... + $resolver->setAllowedTypes('host', 'string'); + $resolver->setAllowedTypes('port', array('null', 'int')); + } } -You are now able to use the class without errors:: +For each option, you can define either just one type or an array of acceptable +types. You can pass any type for which an ``is_()`` method is defined. +Additionally, you may pass fully qualified class or interface names. + +If you pass an invalid option now, an +:class:`Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException` +is thrown:: $mailer = new Mailer(array( - 'host' => 'smtp.example.org', + 'host' => 25, )); - echo $mailer->getHost(); // 'smtp.example.org' + // InvalidOptionsException: The option "host" with value "25" is expected to + // be of type "string" -If you don't pass a required option, a -:class:`Symfony\\Component\\OptionsResolver\\Exception\\MissingOptionsException` -will be thrown. +In sub-classes, you can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedTypes` +to add additional allowed types without erasing the ones already set. + +.. versionadded:: 2.6 + Before Symfony 2.6, `setAllowedTypes()` and `addAllowedTypes()` expected + the values to be given as an array mapping option names to allowed types: -.. tip:: + .. code-block:: php - To determine if an option is required, you can use the - :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isRequired` - method. + $resolver->setAllowedTypes(array('port' => array('null', 'int'))); -Optional Options +Value Validation ~~~~~~~~~~~~~~~~ -Sometimes, an option can be optional (e.g. the ``password`` option in the -``Mailer`` class), but it doesn't have a default value. You can configure -these options by calling -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setOptional`:: +Some options can only take one of a fixed list of predefined values. For +example, suppose the ``Mailer`` class has a ``transport`` option which can be +one of ``sendmail``, ``mail`` and ``smtp``. Use the method +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setAllowedValues` to verify +that the passed option contains one of these values:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { // ... - - $resolver->setOptional(array('password')); + protected function configureOptions(OptionsResolver $resolver) + { + // ... + $resolver->setDefault('transport', 'sendmail'); + $resolver->setAllowedValues('transport', array('sendmail', 'mail', 'smtp')); + } } -Options with defaults are already marked as optional. +If you pass an invalid transport, an +:class:`Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException` +is thrown:: -.. tip:: + $mailer = new Mailer(array( + 'transport' => 'send-mail', + )); - When setting an option as optional, you can't be sure if it's in the array - or not. You have to check if the option exists before using it. + // InvalidOptionsException: The option "transport" has the value "send-mail", + // but is expected to be one of "sendmail", "mail", "smtp" - To avoid checking if it exists everytime, you can also set a default of - ``null`` to an option using the ``setDefaults()`` method (see `Set Default Values`_), - this means the element always exists in the array, but with a default of - ``null``. +For options with more complicated validation schemes, pass a closure which +returns ``true`` for acceptable values and ``false`` for invalid values:: -Default Values that Depend on another Option -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + $resolver->setAllowedValues(array( + // ... + $resolver->setAllowedValues('transport', function ($value) { + // return true or false + }); + )); -Suppose you add a ``port`` option to the ``Mailer`` class, whose default -value you guess based on the encryption. You can do that easily by using a -closure as the default value:: +In sub-classes, you can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedValues` +to add additional allowed values without erasing the ones already set. - use Symfony\Component\OptionsResolver\Options; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; +.. versionadded:: 2.6 + Before Symfony 2.6, `setAllowedValues()` and `addAllowedValues()` expected + the values to be given as an array mapping option names to allowed values: + + .. code-block:: php + + $resolver->setAllowedValues(array('transport' => array('sendmail', 'mail', 'smtp'))); + +Option Normalization +~~~~~~~~~~~~~~~~~~~~ + +Sometimes, option values need to be normalized before you can use them. For +instance, assume that the ``host`` should always start with ``http://``. To do +that, you can write normalizers. Normalizers are executed after validating an +option. You can configure a normalizer by calling +:method:`Symfony\\Components\\OptionsResolver\\OptionsResolver::setNormalizer`:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { // ... - - $resolver->setDefaults(array( - 'encryption' => null, - 'port' => function (Options $options) { - if ('ssl' === $options['encryption']) { - return 465; + protected function configureOptions(OptionsResolver $resolver) + { + // ... + $resolver->setNormalizer('host', function ($options, $value) { + if ('http://' !== substr($value, 0, 7)) { + $value = 'http://'.$value; } - return 25; - }, - )); + return $value; + }); + } } -The :class:`Symfony\\Component\\OptionsResolver\\Options` class implements -:phpclass:`ArrayAccess`, :phpclass:`Iterator` and :phpclass:`Countable`. That -means you can handle it just like a normal array containing the options. +.. versionadded:: 2.6 + The method :method:`Symfony\\Components\\OptionsResolver\\OptionsResolver::setNormalizer` + was introduced in Symfony 2.6. Before, you had to use + :method:`Symfony\\Components\\OptionsResolver\\OptionsResolver::setNormalizers`. -.. caution:: +The normalizer receives the actual ``$value`` and returns the normalized form. +You see that the closure also takes an ``$options`` parameter. This is useful +if you need to use other options during normalization:: - The first argument of the closure must be typehinted as ``Options``, - otherwise it is considered as the value. + // ... + class Mailer + { + // ... + protected function configureOptions(OptionsResolver $resolver) + { + // ... + $resolver->setNormalizer('host', function ($options, $value) { + if (!in_array(substr($value, 0, 7), array('http://', 'https://'))) { + if ('ssl' === $options['encryption']) { + $value = 'https://'.$value; + } else { + $value = 'http://'.$value; + } + } -Overwriting default Values -~~~~~~~~~~~~~~~~~~~~~~~~~~ + return $value; + }); + } + } -A previously set default value can be overwritten by invoking -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setDefaults` -again. When using a closure as the new value it is passed 2 arguments: +Default Values that Depend on another Option +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* ``$options``: an :class:`Symfony\\Component\\OptionsResolver\\Options` - instance with all the other default options -* ``$previousValue``: the previous set default value +Suppose you want to set the default value of the ``port`` option based on the +encryption chosen by the user of the ``Mailer`` class. More precisely, we want +to set the port to ``465`` if SSL is used and to ``25`` otherwise. -.. code-block:: php +You can implement this feature by passing a closure as default value of the +``port`` option. The closure receives the options as argument. Based on these +options, you can return the desired default value:: use Symfony\Component\OptionsResolver\Options; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { // ... - $resolver->setDefaults(array( - 'encryption' => 'ssl', - 'host' => 'localhost', - )); + protected function configureOptions(OptionsResolver $resolver) + { + // ... + $resolver->setDefault('encryption', null); - // ... - $resolver->setDefaults(array( - 'encryption' => 'tls', // simple overwrite - 'host' => function (Options $options, $previousValue) { - return 'localhost' == $previousValue - ? '127.0.0.1' - : $previousValue; - }, - )); + $resolver->setDefault('port', function (Options $options) { + if ('ssl' === $options['encryption']) { + return 465; + } + + return 25; + }); + } } -.. tip:: +.. caution:: - If the previous default value is calculated by an expensive closure and - you don't need access to it, you can use the - :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::replaceDefaults` - method instead. It acts like ``setDefaults`` but simply erases the - previous value to improve performance. This means that the previous - default value is not available when overwriting with another closure:: + The argument of the callable must be type hinted as ``Options``. Otherwise, + the callable is considered as the default value of the option. - use Symfony\Component\OptionsResolver\Options; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; +.. note:: + The closure is only executed if the ``port`` option isn't set by the user + or overwritten in a sub-class. + +A previously set default value can be accessed by adding a second argument to +the closure:: + + // ... + class Mailer + { // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + protected function configureOptions(OptionsResolver $resolver) { // ... $resolver->setDefaults(array( - 'encryption' => 'ssl', - 'heavy' => function (Options $options) { - // Some heavy calculations to create the $result - - return $result; - }, + 'encryption' => null, + 'host' => 'example.org', )); + } + } - $resolver->replaceDefaults(array( - 'encryption' => 'tls', // simple overwrite - 'heavy' => function (Options $options) { - // $previousValue not available - // ... + class GoogleMailer extends Mailer + { + protected function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); - return $someOtherResult; - }, - )); - } + $options->setDefault('host', function (Options $options, $previousValue) { + if ('ssl' === $options['encryption']) { + return 'secure.example.org' + } -.. note:: + // Take default value configured in the base class + return $previousValue; + }); + } + } - Existing option keys that you do not mention when overwriting are preserved. +As seen in the example, this feature is mostly useful if you want to reuse the +default values set in parent classes in sub-classes. -Configure Allowed Values -~~~~~~~~~~~~~~~~~~~~~~~~ +Options without Default Values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Not all values are valid values for options. Suppose the ``Mailer`` class has -a ``transport`` option, it can only be one of ``sendmail``, ``mail`` or -``smtp``. You can configure these allowed values by calling -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setAllowedValues`:: +In some cases, it is useful to define an option without setting a default value. +Mostly, you will need this when you want to know whether an option was passed +or not. If you set a default value for that option, this is not possible:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { // ... + protected function configureOptions(OptionsResolver $resolver) + { + // ... + $resolver->setDefault('port', 25); + } - $resolver->setAllowedValues(array( - 'encryption' => array(null, 'ssl', 'tls'), - )); + // ... + public function sendMail($from, $to) + { + // Is this the default value or did the caller of the class really + // set the port to 25? + if (25 === $this->options['port']) { + // ... + } + } } -There is also an -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedValues` -method, which you can use if you want to add an allowed value to the previously -configured allowed values. +.. versionadded:: 2.6 + The method :method:`Symfony\\Components\\OptionsResolver\\OptionsResolver::setDefined` + was introduced in Symfony 2.6. Before, you had to use + :method:`Symfony\\Components\\OptionsResolver\\OptionsResolver::setOptional`. -.. versionadded:: 2.5 - The callback support for allowed values was introduced in Symfony 2.5. - -If you need to add some more logic to the value validation process, you can pass a callable -as an allowed value:: +You can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setDefined` +to define an option without setting a default value. Then the option will only +be included in the resolved options if it was actually passed to +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::resolve`:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { // ... + protected function configureOptions(OptionsResolver $resolver) + { + // ... + $resolver->setDefined('port'); + } - $resolver->setAllowedValues(array( - 'transport' => function($value) { - return false !== strpos($value, 'mail'); - }, - )); + // ... + public function sendMail($from, $to) + { + if (array_key_exists('port', $this->options)) { + echo 'Set!'; + } else { + echo 'Not Set!'; + } + } } -.. caution:: - - Note that using this together with ``addAllowedValues`` will not work. + $mailer = new Mailer(); + $mailer->sendMail($from, $to); + // => Not Set! -Configure Allowed Types -~~~~~~~~~~~~~~~~~~~~~~~ + $mailer = new Mailer(array( + 'port' => 25, + )); + $mailer->sendMail($from, $to); + // => Set! -You can also specify allowed types. For instance, the ``port`` option can -be anything, but it must be an integer. You can configure these types by calling -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setAllowedTypes`:: +You can also pass an array of option names if you want to define multiple +options in one go:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { // ... - - $resolver->setAllowedTypes(array( - 'port' => 'integer', - )); + protected function configureOptions(OptionsResolver $resolver) + { + // ... + $resolver->setDefined(array('port', 'encryption')); + } } -Possible types are the ones associated with the ``is_*`` PHP functions or a -class name. You can also pass an array of types as the value. For instance, -``array('null', 'string')`` allows ``port`` to be ``null`` or a ``string``. +.. versionadded:: 2.6 + The method :method:`Symfony\\Components\\OptionsResolver\\OptionsResolver::isDefined` + and :method:`Symfony\\Components\\OptionsResolver\\OptionsResolver::getDefinedOptions` + were introduced in Symfony 2.6. + +The methods :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isDefined` +and :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::getDefinedOptions` +let you find out which options are defined:: + + // ... + class GoogleMailer extends Mailer + { + protected function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + if ($resolver->isDefined('host')) { + // One of the following was called: -There is also an -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedTypes` -method, which you can use to add an allowed type to the previous allowed types. + // $resolver->setDefault('host', ...); + // $resolver->setRequired('host'); + // $resolver->setDefined('host'); + } -Normalize the Options -~~~~~~~~~~~~~~~~~~~~~ + $definedOptions = $resolver->getDefinedOptions(); + } + } -Some values need to be normalized before you can use them. For instance, -pretend that the ``host`` should always start with ``http://``. To do that, -you can write normalizers. These closures will be executed after all options -are passed and should return the normalized value. You can configure these -normalizers by calling -:method:`Symfony\\Components\\OptionsResolver\\OptionsResolver::setNormalizers`:: +Performance Tweaks +~~~~~~~~~~~~~~~~~~ + +With the current implementation, the ``configureOptions()`` method will be +called for every single instance of the ``Mailer`` class. Depending on the +amount of option configuration and the number of created instances, this may add +noticeable overhead to your application. If that overhead becomes a problem, you +can change your code to do the configuration only once per class:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { - // ... + private static $resolversByClass = array(); - $resolver->setNormalizers(array( - 'host' => function (Options $options, $value) { - if ('http://' !== substr($value, 0, 7)) { - $value = 'http://'.$value; - } + protected $options; - return $value; - }, - )); + public function __construct(array $options = array()) + { + // Are we a Mailer, a GoogleMailer, ... ? + $class = get_class($this); + + // Did we call configureOptions() before for this class? + if (!isset(self::$resolversByClass[$class])) { + self::$resolversByClass[$class] = new OptionsResolver(); + $this->configureOptions(self::$resolversByClass[$class]); + } + + $this->options = self::$resolversByClass[$class]->resolve($options); + } + + protected function configureOptions(OptionsResolver $resolver) + { + // ... + } } -You see that the closure also gets an ``$options`` parameter. Sometimes, you -need to use the other options for normalizing:: +Now the :class:`Symfony\\Component\\OptionsResolver\\OptionsResolver` instance +will be created once per class and reused from that on. Be aware that this may +lead to memory leaks in long-running applications, if the default options contain +references to objects or object graphs. If that's the case for you, implement a +method ``clearDefaultOptions()`` and call it periodically:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { - // ... + private static $resolversByClass = array(); - $resolver->setNormalizers(array( - 'host' => function (Options $options, $value) { - if (!in_array(substr($value, 0, 7), array('http://', 'https://'))) { - if ($options['ssl']) { - $value = 'https://'.$value; - } else { - $value = 'http://'.$value; - } - } + public static function clearDefaultOptions() + { + self::$resolversByClass = array(); + } - return $value; - }, - )); + // ... } +That's it! You now have all the tools and knowledge needed to easily process +options in your code. + .. _Packagist: https://packagist.org/packages/symfony/options-resolver +.. _Form component: http://symfony.com/doc/current/components/form/introduction.html