Skip to content

Tweaks after proofreading the 2.6 OptionsResolver stuff #4372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 28, 2014
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 60 additions & 38 deletions components/options_resolver.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
The OptionsResolver Component
=============================

The OptionsResolver component is `array_replace()` on steroids.
The OptionsResolver component is :phpfunction:`array_replace` on steroids.
It allows you to create an options system with required options, defaults,
validation (type, value), normalization and more.

Installation
------------
Expand All @@ -20,8 +22,8 @@ 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.
version, please `read the Symfony 2.5 documentation`_. For a list of changes,
see the `CHANGELOG`_.

Usage
-----
Expand All @@ -48,29 +50,35 @@ check which options are set::
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. We can use
options are buried in the business logic of your code. Use the
:phpfunction:`array_replace` to fix that::
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[...] use the :phpfunction:`array_replace` function to fix that [...]


class Mailer
{
// ...

public function __construct(array $options = array())
{
$this->options = array_replace(array(
Expand All @@ -83,27 +91,27 @@ options are buried in the business logic of your code. We can use
}

Now all four options are guaranteed to be set. But what happens if the user of
the ``Mailer`` class does a mistake?
the ``Mailer`` class makes 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.
No error will be shown. In the best case, the bug will appear during testing,
but the developer will spend time looking for the problem. In the worst case,
the bug might not appear until it's deployed to the live system.

Let's use the :class:`Symfony\\Component\\OptionsResolver\\OptionsResolver`
class to fix this problem::
Fortunately, the :class:`Symfony\\Component\\OptionsResolver\\OptionsResolver`
class helps you to fix this problem::

use Symfony\Component\OptionsResolver\Options;

class Mailer
{
// ...

public function __construct(array $options = array())
{
$resolver = new OptionsResolver();
Expand Down Expand Up @@ -136,6 +144,7 @@ code::
class Mailer
{
// ...

public function sendMail($from, $to)
{
$mail = ...;
Expand All @@ -153,6 +162,7 @@ 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();
Expand All @@ -164,10 +174,10 @@ It's a good practice to split the option configuration into a separate method::
protected function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'host' => 'smtp.example.org',
'username' => 'user',
'password' => 'pa$$word',
'port' => 25,
'host' => 'smtp.example.org',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, and much to my dismay, Symfony project has recently decided to stop writing beautifully aligned code (see symfony/symfony#12284). Should we apply this practice to the code displayed in the documentation?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. While we should follow the CS of Symfony as much as possible, I don't think we should do it here. Not aligning items will significantly reduce the readability of the code. In documentation, the code is purely meant for presentation. In the core code, code also have other tasks than presentation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not aligning items will significantly reduce the readability of the code.

Thanks for saying this. I though I was the only one thinking that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you that it is more readable and that we should therefore use it in the documentation. However, I don't think that the readability benefits don't outweigh its drawbacks that apply when such code needs to be reviewed. :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

'username' => 'user',
'password' => 'pa$$word',
'port' => 25,
'encryption' => null,
));
}
Expand Down Expand Up @@ -196,12 +206,13 @@ 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::
For example, to make the ``host`` option required, you can do::

// ...
class Mailer
{
// ...

protected function configureOptions(OptionsResolver $resolver)
{
// ...
Expand All @@ -210,8 +221,8 @@ For example, let's make the ``host`` option required::
}

.. versionadded:: 2.6
Before Symfony 2.6, `setRequired()` accepted only arrays. Since then, single
option names can be passed as well.
As of Symfony 2.6, ``setRequired()`` accepts both an array of options or a
single option. Prior to 2.6, you could only pass arrays.

If you omit a required option, a
:class:`Symfony\\Component\\OptionsResolver\\Exception\\MissingOptionsException`
Expand All @@ -229,6 +240,7 @@ one required option::
class Mailer
{
// ...

protected function configureOptions(OptionsResolver $resolver)
{
// ...
Expand Down Expand Up @@ -268,14 +280,15 @@ retrieve the names of all 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
The difference between this and :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isRequired`
is that this method will return false if a required option has already
been set::

// ...
class Mailer
{
// ...

protected function configureOptions(OptionsResolver $resolver)
{
// ...
Expand Down Expand Up @@ -320,6 +333,7 @@ correctly. To validate the types of the options, call
class Mailer
{
// ...

protected function configureOptions(OptionsResolver $resolver)
{
// ...
Expand All @@ -329,8 +343,8 @@ correctly. To validate the types of the options, call
}

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_<type>()`` method is defined.
Additionally, you may pass fully qualified class or interface names.
types. You can pass any type for which an ``is_<type>()`` function is defined
in PHP. Additionally, you may pass fully qualified class or interface names.

If you pass an invalid option now, an
:class:`Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException`
Expand All @@ -348,9 +362,7 @@ 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
the values to be given as an array mapping option names to allowed types::

$resolver->setAllowedTypes(array('port' => array('null', 'int')));

Expand All @@ -360,13 +372,14 @@ 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\\OptionsResolver::setAllowedValues` to verify
that the passed option contains one of these values::
:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setAllowedValues`
to verify that the passed option contains one of these values::

// ...
class Mailer
{
// ...

protected function configureOptions(OptionsResolver $resolver)
{
// ...
Expand Down Expand Up @@ -420,9 +433,11 @@ option. You can configure a normalizer by calling
class Mailer
{
// ...

protected function configureOptions(OptionsResolver $resolver)
{
// ...

$resolver->setNormalizer('host', function ($options, $value) {
if ('http://' !== substr($value, 0, 7)) {
$value = 'http://'.$value;
Expand Down Expand Up @@ -467,12 +482,12 @@ 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
encryption chosen by the user of the ``Mailer`` class. More precisely, you want
to set the port to ``465`` if SSL is used and to ``25`` otherwise.

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::
You can implement this feature by passing a closure as the 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;

Expand All @@ -498,7 +513,7 @@ options, you can return the desired default value::
.. caution::

The argument of the callable must be type hinted as ``Options``. Otherwise,
the callable is considered as the default value of the option.
the callable itself is considered as the default value of the option.

.. note::

Expand Down Expand Up @@ -546,8 +561,10 @@ Options without Default Values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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::
This is useful if you need to know whether or not the user *actually* set
an option or not. For example, if you set the default value for an option,
it's not possible to know whether the user passed this value or if it simply
comes from the default::

// ...
class Mailer
Expand Down Expand Up @@ -584,6 +601,7 @@ be included in the resolved options if it was actually passed to
class Mailer
{
// ...

protected function configureOptions(OptionsResolver $resolver)
{
// ...
Expand Down Expand Up @@ -637,6 +655,8 @@ let you find out which options are defined::
// ...
class GoogleMailer extends Mailer
{
// ...

protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
Expand Down Expand Up @@ -671,10 +691,10 @@ can change your code to do the configuration only once per class::

public function __construct(array $options = array())
{
// Are we a Mailer, a GoogleMailer, ... ?
// What type of Mailer is this, a Mailer, a GoogleMailer, ... ?
$class = get_class($this);

// Did we call configureOptions() before for this class?
// Was configureOptions() executed before for this class?
if (!isset(self::$resolversByClass[$class])) {
self::$resolversByClass[$class] = new OptionsResolver();
$this->configureOptions(self::$resolversByClass[$class]);
Expand All @@ -693,14 +713,14 @@ 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::
method ``clearOptionsConfig()`` and call it periodically::

// ...
class Mailer
{
private static $resolversByClass = array();

public static function clearDefaultOptions()
public static function clearOptionsConfig()
{
self::$resolversByClass = array();
}
Expand All @@ -713,3 +733,5 @@ options in your code.

.. _Packagist: https://packagist.org/packages/symfony/options-resolver
.. _Form component: http://symfony.com/doc/current/components/form/introduction.html
.. _CHANGELOG: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/OptionsResolver/CHANGELOG.md#260
.. _`read the Symfony 2.5 documentation`: http://symfony.com/doc/2.5/components/options_resolver.html