From 43ae1d8b1bae03577018027a229b3bacce0c8c69 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 21 Aug 2014 19:42:57 +0200 Subject: [PATCH 1/4] [OptionsResolver] Adjusted the OptionsResolver documentation to describe the 2.6 API --- components/options_resolver.rst | 665 +++++++++++++++++++++----------- 1 file changed, 445 insertions(+), 220 deletions(-) diff --git a/components/options_resolver.rst b/components/options_resolver.rst index eaad4bc7eb1..cf7ac0953cb 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 helps you to easily process option arrays. Installation ------------ @@ -19,11 +18,13 @@ You can install the component in 2 different ways: 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. +.. 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. -First, create the ``Mailer`` class:: +Imagine you have a ``Mailer`` class which has four options: ``host``, +``username``, ``password`` and ``port``:: class Mailer { @@ -31,27 +32,63 @@ 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 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); + // ... + } + +This boilerplate is hard to read and repetitive. Also, the default values of the +options are buried in the business logic of your code. Let's use +:method:`Symfony\\Component\\OptionsResolver\\Options::resolve` to fix that:: + + use Symfony\Component\OptionsResolver\Options; // ... public function __construct(array $options = array()) { - $resolver = new OptionsResolver(); - - $this->options = $resolver->resolve($options); + $this->options = Options::resolve($options, array( + 'host' => 'smtp.example.org', + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, + )); } -The options property now is a well defined array with all resolved options -readily available:: +Now all options are guaranteed to be set. Any option that wasn't passed through +``$options`` will be set to the specified default value. Additionally, an +:class:`Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException` +is thrown if an unknown option is passed:: + + $mailer = new Mailer(array( + 'usernme' => 'johndoe', + )); + + // InvalidOptionsException: The option "usernme" does not exist. Known + // options are: "host", "password", "username" + +The rest of your code can now access the values of the options without +boilerplate code:: // ... public function sendMail($from, $to) @@ -60,154 +97,148 @@ readily available:: $mail->setHost($this->options['host']); $mail->setUsername($this->options['username']); $mail->setPassword($this->options['password']); + $mail->setPort($this->options['port']); // ... } -Configuring the OptionsResolver -------------------------------- +Required Options +~~~~~~~~~~~~~~~~ -Now, try to actually use the class:: +If an option must be set by the caller, pass that option to +:method:`Symfony\\Component\\OptionsResolver\\Options::validateRequired`. +For example, let's make the ``host`` option required:: - $mailer = new Mailer(array( - 'host' => 'smtp.example.org', - 'username' => 'user', - 'password' => 'pa$$word', - )); + // ... + public function __construct(array $options = array()) + { + Options::validateRequired($options, 'host'); -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. + $this->options = Options::resolve($options, array( + 'host' => null, + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, + )); + } -.. tip:: +If you omit a required option, a +:class:`Symfony\\Component\\OptionsResolver\\Exception\\MissingOptionsException` +will be thrown:: - To check if an option exists, you can use the - :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isKnown` - function. + $mailer = new Mailer(); -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:: + // MissingOptionsException: The required option "host" is missing. - use Symfony\Component\OptionsResolver\OptionsResolver; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; +The :method:`Symfony\\Component\\OptionsResolver\\Options::validateRequired` +method accepts a single name or an array of option names if you have more than +one required option. - class Mailer - { - protected $options; - - public function __construct(array $options = array()) - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - - $this->options = $resolver->resolve($options); - } +.. note:: - protected function configureOptions(OptionsResolverInterface $resolver) - { - // ... configure the resolver, you will learn this - // in the sections below - } - } + As you can see, the ``host`` option must still be passed to + :method:`Symfony\\Component\\OptionsResolver\\Options::resolve`, + otherwise the method will not accept that option. The default value, + however, can be omitted as the option must be set by the caller. -Set default Values -~~~~~~~~~~~~~~~~~~ +Type Validation +~~~~~~~~~~~~~~~ -Most of the options have a default value. You can configure these options by -calling :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setDefaults`:: +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\\Options::validateTypes`:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + public function __construct(array $options = array()) { // ... + Options::validateTypes($options, array( + 'host' => 'string', + 'port' => array('null', 'int'), + )); - $resolver->setDefaults(array( - 'username' => 'root', + $this->options = Options::resolve($options, array( + 'host' => null, + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, )); } -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. - -Required Options -~~~~~~~~~~~~~~~~ - -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`:: +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. - // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setRequired(array('host')); - } - -You are now able to use the class without errors:: +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' - -If you don't pass a required option, a -:class:`Symfony\\Component\\OptionsResolver\\Exception\\MissingOptionsException` -will be thrown. - -.. tip:: - - To determine if an option is required, you can use the - :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isRequired` - method. + // InvalidOptionsException: The option "host" with value "25" is expected to + // be of type "string" -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\\Options::validateValues` to verify +that the passed option contains one of these values:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + public function __construct(array $options = array()) { // ... + Options::validateValues($options, array( + 'transport' => array('sendmail', 'mail', 'smtp'), + )); - $resolver->setOptional(array('password')); + $this->options = Options::resolve($options, array( + // ... + 'transport' => 'sendmail', + )); } -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', + )); + + // InvalidOptionsException: The option "transport" has the value "send-mail", + // but is expected to be one of "sendmail", "mail", "smtp" - 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. +For options with more complicated validation schemes, pass a callback which +returns ``true`` for acceptable values and ``false`` for invalid values:: - 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``. + Options::validateValues($options, array( + // ... + 'transport' => function ($value) { + // return true or false + }, + )); Default Values that Depend on another Option ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -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:: +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. - use Symfony\Component\OptionsResolver\Options; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; +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:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + public function __construct(array $options = array()) { // ... - $resolver->setDefaults(array( + $this->options = Options::resolve($options, new Options(array( + // ... 'encryption' => null, 'port' => function (Options $options) { if ('ssl' === $options['encryption']) { @@ -216,45 +247,288 @@ closure as the default value:: return 25; }, - )); + ))); + } + +Instead of a simple array, we now pass the default options as +:class:`Symfony\\Component\\OptionsResolver\\Options` instance to +:method:`Symfony\\Component\\OptionsResolver\\Options::resolve`. This class +makes sure that the closure stored in the default value of the ``port`` option +is called. In the closure, you can use the +:class:`Symfony\\Component\\OptionsResolver\\Options` instance just like a +normal option array. + +.. caution:: + + The first argument of the closure must be type hinted as ``Options``. + Otherwise, the closure is considered as the default value of the option. + If the closure is still not called, double check that you passed the default + options as :class:`Symfony\\Component\\OptionsResolver\\Options` instance. + +.. note:: + + The closure is only executed if the ``port`` option isn't set by the user. + +Coding Patterns +~~~~~~~~~~~~~~~ + +If you have a large list of options, the option processing code can take up a +lot of space of your method. To make your code easier to read and maintain, it +is a good practice to put the option definitions into static class properties:: + + class Mailer + { + private static $defaultOptions = array( + 'host' => null, + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, + 'encryption' => null, + ); + + private static $requiredOptions = array( + 'host', + ); + + private static $optionTypes = array( + 'host' => 'string', + 'username' => 'string', + 'password' => 'string', + 'port' => 'int', + ); + + private static $optionValues = array( + 'encryption' => array(null, 'ssl', 'tls'), + ); + + protected $options; + + public function __construct(array $options = array()) + { + Options::validateRequired($options, static::$requiredOptions); + Options::validateTypes($options, static::$optionTypes); + Options::validateValues($options, static::$optionValues); + + $this->options = Options::resolve($options, static::$defaultOptions); + } } -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. +In this way, the class remains easy to read and maintain even with a lot of +options being processed and validated. .. caution:: - The first argument of the closure must be typehinted as ``Options``, - otherwise it is considered as the value. + PHP does not support closures in property definitions. In such cases, you + must move your closure to a static method:: + + private static $defaultOptions = array( + // ... + 'port' => array(__CLASS__, 'getDefaultPort'), + ); + + public static function getDefaultPort(Options $options) + { + if ('ssl' === $options['encryption']) { + return 465; + } + + return 25; + } + +Decoupling the Option Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +So far, the configuration of the options, their allowed types etc. was very +tightly coupled to the code that resolves the options. This is fine in most cases. +In some cases, however, the configuration of options must be distributed across +multiple classes. An example is a class hierarchy that supports the addition of +options by subclasses. In those cases, you can create an +:class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` object and pass that +object everywhere that you want to adjust the option configuration. Then, call +:method:`Symfony\\Component\\OptionsResolver\\Options::resolve` with the +configuration object to resolve the options. + +The following code demonstrates how to write our previous ``Mailer`` class with +an :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` object:: + + use Symfony\Component\OptionsResolver\Options; + use Symfony\Component\OptionsResolver\OptionsConfig; + + class Mailer + { + protected $options; + + public function __construct(array $options = array()) + { + $config = new OptionsConfig(); + $this->configureOptions($config); + + $this->options = Options::resolve($options, $config); + } + + protected function configureOptions(OptionsConfig $config) + { + $config->setDefaults(array( + 'host' => null, + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, + 'encryption' => null, + )); + + $config->setRequired(array( + 'host', + )); + + $config->setAllowedTypes(array( + 'host' => 'string', + 'username' => 'string', + 'password' => 'string', + 'port' => 'int', + )); + + $config->setAllowedValues(array( + 'encryption' => array(null, 'ssl', 'tls'), + )); + } + } + +As you can see, the code is very similar as before. However, the performance +is marginally worse, since the creation of an additional object is required: +the :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` instance. + +Nevertheless, this design also has a benefit: We can extend the ``Mailer`` +class and adjust the options of the parent class in the subclass:: + + use Symfony\Component\OptionsResolver\Options; + use Symfony\Component\OptionsResolver\OptionsConfig; + + class GoogleMailer extends Mailer + { + protected function configureOptions(OptionsConfig $config) + { + $config->setDefaults(array( + 'host' => 'smtp.google.com', + 'port' => 25, + 'encryption' => 'ssl', + )); + + $config->setRequired(array( + 'username', + 'password', + )); + } + } + +The ``host`` option is no longer required now, but defaults to "smtp.google.com". +The ``username`` and ``password`` options, however, are required in the +subclass. + +The :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` has various +useful methods to find out which options are set or required. Check out the +API documentation to find out more about these methods. + +.. note:: + + The :class:`Symfony\\Component\\OptionsResolver\\OptionsResolver` class used + by the Form Component inherits from + :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig`. All the + documentation for ``OptionsConfig`` applies to ``OptionsResolver`` as well. + +Optional Options +~~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` has one feature +that is not available when not using this class: You can specify optional +options. Optional options will be accepted and validated when set. When not set, +however, *no default value* will be added to the options array. Pass the names +of the optional options to +:method:`Symfony\\Component\\OptionsResolver\\OptionsConfig::setOptional`:: + + // ... + protected function configureOptions(OptionsConfig $config) + { + // ... + + $config->setOptional(array('port')); + } + +This is useful if you need to know whether an option was explicitly passed. If +not, it will be missing from the options array:: + + class Mailer + { + // ... + public function __construct(array $options = array()) + { + // ... + + if (array_key_exists('port', $this->options)) { + echo "Set!"; + } else { + echo "Not Set!"; + } + } + } + + $mailer = new Mailer(array( + 'port' => 25, + )); + // Set! + + $mailer = new Mailer(); + // Not Set! + +.. tip:: -Overwriting default Values + If you need this functionality when not using an + :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` object, check + the options before calling + :method:`Symfony\\Component\\OptionsResolver\\Options::resolve`:: + + // ... + public function __construct(array $options = array()) + { + // ... + + if (array_key_exists('port', $options)) { + echo "Set!"; + } else { + echo "Not Set!"; + } + + $this->options = Options::resolve($options, array( + // ... + )); + } + +Overwriting Default Values ~~~~~~~~~~~~~~~~~~~~~~~~~~ A previously set default value can be overwritten by invoking -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setDefaults` +:method:`Symfony\\Component\\OptionsResolver\\OptionsConfig::setDefaults` again. When using a closure as the new value it is passed 2 arguments: * ``$options``: an :class:`Symfony\\Component\\OptionsResolver\\Options` instance with all the other default options -* ``$previousValue``: the previous set default value +* ``$previousValue``: the previously set default value .. code-block:: php use Symfony\Component\OptionsResolver\Options; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; + use Symfony\Component\OptionsResolver\OptionsConfig; // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + protected function configureOptions(OptionsConfig $config) { // ... - $resolver->setDefaults(array( + $config->setDefaults(array( 'encryption' => 'ssl', 'host' => 'localhost', )); // ... - $resolver->setDefaults(array( + $config->setDefaults(array( 'encryption' => 'tls', // simple overwrite 'host' => function (Options $options, $previousValue) { return 'localhost' == $previousValue @@ -267,20 +541,20 @@ again. When using a closure as the new value it is passed 2 arguments: .. tip:: 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:: + you don't need access to it, use the + :method:`Symfony\\Component\\OptionsResolver\\OptionsConfig::replaceDefaults` + method instead. It acts like ``setDefaults`` but erases the previous value + to improve performance. This means that the previous default value is not + available when overwriting with another closure:: use Symfony\Component\OptionsResolver\Options; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; + use Symfony\Component\OptionsResolver\OptionsConfig; // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + protected function configureOptions(OptionsConfig $config) { // ... - $resolver->setDefaults(array( + $config->setDefaults(array( 'encryption' => 'ssl', 'heavy' => function (Options $options) { // Some heavy calculations to create the $result @@ -289,7 +563,7 @@ again. When using a closure as the new value it is passed 2 arguments: }, )); - $resolver->replaceDefaults(array( + $config->replaceDefaults(array( 'encryption' => 'tls', // simple overwrite 'heavy' => function (Options $options) { // $previousValue not available @@ -304,92 +578,21 @@ again. When using a closure as the new value it is passed 2 arguments: Existing option keys that you do not mention when overwriting are preserved. -Configure Allowed 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`:: - - // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) - { - // ... - - $resolver->setAllowedValues(array( - 'encryption' => array(null, 'ssl', 'tls'), - )); - } - -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.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:: - - // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) - { - // ... - - $resolver->setAllowedValues(array( - 'transport' => function($value) { - return false !== strpos($value, 'mail'); - }, - )); - } - -.. caution:: - - Note that using this together with ``addAllowedValues`` will not work. - -Configure Allowed Types -~~~~~~~~~~~~~~~~~~~~~~~ - -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`:: - - // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) - { - // ... - - $resolver->setAllowedTypes(array( - 'port' => 'integer', - )); - } - -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``. - -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. - -Normalize the Options -~~~~~~~~~~~~~~~~~~~~~ +Option Normalization +~~~~~~~~~~~~~~~~~~~~ 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`:: +assume that the ``host`` should always start with ``http://``. To do that, +you can write normalizers. Normalizers are executed after all options were +processed. You can configure these normalizers by calling +:method:`Symfony\\Components\\OptionsResolver\\OptionsConfig::setNormalizers`:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + protected function configureOptions(OptionsConfig $config) { // ... - $resolver->setNormalizers(array( + $config->setNormalizers(array( 'host' => function (Options $options, $value) { if ('http://' !== substr($value, 0, 7)) { $value = 'http://'.$value; @@ -400,15 +603,16 @@ normalizers by calling )); } -You see that the closure also gets an ``$options`` parameter. Sometimes, you -need to use the other options for normalizing:: +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 for the normalization:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + protected function configureOptions(OptionsConfig $config) { // ... - $resolver->setNormalizers(array( + $config->setNormalizers(array( 'host' => function (Options $options, $value) { if (!in_array(substr($value, 0, 7), array('http://', 'https://'))) { if ($options['ssl']) { @@ -423,4 +627,25 @@ need to use the other options for normalizing:: )); } +.. tip:: + + When not using an :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` + object, perform normalization after the call to + :method:`Symfony\\Component\\OptionsResolver\\Options::resolve`:: + + // ... + public function __construct(array $options = array()) + { + $this->options = Options::resolve($options, array( + // ... + )); + + if ('http://' !== substr($this->options['host'], 0, 7)) { + $this->options['host'] = 'http://'.$this->options['host']; + } + } + +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 From ae668939a713442ebeb57bebd4a468b416fcc8f2 Mon Sep 17 00:00:00 2001 From: Peter Rehm Date: Sat, 23 Aug 2014 16:17:51 +0200 Subject: [PATCH 2/4] Added missing mailer class and use statements --- components/options_resolver.rst | 369 +++++++++++++++++++------------- 1 file changed, 217 insertions(+), 152 deletions(-) diff --git a/components/options_resolver.rst b/components/options_resolver.rst index cf7ac0953cb..11899762de0 100644 --- a/components/options_resolver.rst +++ b/components/options_resolver.rst @@ -39,23 +39,26 @@ Imagine you have a ``Mailer`` class which has four options: ``host``, When accessing the ``$options``, you need to add a lot of boilerplate code to check which options are set:: - // ... - public function sendMail($from, $to) + class Mailer { - $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); // ... + 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); + // ... + } } This boilerplate is hard to read and repetitive. Also, the default values of the @@ -64,15 +67,18 @@ options are buried in the business logic of your code. Let's use use Symfony\Component\OptionsResolver\Options; - // ... - public function __construct(array $options = array()) + class Mailer { - $this->options = Options::resolve($options, array( - 'host' => 'smtp.example.org', - 'username' => 'user', - 'password' => 'pa$$word', - 'port' => 25, - )); + // ... + public function __construct(array $options = array()) + { + $this->options = Options::resolve($options, array( + 'host' => 'smtp.example.org', + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, + )); + } } Now all options are guaranteed to be set. Any option that wasn't passed through @@ -90,15 +96,18 @@ is thrown if an unknown option is passed:: The rest of your code can now access the values of the options without boilerplate code:: - // ... - public function sendMail($from, $to) + class Mailer { - $mail = ...; - $mail->setHost($this->options['host']); - $mail->setUsername($this->options['username']); - $mail->setPassword($this->options['password']); - $mail->setPort($this->options['port']); // ... + 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']); + // ... + } } Required Options @@ -108,17 +117,22 @@ If an option must be set by the caller, pass that option to :method:`Symfony\\Component\\OptionsResolver\\Options::validateRequired`. For example, let's make the ``host`` option required:: - // ... - public function __construct(array $options = array()) + use Symfony\Component\OptionsResolver\Options; + + class Mailer { - Options::validateRequired($options, 'host'); - - $this->options = Options::resolve($options, array( - 'host' => null, - 'username' => 'user', - 'password' => 'pa$$word', - 'port' => 25, - )); + // ... + public function __construct(array $options = array()) + { + Options::validateRequired($options, 'host'); + + $this->options = Options::resolve($options, array( + 'host' => null, + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, + )); + } } If you omit a required option, a @@ -147,21 +161,26 @@ 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\\Options::validateTypes`:: - // ... - public function __construct(array $options = array()) + use Symfony\Component\OptionsResolver\Options; + + class Mailer { // ... - Options::validateTypes($options, array( - 'host' => 'string', - 'port' => array('null', 'int'), - )); - - $this->options = Options::resolve($options, array( - 'host' => null, - 'username' => 'user', - 'password' => 'pa$$word', - 'port' => 25, - )); + public function __construct(array $options = array()) + { + // ... + Options::validateTypes($options, array( + 'host' => 'string', + 'port' => array('null', 'int'), + )); + + $this->options = Options::resolve($options, array( + 'host' => null, + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, + )); + } } For each option, you can define either just one type or an array of acceptable @@ -187,18 +206,23 @@ one of ``sendmail``, ``mail`` and ``smtp``. Use the method :method:`Symfony\\Component\\OptionsResolver\\Options::validateValues` to verify that the passed option contains one of these values:: - // ... - public function __construct(array $options = array()) + use Symfony\Component\OptionsResolver\Options; + + class Mailer { // ... - Options::validateValues($options, array( - 'transport' => array('sendmail', 'mail', 'smtp'), - )); - - $this->options = Options::resolve($options, array( + public function __construct(array $options = array()) + { // ... - 'transport' => 'sendmail', - )); + Options::validateValues($options, array( + 'transport' => array('sendmail', 'mail', 'smtp'), + )); + + $this->options = Options::resolve($options, array( + // ... + 'transport' => 'sendmail', + )); + } } If you pass an invalid transport, an :class:`Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException` @@ -232,22 +256,27 @@ 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:: - // ... - public function __construct(array $options = array()) + use Symfony\Component\OptionsResolver\Options; + + class Mailer { // ... - - $this->options = Options::resolve($options, new Options(array( + public function __construct(array $options = array()) + { // ... - 'encryption' => null, - 'port' => function (Options $options) { - if ('ssl' === $options['encryption']) { - return 465; - } - return 25; - }, - ))); + $this->options = Options::resolve($options, new Options(array( + // ... + 'encryption' => null, + 'port' => function (Options $options) { + if ('ssl' === $options['encryption']) { + return 465; + } + + return 25; + }, + ))); + } } Instead of a simple array, we now pass the default options as @@ -276,6 +305,8 @@ If you have a large list of options, the option processing code can take up a lot of space of your method. To make your code easier to read and maintain, it is a good practice to put the option definitions into static class properties:: + use Symfony\Component\OptionsResolver\Options; + class Mailer { private static $defaultOptions = array( @@ -445,12 +476,18 @@ however, *no default value* will be added to the options array. Pass the names of the optional options to :method:`Symfony\\Component\\OptionsResolver\\OptionsConfig::setOptional`:: - // ... - protected function configureOptions(OptionsConfig $config) + use Symfony\Component\OptionsResolver\Options; + use Symfony\Component\OptionsResolver\OptionsConfig; + + class Mailer { // ... + protected function configureOptions(OptionsConfig $config) + { + // ... - $config->setOptional(array('port')); + $config->setOptional(array('port')); + } } This is useful if you need to know whether an option was explicitly passed. If @@ -486,20 +523,25 @@ not, it will be missing from the options array:: the options before calling :method:`Symfony\\Component\\OptionsResolver\\Options::resolve`:: - // ... - public function __construct(array $options = array()) + use Symfony\Component\OptionsResolver\Options; + + class Mailer { // ... + public function __construct(array $options = array()) + { + // ... - if (array_key_exists('port', $options)) { - echo "Set!"; - } else { - echo "Not Set!"; - } + if (array_key_exists('port', $options)) { + echo "Set!"; + } else { + echo "Not Set!"; + } - $this->options = Options::resolve($options, array( - // ... - )); + $this->options = Options::resolve($options, array( + // ... + )); + } } Overwriting Default Values @@ -518,24 +560,27 @@ again. When using a closure as the new value it is passed 2 arguments: use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsConfig; - // ... - protected function configureOptions(OptionsConfig $config) + class Mailer { // ... - $config->setDefaults(array( - 'encryption' => 'ssl', - 'host' => 'localhost', - )); + protected function configureOptions(OptionsConfig $config) + { + // ... + $config->setDefaults(array( + 'encryption' => 'ssl', + 'host' => 'localhost', + )); - // ... - $config->setDefaults(array( - 'encryption' => 'tls', // simple overwrite - 'host' => function (Options $options, $previousValue) { - return 'localhost' == $previousValue - ? '127.0.0.1' - : $previousValue; - }, - )); + // ... + $config->setDefaults(array( + 'encryption' => 'tls', // simple overwrite + 'host' => function (Options $options, $previousValue) { + return 'localhost' == $previousValue + ? '127.0.0.1' + : $previousValue; + }, + )); + } } .. tip:: @@ -550,28 +595,31 @@ again. When using a closure as the new value it is passed 2 arguments: use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsConfig; - // ... - protected function configureOptions(OptionsConfig $config) + class Mailer { // ... - $config->setDefaults(array( - 'encryption' => 'ssl', - 'heavy' => function (Options $options) { - // Some heavy calculations to create the $result - - return $result; - }, - )); - - $config->replaceDefaults(array( - 'encryption' => 'tls', // simple overwrite - 'heavy' => function (Options $options) { - // $previousValue not available - // ... - - return $someOtherResult; - }, - )); + protected function configureOptions(OptionsConfig $config) + { + // ... + $config->setDefaults(array( + 'encryption' => 'ssl', + 'heavy' => function (Options $options) { + // Some heavy calculations to create the $result + + return $result; + }, + )); + + $config->replaceDefaults(array( + 'encryption' => 'tls', // simple overwrite + 'heavy' => function (Options $options) { + // $previousValue not available + // ... + + return $someOtherResult; + }, + )); + } } .. note:: @@ -587,44 +635,56 @@ you can write normalizers. Normalizers are executed after all options were processed. You can configure these normalizers by calling :method:`Symfony\\Components\\OptionsResolver\\OptionsConfig::setNormalizers`:: - // ... - protected function configureOptions(OptionsConfig $config) + use Symfony\Component\OptionsResolver\Options; + use Symfony\Component\OptionsResolver\OptionsConfig; + + class Mailer { // ... + protected function configureOptions(OptionsConfig $config) + { + // ... - $config->setNormalizers(array( - 'host' => function (Options $options, $value) { - if ('http://' !== substr($value, 0, 7)) { - $value = 'http://'.$value; - } + $config->setNormalizers(array( + 'host' => function (Options $options, $value) { + if ('http://' !== substr($value, 0, 7)) { + $value = 'http://'.$value; + } - return $value; - }, - )); + return $value; + }, + )); + } } 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 for the normalization:: - // ... - protected function configureOptions(OptionsConfig $config) + use Symfony\Component\OptionsResolver\Options; + use Symfony\Component\OptionsResolver\OptionsConfig; + + class Mailer { // ... + protected function configureOptions(OptionsConfig $config) + { + // ... - $config->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; + $config->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; + } } - } - return $value; - }, - )); + return $value; + }, + )); + } } .. tip:: @@ -633,15 +693,20 @@ if you need to use other options for the normalization:: object, perform normalization after the call to :method:`Symfony\\Component\\OptionsResolver\\Options::resolve`:: - // ... - public function __construct(array $options = array()) + use Symfony\Component\OptionsResolver\Options; + + class Mailer { - $this->options = Options::resolve($options, array( - // ... - )); + // ... + public function __construct(array $options = array()) + { + $this->options = Options::resolve($options, array( + // ... + )); - if ('http://' !== substr($this->options['host'], 0, 7)) { - $this->options['host'] = 'http://'.$this->options['host']; + if ('http://' !== substr($this->options['host'], 0, 7)) { + $this->options['host'] = 'http://'.$this->options['host']; + } } } From f202fb83e60254b9938aefb00acaef1aa1fdf704 Mon Sep 17 00:00:00 2001 From: Peter Rehm Date: Wed, 17 Sep 2014 07:52:36 +0200 Subject: [PATCH 3/4] Removed duplicate use statements --- components/options_resolver.rst | 51 +++++++++++---------------------- 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/components/options_resolver.rst b/components/options_resolver.rst index 11899762de0..22b1943f9e9 100644 --- a/components/options_resolver.rst +++ b/components/options_resolver.rst @@ -96,6 +96,7 @@ is thrown if an unknown option is passed:: The rest of your code can now access the values of the options without boilerplate code:: + // ... class Mailer { // ... @@ -117,8 +118,7 @@ If an option must be set by the caller, pass that option to :method:`Symfony\\Component\\OptionsResolver\\Options::validateRequired`. For example, let's make the ``host`` option required:: - use Symfony\Component\OptionsResolver\Options; - + // ... class Mailer { // ... @@ -160,9 +160,8 @@ 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\\Options::validateTypes`:: - - use Symfony\Component\OptionsResolver\Options; - + + // ... class Mailer { // ... @@ -206,8 +205,7 @@ one of ``sendmail``, ``mail`` and ``smtp``. Use the method :method:`Symfony\\Component\\OptionsResolver\\Options::validateValues` to verify that the passed option contains one of these values:: - use Symfony\Component\OptionsResolver\Options; - + // ... class Mailer { // ... @@ -256,8 +254,7 @@ 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; - + // ... class Mailer { // ... @@ -305,8 +302,7 @@ If you have a large list of options, the option processing code can take up a lot of space of your method. To make your code easier to read and maintain, it is a good practice to put the option definitions into static class properties:: - use Symfony\Component\OptionsResolver\Options; - + // ... class Mailer { private static $defaultOptions = array( @@ -382,7 +378,7 @@ configuration object to resolve the options. The following code demonstrates how to write our previous ``Mailer`` class with an :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` object:: - use Symfony\Component\OptionsResolver\Options; + // ... use Symfony\Component\OptionsResolver\OptionsConfig; class Mailer @@ -431,9 +427,7 @@ the :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` instance. Nevertheless, this design also has a benefit: We can extend the ``Mailer`` class and adjust the options of the parent class in the subclass:: - use Symfony\Component\OptionsResolver\Options; - use Symfony\Component\OptionsResolver\OptionsConfig; - + // ... class GoogleMailer extends Mailer { protected function configureOptions(OptionsConfig $config) @@ -476,9 +470,7 @@ however, *no default value* will be added to the options array. Pass the names of the optional options to :method:`Symfony\\Component\\OptionsResolver\\OptionsConfig::setOptional`:: - use Symfony\Component\OptionsResolver\Options; - use Symfony\Component\OptionsResolver\OptionsConfig; - + // ... class Mailer { // ... @@ -493,6 +485,7 @@ of the optional options to This is useful if you need to know whether an option was explicitly passed. If not, it will be missing from the options array:: + // ... class Mailer { // ... @@ -523,8 +516,7 @@ not, it will be missing from the options array:: the options before calling :method:`Symfony\\Component\\OptionsResolver\\Options::resolve`:: - use Symfony\Component\OptionsResolver\Options; - + // ... class Mailer { // ... @@ -557,9 +549,7 @@ again. When using a closure as the new value it is passed 2 arguments: .. code-block:: php - use Symfony\Component\OptionsResolver\Options; - use Symfony\Component\OptionsResolver\OptionsConfig; - + // ... class Mailer { // ... @@ -592,9 +582,7 @@ again. When using a closure as the new value it is passed 2 arguments: to improve performance. This means that the previous default value is not available when overwriting with another closure:: - use Symfony\Component\OptionsResolver\Options; - use Symfony\Component\OptionsResolver\OptionsConfig; - + // ... class Mailer { // ... @@ -635,9 +623,7 @@ you can write normalizers. Normalizers are executed after all options were processed. You can configure these normalizers by calling :method:`Symfony\\Components\\OptionsResolver\\OptionsConfig::setNormalizers`:: - use Symfony\Component\OptionsResolver\Options; - use Symfony\Component\OptionsResolver\OptionsConfig; - + // ... class Mailer { // ... @@ -661,9 +647,7 @@ 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 for the normalization:: - use Symfony\Component\OptionsResolver\Options; - use Symfony\Component\OptionsResolver\OptionsConfig; - + // ... class Mailer { // ... @@ -693,8 +677,7 @@ if you need to use other options for the normalization:: object, perform normalization after the call to :method:`Symfony\\Component\\OptionsResolver\\Options::resolve`:: - use Symfony\Component\OptionsResolver\Options; - + // ... class Mailer { // ... From 575c32055fe03ff69bf50c56c1bd6640f505592b Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 14 Oct 2014 17:42:35 +0200 Subject: [PATCH 4/4] Updated OptionsResolver documentation: removed static methods --- components/options_resolver.rst | 802 ++++++++++++++++---------------- 1 file changed, 409 insertions(+), 393 deletions(-) diff --git a/components/options_resolver.rst b/components/options_resolver.rst index 22b1943f9e9..ea1372f4ea6 100644 --- a/components/options_resolver.rst +++ b/components/options_resolver.rst @@ -5,7 +5,7 @@ The OptionsResolver Component ============================= - The OptionsResolver component helps you to easily process option arrays. + The OptionsResolver component is `array_replace()` on steroids. Installation ------------ @@ -15,14 +15,17 @@ 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). -Usage ------ +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 four options: ``host``, ``username``, ``password`` and ``port``:: @@ -62,8 +65,39 @@ check which options are set:: } This boilerplate is hard to read and repetitive. Also, the default values of the -options are buried in the business logic of your code. Let's use -:method:`Symfony\\Component\\OptionsResolver\\Options::resolve` to fix that:: +options are buried in the business logic of your code. We can use +:phpfunction:`array_replace` to fix that:: + + class Mailer + { + // ... + public function __construct(array $options = array()) + { + $this->options = array_replace(array( + 'host' => 'smtp.example.org', + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, + ), $options); + } + } + +Now all four options are guaranteed to be set. But what happens if the user of +the ``Mailer`` class does a mistake? + +.. code-block:: php + + $mailer = new Mailer(array( + 'usernme' => 'johndoe', + )); + +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; @@ -72,29 +106,31 @@ options are buried in the business logic of your code. Let's use // ... public function __construct(array $options = array()) { - $this->options = Options::resolve($options, array( + $resolver = new OptionsResolver(); + $resolver->setDefaults(array( 'host' => 'smtp.example.org', 'username' => 'user', 'password' => 'pa$$word', 'port' => 25, )); + + $this->options = $resolver->resolve($options); } } -Now all options are guaranteed to be set. Any option that wasn't passed through -``$options`` will be set to the specified default value. Additionally, an -:class:`Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException` +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:: $mailer = new Mailer(array( 'usernme' => 'johndoe', )); - // InvalidOptionsException: The option "usernme" does not exist. Known - // options are: "host", "password", "username" + // UndefinedOptionsException: The option "usernme" does not exist. Known + // options are: "host", "password", "port", "username" -The rest of your code can now access the values of the options without -boilerplate code:: +The rest of your code can access the values of the options without boilerplate +code:: // ... class Mailer @@ -111,12 +147,7 @@ boilerplate code:: } } -Required Options -~~~~~~~~~~~~~~~~ - -If an option must be set by the caller, pass that option to -:method:`Symfony\\Component\\OptionsResolver\\Options::validateRequired`. -For example, let's make the ``host`` option required:: +It's a good practice to split the option configuration into a separate method:: // ... class Mailer @@ -124,17 +155,64 @@ For example, let's make the ``host`` option required:: // ... public function __construct(array $options = array()) { - Options::validateRequired($options, 'host'); + $resolver = new OptionsResolver(); + $this->configureOptions($resolver); + + $this->options = $resolver->resolve($options); + } - $this->options = Options::resolve($options, array( - 'host' => null, + protected function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'host' => 'smtp.example.org', 'username' => 'user', 'password' => 'pa$$word', - 'port' => 25, + 'port' => 25, + 'encryption' => null, )); } } +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'); + } + } + +.. 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:: @@ -143,42 +221,110 @@ will be thrown:: // MissingOptionsException: The required option "host" is missing. -The :method:`Symfony\\Component\\OptionsResolver\\Options::validateRequired` +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. +one required option:: -.. note:: + // ... + class Mailer + { + // ... + protected function configureOptions(OptionsResolver $resolver) + { + // ... + $resolver->setRequired(array('host', 'username', 'password')); + } + } - As you can see, the ``host`` option must still be passed to - :method:`Symfony\\Component\\OptionsResolver\\Options::resolve`, - otherwise the method will not accept that option. The default value, - however, can be omitted as the option must be set by the caller. +.. 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(); + } + } + +.. versionadded:: 2.6 + The methods :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isMissing` + and :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::getMissingOptions` + were introduced in Symfony 2.6. + +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 + + $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\\Options::validateTypes`:: - +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setAllowedTypes`:: + // ... class Mailer { // ... - public function __construct(array $options = array()) + protected function configureOptions(OptionsResolver $resolver) { // ... - Options::validateTypes($options, array( - 'host' => 'string', - 'port' => array('null', 'int'), - )); - - $this->options = Options::resolve($options, array( - 'host' => null, - 'username' => 'user', - 'password' => 'pa$$word', - 'port' => 25, - )); + $resolver->setAllowedTypes('host', 'string'); + $resolver->setAllowedTypes('port', array('null', 'int')); } } @@ -186,7 +332,8 @@ 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` +If you pass an invalid option now, an +:class:`Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException` is thrown:: $mailer = new Mailer(array( @@ -196,34 +343,40 @@ is thrown:: // InvalidOptionsException: The option "host" with value "25" is expected to // be of type "string" +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: + + .. code-block:: php + + $resolver->setAllowedTypes(array('port' => array('null', 'int'))); + Value Validation ~~~~~~~~~~~~~~~~ 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\\Options::validateValues` to verify +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setAllowedValues` to verify that the passed option contains one of these values:: // ... class Mailer { // ... - public function __construct(array $options = array()) + protected function configureOptions(OptionsResolver $resolver) { // ... - Options::validateValues($options, array( - 'transport' => array('sendmail', 'mail', 'smtp'), - )); - - $this->options = Options::resolve($options, array( - // ... - 'transport' => 'sendmail', - )); + $resolver->setDefault('transport', 'sendmail'); + $resolver->setAllowedValues('transport', array('sendmail', 'mail', 'smtp')); } } -If you pass an invalid transport, an :class:`Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException` +If you pass an invalid transport, an +:class:`Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException` is thrown:: $mailer = new Mailer(array( @@ -233,467 +386,330 @@ is thrown:: // InvalidOptionsException: The option "transport" has the value "send-mail", // but is expected to be one of "sendmail", "mail", "smtp" -For options with more complicated validation schemes, pass a callback which +For options with more complicated validation schemes, pass a closure which returns ``true`` for acceptable values and ``false`` for invalid values:: - Options::validateValues($options, array( + $resolver->setAllowedValues(array( // ... - 'transport' => function ($value) { + $resolver->setAllowedValues('transport', function ($value) { // return true or false - }, + }); )); -Default Values that Depend on another Option -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +In sub-classes, you can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedValues` +to add additional allowed values without erasing the ones already set. -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. +.. 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: -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:: + .. 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`:: // ... class Mailer { // ... - public function __construct(array $options = array()) + protected function configureOptions(OptionsResolver $resolver) { // ... + $resolver->setNormalizer('host', function ($options, $value) { + if ('http://' !== substr($value, 0, 7)) { + $value = 'http://'.$value; + } - $this->options = Options::resolve($options, new Options(array( - // ... - 'encryption' => null, - 'port' => function (Options $options) { - if ('ssl' === $options['encryption']) { - return 465; - } - - return 25; - }, - ))); + return $value; + }); } } -Instead of a simple array, we now pass the default options as -:class:`Symfony\\Component\\OptionsResolver\\Options` instance to -:method:`Symfony\\Component\\OptionsResolver\\Options::resolve`. This class -makes sure that the closure stored in the default value of the ``port`` option -is called. In the closure, you can use the -:class:`Symfony\\Component\\OptionsResolver\\Options` instance just like a -normal option array. - -.. caution:: - - The first argument of the closure must be type hinted as ``Options``. - Otherwise, the closure is considered as the default value of the option. - If the closure is still not called, double check that you passed the default - options as :class:`Symfony\\Component\\OptionsResolver\\Options` instance. - -.. note:: - - The closure is only executed if the ``port`` option isn't set by the user. - -Coding Patterns -~~~~~~~~~~~~~~~ +.. 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`. -If you have a large list of options, the option processing code can take up a -lot of space of your method. To make your code easier to read and maintain, it -is a good practice to put the option definitions into static class properties:: +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:: // ... class Mailer { - private static $defaultOptions = array( - 'host' => null, - 'username' => 'user', - 'password' => 'pa$$word', - 'port' => 25, - 'encryption' => null, - ); - - private static $requiredOptions = array( - 'host', - ); - - private static $optionTypes = array( - 'host' => 'string', - 'username' => 'string', - 'password' => 'string', - 'port' => 'int', - ); - - private static $optionValues = array( - 'encryption' => array(null, 'ssl', 'tls'), - ); - - protected $options; - - public function __construct(array $options = array()) + // ... + protected function configureOptions(OptionsResolver $resolver) { - Options::validateRequired($options, static::$requiredOptions); - Options::validateTypes($options, static::$optionTypes); - Options::validateValues($options, static::$optionValues); + // ... + $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; + } + } - $this->options = Options::resolve($options, static::$defaultOptions); + return $value; + }); } } -In this way, the class remains easy to read and maintain even with a lot of -options being processed and validated. +Default Values that Depend on another Option +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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. -.. caution:: +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:: - PHP does not support closures in property definitions. In such cases, you - must move your closure to a static method:: + use Symfony\Component\OptionsResolver\Options; - private static $defaultOptions = array( + // ... + class Mailer + { + // ... + protected function configureOptions(OptionsResolver $resolver) + { // ... - 'port' => array(__CLASS__, 'getDefaultPort'), - ); + $resolver->setDefault('encryption', null); - public static function getDefaultPort(Options $options) - { - if ('ssl' === $options['encryption']) { - return 465; - } + $resolver->setDefault('port', function (Options $options) { + if ('ssl' === $options['encryption']) { + return 465; + } - return 25; + return 25; + }); } + } -Decoupling the Option Configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. caution:: -So far, the configuration of the options, their allowed types etc. was very -tightly coupled to the code that resolves the options. This is fine in most cases. -In some cases, however, the configuration of options must be distributed across -multiple classes. An example is a class hierarchy that supports the addition of -options by subclasses. In those cases, you can create an -:class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` object and pass that -object everywhere that you want to adjust the option configuration. Then, call -:method:`Symfony\\Component\\OptionsResolver\\Options::resolve` with the -configuration object to resolve the options. + The argument of the callable must be type hinted as ``Options``. Otherwise, + the callable is considered as the default value of the option. -The following code demonstrates how to write our previous ``Mailer`` class with -an :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` object:: +.. note:: - // ... - use Symfony\Component\OptionsResolver\OptionsConfig; + 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 $options; - - public function __construct(array $options = array()) - { - $config = new OptionsConfig(); - $this->configureOptions($config); - - $this->options = Options::resolve($options, $config); - } - - protected function configureOptions(OptionsConfig $config) + // ... + protected function configureOptions(OptionsResolver $resolver) { - $config->setDefaults(array( - 'host' => null, - 'username' => 'user', - 'password' => 'pa$$word', - 'port' => 25, + // ... + $resolver->setDefaults(array( 'encryption' => null, - )); - - $config->setRequired(array( - 'host', - )); - - $config->setAllowedTypes(array( - 'host' => 'string', - 'username' => 'string', - 'password' => 'string', - 'port' => 'int', - )); - - $config->setAllowedValues(array( - 'encryption' => array(null, 'ssl', 'tls'), + 'host' => 'example.org', )); } } -As you can see, the code is very similar as before. However, the performance -is marginally worse, since the creation of an additional object is required: -the :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` instance. - -Nevertheless, this design also has a benefit: We can extend the ``Mailer`` -class and adjust the options of the parent class in the subclass:: - - // ... class GoogleMailer extends Mailer { - protected function configureOptions(OptionsConfig $config) + protected function configureOptions(OptionsResolver $resolver) { - $config->setDefaults(array( - 'host' => 'smtp.google.com', - 'port' => 25, - 'encryption' => 'ssl', - )); + parent::configureOptions($resolver); - $config->setRequired(array( - 'username', - 'password', - )); + $options->setDefault('host', function (Options $options, $previousValue) { + if ('ssl' === $options['encryption']) { + return 'secure.example.org' + } + + // Take default value configured in the base class + return $previousValue; + }); } } -The ``host`` option is no longer required now, but defaults to "smtp.google.com". -The ``username`` and ``password`` options, however, are required in the -subclass. - -The :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` has various -useful methods to find out which options are set or required. Check out the -API documentation to find out more about these methods. +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. -.. note:: +Options without Default Values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - The :class:`Symfony\\Component\\OptionsResolver\\OptionsResolver` class used - by the Form Component inherits from - :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig`. All the - documentation for ``OptionsConfig`` applies to ``OptionsResolver`` as well. - -Optional Options -~~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` has one feature -that is not available when not using this class: You can specify optional -options. Optional options will be accepted and validated when set. When not set, -however, *no default value* will be added to the options array. Pass the names -of the optional options to -:method:`Symfony\\Component\\OptionsResolver\\OptionsConfig::setOptional`:: +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:: // ... class Mailer { // ... - protected function configureOptions(OptionsConfig $config) + protected function configureOptions(OptionsResolver $resolver) { // ... + $resolver->setDefault('port', 25); + } - $config->setOptional(array('port')); + // ... + 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']) { + // ... + } } } -This is useful if you need to know whether an option was explicitly passed. If -not, it will be missing from the options array:: +.. 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`. + +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`:: // ... class Mailer { // ... - public function __construct(array $options = array()) + protected function configureOptions(OptionsResolver $resolver) { // ... + $resolver->setDefined('port'); + } + // ... + public function sendMail($from, $to) + { if (array_key_exists('port', $this->options)) { - echo "Set!"; + echo 'Set!'; } else { - echo "Not Set!"; + echo 'Not Set!'; } } } + $mailer = new Mailer(); + $mailer->sendMail($from, $to); + // => Not Set! + $mailer = new Mailer(array( 'port' => 25, )); - // Set! - - $mailer = new Mailer(); - // Not Set! - -.. tip:: + $mailer->sendMail($from, $to); + // => Set! - If you need this functionality when not using an - :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` object, check - the options before calling - :method:`Symfony\\Component\\OptionsResolver\\Options::resolve`:: - - // ... - class Mailer - { - // ... - public function __construct(array $options = array()) - { - // ... - - if (array_key_exists('port', $options)) { - echo "Set!"; - } else { - echo "Not Set!"; - } - - $this->options = Options::resolve($options, array( - // ... - )); - } - } - -Overwriting Default Values -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A previously set default value can be overwritten by invoking -:method:`Symfony\\Component\\OptionsResolver\\OptionsConfig::setDefaults` -again. When using a closure as the new value it is passed 2 arguments: - -* ``$options``: an :class:`Symfony\\Component\\OptionsResolver\\Options` - instance with all the other default options -* ``$previousValue``: the previously set default value - -.. code-block:: php +You can also pass an array of option names if you want to define multiple +options in one go:: // ... class Mailer { // ... - protected function configureOptions(OptionsConfig $config) + protected function configureOptions(OptionsResolver $resolver) { // ... - $config->setDefaults(array( - 'encryption' => 'ssl', - 'host' => 'localhost', - )); - - // ... - $config->setDefaults(array( - 'encryption' => 'tls', // simple overwrite - 'host' => function (Options $options, $previousValue) { - return 'localhost' == $previousValue - ? '127.0.0.1' - : $previousValue; - }, - )); + $resolver->setDefined(array('port', 'encryption')); } } -.. tip:: +.. versionadded:: 2.6 + The method :method:`Symfony\\Components\\OptionsResolver\\OptionsResolver::isDefined` + and :method:`Symfony\\Components\\OptionsResolver\\OptionsResolver::getDefinedOptions` + were introduced in Symfony 2.6. - If the previous default value is calculated by an expensive closure and - you don't need access to it, use the - :method:`Symfony\\Component\\OptionsResolver\\OptionsConfig::replaceDefaults` - method instead. It acts like ``setDefaults`` but erases the previous value - to improve performance. This means that the previous default value is not - available when overwriting with another closure:: +The methods :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isDefined` +and :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::getDefinedOptions` +let you find out which options are defined:: - // ... - class Mailer + // ... + class GoogleMailer extends Mailer + { + protected function configureOptions(OptionsResolver $resolver) { - // ... - protected function configureOptions(OptionsConfig $config) - { - // ... - $config->setDefaults(array( - 'encryption' => 'ssl', - 'heavy' => function (Options $options) { - // Some heavy calculations to create the $result - - return $result; - }, - )); - - $config->replaceDefaults(array( - 'encryption' => 'tls', // simple overwrite - 'heavy' => function (Options $options) { - // $previousValue not available - // ... - - return $someOtherResult; - }, - )); - } - } + parent::configureOptions($resolver); -.. note:: + if ($resolver->isDefined('host')) { + // One of the following was called: - Existing option keys that you do not mention when overwriting are preserved. + // $resolver->setDefault('host', ...); + // $resolver->setRequired('host'); + // $resolver->setDefined('host'); + } -Option Normalization -~~~~~~~~~~~~~~~~~~~~ + $definedOptions = $resolver->getDefinedOptions(); + } + } + +Performance Tweaks +~~~~~~~~~~~~~~~~~~ -Some 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 all options were -processed. You can configure these normalizers by calling -:method:`Symfony\\Components\\OptionsResolver\\OptionsConfig::setNormalizers`:: +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:: // ... class Mailer { - // ... - protected function configureOptions(OptionsConfig $config) + private static $resolversByClass = array(); + + protected $options; + + public function __construct(array $options = array()) { - // ... + // Are we a Mailer, a GoogleMailer, ... ? + $class = get_class($this); - $config->setNormalizers(array( - 'host' => function (Options $options, $value) { - if ('http://' !== substr($value, 0, 7)) { - $value = 'http://'.$value; - } + // Did we call configureOptions() before for this class? + if (!isset(self::$resolversByClass[$class])) { + self::$resolversByClass[$class] = new OptionsResolver(); + $this->configureOptions(self::$resolversByClass[$class]); + } - return $value; - }, - )); + $this->options = self::$resolversByClass[$class]->resolve($options); } - } -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 for the normalization:: - - // ... - class Mailer - { - // ... - protected function configureOptions(OptionsConfig $config) + protected function configureOptions(OptionsResolver $resolver) { // ... - - $config->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; - } - } - - return $value; - }, - )); } } -.. tip:: +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:: - When not using an :class:`Symfony\\Component\\OptionsResolver\\OptionsConfig` - object, perform normalization after the call to - :method:`Symfony\\Component\\OptionsResolver\\Options::resolve`:: + // ... + class Mailer + { + private static $resolversByClass = array(); - // ... - class Mailer + public static function clearDefaultOptions() { - // ... - public function __construct(array $options = array()) - { - $this->options = Options::resolve($options, array( - // ... - )); - - if ('http://' !== substr($this->options['host'], 0, 7)) { - $this->options['host'] = 'http://'.$this->options['host']; - } - } + self::$resolversByClass = array(); } + // ... + } + 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