diff --git a/CHANGELOG-2.1.md b/CHANGELOG-2.1.md
index 9711d587d752..dd099755b8b4 100644
--- a/CHANGELOG-2.1.md
+++ b/CHANGELOG-2.1.md
@@ -256,7 +256,6 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c
* forms now don't create an empty object anymore if they are completely
empty and not required. The empty value for such forms is null.
* added constant Guess::VERY_HIGH_CONFIDENCE
- * FormType::getDefaultOptions() now sees default options defined by parent types
* [BC BREAK] FormType::getParent() does not see default options anymore
* [BC BREAK] The methods `add`, `remove`, `setParent`, `bind` and `setData`
in class Form now throw an exception if the form is already bound
@@ -266,6 +265,8 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c
"single_text" unless "with_seconds" is set to true
* checkboxes of in an expanded multiple-choice field don't include the choice
in their name anymore. Their names terminate with "[]" now.
+ * [BC BREAK] FormType::getDefaultOptions() and FormType::getAllowedOptionValues()
+ don't receive an options array anymore.
### HttpFoundation
diff --git a/UPGRADE-2.1.md b/UPGRADE-2.1.md
index b5eb679ec59b..1d011ccc5d7b 100644
--- a/UPGRADE-2.1.md
+++ b/UPGRADE-2.1.md
@@ -326,7 +326,7 @@
```
* The options passed to the `getParent()` method of form types no longer
- contain default options.
+ contain default options. They only contain the options passed by the user.
You should check if options exist before attempting to read their value.
@@ -347,6 +347,42 @@
return isset($options['widget']) && 'single_text' === $options['widget'] ? 'text' : 'choice';
}
```
+
+ * The methods `getDefaultOptions()` and `getAllowedOptionValues()` of form
+ types no longer receive an option array.
+
+ You can specify options that depend on other options using closures instead.
+
+ Before:
+
+ ```
+ public function getDefaultOptions(array $options)
+ {
+ $defaultOptions = array();
+
+ if ($options['multiple']) {
+ $defaultOptions['empty_data'] = array();
+ }
+
+ return $defaultOptions;
+ }
+ ```
+
+ After:
+
+ ```
+ public function getDefaultOptions()
+ {
+ return array(
+ 'empty_data' => function (Options $options, $previousValue) {
+ return $options['multiple'] ? array() : $previousValue;
+ }
+ );
+ }
+ ```
+
+ The second argument `$previousValue` does not have to be specified if not
+ needed.
* The `add()`, `remove()`, `setParent()`, `bind()` and `setData()` methods in
the Form class now throw an exception if the form is already bound.
diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php
index 2b9161477e24..d2644a618a7e 100644
--- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php
+++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php
@@ -19,6 +19,7 @@
use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Options;
abstract class DoctrineType extends AbstractType
{
@@ -42,27 +43,25 @@ public function buildForm(FormBuilder $builder, array $options)
}
}
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
- $defaultOptions = array(
- 'em' => null,
- 'class' => null,
- 'property' => null,
- 'query_builder' => null,
- 'loader' => null,
- 'group_by' => null,
- );
+ $registry = $this->registry;
+ $type = $this;
- $options = array_replace($defaultOptions, $options);
+ $loader = function (Options $options) use ($type, $registry) {
+ if (null !== $options['query_builder']) {
+ $manager = $registry->getManager($options['em']);
- if (!isset($options['choice_list'])) {
- $manager = $this->registry->getManager($options['em']);
-
- if (isset($options['query_builder'])) {
- $options['loader'] = $this->getLoader($manager, $options);
+ return $type->getLoader($manager, $options['query_builder'], $options['class']);
}
- $defaultOptions['choice_list'] = new EntityChoiceList(
+ return null;
+ };
+
+ $choiceList = function (Options $options) use ($registry) {
+ $manager = $registry->getManager($options['em']);
+
+ return new EntityChoiceList(
$manager,
$options['class'],
$options['property'],
@@ -70,9 +69,18 @@ public function getDefaultOptions(array $options)
$options['choices'],
$options['group_by']
);
- }
+ };
- return $defaultOptions;
+ return array(
+ 'em' => null,
+ 'class' => null,
+ 'property' => null,
+ 'query_builder' => null,
+ 'loader' => $loader,
+ 'choices' => null,
+ 'choice_list' => $choiceList,
+ 'group_by' => null,
+ );
}
/**
@@ -82,7 +90,7 @@ public function getDefaultOptions(array $options)
* @param array $options
* @return EntityLoaderInterface
*/
- abstract protected function getLoader(ObjectManager $manager, array $options);
+ abstract public function getLoader(ObjectManager $manager, $queryBuilder, $class);
public function getParent(array $options)
{
diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php
index 9691c3393aa3..b6ef4b3e2ad5 100644
--- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php
+++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php
@@ -23,12 +23,12 @@ class EntityType extends DoctrineType
* @param array $options
* @return ORMQueryBuilderLoader
*/
- protected function getLoader(ObjectManager $manager, array $options)
+ public function getLoader(ObjectManager $manager, $queryBuilder, $class)
{
return new ORMQueryBuilderLoader(
- $options['query_builder'],
+ $queryBuilder,
$manager,
- $options['class']
+ $class
);
}
diff --git a/src/Symfony/Bridge/Propel1/Form/Type/ModelType.php b/src/Symfony/Bridge/Propel1/Form/Type/ModelType.php
index 741b6fd83907..de125a070f29 100644
--- a/src/Symfony/Bridge/Propel1/Form/Type/ModelType.php
+++ b/src/Symfony/Bridge/Propel1/Form/Type/ModelType.php
@@ -14,6 +14,7 @@
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;
/**
@@ -30,9 +31,19 @@ public function buildForm(FormBuilder $builder, array $options)
}
}
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
- $defaultOptions = array(
+ $choiceList = function (Options $options) {
+ return new ModelChoiceList(
+ $options['class'],
+ $options['property'],
+ $options['choices'],
+ $options['query'],
+ $options['group_by']
+ );
+ };
+
+ return array(
'template' => 'choice',
'multiple' => false,
'expanded' => false,
@@ -40,23 +51,10 @@ public function getDefaultOptions(array $options)
'property' => null,
'query' => null,
'choices' => null,
+ 'choice_list' => $choiceList,
'group_by' => null,
'by_reference' => false,
);
-
- $options = array_replace($defaultOptions, $options);
-
- if (!isset($options['choice_list'])) {
- $defaultOptions['choice_list'] = new ModelChoiceList(
- $options['class'],
- $options['property'],
- $options['choices'],
- $options['query'],
- $options['group_by']
- );
- }
-
- return $defaultOptions;
}
public function getParent(array $options)
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Form/UserLoginFormType.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Form/UserLoginFormType.php
index 787bf6b22c8a..e4ccb6c5fdcd 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Form/UserLoginFormType.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Form/UserLoginFormType.php
@@ -76,7 +76,7 @@ public function buildForm(FormBuilder $builder, array $options)
/**
* @see Symfony\Component\Form\AbstractType::getDefaultOptions()
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
/* Note: the form's intention must correspond to that for the form login
* listener in order for the CSRF token to validate successfully.
diff --git a/src/Symfony/Component/Form/AbstractType.php b/src/Symfony/Component/Form/AbstractType.php
index eb7ee6ea844d..7541aa4c0c4e 100644
--- a/src/Symfony/Component/Form/AbstractType.php
+++ b/src/Symfony/Component/Form/AbstractType.php
@@ -96,7 +96,7 @@ public function createBuilder($name, FormFactoryInterface $factory, array $optio
*
* @return array The default options
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array();
}
@@ -108,7 +108,7 @@ public function getDefaultOptions(array $options)
*
* @return array The allowed option values
*/
- public function getAllowedOptionValues(array $options)
+ public function getAllowedOptionValues()
{
return array();
}
diff --git a/src/Symfony/Component/Form/AbstractTypeExtension.php b/src/Symfony/Component/Form/AbstractTypeExtension.php
index afaeb18a2c48..7d9247b53547 100644
--- a/src/Symfony/Component/Form/AbstractTypeExtension.php
+++ b/src/Symfony/Component/Form/AbstractTypeExtension.php
@@ -65,7 +65,7 @@ public function buildViewBottomUp(FormView $view, FormInterface $form)
*
* @return array
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array();
}
@@ -77,7 +77,7 @@ public function getDefaultOptions(array $options)
*
* @return array The allowed option values
*/
- public function getAllowedOptionValues(array $options)
+ public function getAllowedOptionValues()
{
return array();
}
diff --git a/src/Symfony/Component/Form/DefaultOptions.php b/src/Symfony/Component/Form/DefaultOptions.php
new file mode 100644
index 000000000000..4d77a7db470a
--- /dev/null
+++ b/src/Symfony/Component/Form/DefaultOptions.php
@@ -0,0 +1,320 @@
+
+ *
+ * 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 concrete
+ * 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:
+ *
+ *
+ * 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,
+ * ));
+ *
+ *
+ * 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:
+ *
+ *
+ * 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'
+ * ));
+ *
+ *
+ * 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:
+ *
+ *
+ * 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',
+ * ));
+ *
+ *
+ * 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.
+ *
+ *
+ * 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"
+ * ));
+ *
+ *
+ * The closure receives as its first parameter a container of class Options
+ * that contains the concrete 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.
+ *
+ *
+ * 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"
+ * ));
+ *
+ *
+ * @author Bernhard Schussek
+ */
+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)));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Symfony/Component/Form/Exception/InvalidOptionException.php b/src/Symfony/Component/Form/Exception/InvalidOptionException.php
new file mode 100644
index 000000000000..54d782b3e105
--- /dev/null
+++ b/src/Symfony/Component/Form/Exception/InvalidOptionException.php
@@ -0,0 +1,16 @@
+
+ *
+ * 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 InvalidOptionException extends FormException
+{
+}
diff --git a/src/Symfony/Component/Form/Exception/InvalidOptionsException.php b/src/Symfony/Component/Form/Exception/InvalidOptionsException.php
deleted file mode 100644
index bde0a5069a60..000000000000
--- a/src/Symfony/Component/Form/Exception/InvalidOptionsException.php
+++ /dev/null
@@ -1,29 +0,0 @@
-
- *
- * 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 InvalidOptionsException extends FormException
-{
- private $options;
-
- public function __construct($message, array $options)
- {
- parent::__construct($message);
-
- $this->options = $options;
- }
-
- public function getOptions()
- {
- return $this->options;
- }
-}
diff --git a/src/Symfony/Component/Form/Exception/MissingOptionsException.php b/src/Symfony/Component/Form/Exception/MissingOptionsException.php
deleted file mode 100644
index c731b5598ea4..000000000000
--- a/src/Symfony/Component/Form/Exception/MissingOptionsException.php
+++ /dev/null
@@ -1,29 +0,0 @@
-
- *
- * 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 MissingOptionsException extends FormException
-{
- private $options;
-
- public function __construct($message, array $options)
- {
- parent::__construct($message);
-
- $this->options = $options;
- }
-
- public function getOptions()
- {
- return $this->options;
- }
-}
diff --git a/src/Symfony/Component/Form/Exception/OptionDefinitionException.php b/src/Symfony/Component/Form/Exception/OptionDefinitionException.php
new file mode 100644
index 000000000000..dfd87cc7e5b9
--- /dev/null
+++ b/src/Symfony/Component/Form/Exception/OptionDefinitionException.php
@@ -0,0 +1,16 @@
+
+ *
+ * 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
+{
+}
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/BirthdayType.php b/src/Symfony/Component/Form/Extension/Core/Type/BirthdayType.php
index aea9a701e847..bab646003d08 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/BirthdayType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/BirthdayType.php
@@ -18,7 +18,7 @@ class BirthdayType extends AbstractType
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'years' => range(date('Y') - 120, date('Y')),
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php b/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php
index ddbf8ae897e2..8ab2c6843d0a 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php
@@ -44,7 +44,7 @@ public function buildView(FormView $view, FormInterface $form)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'value' => '1',
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php
index 0b7c45053c8e..1ed495248bb6 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php
@@ -12,6 +12,7 @@
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;
@@ -42,13 +43,6 @@ public function buildForm(FormBuilder $builder, array $options)
throw new FormException('Either the option "choices" or "choice_list" must be set.');
}
- if (!$options['choice_list']) {
- $options['choice_list'] = new SimpleChoiceList(
- $options['choices'],
- $options['preferred_choices']
- );
- }
-
if ($options['expanded']) {
$this->addSubFields($builder, $options['choice_list']->getPreferredViews(), $options);
$this->addSubFields($builder, $options['choice_list']->getRemainingViews(), $options);
@@ -61,9 +55,6 @@ public function buildForm(FormBuilder $builder, array $options)
} elseif (false === $options['empty_value']) {
// an empty value should be added but the user decided otherwise
$emptyValue = null;
- } elseif (null === $options['empty_value']) {
- // user did not made a decision, so we put a blank empty value
- $emptyValue = $options['required'] ? null : '';
} else {
// empty value has been set explicitly
$emptyValue = $options['empty_value'];
@@ -152,19 +143,36 @@ public function buildViewBottomUp(FormView $view, FormInterface $form)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
- $multiple = isset($options['multiple']) && $options['multiple'];
- $expanded = isset($options['expanded']) && $options['expanded'];
+ $choiceList = function (Options $options) {
+ return new SimpleChoiceList(
+ // Harden against NULL values (like in EntityType and ModelType)
+ null !== $options['choices'] ? $options['choices'] : array(),
+ $options['preferred_choices']
+ );
+ };
+
+ $emptyData = function (Options $options) {
+ if ($options['multiple'] || $options['expanded']) {
+ return array();
+ }
+
+ return '';
+ };
+
+ $emptyValue = function (Options $options) {
+ return $options['required'] ? null : '';
+ };
return array(
'multiple' => false,
'expanded' => false,
- 'choice_list' => null,
- 'choices' => null,
+ 'choice_list' => $choiceList,
+ 'choices' => array(),
'preferred_choices' => array(),
- 'empty_data' => $multiple || $expanded ? array() : '',
- 'empty_value' => $multiple || $expanded || !isset($options['empty_value']) ? null : '',
+ 'empty_data' => $emptyData,
+ 'empty_value' => $emptyValue,
'error_bubbling' => false,
);
}
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php b/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php
index 5eb8285d319f..483b56a6751f 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php
@@ -75,7 +75,7 @@ public function buildViewBottomUp(FormView $view, FormInterface $form)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'allow_add' => false,
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php b/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php
index b3e1e43d29d7..bb4f7decbae6 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php
@@ -20,7 +20,7 @@ class CountryType extends AbstractType
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'choices' => Locale::getDisplayCountries(\Locale::getDefault()),
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php
index e7d75128a363..6eba72d4f6b6 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php
@@ -128,7 +128,7 @@ public function buildView(FormView $view, FormInterface $form)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'input' => 'datetime',
@@ -164,7 +164,7 @@ public function getDefaultOptions(array $options)
/**
* {@inheritdoc}
*/
- public function getAllowedOptionValues(array $options)
+ public function getAllowedOptionValues()
{
return array(
'input' => array(
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php
index b634e5f72a1d..eb5e85d1a448 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php
@@ -161,7 +161,7 @@ public function buildViewBottomUp(FormView $view, FormInterface $form)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'years' => range(date('Y') - 5, date('Y') + 5),
@@ -188,7 +188,7 @@ public function getDefaultOptions(array $options)
/**
* {@inheritdoc}
*/
- public function getAllowedOptionValues(array $options)
+ public function getAllowedOptionValues()
{
return array(
'input' => array(
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FieldType.php b/src/Symfony/Component/Form/Extension/Core/Type/FieldType.php
index d9bbe6222c3b..f68c878aadda 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/FieldType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/FieldType.php
@@ -12,6 +12,7 @@
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;
@@ -129,11 +130,38 @@ public function buildView(FormView $view, FormInterface $form)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
- $defaultOptions = array(
+ // Derive "data_class" option from passed "data" object
+ $dataClass = function (Options $options) {
+ if (is_object($options['data'])) {
+ return get_class($options['data']);
+ }
+
+ return null;
+ };
+
+ // Derive "empty_data" closure from "data_class" option
+ $emptyData = function (Options $options) {
+ $class = $options['data_class'];
+
+ if (null !== $class) {
+ return function (FormInterface $form) use ($class) {
+ if ($form->isEmpty() && !$form->isRequired()) {
+ return null;
+ }
+
+ return new $class();
+ };
+ }
+
+ return '';
+ };
+
+ return array(
'data' => null,
- 'data_class' => null,
+ 'data_class' => $dataClass,
+ 'empty_data' => $emptyData,
'trim' => true,
'required' => true,
'read_only' => false,
@@ -150,28 +178,6 @@ public function getDefaultOptions(array $options)
'invalid_message_parameters' => array(),
'translation_domain' => 'messages',
);
-
- $class = isset($options['data_class']) ? $options['data_class'] : null;
-
- // If no data class is set explicitly and an object is passed as data,
- // use the class of that object as data class
- if (!$class && isset($options['data']) && is_object($options['data'])) {
- $defaultOptions['data_class'] = $class = get_class($options['data']);
- }
-
- if ($class) {
- $defaultOptions['empty_data'] = function (FormInterface $form) use ($class) {
- if ($form->isEmpty() && !$form->isRequired()) {
- return null;
- }
-
- return new $class();
- };
- } else {
- $defaultOptions['empty_data'] = '';
- }
-
- return $defaultOptions;
}
/**
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php
index ca2c87ce0ecf..36e0c0c63bb1 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php
@@ -12,6 +12,7 @@
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;
@@ -50,20 +51,23 @@ public function buildViewBottomUp(FormView $view, FormInterface $form)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
- $defaultOptions = array(
+ $emptyData = function (Options $options, $currentValue) {
+ if (empty($options['data_class'])) {
+ return array();
+ }
+
+ return $currentValue;
+ };
+
+ return array(
+ 'empty_data' => $emptyData,
'virtual' => false,
// Errors in forms bubble by default, so that form errors will
// end up as global errors in the root form
'error_bubbling' => true,
);
-
- if (empty($options['data_class'])) {
- $defaultOptions['empty_data'] = array();
- }
-
- return $defaultOptions;
}
/**
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/HiddenType.php b/src/Symfony/Component/Form/Extension/Core/Type/HiddenType.php
index 8e6039eee029..71066250624a 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/HiddenType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/HiddenType.php
@@ -18,7 +18,7 @@ class HiddenType extends AbstractType
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
// hidden fields cannot have a required attribute
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php b/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php
index 128a01456f9a..9beaed51029b 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php
@@ -33,7 +33,7 @@ public function buildForm(FormBuilder $builder, array $options)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
// default precision is locale specific (usually around 3)
@@ -47,7 +47,7 @@ public function getDefaultOptions(array $options)
/**
* {@inheritdoc}
*/
- public function getAllowedOptionValues(array $options)
+ public function getAllowedOptionValues()
{
return array(
'rounding_mode' => array(
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php b/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php
index 7d722a13e5c6..ede5be1f4d13 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php
@@ -20,7 +20,7 @@ class LanguageType extends AbstractType
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'choices' => Locale::getDisplayLanguages(\Locale::getDefault()),
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php b/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php
index 66e5da9ed2c5..d953b82cf27a 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php
@@ -20,7 +20,7 @@ class LocaleType extends AbstractType
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'choices' => Locale::getDisplayLocales(\Locale::getDefault()),
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MoneyType.php b/src/Symfony/Component/Form/Extension/Core/Type/MoneyType.php
index 7a8bd99fd60c..1f87f3a2a687 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/MoneyType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/MoneyType.php
@@ -48,7 +48,7 @@ public function buildView(FormView $view, FormInterface $form)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'precision' => 2,
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php b/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php
index 00fec267665e..21ea17046e76 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php
@@ -32,7 +32,7 @@ public function buildForm(FormBuilder $builder, array $options)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
// default precision is locale specific (usually around 3)
@@ -45,7 +45,7 @@ public function getDefaultOptions(array $options)
/**
* {@inheritdoc}
*/
- public function getAllowedOptionValues(array $options)
+ public function getAllowedOptionValues()
{
return array(
'rounding_mode' => array(
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/PasswordType.php b/src/Symfony/Component/Form/Extension/Core/Type/PasswordType.php
index 7451080e1f29..d2ffed8d1daa 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/PasswordType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/PasswordType.php
@@ -39,7 +39,7 @@ public function buildView(FormView $view, FormInterface $form)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'always_empty' => true,
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/PercentType.php b/src/Symfony/Component/Form/Extension/Core/Type/PercentType.php
index f0804be4c2ce..ea6550869a24 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/PercentType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/PercentType.php
@@ -28,7 +28,7 @@ public function buildForm(FormBuilder $builder, array $options)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'precision' => 0,
@@ -39,7 +39,7 @@ public function getDefaultOptions(array $options)
/**
* {@inheritdoc}
*/
- public function getAllowedOptionValues(array $options)
+ public function getAllowedOptionValues()
{
return array(
'type' => array(
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/RepeatedType.php b/src/Symfony/Component/Form/Extension/Core/Type/RepeatedType.php
index 18a4f4fe9991..034f3d6a9ae0 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/RepeatedType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/RepeatedType.php
@@ -39,7 +39,7 @@ public function buildForm(FormBuilder $builder, array $options)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'type' => 'text',
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php
index e730f9eb828e..3f7258bb82e7 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php
@@ -134,7 +134,7 @@ public function buildView(FormView $view, FormInterface $form)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'hours' => range(0, 23),
@@ -161,7 +161,7 @@ public function getDefaultOptions(array $options)
/**
* {@inheritdoc}
*/
- public function getAllowedOptionValues(array $options)
+ public function getAllowedOptionValues()
{
return array(
'input' => array(
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php
index c143562a4472..3b162484ab50 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Options;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
class TimezoneType extends AbstractType
@@ -20,20 +21,16 @@ class TimezoneType extends AbstractType
* Stores the available timezone choices
* @var array
*/
- static protected $timezones;
+ static private $timezones;
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
- $defaultOptions = array();
-
- if (empty($options['choice_list']) && empty($options['choices'])) {
- $defaultOptions['choices'] = self::getTimezones();
- }
-
- return $defaultOptions;
+ return array(
+ 'choices' => self::getTimezones(),
+ );
}
/**
@@ -62,7 +59,7 @@ public function getName()
*
* @return array The timezone choices
*/
- static private function getTimezones()
+ static public function getTimezones()
{
if (null === static::$timezones) {
static::$timezones = array();
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/UrlType.php b/src/Symfony/Component/Form/Extension/Core/Type/UrlType.php
index 246403d1bd4a..5a7e768e8ea2 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/UrlType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/UrlType.php
@@ -28,7 +28,7 @@ public function buildForm(FormBuilder $builder, array $options)
/**
* {@inheritdoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'default_protocol' => 'http',
diff --git a/src/Symfony/Component/Form/Extension/Csrf/Type/ChoiceTypeCsrfExtension.php b/src/Symfony/Component/Form/Extension/Csrf/Type/ChoiceTypeCsrfExtension.php
index 3711b5198d3f..dc1e6db15bd1 100644
--- a/src/Symfony/Component/Form/Extension/Csrf/Type/ChoiceTypeCsrfExtension.php
+++ b/src/Symfony/Component/Form/Extension/Csrf/Type/ChoiceTypeCsrfExtension.php
@@ -15,7 +15,7 @@
class ChoiceTypeCsrfExtension extends AbstractTypeExtension
{
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array('csrf_protection' => false);
}
diff --git a/src/Symfony/Component/Form/Extension/Csrf/Type/CsrfType.php b/src/Symfony/Component/Form/Extension/Csrf/Type/CsrfType.php
index 3e2ffc328083..97c69d8a5ab1 100644
--- a/src/Symfony/Component/Form/Extension/Csrf/Type/CsrfType.php
+++ b/src/Symfony/Component/Form/Extension/Csrf/Type/CsrfType.php
@@ -54,7 +54,7 @@ public function buildForm(FormBuilder $builder, array $options)
/**
* {@inheritDoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'csrf_provider' => $this->csrfProvider,
diff --git a/src/Symfony/Component/Form/Extension/Csrf/Type/DateTypeCsrfExtension.php b/src/Symfony/Component/Form/Extension/Csrf/Type/DateTypeCsrfExtension.php
index 797368590580..dc54e94ddf6b 100644
--- a/src/Symfony/Component/Form/Extension/Csrf/Type/DateTypeCsrfExtension.php
+++ b/src/Symfony/Component/Form/Extension/Csrf/Type/DateTypeCsrfExtension.php
@@ -15,7 +15,7 @@
class DateTypeCsrfExtension extends AbstractTypeExtension
{
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array('csrf_protection' => false);
}
diff --git a/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php b/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php
index d61ac97a5d58..8e2a57459add 100644
--- a/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php
+++ b/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php
@@ -76,7 +76,7 @@ public function buildViewBottomUp(FormView $view, FormInterface $form)
/**
* {@inheritDoc}
*/
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'csrf_protection' => $this->enabled,
diff --git a/src/Symfony/Component/Form/Extension/Csrf/Type/RepeatedTypeCsrfExtension.php b/src/Symfony/Component/Form/Extension/Csrf/Type/RepeatedTypeCsrfExtension.php
index 71adaadb1f7c..1115ea4d69ce 100644
--- a/src/Symfony/Component/Form/Extension/Csrf/Type/RepeatedTypeCsrfExtension.php
+++ b/src/Symfony/Component/Form/Extension/Csrf/Type/RepeatedTypeCsrfExtension.php
@@ -15,7 +15,7 @@
class RepeatedTypeCsrfExtension extends AbstractTypeExtension
{
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array('csrf_protection' => false);
}
diff --git a/src/Symfony/Component/Form/Extension/Csrf/Type/TimeTypeCsrfExtension.php b/src/Symfony/Component/Form/Extension/Csrf/Type/TimeTypeCsrfExtension.php
index 26d875954de2..dbd7c0d2dfbc 100644
--- a/src/Symfony/Component/Form/Extension/Csrf/Type/TimeTypeCsrfExtension.php
+++ b/src/Symfony/Component/Form/Extension/Csrf/Type/TimeTypeCsrfExtension.php
@@ -15,7 +15,7 @@
class TimeTypeCsrfExtension extends AbstractTypeExtension
{
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array('csrf_protection' => false);
}
diff --git a/src/Symfony/Component/Form/Extension/Validator/Type/FieldTypeValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/Type/FieldTypeValidatorExtension.php
index 1cd0b10f595f..fb8998323beb 100644
--- a/src/Symfony/Component/Form/Extension/Validator/Type/FieldTypeValidatorExtension.php
+++ b/src/Symfony/Component/Form/Extension/Validator/Type/FieldTypeValidatorExtension.php
@@ -42,7 +42,7 @@ public function buildForm(FormBuilder $builder, array $options)
->addValidator(new DelegatingValidator($this->validator));
}
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'validation_groups' => null,
diff --git a/src/Symfony/Component/Form/FormFactory.php b/src/Symfony/Component/Form/FormFactory.php
index a474c0ab9079..e2c3d23237e7 100644
--- a/src/Symfony/Component/Form/FormFactory.php
+++ b/src/Symfony/Component/Form/FormFactory.php
@@ -219,9 +219,9 @@ public function createNamedBuilder($type, $name, $data = null, array $options =
$builder = null;
$types = array();
- $defaultOptions = array();
$optionValues = array();
- $passedOptions = $options;
+ $knownOptions = array();
+ $defaultOptions = new DefaultOptions();
// Bottom-up determination of the type hierarchy
// Start with the actual type and look for the parent type
@@ -249,52 +249,36 @@ public function createNamedBuilder($type, $name, $data = null, array $options =
$type = $type->getParent($options);
}
- // Top-down determination of the options and default options
+ // Top-down determination of the default options
foreach ($types as $type) {
// Merge the default options of all types to an array of default
// options. Default options of children override default options
// of parents.
- // Default options of ancestors are already visible in the $options
- // array passed to the following methods.
- $defaultOptions = array_replace($defaultOptions, $type->getDefaultOptions($options));
- $optionValues = array_merge_recursive($optionValues, $type->getAllowedOptionValues($options));
+ $typeOptions = $type->getDefaultOptions();
+ $defaultOptions->add($typeOptions);
+ $defaultOptions->addAllowedValues($type->getAllowedOptionValues());
+ $knownOptions = array_merge($knownOptions, array_keys($typeOptions));
foreach ($type->getExtensions() as $typeExtension) {
- $defaultOptions = array_replace($defaultOptions, $typeExtension->getDefaultOptions($options));
- $optionValues = array_merge_recursive($optionValues, $typeExtension->getAllowedOptionValues($options));
+ $extensionOptions = $typeExtension->getDefaultOptions();
+ $defaultOptions->add($extensionOptions);
+ $defaultOptions->addAllowedValues($typeExtension->getAllowedOptionValues());
+ $knownOptions = array_merge($knownOptions, array_keys($extensionOptions));
}
-
- // In each turn, the options are replaced by the combination of
- // the currently known default options and the passed options.
- // It is important to merge with $passedOptions and not with
- // $options, otherwise default options of parents would override
- // default options of child types.
- $options = array_replace($defaultOptions, $passedOptions);
}
+ // Resolve concrete type
$type = end($types);
- $knownOptions = array_keys($defaultOptions);
+
+ // Validate options required by the factory
$diff = array_diff(self::$requiredOptions, $knownOptions);
if (count($diff) > 0) {
throw new TypeDefinitionException(sprintf('Type "%s" should support the option(s) "%s"', $type->getName(), implode('", "', $diff)));
}
- $diff = array_diff(array_keys($passedOptions), $knownOptions);
-
- if (count($diff) > 1) {
- throw new CreationException(sprintf('The options "%s" do not exist. Known options are: "%s"', implode('", "', $diff), implode('", "', $knownOptions)));
- }
-
- if (count($diff) > 0) {
- throw new CreationException(sprintf('The option "%s" does not exist. Known options are: "%s"', current($diff), implode('", "', $knownOptions)));
- }
-
- foreach ($optionValues as $option => $allowedValues) {
- if (!in_array($options[$option], $allowedValues, true)) {
- throw new CreationException(sprintf('The option "%s" has the value "%s", but is expected to be one of "%s"', $option, $options[$option], implode('", "', $allowedValues)));
- }
- }
+ // Resolve options
+ $options = $defaultOptions->resolve($options);
for ($i = 0, $l = count($types); $i < $l && !$builder; ++$i) {
$builder = $types[$i]->createBuilder($name, $this, $options);
diff --git a/src/Symfony/Component/Form/FormTypeExtensionInterface.php b/src/Symfony/Component/Form/FormTypeExtensionInterface.php
index fbc163401ed2..8c72c7861632 100644
--- a/src/Symfony/Component/Form/FormTypeExtensionInterface.php
+++ b/src/Symfony/Component/Form/FormTypeExtensionInterface.php
@@ -59,7 +59,7 @@ function buildViewBottomUp(FormView $view, FormInterface $form);
*
* @return array
*/
- function getDefaultOptions(array $options);
+ function getDefaultOptions();
/**
* Returns the allowed option values for each option (if any).
@@ -68,7 +68,7 @@ function getDefaultOptions(array $options);
*
* @return array The allowed option values
*/
- function getAllowedOptionValues(array $options);
+ function getAllowedOptionValues();
/**
diff --git a/src/Symfony/Component/Form/FormTypeInterface.php b/src/Symfony/Component/Form/FormTypeInterface.php
index c767f1f20475..b4236882bcf5 100644
--- a/src/Symfony/Component/Form/FormTypeInterface.php
+++ b/src/Symfony/Component/Form/FormTypeInterface.php
@@ -79,7 +79,7 @@ function createBuilder($name, FormFactoryInterface $factory, array $options);
*
* @return array The default options
*/
- function getDefaultOptions(array $options);
+ function getDefaultOptions();
/**
* Returns the allowed option values for each option (if any).
@@ -88,7 +88,7 @@ function getDefaultOptions(array $options);
*
* @return array The allowed option values
*/
- function getAllowedOptionValues(array $options);
+ function getAllowedOptionValues();
/**
* Returns the name of the parent type.
diff --git a/src/Symfony/Component/Form/LazyOption.php b/src/Symfony/Component/Form/LazyOption.php
new file mode 100644
index 000000000000..060d77bdfa2a
--- /dev/null
+++ b/src/Symfony/Component/Form/LazyOption.php
@@ -0,0 +1,73 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form;
+
+use Closure;
+
+/**
+ * An option that is evaluated lazily using a closure.
+ *
+ * @author Bernhard Schussek
+ *
+ * @see DefaultOptions
+ */
+class LazyOption
+{
+ /**
+ * The underlying closure.
+ * @var Closure
+ */
+ private $closure;
+
+ /**
+ * The previous default value of the option.
+ * @var mixed
+ */
+ private $previousValue;
+
+ /**
+ * Creates a new lazy option.
+ *
+ * @param Closure $closure The closure used for initializing the
+ * option value.
+ * @param mixed $previousValue The previous value of the option. This
+ * value is passed to the closure when it is
+ * evaluated.
+ *
+ * @see evaluate()
+ */
+ public function __construct(Closure $closure, $previousValue)
+ {
+ $this->closure = $closure;
+ $this->previousValue = $previousValue;
+ }
+
+ /**
+ * Evaluates the underyling closure and returns its result.
+ *
+ * The given Options instance is passed to the closure as first argument.
+ * The previous default value set in the constructor is passed as second
+ * argument.
+ *
+ * @param Options $options The container with all concrete options.
+ *
+ * @return mixed The result of the closure.
+ */
+ public function evaluate(Options $options)
+ {
+ if ($this->previousValue instanceof self) {
+ $this->previousValue = $this->previousValue->evaluate($options);
+ }
+
+ return $this->closure->__invoke($options, $this->previousValue);
+ }
+}
\ No newline at end of file
diff --git a/src/Symfony/Component/Form/Options.php b/src/Symfony/Component/Form/Options.php
new file mode 100644
index 000000000000..028e4ac19bca
--- /dev/null
+++ b/src/Symfony/Component/Form/Options.php
@@ -0,0 +1,364 @@
+
+ *
+ * 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:
+ *
+ *
+ * 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,
+ * ));
+ *
+ *
+ * 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:
+ *
+ *
+ * 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'
+ * ));
+ *
+ *
+ * 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:
+ *
+ *
+ * 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',
+ * ));
+ *
+ *
+ * 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.
+ *
+ *
+ * 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"
+ * ));
+ *
+ *
+ * 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.
+ *
+ *
+ * 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"
+ * ));
+ *
+ *
+ * @author Bernhard Schussek
+ */
+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);
+ }
+}
\ No newline at end of file
diff --git a/src/Symfony/Component/Form/Tests/DefaultOptionsTest.php b/src/Symfony/Component/Form/Tests/DefaultOptionsTest.php
new file mode 100644
index 000000000000..9f32b72cf177
--- /dev/null
+++ b/src/Symfony/Component/Form/Tests/DefaultOptionsTest.php
@@ -0,0 +1,76 @@
+
+ *
+ * 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',
+ ));
+ }
+}
\ No newline at end of file
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php
index 50d1453dd19c..d60e060a3e5c 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php
@@ -90,7 +90,7 @@ public function testChoiceListOptionExpectsChoiceListInterface()
}
/**
- * @expectedException Symfony\Component\Form\Exception\FormException
+ * expectedException Symfony\Component\Form\Exception\FormException
*/
public function testEitherChoiceListOrChoicesMustBeSet()
{
diff --git a/src/Symfony/Component/Form/Tests/Fixtures/AuthorType.php b/src/Symfony/Component/Form/Tests/Fixtures/AuthorType.php
new file mode 100644
index 000000000000..43569843f027
--- /dev/null
+++ b/src/Symfony/Component/Form/Tests/Fixtures/AuthorType.php
@@ -0,0 +1,31 @@
+add('firstName')
+ ->add('lastName')
+ ;
+ }
+
+ public function getName()
+ {
+ return 'author';
+ }
+
+ public function getDefaultOptions()
+ {
+ return array(
+ 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author',
+ );
+ }
+}
diff --git a/src/Symfony/Component/Form/Tests/Fixtures/FooType.php b/src/Symfony/Component/Form/Tests/Fixtures/FooType.php
index d1974b2c36f2..d85ae2c72add 100644
--- a/src/Symfony/Component/Form/Tests/Fixtures/FooType.php
+++ b/src/Symfony/Component/Form/Tests/Fixtures/FooType.php
@@ -34,7 +34,7 @@ public function createBuilder($name, FormFactoryInterface $factory, array $optio
return new FormBuilder($name, $factory, new EventDispatcher());
}
- public function getDefaultOptions(array $options)
+ public function getDefaultOptions()
{
return array(
'data' => null,
@@ -44,7 +44,7 @@ public function getDefaultOptions(array $options)
);
}
- public function getAllowedOptionValues(array $options)
+ public function getAllowedOptionValues()
{
return array(
'a_or_b' => array('a', 'b'),
diff --git a/src/Symfony/Component/Form/Tests/Fixtures/FooTypeBarExtension.php b/src/Symfony/Component/Form/Tests/Fixtures/FooTypeBarExtension.php
index 2534a5189bbe..31f929363428 100644
--- a/src/Symfony/Component/Form/Tests/Fixtures/FooTypeBarExtension.php
+++ b/src/Symfony/Component/Form/Tests/Fixtures/FooTypeBarExtension.php
@@ -21,7 +21,7 @@ public function buildForm(FormBuilder $builder, array $options)
$builder->setAttribute('bar', 'x');
}
- public function getAllowedOptionValues(array $options)
+ public function getAllowedOptionValues()
{
return array(
'a_or_b' => array('c'),
diff --git a/src/Symfony/Component/Form/Tests/FormFactoryTest.php b/src/Symfony/Component/Form/Tests/FormFactoryTest.php
index 3f206a798ac0..18c70bc45708 100644
--- a/src/Symfony/Component/Form/Tests/FormFactoryTest.php
+++ b/src/Symfony/Component/Form/Tests/FormFactoryTest.php
@@ -16,6 +16,8 @@
use Symfony\Component\Form\Guess\Guess;
use Symfony\Component\Form\Guess\ValueGuess;
use Symfony\Component\Form\Guess\TypeGuess;
+use Symfony\Component\Form\Tests\Fixtures\Author;
+use Symfony\Component\Form\Tests\Fixtures\AuthorType;
use Symfony\Component\Form\Tests\Fixtures\TestExtension;
use Symfony\Component\Form\Tests\Fixtures\FooType;
use Symfony\Component\Form\Tests\Fixtures\FooTypeBarExtension;
@@ -277,7 +279,7 @@ public function testCreateNamedBuilderExpectsBuilderToBeReturned()
}
/**
- * @expectedException Symfony\Component\Form\Exception\CreationException
+ * @expectedException Symfony\Component\Form\Exception\InvalidOptionException
*/
public function testCreateNamedBuilderExpectsOptionsToExist()
{
@@ -290,7 +292,7 @@ public function testCreateNamedBuilderExpectsOptionsToExist()
}
/**
- * @expectedException Symfony\Component\Form\Exception\CreationException
+ * @expectedException Symfony\Component\Form\Exception\InvalidOptionException
*/
public function testCreateNamedBuilderExpectsOptionsToBeInValidRange()
{
@@ -511,11 +513,13 @@ public function testUnknownOptions()
$factory = new FormFactory(array(new \Symfony\Component\Form\Extension\Core\CoreExtension()));
- $this->setExpectedException('Symfony\Component\Form\Exception\CreationException',
- 'The options "invalid", "unknown" do not exist. Known options are: "data", "data_class", ' .
- '"trim", "required", "read_only", "disabled", "max_length", "pattern", "property_path", "by_reference", ' .
- '"error_bubbling", "error_mapping", "label", "attr", "invalid_message", "invalid_message_parameters", ' .
- '"translation_domain", "empty_data"'
+ $this->setExpectedException('Symfony\Component\Form\Exception\InvalidOptionException',
+ 'The options "invalid", "unknown" do not exist. Known options are: ' .
+ '"attr", "by_reference", "data", "data_class", "disabled", ' .
+ '"empty_data", "error_bubbling", "error_mapping", "invalid_message", ' .
+ '"invalid_message_parameters", "label", "max_length", "pattern", ' .
+ '"property_path", "read_only", "required", "translation_domain", ' .
+ '"trim"'
);
$factory->createNamedBuilder($type, "text", "value", array("invalid" => "opt", "unknown" => "opt"));
}
@@ -526,15 +530,31 @@ public function testUnknownOption()
$factory = new FormFactory(array(new \Symfony\Component\Form\Extension\Core\CoreExtension()));
- $this->setExpectedException('Symfony\Component\Form\Exception\CreationException',
- 'The option "unknown" does not exist. Known options are: "data", "data_class", ' .
- '"trim", "required", "read_only", "disabled", "max_length", "pattern", "property_path", "by_reference", ' .
- '"error_bubbling", "error_mapping", "label", "attr", "invalid_message", "invalid_message_parameters", ' .
- '"translation_domain", "empty_data"'
+ $this->setExpectedException('Symfony\Component\Form\Exception\InvalidOptionException',
+ 'The option "unknown" does not exist. Known options are: "attr", ' .
+ '"by_reference", "data", "data_class", "disabled", "empty_data", ' .
+ '"error_bubbling", "error_mapping", "invalid_message", ' .
+ '"invalid_message_parameters", "label", "max_length", "pattern", ' .
+ '"property_path", "read_only", "required", "translation_domain", ' .
+ '"trim"'
);
$factory->createNamedBuilder($type, "text", "value", array("unknown" => "opt"));
}
+ public function testFieldTypeCreatesDefaultValueForEmptyDataOption()
+ {
+ $factory = new FormFactory(array(new \Symfony\Component\Form\Extension\Core\CoreExtension()));
+
+ $form = $factory->createNamedBuilder(new AuthorType(), 'author')->getForm();
+ $form->bind(array('firstName' => 'John', 'lastName' => 'Smith'));
+
+ $author = new Author();
+ $author->firstName = 'John';
+ $author->setLastName('Smith');
+
+ $this->assertEquals($author, $form->getData());
+ }
+
private function createMockFactory(array $methods = array())
{
return $this->getMockBuilder('Symfony\Component\Form\FormFactory')
diff --git a/src/Symfony/Component/Form/Tests/OptionsTest.php b/src/Symfony/Component/Form/Tests/OptionsTest.php
new file mode 100644
index 000000000000..89b0a07f7f5c
--- /dev/null
+++ b/src/Symfony/Component/Form/Tests/OptionsTest.php
@@ -0,0 +1,168 @@
+
+ *
+ * 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'];
+ }
+}
\ No newline at end of file