[Form] Moved Options classes to new OptionsParser component

This commit is contained in:
Bernhard Schussek 2012-04-17 18:14:09 +02:00
parent 46ffbd5282
commit b9d053edb2
31 changed files with 819 additions and 602 deletions

View File

@ -42,6 +42,7 @@
"symfony/http-foundation": "self.version",
"symfony/http-kernel": "self.version",
"symfony/locale": "self.version",
"symfony/options-parser": "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\OptionsParser\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\OptionsParser\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\OptionsParser\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\OptionsParser\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\OptionsParser\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\OptionsParser\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\OptionsParser\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\OptionsParser\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\OptionsParser\OptionsParser;
class FormFactory implements FormFactoryInterface
{
@ -220,7 +221,7 @@ class FormFactory implements FormFactoryInterface
$types = array();
$optionValues = array();
$knownOptions = array();
$defaultOptions = new DefaultOptions();
$optionsParser = new OptionsParser();
// 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());
$optionsParser->setDefaults($typeOptions);
$optionsParser->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());
$optionsParser->setDefaults($extensionOptions);
$optionsParser->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 = $optionsParser->parse($options);
for ($i = 0, $l = count($types); $i < $l && !$builder; ++$i) {
$builder = $types[$i]->createBuilder($name, $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\OptionsParser\Exception\InvalidOptionsException
*/
public function testInvalidWidgetOption()
{
@ -33,7 +33,7 @@ class DateTypeTest extends LocalizedTestCase
}
/**
* @expectedException Symfony\Component\Form\Exception\FormException
* @expectedException Symfony\Component\OptionsParser\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\OptionsParser\Exception\InvalidOptionsException
*/
public function testCreateNamedBuilderExpectsOptionsToExist()
{
@ -292,7 +292,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidOptionException
* @expectedException Symfony\Component\OptionsParser\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

@ -18,7 +18,8 @@
"require": {
"php": ">=5.3.3",
"symfony/event-dispatcher": "2.1.*",
"symfony/locale": "2.1.*"
"symfony/locale": "2.1.*",
"symfony/options-parser": "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\OptionsParser\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\OptionsParser\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\OptionsParser\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\OptionsParser\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\OptionsParser;
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

@ -9,164 +9,16 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
namespace Symfony\Component\OptionsParser;
use ArrayAccess;
use Iterator;
use OutOfBoundsException;
use Symfony\Component\Form\Exception\OptionDefinitionException;
use Symfony\Component\OptionsParser\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
@ -312,16 +164,6 @@ class Options implements ArrayAccess, Iterator
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()
*/

View File

@ -0,0 +1,249 @@
<?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\OptionsParser;
use Symfony\Component\OptionsParser\Exception\OptionDefinitionException;
use Symfony\Component\OptionsParser\Exception\InvalidOptionsException;
use Symfony\Component\OptionsParser\Exception\MissingOptionsException;
/**
* Helper for merging default and concrete option values.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class OptionsParser
{
/**
* The default option values.
* @var Options
*/
private $defaultOptions;
/**
* The options known by the parser.
* @var array
*/
private $knownOptions = array();
/**
* The options required to be passed to parse().
* @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 $options 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)
*/
public function setDefaults(array $defaultValues)
{
foreach ($defaultValues as $option => $value) {
$this->defaultOptions[$option] = $value;
$this->knownOptions[$option] = true;
}
}
/**
* 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
* parse(), 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.
*
* @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;
}
}
/**
* Sets required options.
*
* If these options are not passed to parse(), an exception will be thrown.
*
* @param array $optionNames A list of option names.
*
* @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;
}
}
/**
* 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.
*
* @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);
}
/**
* 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.
*
* @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);
}
/**
* 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 parse(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[$option] = $value;
}
// Resolve options
$combinedOptions = iterator_to_array($combinedOptions);
// Validate against allowed values
$this->validateOptionValues($combinedOptions);
return $combinedOptions;
}
/**
* 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.
*/
private function validateOptionNames(array $optionNames)
{
ksort($this->knownOptions);
$knownOptions = array_keys($this->knownOptions);
$diff = array_diff($optionNames, $knownOptions);
sort($diff);
if (count($diff) > 1) {
throw new InvalidOptionsException(sprintf('The options "%s" do not exist. Known options are: "%s"', implode('", "', $diff), implode('", "', $knownOptions)));
}
if (count($diff) > 0) {
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) > 1) {
throw new MissingOptionsException(sprintf('The options "%s" are missing.', implode('", "', $diff)));
}
if (count($diff) > 0) {
throw new MissingOptionsException(sprintf('The 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,103 @@
OptionsParser Component
======================
OptionsParser helps to configure objects with option arrays.
It supports default values on different levels of your class hierarchy,
required options and lazy options where the default value depends on the
concrete value of a different 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.
use Symfony\Component\OptionsParser\OptionsParser;
use Symfony\Component\OptionsParser\Options;
class Person
{
protected $options;
public function __construct(array $options = array())
{
$parser = new OptionsParser();
$this->setOptions($parser);
$this->options = $parser->parse($options);
}
protected function setOptions(OptionsParser $parser)
{
$parser->setRequired(array(
'firstName',
'lastName',
'age',
));
$parser->setDefaults(array(
'age' => null,
'gender' => function (Options $options) {
if (self::isKnownMaleName($options['firstName'])) {
return 'male';
}
return 'female';
},
));
$parser->setAllowedValues(array(
'gender' => array('male', 'female'),
));
}
}
We can now easily instantiate a Person object:
// 'gender' is implicitely 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 `setOptions`
method:
use Symfony\Component\OptionsParser\OptionsParser;
use Symfony\Component\OptionsParser\Options;
class Employee extends Person
{
protected function setOptions(OptionsParser $parser)
{
parent::setOptions($parser);
$parser->setRequired(array(
'birthDate',
));
$parser->setDefaults(array(
// $previousValue contains the default value configured in the
// parent class
'age' => function (Options $options, $previousValue) {
return self::configureAgeFromBirthDate($options['birthDate']);
}
));
}
}
Resources
---------
You can run the unit tests with the following command:
phpunit -c src/Symfony/Component/OptionsParser/

View File

@ -0,0 +1,271 @@
<?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\OptionsParser\Tests;
use Symfony\Component\OptionsParser\OptionsParser;
use Symfony\Component\OptionsParser\Options;
class OptionsParserTest extends \PHPUnit_Framework_TestCase
{
private $options;
protected function setUp()
{
$this->parser = new OptionsParser();
}
public function testParse()
{
$this->parser->setDefaults(array(
'one' => '1',
'two' => '2',
));
$options = array(
'two' => '20',
);
$this->assertEquals(array(
'one' => '1',
'two' => '20',
), $this->parser->parse($options));
}
public function testParseLazy()
{
$this->parser->setDefaults(array(
'one' => '1',
'two' => function (Options $options) {
return '20';
},
));
$this->assertEquals(array(
'one' => '1',
'two' => '20',
), $this->parser->parse(array()));
}
public function testParseLazyDependencyOnOptional()
{
$this->parser->setDefaults(array(
'one' => '1',
'two' => function (Options $options) {
return $options['one'] . '2';
},
));
$options = array(
'one' => '10',
);
$this->assertEquals(array(
'one' => '10',
'two' => '102',
), $this->parser->parse($options));
}
public function testParseLazyDependencyOnMissingOptionalWithoutDefault()
{
$test = $this;
$this->parser->setOptional(array(
'one',
));
$this->parser->setDefaults(array(
'two' => function (Options $options) use ($test) {
$test->assertFalse(isset($options['one']));
return '2';
},
));
$options = array(
);
$this->assertEquals(array(
'two' => '2',
), $this->parser->parse($options));
}
public function testParseLazyDependencyOnOptionalWithoutDefault()
{
$test = $this;
$this->parser->setOptional(array(
'one',
));
$this->parser->setDefaults(array(
'two' => function (Options $options) use ($test) {
$test->assertTrue(isset($options['one']));
return $options['one'] . '2';
},
));
$options = array(
'one' => '10',
);
$this->assertEquals(array(
'one' => '10',
'two' => '102',
), $this->parser->parse($options));
}
public function testParseLazyDependencyOnRequired()
{
$this->parser->setRequired(array(
'one',
));
$this->parser->setDefaults(array(
'two' => function (Options $options) {
return $options['one'] . '2';
},
));
$options = array(
'one' => '10',
);
$this->assertEquals(array(
'one' => '10',
'two' => '102',
), $this->parser->parse($options));
}
/**
* @expectedException Symfony\Component\OptionsParser\Exception\InvalidOptionsException
*/
public function testParseFailsIfNonExistingOption()
{
$this->parser->setDefaults(array(
'one' => '1',
));
$this->parser->setRequired(array(
'two',
));
$this->parser->setOptional(array(
'three',
));
$this->parser->parse(array(
'foo' => 'bar',
));
}
/**
* @expectedException Symfony\Component\OptionsParser\Exception\MissingOptionsException
*/
public function testParseFailsIfMissingRequiredOption()
{
$this->parser->setRequired(array(
'one',
));
$this->parser->setDefaults(array(
'two' => '2',
));
$this->parser->parse(array(
'two' => '20',
));
}
public function testParseSucceedsIfOptionValueAllowed()
{
$this->parser->setDefaults(array(
'one' => '1',
));
$this->parser->setAllowedValues(array(
'one' => array('1', 'one'),
));
$options = array(
'one' => 'one',
);
$this->assertEquals(array(
'one' => 'one',
), $this->parser->parse($options));
}
public function testParseSucceedsIfOptionValueAllowed2()
{
$this->parser->setDefaults(array(
'one' => '1',
'two' => '2',
));
$this->parser->addAllowedValues(array(
'one' => array('1'),
'two' => array('2'),
));
$this->parser->addAllowedValues(array(
'one' => array('one'),
'two' => array('two'),
));
$options = array(
'one' => '1',
'two' => 'two',
);
$this->assertEquals(array(
'one' => '1',
'two' => 'two',
), $this->parser->parse($options));
}
/**
* @expectedException Symfony\Component\OptionsParser\Exception\InvalidOptionsException
*/
public function testParseFailsIfOptionValueNotAllowed()
{
$this->parser->setDefaults(array(
'one' => '1',
));
$this->parser->setAllowedValues(array(
'one' => array('1', 'one'),
));
$this->parser->parse(array(
'one' => '2',
));
}
/**
* @expectedException Symfony\Component\OptionsParser\Exception\OptionDefinitionException
*/
public function testSetRequiredFailsIfDefaultIsPassed()
{
$this->parser->setRequired(array(
'one' => '1',
));
}
/**
* @expectedException Symfony\Component\OptionsParser\Exception\OptionDefinitionException
*/
public function testSetOptionalFailsIfDefaultIsPassed()
{
$this->parser->setOptional(array(
'one' => '1',
));
}
}

View File

@ -9,9 +9,9 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests;
namespace Symfony\Component\OptionsParser\Tests;
use Symfony\Component\Form\Options;
use Symfony\Component\OptionsParser\Options;
class OptionsTest extends \PHPUnit_Framework_TestCase
{
@ -50,7 +50,7 @@ class OptionsTest extends \PHPUnit_Framework_TestCase
}
/**
* @expectedException Symfony\Component\Form\Exception\OptionDefinitionException
* @expectedException Symfony\Component\OptionsParser\Exception\OptionDefinitionException
*/
public function testSetNotSupportedAfterGet()
{
@ -60,7 +60,7 @@ class OptionsTest extends \PHPUnit_Framework_TestCase
}
/**
* @expectedException Symfony\Component\Form\Exception\OptionDefinitionException
* @expectedException Symfony\Component\OptionsParser\Exception\OptionDefinitionException
*/
public function testUnsetNotSupportedAfterGet()
{
@ -151,7 +151,7 @@ class OptionsTest extends \PHPUnit_Framework_TestCase
}
/**
* @expectedException Symfony\Component\Form\Exception\OptionDefinitionException
* @expectedException Symfony\Component\OptionsParser\Exception\OptionDefinitionException
*/
public function testLazyOptionDisallowCyclicDependencies()
{

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\OptionsParser')) {
if (file_exists($file = __DIR__.'/../'.substr(str_replace('\\', '/', $class), strlen('Symfony\Component\OptionsParser')).'.php')) {
require_once $file;
}
}
});

View File

@ -0,0 +1,30 @@
{
"name": "symfony/options-parser",
"type": "library",
"description": "Symfony OptionsParser Component",
"keywords": [],
"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.2"
},
"autoload": {
"psr-0": { "Symfony\\Component\\OptionsParser": "" }
},
"target-dir": "Symfony/Component/OptionsParser",
"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 OptionsParser Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Resources</directory>
<directory>./Tests</directory>
</exclude>
</whitelist>
</filter>
</phpunit>