merged branch bschussek/options (PR #3968)

Commits
-------

95727ff [OptionsResolver] Updated PHP requirements to 5.3.3
1c5f6c7 [OptionsResolver] Fixed issues mentioned in the PR comments
d60626e [OptionsResolver] Fixed clear() and remove() method in Options class
2b46975 [OptionsResolver] Fixed Options::replace() method
16f7d20 [OptionsResolver] Improved implementation and clarity of the Options class
6ce68b1 [OptionsResolver] Removed reference to non-existing property
9c76750 [OptionsResolver] Fixed doc and block nesting
876fd9b [OptionsResolver] Implemented fluid interface
95454f5 [OptionsResolver] Fixed typos
256b708 [OptionsParser] Renamed OptionsParser to OptionsResolver
04522ca [OptionsParser] Added method replaceDefaults()
b9d053e [Form] Moved Options classes to new OptionsParser component

Discussion
----------

Extracted OptionsResolver component out of Form

Bug fix: no
Feature addition: yes
Backwards compatibility break: no
Symfony2 tests pass: yes
Fixes the following tickets: -
Todo: -

![Travis Build Status](https://secure.travis-ci.org/bschussek/symfony.png?branch=options)

This PR refactors the options-related code of the Form component into a separate component. See the README file for usage examples.

---------------------------------------------------------------------------

by schmittjoh at 2012-04-17T18:11:03Z

To me it seems like we have some redundancy with the Config/Definition component. I'm wondering if these two can/should be merged somehow?

---------------------------------------------------------------------------

by kriswallsmith at 2012-04-17T18:14:44Z

I would also suggest merging this into the Config component. Its current name too closely resembles Python's optparser lib, which could create confusion.

---------------------------------------------------------------------------

by bschussek at 2012-04-17T18:18:49Z

Merge conflict artifacts are fixed now.

@schmittjoh Do we? Isn't the idea of the Config component to read complex configuration from different configuration providers? (YAML, XML, Annotations etc.)

The idea of this parser is to be highly performant and to be usable in simple classes. If this can be achieved with the Config component, I'm happy to learn more.

---------------------------------------------------------------------------

by schmittjoh at 2012-04-17T18:27:08Z

The config component is basically a super intelligent version of array_merge and the like.

About performance, I haven't really done any tests to say something about the impact. I think it's safe to say that it would be at least slower than your implementation in its current form due to the additional indirection. However, we could probably add a caching layer.

---------------------------------------------------------------------------

by bschussek at 2012-04-17T18:31:22Z

Have you checked the README I wrote? Are you sure the Config component is intended for the same purpose and not *way* too complex in this case?

---------------------------------------------------------------------------

by stof at 2012-04-17T18:51:14Z

You also forgot to update the ``replace`` section of the root composer.json file.

And regarding doing such thing with the Config Definition stuff, it would be more difficult: it builds the tree of values with their defaults, and then merges stuff coming from different sources. The form component however receives defaults from different places (which also define the allowed keys at the same time) and then receives user options only once. And it needs to handle easily default values which depend from other values. So I think both implementations are useful for different needs (however, we could argue about making it a subnamespace in the Config component, but this would add yet another different stuff in it)

---------------------------------------------------------------------------

by jalliot at 2012-04-17T18:58:03Z

@bschussek You need to add this component to the main composer.json too.

---------------------------------------------------------------------------

by lsmith77 at 2012-04-18T06:54:17Z

doesn't this overlap a bit with the ``TreeBuilder`` in the Config component?

---------------------------------------------------------------------------

by lsmith77 at 2012-04-18T06:59:12Z

ah just saw @stof's comment .. i think the biggest argument against TreeBuilder is that it was designed for a very specific purpose and performance wasn't one of them. where as Form needs something that performs fast. so yeah i do see different use cases, but i don't think this means we should have a new component.

furthermore while i haven't read the code in details i am surprised it doesn't make use of http://php.net/manual/en/function.array-replace-recursive.php to merge defaults into a user supplied options array.

---------------------------------------------------------------------------

by bschussek at 2012-04-18T08:10:49Z

@stof, @jalliot: Fixed.

> furthermore while i haven't read the code in details i am surprised it doesn't make use of http://php.net/manual/en/function.array-replace-recursive.php to merge defaults into a user supplied options array.

@lsmith77: Because that's not what this component does. The key feature of this component is to resolve default values of options that depend on the *concrete* values of other options. I invite you to read the README.

Is it a good idea to merge this into Config? I think that both components address different audiences and different purposes. The idea of this one is to initialize classes with simple, run-time provided arrays. The idea of Config is to load and validate complex configurations from storage providers, such as the filesystem.

---------------------------------------------------------------------------

by bschussek at 2012-04-18T08:18:48Z

Note: Not all relevant code of this component is shown in the diff. The (crucial) Options and LazyOption classes have only been moved out of Form.

---------------------------------------------------------------------------

by lsmith77 at 2012-04-18T08:20:02Z

> Is it a good idea to merge this into Config? I think that both components address different audiences and different purposes. The idea of this one is to initialize classes with simple, run-time provided arrays. The idea of Config is to load and validate complex configuration values from the filesystem (typically).

decoupled is all fine, but to me this feels a bit too granular. but i am just expressing a gut feeling here

---------------------------------------------------------------------------

by jalliot at 2012-04-18T08:34:03Z

I think too it should be included in the config component (maybe in a subnamespace). Indeed the behaviour is too different to be merged into the current component but its purpose is similar and is all about *configuration* (hence the name of the component). Otherwise we could also split the current Config component into smaller components as it seems to me there are already parts of it that are totally unrelated to each other.

---------------------------------------------------------------------------

by bschussek at 2012-04-18T11:30:55Z

@jalliot Can you go into detail which parts that are and what changes you suggest?

@kriswallsmith Any other naming suggestion?

---------------------------------------------------------------------------

by jalliot at 2012-04-18T11:34:35Z

@bschussek I don't know the current component well enough but that's the impression I had last time I looked at its code but I may be wrong.

---------------------------------------------------------------------------

by stof at 2012-04-18T19:30:43Z

@bschussek the Definition subnamespace of the Config component is standalone. It is not directly related to the Loader part

---------------------------------------------------------------------------

by bschussek at 2012-04-19T09:32:48Z

@stof So what do you recommend?

I think this is also a question of marketing. Is the Definition subnamespace intended to be used totally separately of the loaders? What are the use cases? If there are good use cases, it makes sense to me to extract the Definition part into a separate component. Otherwise not.

It is also a question of marketing, because the purpose of a component should be communicable in simple words (quoting @fabpot). The purpose of Config is (copied from the README):

> Config provides the infrastructure for loading configurations from different data sources and optionally monitoring these data sources for changes. There are additional tools for validating, normalizing and handling of defaults that can optionally be used to convert from different formats to arrays.

I think this purpose is completely different than that of OptionsParser.

---------------------------------------------------------------------------

by stof at 2012-04-19T11:39:50Z

The current description itself shows the current state: what is advocated as the main goal of the component (and was the original part) is the loader stuff. But the Definition part (mentioned as "additional tools") is bigger in term of LOC

---------------------------------------------------------------------------

by bschussek at 2012-04-19T11:55:17Z

@stof: Yes, this is a fact, but what's your opinion? How do we proceed with this PR?

---------------------------------------------------------------------------

by stof at 2012-04-19T12:21:44Z

Well, my opinion is that the current Config component may deserve to be split into 2 components (as someone may need only part of it). But this would be a huge BC break. @fabpot what do you think ?

---------------------------------------------------------------------------

by bschussek at 2012-04-23T10:14:57Z

@fabpot Can we merge this?

---------------------------------------------------------------------------

by fabpot at 2012-05-10T06:45:20Z

@bschussek I'm +1 for this PR but as mentioned by @kriswallsmith, we must find another name as `OptionsParser` immediately make me think of something related to the CLI.

---------------------------------------------------------------------------

by stof at 2012-05-10T06:47:45Z

However, after thinking about it again, I would vote for keeping it in its own component instead of adding yet another independant part in Config, to avoid forcing Form users to get the whole Config component

---------------------------------------------------------------------------

by bschussek at 2012-05-10T09:09:36Z

I'm having difficulties finding a better name. The main difference to CLI option parsers is that these actualy *parse* a string, while this class only receives an array of options (does not do any parsing). Otherwise both have the same purpose.

A couple of other suggestions:

* OptionsLoader (likely confused with our filesystem loaders)
* OptionsResolver
* OptionsMerger
* OptionsMatcher (not accurate)
* OptionsBuilder (likely confused with the builder pattern)
* OptionsJoiner
* OptionsBag (likely confused with the session bags)
* OptionsConfig (likely confused with Config)
* OptionsDefinition (likely confused with Config\Definition)
* OptionsSpec
* OptionsCombiner
* OptionsInitializer
* OptionsComposer

The difficulty is to find a name that best reflects its purpose:

```
$parser->setDefaults(...);
$parser->setRequired(...);
$parser->setOptional(...);
$parser->setAllowedValues(...)
$parser->parse($userOptions);
```

The only of the above examples that makes sense to me here is OptionsResolver -> resolve($userOptions).

Ideas?

---------------------------------------------------------------------------

by stof at 2012-05-10T09:56:54Z

OptionsResolver seems a better name than OptionsParser

---------------------------------------------------------------------------

by luxifer at 2012-05-10T09:59:45Z

Agree with @stof

---------------------------------------------------------------------------

by r1pp3rj4ck at 2012-05-10T10:03:53Z

I don't really like the plural in the name, but OptionsResolver seems better than OptionsParser. OptionResolver maybe?

---------------------------------------------------------------------------

by sstok at 2012-05-10T10:10:14Z

@r1pp3rj4ck Options makes more sense as they can be nested/deeper, and thus are multiple.

Agree with @stof also.

---------------------------------------------------------------------------

by r1pp3rj4ck at 2012-05-10T10:13:01Z

@sstok well, we have multiple events too and the name is EventDispatcher, not EventsDispatcher. Actually none of the component names are plural.

---------------------------------------------------------------------------

by newicz at 2012-05-10T10:33:50Z

OptionsResolver - I find it suggesting that there is some kind of problem to be resolved and there's not,
maybe OptionsDefiner but it isn't good aswell this is a tough one
This commit is contained in:
Fabien Potencier 2012-05-15 10:14:33 +02:00
commit bd07b8919d
33 changed files with 1565 additions and 969 deletions

View File

@ -42,6 +42,7 @@
"symfony/http-foundation": "self.version",
"symfony/http-kernel": "self.version",
"symfony/locale": "self.version",
"symfony/options-resolver": "self.version",
"symfony/process": "self.version",
"symfony/routing": "self.version",
"symfony/security": "self.version",

View File

@ -19,7 +19,7 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface;
use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Options;
use Symfony\Component\OptionsResolver\Options;
abstract class DoctrineType extends AbstractType
{

View File

@ -14,8 +14,8 @@ namespace Symfony\Bridge\Propel1\Form\Type;
use Symfony\Bridge\Propel1\Form\ChoiceList\ModelChoiceList;
use Symfony\Bridge\Propel1\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Options;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\OptionsResolver\Options;
/**
* ModelType class.

View File

@ -1,320 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
use Symfony\Component\Form\Exception\OptionDefinitionException;
use Symfony\Component\Form\Exception\InvalidOptionException;
/**
* Helper for specifying and resolving inter-dependent options.
*
* Options are a common pattern for initializing classes in PHP. Avoiding the
* problems related to this approach is however a non-trivial task. Usually,
* both classes and subclasses should be able to set default option values.
* These default options should be overridden by the options passed to the
* constructor. Last but not least, the (default) values of some options may
* depend on the values of other options, which themselves may depend on other
* options and so on.
*
* DefaultOptions resolves these problems. It allows you to:
*
* - Define default option values
* - Define options in layers that correspond to your class hierarchy. Each
* layer may depend on the default value set in the higher layers.
* - Define default values for options that depend on the <em>concrete</em>
* values of other options.
* - Resolve the concrete option values by passing the options set by the
* user.
*
* You can use it in your classes by implementing the following pattern:
*
* <code>
* class Car
* {
* protected $options;
*
* public function __construct(array $options)
* {
* $defaultOptions = new DefaultOptions();
* $this->addDefaultOptions($defaultOptions);
*
* $this->options = $defaultOptions->resolve($options);
* }
*
* protected function addDefaultOptions(DefaultOptions $options)
* {
* $options->add(array(
* 'make' => 'VW',
* 'year' => '1999',
* ));
* }
* }
*
* $car = new Car(array(
* 'make' => 'Mercedes',
* 'year' => 2005,
* ));
* </code>
*
* By calling add(), new default options are added to the container. The method
* resolve() accepts an array of options passed by the user that are matched
* against the defined options. If any option is not recognized, an exception
* is thrown. Finally, resolve() returns the merged default and user options.
*
* You can now easily add or override options in subclasses:
*
* <code>
* class Renault extends Car
* {
* protected function addDefaultOptions(DefaultOptions $options)
* {
* parent::addDefaultOptions($options);
*
* $options->add(array(
* 'make' => 'Renault',
* 'gear' => 'auto',
* ));
* }
* }
*
* $renault = new Renault(array(
* 'year' => 1997,
* 'gear' => 'manual'
* ));
* </code>
*
* IMPORTANT: parent::addDefaultOptions() must always be called before adding
* new default options!
*
* In the previous example, it makes sense to restrict the option "gear" to
* a set of allowed values:
*
* <code>
* class Renault extends Car
* {
* protected function addDefaultOptions(DefaultOptions $options)
* {
* // ... like above ...
*
* $options->addAllowedValues(array(
* 'gear' => array('auto', 'manual'),
* ));
* }
* }
*
* // Fails!
* $renault = new Renault(array(
* 'gear' => 'v6',
* ));
* </code>
*
* Now it is impossible to pass a value in the "gear" option that is not
* expected.
*
* Last but not least, you can define options that depend on other options.
* For example, depending on the "make" you could preset the country that the
* car is registered in.
*
* <code>
* class Car
* {
* protected function addDefaultOptions(DefaultOptions $options)
* {
* $options->add(array(
* 'make' => 'VW',
* 'year' => '1999',
* 'country' => function (Options $options) {
* if ('VW' === $options['make']) {
* return 'DE';
* }
*
* return null;
* },
* ));
* }
* }
*
* $car = new Car(array(
* 'make' => 'VW', // => "country" is "DE"
* ));
* </code>
*
* The closure receives as its first parameter a container of class Options
* that contains the <em>concrete</em> options determined upon resolving. The
* closure is executed once resolve() is called.
*
* The closure also receives a second parameter $previousValue that contains the
* value defined by the parent layer of the hierarchy. If the option has not
* been defined in any parent layer, the second parameter is NULL.
*
* <code>
* class Renault extends Car
* {
* protected function addDefaultOptions(DefaultOptions $options)
* {
* $options->add(array(
* 'country' => function (Options $options, $previousValue) {
* if ('Renault' === $options['make']) {
* return 'FR';
* }
*
* // return default value defined in Car
* return $previousValue;
* },
* ));
* }
* }
*
* $renault = new Renault(array(
* 'make' => 'VW', // => "country" is still "DE"
* ));
* </code>
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class DefaultOptions
{
/**
* The container resolving the options.
* @var Options
*/
private $options;
/**
* A list of accepted values for each option.
* @var array
*/
private $allowedValues = array();
/**
* Creates a new instance.
*/
public function __construct()
{
$this->options = new Options();
}
/**
* Adds default options.
*
* @param array $options A list of option names as keys and option values
* as values. The option values may be closures
* of the following signatures:
*
* - function (Options $options)
* - function (Options $options, $previousValue)
*/
public function add(array $options)
{
foreach ($options as $option => $value) {
$this->options[$option] = $value;
}
}
/**
* Adds allowed values for a list of options.
*
* @param array $allowedValues A list of option names as keys and arrays
* with values acceptable for that option as
* values.
*
* @throws InvalidOptionException If an option has not been defined for
* which an allowed value is set.
*/
public function addAllowedValues(array $allowedValues)
{
$this->validateOptionNames(array_keys($allowedValues));
$this->allowedValues = array_merge_recursive($this->allowedValues, $allowedValues);
}
/**
* Resolves the final option values by merging default options with user
* options.
*
* @param array $userOptions The options passed by the user.
*
* @return array A list of options and their final values.
*
* @throws InvalidOptionException If any of the passed options has not
* been defined or does not contain an
* allowed value.
* @throws OptionDefinitionException If a cyclic dependency is detected
* between option closures.
*/
public function resolve(array $userOptions)
{
// Make sure this method can be called multiple times
$options = clone $this->options;
$this->validateOptionNames(array_keys($userOptions));
// Override options set by the user
foreach ($userOptions as $option => $value) {
$options[$option] = $value;
}
// Resolve options
$options = iterator_to_array($options);
// Validate against allowed values
$this->validateOptionValues($options);
return $options;
}
/**
* Validates that the given option names exist and throws an exception
* otherwise.
*
* @param array $optionNames A list of option names.
*
* @throws InvalidOptionException If any of the options has not been
* defined.
*/
private function validateOptionNames(array $optionNames)
{
$knownOptions = $this->options->getNames();
$diff = array_diff($optionNames, $knownOptions);
if (count($diff) > 0) {
sort($knownOptions);
sort($diff);
}
if (count($diff) > 1) {
throw new InvalidOptionException(sprintf('The options "%s" do not exist. Known options are: "%s"', implode('", "', $diff), implode('", "', $knownOptions)));
}
if (count($diff) > 0) {
throw new InvalidOptionException(sprintf('The option "%s" does not exist. Known options are: "%s"', current($diff), implode('", "', $knownOptions)));
}
}
/**
* Validates that the given option values match the allowed values and
* throws an exception otherwise.
*
* @param array $options A list of option values.
*
* @throws InvalidOptionException If any of the values does not match the
* allowed values of the option.
*/
private function validateOptionValues(array $options)
{
foreach ($this->allowedValues as $option => $allowedValues) {
if (!in_array($options[$option], $allowedValues, true)) {
throw new InvalidOptionException(sprintf('The option "%s" has the value "%s", but is expected to be one of "%s"', $option, $options[$option], implode('", "', $allowedValues)));
}
}
}
}

View File

@ -1,16 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Exception;
class OptionDefinitionException extends FormException
{
}

View File

@ -12,7 +12,6 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Options;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
@ -27,6 +26,7 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransform
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToBooleanArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToBooleanArrayTransformer;
use Symfony\Component\OptionsResolver\Options;
class ChoiceType extends AbstractType
{

View File

@ -21,7 +21,7 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransfo
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
use Symfony\Component\Form\Options;
use Symfony\Component\OptionsResolver\Options;
class DateTimeType extends AbstractType
{

View File

@ -22,7 +22,7 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransfo
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\Form\Options;
use Symfony\Component\OptionsResolver\Options;
class DateType extends AbstractType
{

View File

@ -12,7 +12,6 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\EventDispatcher\EventDispatcher;
/**
* Deprecated. You should extend FormType instead.

View File

@ -12,7 +12,6 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Options;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormInterface;
@ -23,6 +22,7 @@ use Symfony\Component\Form\Extension\Core\EventListener\ValidationListener;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\OptionsResolver\Options;
class FormType extends AbstractType
{

View File

@ -20,7 +20,7 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransf
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Options;
use Symfony\Component\OptionsResolver\Options;
class TimeType extends AbstractType
{

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
use Symfony\Component\OptionsResolver\Options;
class TimezoneType extends AbstractType
{

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\Form;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\TypeDefinitionException;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FormFactory implements FormFactoryInterface
{
@ -220,7 +221,7 @@ class FormFactory implements FormFactoryInterface
$types = array();
$optionValues = array();
$knownOptions = array();
$defaultOptions = new DefaultOptions();
$optionsResolver = new OptionsResolver();
// Bottom-up determination of the type hierarchy
// Start with the actual type and look for the parent type
@ -254,14 +255,14 @@ class FormFactory implements FormFactoryInterface
// options. Default options of children override default options
// of parents.
$typeOptions = $type->getDefaultOptions();
$defaultOptions->add($typeOptions);
$defaultOptions->addAllowedValues($type->getAllowedOptionValues());
$optionsResolver->setDefaults($typeOptions);
$optionsResolver->addAllowedValues($type->getAllowedOptionValues());
$knownOptions = array_merge($knownOptions, array_keys($typeOptions));
foreach ($type->getExtensions() as $typeExtension) {
$extensionOptions = $typeExtension->getDefaultOptions();
$defaultOptions->add($extensionOptions);
$defaultOptions->addAllowedValues($typeExtension->getAllowedOptionValues());
$optionsResolver->setDefaults($extensionOptions);
$optionsResolver->addAllowedValues($typeExtension->getAllowedOptionValues());
$knownOptions = array_merge($knownOptions, array_keys($extensionOptions));
}
}
@ -277,7 +278,7 @@ class FormFactory implements FormFactoryInterface
}
// Resolve options
$options = $defaultOptions->resolve($options);
$options = $optionsResolver->resolve($options);
for ($i = 0, $l = count($types); $i < $l && !$builder; ++$i) {
$builder = $types[$i]->createBuilder($name, $this, $options);

View File

@ -1,364 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
use ArrayAccess;
use Iterator;
use OutOfBoundsException;
use Symfony\Component\Form\Exception\OptionDefinitionException;
/**
* Container for resolving inter-dependent options.
*
* Options are a common pattern for resolved classes in PHP. Avoiding the
* problems related to this approach is however a non-trivial task. Usually,
* both classes and subclasses should be able to set default option values.
* These default options should be overridden by the options passed to the
* constructor. Last but not least, the (default) values of some options may
* depend on the values of other options, which themselves may depend on other
* options.
*
* This class resolves these problems. You can use it in your classes by
* implementing the following pattern:
*
* <code>
* class Car
* {
* protected $options;
*
* public function __construct(array $options)
* {
* $_options = new Options();
* $this->addDefaultOptions($_options);
*
* $this->options = $_options->resolve($options);
* }
*
* protected function addDefaultOptions(Options $options)
* {
* $options->add(array(
* 'make' => 'VW',
* 'year' => '1999',
* ));
* }
* }
*
* $car = new Car(array(
* 'make' => 'Mercedes',
* 'year' => 2005,
* ));
* </code>
*
* By calling add(), new default options are added to the container. The method
* resolve() accepts an array of options passed by the user that are matched
* against the allowed options. If any option is not recognized, an exception
* is thrown. Finally, resolve() returns the merged default and user options.
*
* You can now easily add or override options in subclasses:
*
* <code>
* class Renault extends Car
* {
* protected function addDefaultOptions(Options $options)
* {
* parent::addDefaultOptions($options);
*
* $options->add(array(
* 'make' => 'Renault',
* 'gear' => 'auto',
* ));
* }
* }
*
* $renault = new Renault(array(
* 'year' => 1997,
* 'gear' => 'manual'
* ));
* </code>
*
* IMPORTANT: parent::addDefaultOptions() must always be called before adding
* new options!
*
* In the previous example, it makes sense to restrict the option "gear" to
* a set of allowed values:
*
* <code>
* class Renault extends Car
* {
* protected function addDefaultOptions(Options $options)
* {
* // ... like above ...
*
* $options->addAllowedValues(array(
* 'gear' => array('auto', 'manual'),
* ));
* }
* }
*
* // Fails!
* $renault = new Renault(array(
* 'gear' => 'v6',
* ));
* </code>
*
* Now it is impossible to pass a value in the "gear" option that is not
* expected.
*
* Last but not least, you can define options that depend on other options.
* For example, depending on the "make" you could preset the country that the
* car is registered in.
*
* <code>
* class Car
* {
* protected function addDefaultOptions(Options $options)
* {
* $options->add(array(
* 'make' => 'VW',
* 'year' => '1999',
* 'country' => function (Options $options) {
* if ('VW' === $options['make']) {
* return 'DE';
* }
*
* return null;
* },
* ));
* }
* }
*
* $car = new Car(array(
* 'make' => 'VW', // => "country" is "DE"
* ));
* </code>
*
* When overriding an option with a closure in subclasses, you can make use of
* the second parameter $parentValue in which the value defined by the parent
* class is stored.
*
* <code>
* class Renault extends Car
* {
* protected function addDefaultOptions(Options $options)
* {
* $options->add(array(
* 'country' => function (Options $options, $parentValue) {
* if ('Renault' === $options['make']) {
* return 'FR';
* }
*
* return $parentValue;
* },
* ));
* }
* }
*
* $renault = new Renault(array(
* 'make' => 'VW', // => "country" is still "DE"
* ));
* </code>
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class Options implements ArrayAccess, Iterator
{
/**
* A list of option values and LazyOption instances.
* @var array
*/
private $options = array();
/**
* A list of Boolean locks for each LazyOption.
* @var array
*/
private $lock = array();
/**
* Whether the options have already been resolved.
*
* Once resolved, no new options can be added or changed anymore.
*
* @var Boolean
*/
private $resolved = false;
/**
* Returns whether the given option exists.
*
* @param string $option The option name.
*
* @return Boolean Whether the option exists.
*
* @see ArrayAccess::offsetExists()
*/
public function offsetExists($option)
{
return isset($this->options[$option]);
}
/**
* Returns the value of the given option.
*
* After reading an option for the first time, this object becomes
*
* @param string $option The option name.
*
* @return mixed The option value.
*
* @throws OutOfBoundsException If the option does not exist.
* @throws OptionDefinitionException If a cyclic dependency is detected
* between two lazy options.
*
* @see ArrayAccess::offsetGet()
*/
public function offsetGet($option)
{
if (!array_key_exists($option, $this->options)) {
throw new OutOfBoundsException('The option "' . $option . '" does not exist');
}
$this->resolved = true;
if ($this->options[$option] instanceof LazyOption) {
if ($this->lock[$option]) {
$conflicts = array_keys(array_filter($this->lock, function ($locked) {
return $locked;
}));
throw new OptionDefinitionException('The options "' . implode('", "', $conflicts) . '" have a cyclic dependency');
}
$this->lock[$option] = true;
$this->options[$option] = $this->options[$option]->evaluate($this);
$this->lock[$option] = false;
}
return $this->options[$option];
}
/**
* Sets the value of a given option.
*
* @param string $option The name of the option.
* @param mixed $value The value of the option. May be a closure with a
* signature as defined in DefaultOptions::add().
*
* @throws OptionDefinitionException If options have already been read.
* Once options are read, the container
* becomes immutable.
*
* @see DefaultOptions::add()
* @see ArrayAccess::offsetSet()
*/
public function offsetSet($option, $value)
{
// Setting is not possible once an option is read, because then lazy
// options could manipulate the state of the object, leading to
// inconsistent results.
if ($this->resolved) {
throw new OptionDefinitionException('Options cannot be set after reading options');
}
$newValue = $value;
// If an option is a closure that should be evaluated lazily, store it
// inside a LazyOption instance.
if ($newValue instanceof \Closure) {
$reflClosure = new \ReflectionFunction($newValue);
$params = $reflClosure->getParameters();
$isLazyOption = count($params) >= 1 && null !== $params[0]->getClass() && __CLASS__ === $params[0]->getClass()->getName();
if ($isLazyOption) {
$currentValue = isset($this->options[$option]) ? $this->options[$option] : null;
$newValue = new LazyOption($newValue, $currentValue);
}
// Store locks for lazy options to detect cyclic dependencies
$this->lock[$option] = false;
}
$this->options[$option] = $newValue;
}
/**
* Removes an option with the given name.
*
* @param string $option The option name.
*
* @throws OptionDefinitionException If options have already been read.
* Once options are read, the container
* becomes immutable.
*
* @see ArrayAccess::offsetUnset()
*/
public function offsetUnset($option)
{
if ($this->resolved) {
throw new OptionDefinitionException('Options cannot be unset after reading options');
}
unset($this->options[$option]);
unset($this->allowedValues[$option]);
unset($this->lock[$option]);
}
/**
* Returns the names of all defined options.
*
* @return array An array of option names.
*/
public function getNames()
{
return array_keys($this->options);
}
/**
* @see Iterator::current()
*/
public function current()
{
return $this->offsetGet($this->key());
}
/**
* @see Iterator::next()
*/
public function next()
{
next($this->options);
}
/**
* @see Iterator::key()
*/
public function key()
{
return key($this->options);
}
/**
* @see Iterator::valid()
*/
public function valid()
{
return null !== $this->key();
}
/**
* @see Iterator::rewind()
*/
public function rewind()
{
reset($this->options);
}
}

View File

@ -1,76 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests;
use Symfony\Component\Form\DefaultOptions;
use Symfony\Component\Form\Options;
class DefaultOptionsTest extends \PHPUnit_Framework_TestCase
{
private $options;
protected function setUp()
{
$this->options = new DefaultOptions();
}
public function testResolve()
{
$this->options->add(array(
'foo' => 'bar',
'bam' => function (Options $options) {
return 'baz';
},
));
$result = array(
'foo' => 'fee',
'bam' => 'baz',
);
$this->assertEquals($result, $this->options->resolve(array(
'foo' => 'fee',
)));
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidOptionException
*/
public function testResolveFailsIfNonExistingOption()
{
$this->options->add(array(
'foo' => 'bar',
));
$this->options->resolve(array(
'non_existing' => 'option',
));
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidOptionException
*/
public function testResolveFailsIfOptionValueNotAllowed()
{
$this->options->add(array(
'foo' => 'bar',
));
$this->options->addAllowedValues(array(
'foo' => array('bar', 'baz'),
));
$this->options->resolve(array(
'foo' => 'bam',
));
}
}

View File

@ -23,7 +23,7 @@ class DateTypeTest extends LocalizedTestCase
}
/**
* @expectedException Symfony\Component\Form\Exception\FormException
* @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testInvalidWidgetOption()
{
@ -33,7 +33,7 @@ class DateTypeTest extends LocalizedTestCase
}
/**
* @expectedException Symfony\Component\Form\Exception\FormException
* @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testInvalidInputOption()
{

View File

@ -279,7 +279,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidOptionException
* @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testCreateNamedBuilderExpectsOptionsToExist()
{
@ -292,7 +292,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidOptionException
* @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testCreateNamedBuilderExpectsOptionsToBeInValidRange()
{
@ -608,7 +608,6 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($parentBuilder, $builder->getParent());
}
public function testFormTypeCreatesDefaultValueForEmptyDataOption()
{
$factory = new FormFactory(array(new \Symfony\Component\Form\Extension\Core\CoreExtension()));

View File

@ -1,168 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests;
use Symfony\Component\Form\Options;
class OptionsTest extends \PHPUnit_Framework_TestCase
{
private $options;
protected function setUp()
{
$this->options = new Options();
}
public function testArrayAccess()
{
$this->assertFalse(isset($this->options['foo']));
$this->assertFalse(isset($this->options['bar']));
$this->options['foo'] = 0;
$this->options['bar'] = 1;
$this->assertTrue(isset($this->options['foo']));
$this->assertTrue(isset($this->options['bar']));
unset($this->options['bar']);
$this->assertTrue(isset($this->options['foo']));
$this->assertFalse(isset($this->options['bar']));
$this->assertEquals(0, $this->options['foo']);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testGetNonExisting()
{
$this->options['foo'];
}
/**
* @expectedException Symfony\Component\Form\Exception\OptionDefinitionException
*/
public function testSetNotSupportedAfterGet()
{
$this->options['foo'] = 'bar';
$this->options['foo'];
$this->options['foo'] = 'baz';
}
/**
* @expectedException Symfony\Component\Form\Exception\OptionDefinitionException
*/
public function testUnsetNotSupportedAfterGet()
{
$this->options['foo'] = 'bar';
$this->options['foo'];
unset($this->options['foo']);
}
public function testLazyOption()
{
$test = $this;
$this->options['foo'] = function (Options $options) use ($test) {
return 'dynamic';
};
$this->assertEquals('dynamic', $this->options['foo']);
}
public function testLazyOptionWithEagerCurrentValue()
{
$test = $this;
// defined by superclass
$this->options['foo'] = 'bar';
// defined by subclass
$this->options['foo'] = function (Options $options, $currentValue) use ($test) {
$test->assertEquals('bar', $currentValue);
return 'dynamic';
};
$this->assertEquals('dynamic', $this->options['foo']);
}
public function testLazyOptionWithLazyCurrentValue()
{
$test = $this;
// defined by superclass
$this->options['foo'] = function (Options $options) {
return 'bar';
};
// defined by subclass
$this->options['foo'] = function (Options $options, $currentValue) use ($test) {
$test->assertEquals('bar', $currentValue);
return 'dynamic';
};
$this->assertEquals('dynamic', $this->options['foo']);
}
public function testLazyOptionWithEagerDependency()
{
$test = $this;
$this->options['foo'] = 'bar';
$this->options['bam'] = function (Options $options) use ($test) {
$test->assertEquals('bar', $options['foo']);
return 'dynamic';
};
$this->assertEquals('bar', $this->options['foo']);
$this->assertEquals('dynamic', $this->options['bam']);
}
public function testLazyOptionWithLazyDependency()
{
$test = $this;
$this->options['foo'] = function (Options $options) {
return 'bar';
};
$this->options['bam'] = function (Options $options) use ($test) {
$test->assertEquals('bar', $options['foo']);
return 'dynamic';
};
$this->assertEquals('bar', $this->options['foo']);
$this->assertEquals('dynamic', $this->options['bam']);
}
/**
* @expectedException Symfony\Component\Form\Exception\OptionDefinitionException
*/
public function testLazyOptionDisallowCyclicDependencies()
{
$this->options['foo'] = function (Options $options) {
$options['bam'];
};
$this->options['bam'] = function (Options $options) {
$options['foo'];
};
$this->options['foo'];
}
}

View File

@ -18,7 +18,8 @@
"require": {
"php": ">=5.3.3",
"symfony/event-dispatcher": "2.1.*",
"symfony/locale": "2.1.*"
"symfony/locale": "2.1.*",
"symfony/options-resolver": "2.1.*"
},
"require-dev": {
"symfony/validator": "2.1.*",

View File

@ -9,8 +9,13 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Exception;
namespace Symfony\Component\OptionsResolver\Exception;
class InvalidOptionException extends FormException
/**
* Marker interface for the Options component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ExceptionInterface
{
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\OptionsResolver\Exception;
/**
* Exception thrown when an invalid option is passed.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class InvalidOptionsException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\OptionsResolver\Exception;
/**
* Exception thrown when a required option is missing.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class MissingOptionsException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\OptionsResolver\Exception;
/**
* Thrown when an option definition is invalid.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class OptionDefinitionException extends \RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2004-2012 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
namespace Symfony\Component\OptionsResolver;
use Closure;
@ -17,8 +17,6 @@ use Closure;
* An option that is evaluated lazily using a closure.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @see DefaultOptions
*/
class LazyOption
{

View File

@ -0,0 +1,439 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\OptionsResolver;
use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException;
/**
* Container for resolving inter-dependent options.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class Options implements \ArrayAccess, \Iterator, \Countable
{
/**
* A list of option values and LazyOption instances.
* @var array
*/
private $options = array();
/**
* A list storing the names of all LazyOption instances as keys.
* @var array
*/
private $lazy = array();
/**
* A list of Boolean locks for each LazyOption.
* @var array
*/
private $lock = array();
/**
* Whether at least one option has already been read.
*
* Once read, the options cannot be changed anymore. This is
* necessary in order to avoid inconsistencies during the resolving
* process. If any option is changed after being read, all evaluated
* lazy options that depend on this option would become invalid.
*
* @var Boolean
*/
private $reading = false;
/**
* Sets the value of a given option.
*
* You can set lazy options by passing a closure with the following
* signature:
*
* <code>
* function (Options $options)
* </code>
*
* This closure will be evaluated once the option is read using
* {@link get()}. The closure has access to the resolved values of
* other options through the passed {@link Options} instance.
*
* @param string $option The name of the option.
* @param mixed $value The value of the option.
*
* @throws OptionDefinitionException If options have already been read.
* Once options are read, the container
* becomes immutable.
*/
public function set($option, $value)
{
// Setting is not possible once an option is read, because then lazy
// options could manipulate the state of the object, leading to
// inconsistent results.
if ($this->reading) {
throw new OptionDefinitionException('Options cannot be set anymore once options have been read.');
}
// Setting is equivalent to overloading while discarding the previous
// option value
unset($this->options[$option]);
$this->overload($option, $value);
}
/**
* Replaces the contents of the container with the given options.
*
* This method is a shortcut for {@link clear()} with subsequent
* calls to {@link set()}.
*
* @param array $options The options to set.
*
* @throws OptionDefinitionException If options have already been read.
* Once options are read, the container
* becomes immutable.
*/
public function replace(array $options)
{
if ($this->reading) {
throw new OptionDefinitionException('Options cannot be replaced anymore once options have been read.');
}
$this->options = array();
foreach ($options as $option => $value) {
$this->set($option, $value);
}
}
/**
* Overloads the value of a given option.
*
* Contrary to {@link set()}, this method keeps the previous default
* value of the option so that you can access it if you pass a closure.
* Passed closures should have the following signature:
*
* <code>
* function (Options $options, $previousValue)
* </code>
*
* The second parameter passed to the closure is the previous default
* value of the option.
*
* @param string $option The option name.
* @param mixed $value The option value.
*
* @throws OptionDefinitionException If options have already been read.
* Once options are read, the container
* becomes immutable.
*/
public function overload($option, $value)
{
if ($this->reading) {
throw new OptionDefinitionException('Options cannot be overloaded anymore once options have been read.');
}
$newValue = $value;
// Reset lazy flag and locks by default
unset($this->lock[$option]);
unset($this->lazy[$option]);
// If an option is a closure that should be evaluated lazily, store it
// inside a LazyOption instance.
if ($this->isEvaluatedLazily($value)) {
$currentValue = isset($this->options[$option]) ? $this->options[$option] : null;
$newValue = new LazyOption($value, $currentValue);
// Store locks for lazy options to detect cyclic dependencies
$this->lock[$option] = false;
// Store which options are lazy for more efficient resolving
$this->lazy[$option] = true;
}
$this->options[$option] = $newValue;
}
/**
* Returns the value of the given option.
*
* If the option was a lazy option, it is evaluated now.
*
* @param string $option The option name.
*
* @return mixed The option value.
*
* @throws \OutOfBoundsException If the option does not exist.
* @throws OptionDefinitionException If a cyclic dependency is detected
* between two lazy options.
*/
public function get($option)
{
$this->reading = true;
if (!array_key_exists($option, $this->options)) {
throw new \OutOfBoundsException('The option "' . $option . '" does not exist.');
}
if (isset($this->lazy[$option])) {
$this->resolve($option);
}
return $this->options[$option];
}
/**
* Returns whether the given option exists.
*
* @param string $option The option name.
*
* @return Boolean Whether the option exists.
*/
public function has($option)
{
return isset($this->options[$option]);
}
/**
* Removes the option with the given name.
*
* @param string $option The option name.
*
* @throws OptionDefinitionException If options have already been read.
* Once options are read, the container
* becomes immutable.
*/
public function remove($option)
{
if ($this->reading) {
throw new OptionDefinitionException('Options cannot be removed anymore once options have been read.');
}
unset($this->options[$option]);
unset($this->lock[$option]);
unset($this->lazy[$option]);
}
/**
* Removes all options.
*
* @throws OptionDefinitionException If options have already been read.
* Once options are read, the container
* becomes immutable.
*/
public function clear()
{
if ($this->reading) {
throw new OptionDefinitionException('Options cannot be cleared anymore once options have been read.');
}
$this->options = array();
$this->lock = array();
$this->lazy = array();
}
/**
* Returns the values of all options.
*
* Lazy options are evaluated at this point.
*
* @return array The option values.
*/
public function all()
{
$this->reading = true;
// Create a copy because resolve() modifies the array
$lazy = $this->lazy;
foreach ($lazy as $option => $isLazy) {
$this->resolve($option);
}
return $this->options;
}
/**
* Equivalent to {@link has()}.
*
* @param string $option The option name.
*
* @return Boolean Whether the option exists.
*
* @see \ArrayAccess::offsetExists()
*/
public function offsetExists($option)
{
return $this->has($option);
}
/**
* Equivalent to {@link get()}.
*
* @param string $option The option name.
*
* @return mixed The option value.
*
* @throws \OutOfBoundsException If the option does not exist.
* @throws OptionDefinitionException If a cyclic dependency is detected
* between two lazy options.
*
* @see \ArrayAccess::offsetGet()
*/
public function offsetGet($option)
{
return $this->get($option);
}
/**
* Equivalent to {@link set()}.
*
* @param string $option The name of the option.
* @param mixed $value The value of the option. May be a closure with a
* signature as defined in DefaultOptions::add().
*
* @throws OptionDefinitionException If options have already been read.
* Once options are read, the container
* becomes immutable.
*
* @see \ArrayAccess::offsetSet()
*/
public function offsetSet($option, $value)
{
$this->set($option, $value);
}
/**
* Equivalent to {@link remove()}.
*
* @param string $option The option name.
*
* @throws OptionDefinitionException If options have already been read.
* Once options are read, the container
* becomes immutable.
*
* @see \ArrayAccess::offsetUnset()
*/
public function offsetUnset($option)
{
$this->remove($option);
}
/**
* {@inheritdoc}
*/
public function current()
{
return $this->offsetGet($this->key());
}
/**
* {@inheritdoc}
*/
public function next()
{
next($this->options);
}
/**
* {@inheritdoc}
*/
public function key()
{
return key($this->options);
}
/**
* {@inheritdoc}
*/
public function valid()
{
return null !== $this->key();
}
/**
* {@inheritdoc}
*/
public function rewind()
{
reset($this->options);
}
/**
* {@inheritdoc}
*/
public function count()
{
return count($this->options);
}
/**
* Evaluates the given option if it is a lazy option.
*
* The evaluated value is written into the options array. The closure for
* evaluating the option is discarded afterwards.
*
* @param string $option The option to evaluate.
*
* @throws OptionDefinitionException If the option has a cyclic dependency
* on another option.
*/
private function resolve($option)
{
if ($this->options[$option] instanceof LazyOption) {
if ($this->lock[$option]) {
$conflicts = array_keys(array_filter($this->lock, function ($locked)
{
return $locked;
}));
throw new OptionDefinitionException('The options "' . implode('", "',
$conflicts) . '" have a cyclic dependency.');
}
$this->lock[$option] = true;
$this->options[$option] = $this->options[$option]->evaluate($this);
$this->lock[$option] = false;
// The option now isn't lazy anymore
unset($this->lazy[$option]);
}
}
/**
* Returns whether the option is a lazy option closure.
*
* Lazy option closure expect an {@link Options} instance
* in their first parameter.
*
* @param mixed $value The option value to test.
*
* @return Boolean Whether it is a lazy option closure.
*/
static private function isEvaluatedLazily($value)
{
if (!$value instanceof \Closure) {
return false;
}
$reflClosure = new \ReflectionFunction($value);
$params = $reflClosure->getParameters();
if (count($params) < 1) {
return false;
}
if (null === $params[0]->getClass()) {
return false;
}
return __CLASS__ === $params[0]->getClass()->getName();
}
}

View File

@ -0,0 +1,298 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\OptionsResolver;
use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
/**
* Helper for merging default and concrete option values.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class OptionsResolver
{
/**
* The default option values.
* @var Options
*/
private $defaultOptions;
/**
* The options known by the resolver.
* @var array
*/
private $knownOptions = array();
/**
* The options required to be passed to resolve().
* @var array
*/
private $requiredOptions = array();
/**
* A list of accepted values for each option.
* @var array
*/
private $allowedValues = array();
/**
* Creates a new instance.
*/
public function __construct()
{
$this->defaultOptions = new Options();
}
/**
* Sets default option values.
*
* @param array $defaultValues A list of option names as keys and default values
* as values. The option values may be closures
* of the following signatures:
*
* - function (Options $options)
* - function (Options $options, $previousValue)
*
* @return OptionsResolver The resolver instance.
*/
public function setDefaults(array $defaultValues)
{
foreach ($defaultValues as $option => $value) {
$this->defaultOptions->overload($option, $value);
$this->knownOptions[$option] = true;
}
return $this;
}
/**
* Replaces default option values.
*
* Old defaults are erased, which means that closures passed here can't
* access the previous default value. This may be useful to improve
* performance if the previous default value is calculated by an expensive
* closure.
*
* @param array $defaultValues A list of option names as keys and default values
* as values. The option values may be closures
* of the following signature:
*
* - function (Options $options)
*
* @return OptionsResolver The resolver instance.
*/
public function replaceDefaults(array $defaultValues)
{
foreach ($defaultValues as $option => $value) {
$this->defaultOptions->set($option, $value);
$this->knownOptions[$option] = true;
}
return $this;
}
/**
* Sets optional options.
*
* This method is identical to `setDefaults`, only that no default values
* are configured for the options. If these options are not passed to
* resolve(), they will be missing in the final options array. This can be
* helpful if you want to determine whether an option has been set or not.
*
* @param array $optionNames A list of option names.
*
* @return OptionsResolver The resolver instance.
*
* @throws OptionDefinitionException When trying to pass default values.
*/
public function setOptional(array $optionNames)
{
foreach ($optionNames as $key => $option) {
if (!is_int($key)) {
throw new OptionDefinitionException('You should not pass default values to setOptional()');
}
$this->knownOptions[$option] = true;
}
return $this;
}
/**
* Sets required options.
*
* If these options are not passed to resolve(), an exception will be thrown.
*
* @param array $optionNames A list of option names.
*
* @return OptionsResolver The resolver instance.
*
* @throws OptionDefinitionException When trying to pass default values.
*/
public function setRequired(array $optionNames)
{
foreach ($optionNames as $key => $option) {
if (!is_int($key)) {
throw new OptionDefinitionException('You should not pass default values to setRequired()');
}
$this->knownOptions[$option] = true;
$this->requiredOptions[$option] = true;
}
return $this;
}
/**
* Sets allowed values for a list of options.
*
* @param array $allowedValues A list of option names as keys and arrays
* with values acceptable for that option as
* values.
*
* @return OptionsResolver The resolver instance.
*
* @throws InvalidOptionsException If an option has not been defined for
* which an allowed value is set.
*/
public function setAllowedValues(array $allowedValues)
{
$this->validateOptionNames(array_keys($allowedValues));
$this->allowedValues = array_replace($this->allowedValues, $allowedValues);
return $this;
}
/**
* Adds allowed values for a list of options.
*
* The values are merged with the allowed values defined previously.
*
* @param array $allowedValues A list of option names as keys and arrays
* with values acceptable for that option as
* values.
*
* @return OptionsResolver The resolver instance.
*
* @throws InvalidOptionsException If an option has not been defined for
* which an allowed value is set.
*/
public function addAllowedValues(array $allowedValues)
{
$this->validateOptionNames(array_keys($allowedValues));
$this->allowedValues = array_merge_recursive($this->allowedValues, $allowedValues);
return $this;
}
/**
* Returns the combination of the default and the passed options.
*
* @param array $options The custom option values.
*
* @return array A list of options and their values.
*
* @throws InvalidOptionsException If any of the passed options has not
* been defined or does not contain an
* allowed value.
* @throws MissingOptionsException If a required option is missing.
* @throws OptionDefinitionException If a cyclic dependency is detected
* between two lazy options.
*/
public function resolve(array $options)
{
$this->validateOptionNames(array_keys($options));
// Make sure this method can be called multiple times
$combinedOptions = clone $this->defaultOptions;
// Override options set by the user
foreach ($options as $option => $value) {
$combinedOptions->set($option, $value);
}
// Resolve options
$resolvedOptions = $combinedOptions->all();
// Validate against allowed values
$this->validateOptionValues($resolvedOptions);
return $resolvedOptions;
}
/**
* Validates that the given option names exist and throws an exception
* otherwise.
*
* @param array $optionNames A list of option names.
*
* @throws InvalidOptionsException If any of the options has not been
* defined.
* @throws MissingOptionsException If a required option is missing.
*/
private function validateOptionNames(array $optionNames)
{
ksort($this->knownOptions);
$knownOptions = array_keys($this->knownOptions);
$diff = array_diff($optionNames, $knownOptions);
sort($diff);
if (count($diff) > 0) {
if (count($diff) > 1) {
throw new InvalidOptionsException(sprintf('The options "%s" do not exist. Known options are: "%s"', implode('", "', $diff), implode('", "', $knownOptions)));
}
throw new InvalidOptionsException(sprintf('The option "%s" does not exist. Known options are: "%s"', current($diff), implode('", "', $knownOptions)));
}
ksort($this->requiredOptions);
$requiredOptions = array_keys($this->requiredOptions);
$diff = array_diff($requiredOptions, $optionNames);
sort($diff);
if (count($diff) > 0) {
if (count($diff) > 1) {
throw new MissingOptionsException(sprintf('The required options "%s" are missing.',
implode('",
"', $diff)));
}
throw new MissingOptionsException(sprintf('The required option "%s" is missing.', current($diff)));
}
}
/**
* Validates that the given option values match the allowed values and
* throws an exception otherwise.
*
* @param array $options A list of option values.
*
* @throws InvalidOptionsException If any of the values does not match the
* allowed values of the option.
*/
private function validateOptionValues(array $options)
{
foreach ($this->allowedValues as $option => $allowedValues) {
if (!in_array($options[$option], $allowedValues, true)) {
throw new InvalidOptionsException(sprintf('The option "%s" has the value "%s", but is expected to be one of "%s"', $option, $options[$option], implode('", "', $allowedValues)));
}
}
}
}

View File

@ -0,0 +1,104 @@
OptionsResolver Component
=========================
OptionsResolver helps at configuring objects with option arrays.
It supports default values on different levels of your class hierarchy,
option constraints (required vs. optional, allowed values) and lazy options
whose default value depends on the value of another option.
The following example demonstrates a Person class with two required options
"firstName" and "lastName" and two optional options "age" and "gender", where
the default value of "gender" is derived from the passed first name, if
possible, and may only be one of "male" and "female".
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\Options;
class Person
{
protected $options;
public function __construct(array $options = array())
{
$resolver = new OptionsResolver();
$this->configure($resolver);
$this->options = $resolver->resolve($options);
}
protected function configure(OptionsResolver $resolver)
{
$resolver->setRequired(array(
'firstName',
'lastName',
));
$resolver->setDefaults(array(
'age' => null,
'gender' => function (Options $options) {
if (self::isKnownMaleName($options['firstName'])) {
return 'male';
}
return 'female';
},
));
$resolver->setAllowedValues(array(
'gender' => array('male', 'female'),
));
}
}
We can now easily instantiate a Person object:
// 'gender' is implicitly set to 'female'
$person = new Person(array(
'firstName' => 'Jane',
'lastName' => 'Doe',
));
We can also override the default values of the optional options:
$person = new Person(array(
'firstName' => 'Abdullah',
'lastName' => 'Mogashi',
'gender' => 'male',
'age' => 30,
));
Options can be added or changed in subclasses by overriding the `configure`
method:
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\Options;
class Employee extends Person
{
protected function configure(OptionsResolver $resolver)
{
parent::configure($resolver);
$resolver->setRequired(array(
'birthDate',
));
$resolver->setDefaults(array(
// $previousValue contains the default value configured in the
// parent class
'age' => function (Options $options, $previousValue) {
return self::calculateAge($options['birthDate']);
}
));
}
}
Resources
---------
You can run the unit tests with the following command:
phpunit

View File

@ -0,0 +1,317 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\OptionsResolver\Tests;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\Options;
class OptionsResolverTest extends \PHPUnit_Framework_TestCase
{
/**
* @var OptionsResolver
*/
private $resolver;
protected function setUp()
{
$this->resolver = new OptionsResolver();
}
public function testResolve()
{
$this->resolver->setDefaults(array(
'one' => '1',
'two' => '2',
));
$options = array(
'two' => '20',
);
$this->assertEquals(array(
'one' => '1',
'two' => '20',
), $this->resolver->resolve($options));
}
public function testResolveLazy()
{
$this->resolver->setDefaults(array(
'one' => '1',
'two' => function (Options $options) {
return '20';
},
));
$this->assertEquals(array(
'one' => '1',
'two' => '20',
), $this->resolver->resolve(array()));
}
public function testResolveLazyDependencyOnOptional()
{
$this->resolver->setDefaults(array(
'one' => '1',
'two' => function (Options $options) {
return $options['one'] . '2';
},
));
$options = array(
'one' => '10',
);
$this->assertEquals(array(
'one' => '10',
'two' => '102',
), $this->resolver->resolve($options));
}
public function testResolveLazyDependencyOnMissingOptionalWithoutDefault()
{
$test = $this;
$this->resolver->setOptional(array(
'one',
));
$this->resolver->setDefaults(array(
'two' => function (Options $options) use ($test) {
/* @var \PHPUnit_Framework_TestCase $test */
$test->assertFalse(isset($options['one']));
return '2';
},
));
$options = array(
);
$this->assertEquals(array(
'two' => '2',
), $this->resolver->resolve($options));
}
public function testResolveLazyDependencyOnOptionalWithoutDefault()
{
$test = $this;
$this->resolver->setOptional(array(
'one',
));
$this->resolver->setDefaults(array(
'two' => function (Options $options) use ($test) {
/* @var \PHPUnit_Framework_TestCase $test */
$test->assertTrue(isset($options['one']));
return $options['one'] . '2';
},
));
$options = array(
'one' => '10',
);
$this->assertEquals(array(
'one' => '10',
'two' => '102',
), $this->resolver->resolve($options));
}
public function testResolveLazyDependencyOnRequired()
{
$this->resolver->setRequired(array(
'one',
));
$this->resolver->setDefaults(array(
'two' => function (Options $options) {
return $options['one'] . '2';
},
));
$options = array(
'one' => '10',
);
$this->assertEquals(array(
'one' => '10',
'two' => '102',
), $this->resolver->resolve($options));
}
public function testResolveLazyReplaceDefaults()
{
$test = $this;
$this->resolver->setDefaults(array(
'one' => function (Options $options) use ($test) {
/* @var \PHPUnit_Framework_TestCase $test */
$test->fail('Previous closure should not be executed');
},
));
$this->resolver->replaceDefaults(array(
'one' => function (Options $options, $previousValue) {
return '1';
},
));
$this->assertEquals(array(
'one' => '1',
), $this->resolver->resolve(array()));
}
/**
* @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testResolveFailsIfNonExistingOption()
{
$this->resolver->setDefaults(array(
'one' => '1',
));
$this->resolver->setRequired(array(
'two',
));
$this->resolver->setOptional(array(
'three',
));
$this->resolver->resolve(array(
'foo' => 'bar',
));
}
/**
* @expectedException Symfony\Component\OptionsResolver\Exception\MissingOptionsException
*/
public function testResolveFailsIfMissingRequiredOption()
{
$this->resolver->setRequired(array(
'one',
));
$this->resolver->setDefaults(array(
'two' => '2',
));
$this->resolver->resolve(array(
'two' => '20',
));
}
public function testResolveSucceedsIfOptionValueAllowed()
{
$this->resolver->setDefaults(array(
'one' => '1',
));
$this->resolver->setAllowedValues(array(
'one' => array('1', 'one'),
));
$options = array(
'one' => 'one',
);
$this->assertEquals(array(
'one' => 'one',
), $this->resolver->resolve($options));
}
public function testResolveSucceedsIfOptionValueAllowed2()
{
$this->resolver->setDefaults(array(
'one' => '1',
'two' => '2',
));
$this->resolver->addAllowedValues(array(
'one' => array('1'),
'two' => array('2'),
));
$this->resolver->addAllowedValues(array(
'one' => array('one'),
'two' => array('two'),
));
$options = array(
'one' => '1',
'two' => 'two',
);
$this->assertEquals(array(
'one' => '1',
'two' => 'two',
), $this->resolver->resolve($options));
}
/**
* @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testResolveFailsIfOptionValueNotAllowed()
{
$this->resolver->setDefaults(array(
'one' => '1',
));
$this->resolver->setAllowedValues(array(
'one' => array('1', 'one'),
));
$this->resolver->resolve(array(
'one' => '2',
));
}
/**
* @expectedException Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
*/
public function testSetRequiredFailsIfDefaultIsPassed()
{
$this->resolver->setRequired(array(
'one' => '1',
));
}
/**
* @expectedException Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
*/
public function testSetOptionalFailsIfDefaultIsPassed()
{
$this->resolver->setOptional(array(
'one' => '1',
));
}
public function testFluidInterface()
{
$this->resolver->setDefaults(array('one' => '1'))
->replaceDefaults(array('one' => '2'))
->setAllowedValues(array('one' => array('1', '2')))
->addAllowedValues(array('one' => array('3')))
->setRequired(array('two'))
->setOptional(array('three'));
$options = array(
'two' => '2',
);
$this->assertEquals(array(
'one' => '2',
'two' => '2',
), $this->resolver->resolve($options));
}
}

View File

@ -0,0 +1,218 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\OptionsResolver\Tests;
use Symfony\Component\OptionsResolver\Options;
class OptionsTest extends \PHPUnit_Framework_TestCase
{
/**
* @var Options
*/
private $options;
protected function setUp()
{
$this->options = new Options();
}
public function testArrayAccess()
{
$this->assertFalse(isset($this->options['foo']));
$this->assertFalse(isset($this->options['bar']));
$this->options['foo'] = 0;
$this->options['bar'] = 1;
$this->assertTrue(isset($this->options['foo']));
$this->assertTrue(isset($this->options['bar']));
unset($this->options['bar']);
$this->assertTrue(isset($this->options['foo']));
$this->assertFalse(isset($this->options['bar']));
$this->assertEquals(0, $this->options['foo']);
}
public function testCountable()
{
$this->options->set('foo', 0);
$this->options->set('bar', 1);
$this->assertCount(2, $this->options);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testGetNonExisting()
{
$this->options->get('foo');
}
/**
* @expectedException Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
*/
public function testSetNotSupportedAfterGet()
{
$this->options->set('foo', 'bar');
$this->options->get('foo');
$this->options->set('foo', 'baz');
}
/**
* @expectedException Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
*/
public function testRemoveNotSupportedAfterGet()
{
$this->options->set('foo', 'bar');
$this->options->get('foo');
$this->options->remove('foo');
}
public function testSetLazyOption()
{
$test = $this;
$this->options->set('foo', function (Options $options) use ($test) {
return 'dynamic';
});
$this->assertEquals('dynamic', $this->options->get('foo'));
}
public function testSetDiscardsPreviousValue()
{
$test = $this;
// defined by superclass
$this->options->set('foo', 'bar');
// defined by subclass
$this->options->set('foo', function (Options $options, $previousValue) use ($test) {
/* @var \PHPUnit_Framework_TestCase $test */
$test->assertNull($previousValue);
return 'dynamic';
});
$this->assertEquals('dynamic', $this->options->get('foo'));
}
public function testOverloadKeepsPreviousValue()
{
$test = $this;
// defined by superclass
$this->options->set('foo', 'bar');
// defined by subclass
$this->options->overload('foo', function (Options $options, $previousValue) use ($test) {
/* @var \PHPUnit_Framework_TestCase $test */
$test->assertEquals('bar', $previousValue);
return 'dynamic';
});
$this->assertEquals('dynamic', $this->options->get('foo'));
}
public function testPreviousValueIsEvaluatedIfLazy()
{
$test = $this;
// defined by superclass
$this->options->set('foo', function (Options $options) {
return 'bar';
});
// defined by subclass
$this->options->overload('foo', function (Options $options, $previousValue) use ($test) {
/* @var \PHPUnit_Framework_TestCase $test */
$test->assertEquals('bar', $previousValue);
return 'dynamic';
});
$this->assertEquals('dynamic', $this->options->get('foo'));
}
public function testLazyOptionCanAccessOtherOptions()
{
$test = $this;
$this->options->set('foo', 'bar');
$this->options->set('bam', function (Options $options) use ($test) {
/* @var \PHPUnit_Framework_TestCase $test */
$test->assertEquals('bar', $options->get('foo'));
return 'dynamic';
});
$this->assertEquals('bar', $this->options->get('foo'));
$this->assertEquals('dynamic', $this->options->get('bam'));
}
public function testLazyOptionCanAccessOtherLazyOptions()
{
$test = $this;
$this->options->set('foo', function (Options $options) {
return 'bar';
});
$this->options->set('bam', function (Options $options) use ($test) {
/* @var \PHPUnit_Framework_TestCase $test */
$test->assertEquals('bar', $options->get('foo'));
return 'dynamic';
});
$this->assertEquals('bar', $this->options->get('foo'));
$this->assertEquals('dynamic', $this->options->get('bam'));
}
/**
* @expectedException Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
*/
public function testFailForCyclicDependencies()
{
$this->options->set('foo', function (Options $options) {
$options->get('bam');
});
$this->options->set('bam', function (Options $options) {
$options->get('foo');
});
$this->options->get('foo');
}
public function testReplaceClearsAndSets()
{
$this->options->set('one', '1');
$this->options->replace(array(
'two' => '2',
'three' => function (Options $options) {
return '2' === $options['two'] ? '3' : 'foo';
}
));
$this->assertEquals(array(
'two' => '2',
'three' => '3',
), $this->options->all());
}
}

View File

@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
spl_autoload_register(function ($class) {
if (0 === strpos(ltrim($class, '/'), 'Symfony\Component\OptionsResolver')) {
if (file_exists($file = __DIR__.'/../'.substr(str_replace('\\', '/', $class), strlen('Symfony\Component\OptionsResolver')).'.php')) {
require_once $file;
}
}
});

View File

@ -0,0 +1,30 @@
{
"name": "symfony/options-resolver",
"type": "library",
"description": "Symfony OptionsResolver Component",
"keywords": ["options", "config", "configuration"],
"homepage": "http://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "http://symfony.com/contributors"
}
],
"require": {
"php": ">=5.3.3"
},
"autoload": {
"psr-0": { "Symfony\\Component\\OptionsResolver": "" }
},
"target-dir": "Symfony/Component/OptionsResolver",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
}
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="Tests/bootstrap.php"
>
<testsuites>
<testsuite name="Symfony OptionsResolver Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Resources</directory>
<directory>./Tests</directory>
</exclude>
</whitelist>
</filter>
</phpunit>