merged branch bschussek/issue3354 (PR #3789)

Commits
-------

8329087 [Form] Moved calculation of ChoiceType options to closures
5adec19 [Form] Fixed typos
cb87ccb [Form] Failing test for empty_data option BC break
b733045 [Form] Fixed option support in Form component

Discussion
----------

[Form] Fixed option support in Form component

Bug fix: yes
Feature addition: no
Backwards compatibility break: yes
Symfony2 tests pass: yes
Fixes the following tickets: #3354, #3512, #3685, #3694
Todo: -

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

This PR also introduces a new helper `DefaultOptions` for solving option graphs. It accepts default options to be defined on various layers of your class hierarchy. These options can then be merged with the options passed by the user. This is called *resolving*.

The important feature of this utility is that it lets you define *lazy options*. Lazy options are specified using closures that are evaluated when resolving and thus have access to the resolved values of other (potentially lazy) options. The class detects cyclic option dependencies and fails with an exception in this case.

For more information, check the inline documentation of the `DefaultOptions` class and the UPGRADE file.

@fabpot: Might this be worth a separate component? (in total the utility consists of five classes with two associated tests)

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

by beberlei at 2012-04-05T08:54:10Z

"The important feature of this utility is that it lets you define lazy options. Lazy options are specified using closures"

What about options that are closures? are those differentiated?

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

by bschussek at 2012-04-05T08:57:35Z

@beberlei Yes. Closures for lazy options receive a Symfony\Component\Form\Options instance as first argument. All other closures are interpreted as normal values.

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

by stof at 2012-04-05T11:09:49Z

I'm wondering if these classes should go in the Config component. My issue with it is that it would add a required dependency to the Config component and that the Config component mixes many different things in it already (the loader part, the resource part, the definition part...)

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

by sstok at 2012-04-06T13:36:36Z

Sharing the Options class would be great, and its more then one class so why not give it its own Component folder?
Filesystem is just one class, and that has its own folder.

Great job on the class bschussek 👏

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

by bschussek at 2012-04-10T12:32:34Z

@fabpot Any input?

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

by bschussek at 2012-04-10T13:54:13Z

@fabpot Apart from the decision about the final location of DefaultOptions et al., could you merge this soon? This would make my work a bit easier since this one is a blocker.

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

by fabpot at 2012-04-10T18:08:18Z

@bschussek: Can you rebase on master? I will merge afterwards. Thanks.
This commit is contained in:
Fabien Potencier 2012-04-11 17:03:28 +02:00
commit 05842c54b8
53 changed files with 1313 additions and 245 deletions

View File

@ -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

View File

@ -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.

View File

@ -19,6 +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;
abstract class DoctrineType extends AbstractType
{
@ -42,27 +43,25 @@ abstract class DoctrineType extends AbstractType
}
}
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 @@ abstract class DoctrineType extends AbstractType
$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 @@ abstract class DoctrineType extends AbstractType
* @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)
{

View File

@ -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
);
}

View File

@ -14,6 +14,7 @@ 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;
/**
@ -30,9 +31,19 @@ class ModelType extends AbstractType
}
}
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 @@ class ModelType extends AbstractType
'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)

View File

@ -76,7 +76,7 @@ class UserLoginFormType extends AbstractType
/**
* @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.

View File

@ -96,7 +96,7 @@ abstract class AbstractType implements FormTypeInterface
*
* @return array The default options
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array();
}
@ -108,7 +108,7 @@ abstract class AbstractType implements FormTypeInterface
*
* @return array The allowed option values
*/
public function getAllowedOptionValues(array $options)
public function getAllowedOptionValues()
{
return array();
}

View File

@ -65,7 +65,7 @@ abstract class AbstractTypeExtension implements FormTypeExtensionInterface
*
* @return array
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array();
}
@ -77,7 +77,7 @@ abstract class AbstractTypeExtension implements FormTypeExtensionInterface
*
* @return array The allowed option values
*/
public function getAllowedOptionValues(array $options)
public function getAllowedOptionValues()
{
return array();
}

View File

@ -0,0 +1,320 @@
<?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

@ -0,0 +1,16 @@
<?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 InvalidOptionException extends FormException
{
}

View File

@ -1,29 +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 InvalidOptionsException extends FormException
{
private $options;
public function __construct($message, array $options)
{
parent::__construct($message);
$this->options = $options;
}
public function getOptions()
{
return $this->options;
}
}

View File

@ -1,29 +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 MissingOptionsException extends FormException
{
private $options;
public function __construct($message, array $options)
{
parent::__construct($message);
$this->options = $options;
}
public function getOptions()
{
return $this->options;
}
}

View File

@ -0,0 +1,16 @@
<?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

@ -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')),

View File

@ -44,7 +44,7 @@ class CheckboxType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'value' => '1',

View File

@ -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 @@ class ChoiceType extends AbstractType
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 @@ class ChoiceType extends AbstractType
} 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 @@ class ChoiceType extends AbstractType
/**
* {@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,
);
}

View File

@ -75,7 +75,7 @@ class CollectionType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'allow_add' => false,

View File

@ -20,7 +20,7 @@ class CountryType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'choices' => Locale::getDisplayCountries(\Locale::getDefault()),

View File

@ -128,7 +128,7 @@ class DateTimeType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'input' => 'datetime',
@ -164,7 +164,7 @@ class DateTimeType extends AbstractType
/**
* {@inheritdoc}
*/
public function getAllowedOptionValues(array $options)
public function getAllowedOptionValues()
{
return array(
'input' => array(

View File

@ -161,7 +161,7 @@ class DateType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'years' => range(date('Y') - 5, date('Y') + 5),
@ -188,7 +188,7 @@ class DateType extends AbstractType
/**
* {@inheritdoc}
*/
public function getAllowedOptionValues(array $options)
public function getAllowedOptionValues()
{
return array(
'input' => array(

View File

@ -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 @@ class FieldType extends AbstractType
/**
* {@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 @@ class FieldType extends AbstractType
'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;
}
/**

View File

@ -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 @@ class FormType extends AbstractType
/**
* {@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;
}
/**

View File

@ -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

View File

@ -33,7 +33,7 @@ class IntegerType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
// default precision is locale specific (usually around 3)
@ -47,7 +47,7 @@ class IntegerType extends AbstractType
/**
* {@inheritdoc}
*/
public function getAllowedOptionValues(array $options)
public function getAllowedOptionValues()
{
return array(
'rounding_mode' => array(

View File

@ -20,7 +20,7 @@ class LanguageType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'choices' => Locale::getDisplayLanguages(\Locale::getDefault()),

View File

@ -20,7 +20,7 @@ class LocaleType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'choices' => Locale::getDisplayLocales(\Locale::getDefault()),

View File

@ -48,7 +48,7 @@ class MoneyType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'precision' => 2,

View File

@ -32,7 +32,7 @@ class NumberType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
// default precision is locale specific (usually around 3)
@ -45,7 +45,7 @@ class NumberType extends AbstractType
/**
* {@inheritdoc}
*/
public function getAllowedOptionValues(array $options)
public function getAllowedOptionValues()
{
return array(
'rounding_mode' => array(

View File

@ -39,7 +39,7 @@ class PasswordType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'always_empty' => true,

View File

@ -28,7 +28,7 @@ class PercentType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'precision' => 0,
@ -39,7 +39,7 @@ class PercentType extends AbstractType
/**
* {@inheritdoc}
*/
public function getAllowedOptionValues(array $options)
public function getAllowedOptionValues()
{
return array(
'type' => array(

View File

@ -39,7 +39,7 @@ class RepeatedType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'type' => 'text',

View File

@ -134,7 +134,7 @@ class TimeType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'hours' => range(0, 23),
@ -161,7 +161,7 @@ class TimeType extends AbstractType
/**
* {@inheritdoc}
*/
public function getAllowedOptionValues(array $options)
public function getAllowedOptionValues()
{
return array(
'input' => array(

View File

@ -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 @@ class TimezoneType extends AbstractType
*
* @return array The timezone choices
*/
static private function getTimezones()
static public function getTimezones()
{
if (null === static::$timezones) {
static::$timezones = array();

View File

@ -28,7 +28,7 @@ class UrlType extends AbstractType
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'default_protocol' => 'http',

View File

@ -15,7 +15,7 @@ use Symfony\Component\Form\AbstractTypeExtension;
class ChoiceTypeCsrfExtension extends AbstractTypeExtension
{
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array('csrf_protection' => false);
}

View File

@ -54,7 +54,7 @@ class CsrfType extends AbstractType
/**
* {@inheritDoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'csrf_provider' => $this->csrfProvider,

View File

@ -15,7 +15,7 @@ use Symfony\Component\Form\AbstractTypeExtension;
class DateTypeCsrfExtension extends AbstractTypeExtension
{
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array('csrf_protection' => false);
}

View File

@ -76,7 +76,7 @@ class FormTypeCsrfExtension extends AbstractTypeExtension
/**
* {@inheritDoc}
*/
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'csrf_protection' => $this->enabled,

View File

@ -15,7 +15,7 @@ use Symfony\Component\Form\AbstractTypeExtension;
class RepeatedTypeCsrfExtension extends AbstractTypeExtension
{
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array('csrf_protection' => false);
}

View File

@ -15,7 +15,7 @@ use Symfony\Component\Form\AbstractTypeExtension;
class TimeTypeCsrfExtension extends AbstractTypeExtension
{
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array('csrf_protection' => false);
}

View File

@ -42,7 +42,7 @@ class FieldTypeValidatorExtension extends AbstractTypeExtension
->addValidator(new DelegatingValidator($this->validator));
}
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'validation_groups' => null,

View File

@ -219,9 +219,9 @@ class FormFactory implements FormFactoryInterface
$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 @@ class FormFactory implements FormFactoryInterface
$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);

View File

@ -59,7 +59,7 @@ interface FormTypeExtensionInterface
*
* @return array
*/
function getDefaultOptions(array $options);
function getDefaultOptions();
/**
* Returns the allowed option values for each option (if any).
@ -68,7 +68,7 @@ interface FormTypeExtensionInterface
*
* @return array The allowed option values
*/
function getAllowedOptionValues(array $options);
function getAllowedOptionValues();
/**

View File

@ -79,7 +79,7 @@ interface FormTypeInterface
*
* @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 @@ interface FormTypeInterface
*
* @return array The allowed option values
*/
function getAllowedOptionValues(array $options);
function getAllowedOptionValues();
/**
* Returns the name of the parent type.

View File

@ -0,0 +1,73 @@
<?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 Closure;
/**
* An option that is evaluated lazily using a closure.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @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);
}
}

View File

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

View File

@ -0,0 +1,76 @@
<?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

@ -90,7 +90,7 @@ class ChoiceTypeTest extends TypeTestCase
}
/**
* @expectedException Symfony\Component\Form\Exception\FormException
* expectedException Symfony\Component\Form\Exception\FormException
*/
public function testEitherChoiceListOrChoicesMustBeSet()
{

View File

@ -0,0 +1,31 @@
<?php
namespace Symfony\Component\Form\Tests\Fixtures;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class AuthorType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('firstName')
->add('lastName')
;
}
public function getName()
{
return 'author';
}
public function getDefaultOptions()
{
return array(
'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author',
);
}
}

View File

@ -34,7 +34,7 @@ class FooType extends AbstractType
return new FormBuilder($name, $factory, new EventDispatcher());
}
public function getDefaultOptions(array $options)
public function getDefaultOptions()
{
return array(
'data' => null,
@ -44,7 +44,7 @@ class FooType extends AbstractType
);
}
public function getAllowedOptionValues(array $options)
public function getAllowedOptionValues()
{
return array(
'a_or_b' => array('a', 'b'),

View File

@ -21,7 +21,7 @@ class FooTypeBarExtension extends AbstractTypeExtension
$builder->setAttribute('bar', 'x');
}
public function getAllowedOptionValues(array $options)
public function getAllowedOptionValues()
{
return array(
'a_or_b' => array('c'),

View File

@ -16,6 +16,8 @@ use Symfony\Component\Form\FormFactory;
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 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase
}
/**
* @expectedException Symfony\Component\Form\Exception\CreationException
* @expectedException Symfony\Component\Form\Exception\InvalidOptionException
*/
public function testCreateNamedBuilderExpectsOptionsToExist()
{
@ -290,7 +292,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase
}
/**
* @expectedException Symfony\Component\Form\Exception\CreationException
* @expectedException Symfony\Component\Form\Exception\InvalidOptionException
*/
public function testCreateNamedBuilderExpectsOptionsToBeInValidRange()
{
@ -511,11 +513,13 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase
$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 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase
$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')

View File

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