From 87b16e7015c02e794c33dbfc05812cf070b4ac68 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 17 Jan 2012 00:46:26 +0100 Subject: [PATCH 01/13] [Form] Greatly improved ChoiceListInterface and all of its implementations Fixes #2869, fixes #3021, fixes #1919, fixes #3153. --- CHANGELOG-2.1.md | 31 + UPGRADE-2.1.md | 47 +- .../Form/ChoiceList/EntityChoiceList.php | 510 ++++++++------ .../Form/ChoiceList/EntityLoaderInterface.php | 12 +- .../Form/ChoiceList/ORMQueryBuilderLoader.php | 16 + .../CollectionToArrayTransformer.php | 63 ++ .../EntitiesToArrayTransformer.php | 98 --- .../DataTransformer/EntityToIdTransformer.php | 83 --- .../Doctrine/Form/Type/DoctrineType.php | 8 +- .../views/Form/form_div_layout.html.twig | 12 +- .../views/Form/choice_options.html.php | 12 +- .../Form/Exception/StringCastException.php | 16 + .../Core/ChoiceList/ArrayChoiceList.php | 69 -- .../Extension/Core/ChoiceList/ChoiceList.php | 649 ++++++++++++++++++ .../Core/ChoiceList/ChoiceListInterface.php | 118 +++- .../Core/ChoiceList/ComplexChoiceList.php | 44 ++ .../Core/ChoiceList/MonthChoiceList.php | 58 -- .../Core/ChoiceList/ObjectChoiceList.php | 201 ++++++ .../Core/ChoiceList/PaddedChoiceList.php | 59 -- .../Core/ChoiceList/SimpleChoiceList.php | 133 ++++ .../Core/ChoiceList/TimezoneChoiceList.php | 63 -- ...hp => ChoiceToBooleanArrayTransformer.php} | 42 +- .../ChoiceToValueTransformer.php | 63 ++ ...p => ChoicesToBooleanArrayTransformer.php} | 44 +- ...mer.php => ChoicesToValuesTransformer.php} | 30 +- .../ScalarToChoiceTransformer.php | 37 - .../EventListener/FixRadioInputListener.php | 18 +- .../Form/Extension/Core/Type/ChoiceType.php | 73 +- .../Form/Extension/Core/Type/CountryType.php | 3 + .../Form/Extension/Core/Type/DateType.php | 59 +- .../Form/Extension/Core/Type/LanguageType.php | 2 + .../Form/Extension/Core/Type/LocaleType.php | 2 + .../Form/Extension/Core/Type/RadioType.php | 28 +- .../Form/Extension/Core/Type/TimeType.php | 35 +- .../Form/Extension/Core/Type/TimezoneType.php | 54 +- src/Symfony/Component/Form/Util/FormUtil.php | 18 - .../Form/ChoiceList/EntityChoiceListTest.php | 26 +- .../Doctrine/Form/Type/EntityTypeTest.php | 65 +- .../Component/Form/AbstractLayoutTest.php | 158 +++-- .../Core/ChoiceList/ArrayChoiceListTest.php | 53 -- .../Core/ChoiceList/ComplexChoiceListTest.php | 143 ++++ .../Core/ChoiceList/MonthChoiceListTest.php | 95 --- .../Core/ChoiceList/ObjectChoiceListTest.php | 249 +++++++ .../Core/ChoiceList/PaddedChoiceListTest.php | 54 -- .../Core/ChoiceList/SimpleChoiceListTest.php | 222 ++++++ ...t.php => ChoiceToValueTransformerTest.php} | 27 +- ...php => ChoicesToValuesTransformerTest.php} | 20 +- .../FixRadioInputListenerTest.php | 31 +- .../Extension/Core/Type/ChoiceTypeTest.php | 248 ++++++- .../Extension/Core/Type/CountryTypeTest.php | 21 +- .../Extension/Core/Type/DateTimeTypeTest.php | 3 + .../Form/Extension/Core/Type/DateTypeTest.php | 54 +- .../Extension/Core/Type/LanguageTypeTest.php | 21 +- .../Extension/Core/Type/LocaleTypeTest.php | 13 +- .../Extension/Core/Type/RadioTypeTest.php | 26 - .../Form/Extension/Core/Type/TimeTypeTest.php | 6 +- .../Extension/Core/Type/TimezoneTypeTest.php | 9 +- .../Component/Form/Util/FormUtilTest.php | 51 +- 58 files changed, 3083 insertions(+), 1322 deletions(-) create mode 100644 src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php delete mode 100644 src/Symfony/Bridge/Doctrine/Form/DataTransformer/EntitiesToArrayTransformer.php delete mode 100644 src/Symfony/Bridge/Doctrine/Form/DataTransformer/EntityToIdTransformer.php create mode 100644 src/Symfony/Component/Form/Exception/StringCastException.php delete mode 100644 src/Symfony/Component/Form/Extension/Core/ChoiceList/ArrayChoiceList.php create mode 100644 src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php create mode 100644 src/Symfony/Component/Form/Extension/Core/ChoiceList/ComplexChoiceList.php delete mode 100644 src/Symfony/Component/Form/Extension/Core/ChoiceList/MonthChoiceList.php create mode 100644 src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php delete mode 100644 src/Symfony/Component/Form/Extension/Core/ChoiceList/PaddedChoiceList.php create mode 100644 src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php delete mode 100644 src/Symfony/Component/Form/Extension/Core/ChoiceList/TimezoneChoiceList.php rename src/Symfony/Component/Form/Extension/Core/DataTransformer/{ScalarToBooleanChoicesTransformer.php => ChoiceToBooleanArrayTransformer.php} (69%) create mode 100644 src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php rename src/Symfony/Component/Form/Extension/Core/DataTransformer/{ArrayToBooleanChoicesTransformer.php => ChoicesToBooleanArrayTransformer.php} (66%) rename src/Symfony/Component/Form/Extension/Core/DataTransformer/{ArrayToChoicesTransformer.php => ChoicesToValuesTransformer.php} (59%) delete mode 100644 src/Symfony/Component/Form/Extension/Core/DataTransformer/ScalarToChoiceTransformer.php delete mode 100644 tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ArrayChoiceListTest.php create mode 100644 tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ComplexChoiceListTest.php delete mode 100644 tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/MonthChoiceListTest.php create mode 100644 tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ObjectChoiceListTest.php delete mode 100644 tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/PaddedChoiceListTest.php create mode 100644 tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/SimpleChoiceListTest.php rename tests/Symfony/Tests/Component/Form/Extension/Core/DataTransformer/{ScalarToChoiceTransformerTest.php => ChoiceToValueTransformerTest.php} (69%) rename tests/Symfony/Tests/Component/Form/Extension/Core/DataTransformer/{ArrayToChoicesTransformerTest.php => ChoicesToValuesTransformerTest.php} (68%) diff --git a/CHANGELOG-2.1.md b/CHANGELOG-2.1.md index 9dd5555796..7e1cfe65fc 100644 --- a/CHANGELOG-2.1.md +++ b/CHANGELOG-2.1.md @@ -166,6 +166,37 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c * added support for empty form name at root level, this enables rendering forms without form name prefix in field names + * [BC BREAK] greatly improved `ChoiceListInterface` and all of its + implementations. `EntityChoiceList` was adapted, the methods `getEntities()`, + `getEntitiesByByKeys()`, `getIdentifier()` and `getIdentifierValues()` were + removed/made private. Instead of the first two you can use `getChoices()` + and `getChoicesByValues()`, for the latter two no replacement exists. + `ArrayChoiceList` was replaced by `SimpleChoiceList`. + `PaddedChoiceList`, `MonthChoiceList` and `TimezoneChoiceList` were removed. + Their functionality was merged into `DateType`, `TimeType` and `TimezoneType`. + + * [BC BREAK] removed `EntitiesToArrayTransformer` and `EntityToIdTransformer`. + The former has been replaced by `CollectionToArrayTransformer` in combination + with `EntityChoiceList`, the latter is not required in the core anymore. + + * [BC BREAK] renamed + + * `ArrayToBooleanChoicesTransformer` to `ChoicesToBooleanArrayTransformer` + * `ScalarToBooleanChoicesTransformer` to `ChoiceToBooleanArrayTransformer` + * `ArrayToChoicesTransformer` to `ChoicesToValuesTransformer` + * `ScalarToChoiceTransformer` to `ChoiceToValueTransformer` + + to be consistent with the naming in `ChoiceListInterface`. + + * [BC BREAK] removed `FormUtil::toArrayKey()` and `FormUtil::toArrayKeys()`. + They were merged into `ChoiceList` and have no public equivalent anymore. + + * added `ComplexChoiceList` and `ObjectChoiceList`. Both let you select amongst + objects in a choice field, but feature different constructors. + * choice fields now throw a `FormException` if neither the "choices" nor the + "choice_list" option is set + * the radio field is now a child type of the checkbox field + ### HttpFoundation * added support for streamed responses diff --git a/UPGRADE-2.1.md b/UPGRADE-2.1.md index 985463f06c..00a86f6833 100644 --- a/UPGRADE-2.1.md +++ b/UPGRADE-2.1.md @@ -76,4 +76,49 @@ UPGRADE FROM 2.0 to 2.1 If you don't want to set the `Valid` constraint, or if there is no reference from the data of the parent form to the data of the child form, you can enable BC behaviour by setting the option "cascade_validation" to `true` on - the parent form. \ No newline at end of file + the parent form. + +* In the template of the choice type, instead of a single "choices" variable + there are now two variables: "choices" and "choice_labels". + "choices" contains the choices in the values (before they were in the keys) + and "choice_labels" contains the matching labels. Both arrays have + identical keys. + + Before: + + {% for choice, label in choices %} + + {% endfor %} + + After: + + {% for index, choice in choices %} + + {% endfor %} + +* The strategy for generating the HTML attributes "id" and "name" + of choices in a choice field has changed. Instead of appending the choice + value, a generated integer is now appended by default. Take care if your + Javascript relies on that. If you can guarantee that your choice values only + contain ASCII letters, digits, letters, colons and underscores, you can + restore the old behaviour by setting the option "index_strategy" of the + choice field to `ChoiceList::COPY_CHOICE`. + +* The strategy for generating the HTML attributes "value" of choices in a + choice field has changed. Instead of using the choice value, a generated + integer is now stored. Again, take care if your Javascript reads this value. + If your choice field is a non-expanded single-choice field, or if + the choices are guaranteed not to contain the empty string '' (which is the + case when you added it manually or when the field is a single-choice field + and is not required), you can restore the old behaviour by setting the + option "value_strategy" to `ChoiceList::COPY_CHOICE`. + of choices in a choice field has changed. Instead of appending the choice + value, a generated integer is now appended by default. Take care if your + Javascript relies on that. If you can guarantee that your choice values only + contain ASCII letters, digits, letters, colons and underscores, you can + restore the old behaviour by setting the option "index_strategy" of the + choice field to `ChoiceList::COPY_CHOICE`. diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index f81d58bd4f..ddda8141a3 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -13,11 +13,17 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList; use Symfony\Component\Form\Util\PropertyPath; use Symfony\Component\Form\Exception\FormException; +use Symfony\Component\Form\Exception\StringCastException; use Symfony\Component\Form\Exception\UnexpectedTypeException; -use Symfony\Component\Form\Extension\Core\ChoiceList\ArrayChoiceList; +use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; use Doctrine\Common\Persistence\ObjectManager; -class EntityChoiceList extends ArrayChoiceList +/** + * A choice list presenting a list of Doctrine entities as choices + * + * @author Bernhard Schussek + */ +class EntityChoiceList extends ObjectChoiceList { /** * @var ObjectManager @@ -34,19 +40,6 @@ class EntityChoiceList extends ArrayChoiceList */ private $classMetadata; - /** - * The entities from which the user can choose - * - * This array is either indexed by ID (if the ID is a single field) - * or by key in the choices array (if the ID consists of multiple fields) - * - * This property is initialized by initializeChoices(). It should only - * be accessed through getEntity() and getEntities(). - * - * @var array - */ - private $entities = array(); - /** * Contains the query builder that builds the query for fetching the * entities @@ -67,223 +60,336 @@ class EntityChoiceList extends ArrayChoiceList private $identifier = array(); /** - * Property path to access the key value of this choice-list. + * Whether the entities have already been loaded. * - * @var PropertyPath + * @var Boolean */ - private $propertyPath; + private $loaded = false; /** - * Closure or PropertyPath string on Entity to use for grouping of entities + * Creates a new entity choice list. * - * @var mixed - */ - private $groupBy; - - /** - * Constructor. - * - * @param ObjectManager $manager An EntityManager instance + * @param ObjectManager $manager An EntityManager instance * @param string $class The class name - * @param string $property The property name + * @param string $labelPath The property path used for the label * @param EntityLoaderInterface $entityLoader An optional query builder - * @param array|\Closure $choices An array of choices or a function returning an array - * @param string $groupBy + * @param array $entities An array of choices + * @param string $groupPath */ - public function __construct(ObjectManager $manager, $class, $property = null, EntityLoaderInterface $entityLoader = null, $choices = null, $groupBy = null) + public function __construct(ObjectManager $manager, $class, $labelPath = null, EntityLoaderInterface $entityLoader = null, $entities = null, $groupPath = null) { $this->em = $manager; $this->class = $class; $this->entityLoader = $entityLoader; $this->classMetadata = $manager->getClassMetadata($class); $this->identifier = $this->classMetadata->getIdentifierFieldNames(); - $this->groupBy = $groupBy; + $this->loaded = is_array($entities) || $entities instanceof \Traversable; - // The property option defines, which property (path) is used for - // displaying entities as strings - if ($property) { - $this->propertyPath = new PropertyPath($property); - } elseif (!method_exists($this->classMetadata->getName(), '__toString')) { - // Otherwise expect a __toString() method in the entity - throw new FormException('Entities passed to the choice field must have a "__toString()" method defined (or you can also override the "property" option).'); + if (!$this->loaded) { + // Make sure the constraints of the parent constructor are + // fulfilled + $entities = array(); } - if (!is_array($choices) && !$choices instanceof \Closure && !is_null($choices)) { - throw new UnexpectedTypeException($choices, 'array or \Closure or null'); - } - - $this->choices = $choices; + parent::__construct($entities, $labelPath, array(), $groupPath); } /** - * Initializes the choices and returns them. + * Returns the list of entities * - * If the entities were passed in the "choices" option, this method - * does not have any significant overhead. Otherwise, if a query builder - * was passed in the "query_builder" option, this builder is now used - * to construct a query which is executed. In the last case, all entities - * for the underlying class are fetched from the repository. + * @return array * - * @return array An array of choices + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface */ - protected function load() + public function getChoices() { - parent::load(); + if (!$this->loaded) { + $this->load(); + } - if (is_array($this->choices)) { - $entities = $this->choices; - } elseif ($entityLoader = $this->entityLoader) { - $entities = $entityLoader->getEntities(); + return parent::getChoices(); + } + + /** + * Returns the labels for the entities + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getLabels() + { + if (!$this->loaded) { + $this->load(); + } + + return parent::getLabels(); + } + + /** + * Returns the values for the entities + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getValues() + { + if (!$this->loaded) { + $this->load(); + } + + return parent::getValues(); + } + + /** + * Returns the values of the entities that should be presented to the user + * with priority. + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getPreferredValues() + { + if (!$this->loaded) { + $this->load(); + } + + return parent::getPreferredValues(); + } + + /** + * Returns the values of the entities that should be presented to the user + * with priority as nested array with the choice groups as top-level keys. + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getPreferredValueHierarchy() + { + if (!$this->loaded) { + $this->load(); + } + + return parent::getPreferredValueHierarchy(); + } + + /** + * Returns the values of the entities that are not preferred. + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getRemainingValues() + { + if (!$this->loaded) { + $this->load(); + } + + return parent::getRemainingValues(); + } + + /** + * Returns the values of the entities that are not preferred as nested array + * with the choice groups as top-level keys. + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getRemainingValueHierarchy() + { + if (!$this->loaded) { + $this->load(); + } + + return parent::getRemainingValueHierarchy(); + } + + /** + * Returns the entities corresponding to the given values. + * + * @param array $values + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getChoicesForValues(array $values) + { + if (!$this->loaded) { + // Optimize performance in case we have an entity loader and + // a single-field identifier + if (count($this->identifier) === 1 && $this->entityLoader) { + return $this->entityLoader->getEntitiesByIds(current($this->identifier), $values); + } + + $this->load(); + } + + return parent::getChoicesForValues($values); + } + + /** + * Returns the values corresponding to the given entities. + * + * @param array $entities + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getValuesForChoices(array $entities) + { + if (!$this->loaded) { + // Optimize performance for single-field identifiers. We already + // know that the IDs are used as values + + // Attention: This optimization does not check choices for existence + if (count($this->identifier) === 1) { + $values = array(); + + foreach ($entities as $entity) { + if ($entity instanceof $this->class) { + // Make sure to convert to the right format + $values[] = $this->fixValue(current($this->getIdentifierValues($entity))); + } + } + + return $values; + } + + $this->load(); + } + + return parent::getValuesForChoices($entities); + } + + /** + * Returns the indices corresponding to the given entities. + * + * @param array $entities + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getIndicesForChoices(array $entities) + { + if (!$this->loaded) { + // Optimize performance for single-field identifiers. We already + // know that the IDs are used as indices + + // Attention: This optimization does not check choices for existence + if (count($this->identifier) === 1) { + $indices = array(); + + foreach ($entities as $entity) { + if ($entity instanceof $this->class) { + // Make sure to convert to the right format + $indices[] = $this->fixIndex(current($this->getIdentifierValues($entity))); + } + } + + return $indices; + } + + $this->load(); + } + + return parent::getIndicesForChoices($entities); + } + + /** + * Returns the entities corresponding to the given values. + * + * @param array $values + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getIndicesForValues(array $values) + { + if (!$this->loaded) { + // Optimize performance for single-field identifiers. We already + // know that the IDs are used as indices and values + + // Attention: This optimization does not check values for existence + if (count($this->identifier) === 1) { + return $this->fixIndices($values); + } + + $this->load(); + } + + return parent::getIndicesForValues($values); + } + + /** + * Creates a new unique index for this entity. + * + * If the entity has a single-field identifier, this identifier is used. + * + * Otherwise a new integer is generated. + * + * @param mixed $choice The choice to create an index for + * + * @return integer|string A unique index containing only ASCII letters, + * digits and underscores. + */ + protected function createIndex($entity) + { + if (count($this->identifier) === 1) { + return current($this->getIdentifierValues($entity)); + } + + return parent::createIndex($entity); + } + + /** + * Creates a new unique value for this entity. + * + * If the entity has a single-field identifier, this identifier is used. + * + * Otherwise a new integer is generated. + * + * @param mixed $choice The choice to create a value for + * + * @return integer|string A unique value without character limitations. + */ + protected function createValue($entity) + { + if (count($this->identifier) === 1) { + return current($this->getIdentifierValues($entity)); + } + + return parent::createValue($entity); + } + + /** + * Loads the list with entities. + */ + private function load() + { + if ($this->entityLoader) { + $entities = $this->entityLoader->getEntities(); } else { $entities = $this->em->getRepository($this->class)->findAll(); } - $this->choices = array(); - $this->entities = array(); - - if ($this->groupBy) { - $entities = $this->groupEntities($entities, $this->groupBy); + try { + // The second parameter $labels is ignored by ObjectChoiceList + // The third parameter $preferredChoices is currently not supported + parent::initialize($entities, array(), array()); + } catch (StringCastException $e) { + throw new StringCastException('Objects in the entity field must have a "__toString()" method defined. Alternatively you can set the "property" option.', null, $e); } - $this->loadEntities($entities); - - return $this->choices; - } - - private function groupEntities($entities, $groupBy) - { - $grouped = array(); - $path = new PropertyPath($groupBy); - - foreach ($entities as $entity) { - // Get group name from property path - try { - $group = (string) $path->getValue($entity); - } catch (UnexpectedTypeException $e) { - // PropertyPath cannot traverse entity - $group = null; - } - - if (empty($group)) { - $grouped[] = $entity; - } else { - $grouped[$group][] = $entity; - } - } - - return $grouped; - } - - /** - * Converts entities into choices with support for groups. - * - * The choices are generated from the entities. If the entities have a - * composite identifier, the choices are indexed using ascending integers. - * Otherwise the identifiers are used as indices. - * - * If the option "property" was passed, the property path in that option - * is used as option values. Otherwise this method tries to convert - * objects to strings using __toString(). - * - * @param array $entities An array of entities - * @param string $group A group name - */ - private function loadEntities($entities, $group = null) - { - foreach ($entities as $key => $entity) { - if (is_array($entity)) { - // Entities are in named groups - $this->loadEntities($entity, $key); - continue; - } - - if ($this->propertyPath) { - // If the property option was given, use it - $value = $this->propertyPath->getValue($entity); - } else { - $value = (string) $entity; - } - - if (count($this->identifier) > 1) { - // When the identifier consists of multiple field, use - // naturally ordered keys to refer to the choices - $id = $key; - } else { - // When the identifier is a single field, index choices by - // entity ID for performance reasons - $id = current($this->getIdentifierValues($entity)); - } - - if (null === $group) { - // Flat list of choices - $this->choices[$id] = $value; - } else { - // Nested choices - $this->choices[$group][$id] = $value; - } - - $this->entities[$id] = $entity; - } - } - - /** - * Returns the fields of which the identifier of the underlying class consists. - * - * @return array - */ - public function getIdentifier() - { - return $this->identifier; - } - - /** - * Returns the according entities for the choices. - * - * If the choices were not initialized, they are initialized now. This - * is an expensive operation, except if the entities were passed in the - * "choices" option. - * - * @return array An array of entities - */ - public function getEntities() - { - if (!$this->loaded) { - $this->load(); - } - - return $this->entities; - } - - /** - * Returns the entities for the given keys. - * - * If the underlying entities have composite identifiers, the choices - * are initialized. The key is expected to be the index in the choices - * array in this case. - * - * If they have single identifiers, they are either fetched from the - * internal entity cache (if filled) or loaded from the database. - * - * @param array $keys The choice key (for entities with composite - * identifiers) or entity ID (for entities with single - * identifiers) - * @return object[] The matching entity - */ - public function getEntitiesByKeys(array $keys) - { - if (!$this->loaded) { - $this->load(); - } - - $found = array(); - - foreach ($keys as $key) { - if (isset($this->entities[$key])) { - $found[] = $this->entities[$key]; - } - } - - return $found; + $this->loaded = true; } /** @@ -299,7 +405,7 @@ class EntityChoiceList extends ArrayChoiceList * * @throws FormException If the entity does not exist in Doctrine's identity map */ - public function getIdentifierValues($entity) + private function getIdentifierValues($entity) { if (!$this->em->contains($entity)) { throw new FormException('Entities passed to the choice field must be managed'); diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php index a64496e2f6..4fc8e6ba99 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php @@ -19,9 +19,19 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList; interface EntityLoaderInterface { /** - * Return an array of entities that are valid choices in the corresponding choice list. + * Returns an array of entities that are valid choices in the corresponding choice list. * * @return array */ function getEntities(); + + /** + * Returns an array of entities matching the given identifiers. + * + * @param string $identifier + * @param array $values + * + * @return array + */ + function getEntitiesByIds($identifier, array $values); } diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index eb585f0b9e..5dd51fb10b 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList; use Symfony\Component\Form\Exception\UnexpectedTypeException; use Doctrine\ORM\QueryBuilder; +use Doctrine\DBAL\Connection; /** * Getting Entities through the ORM QueryBuilder @@ -62,4 +63,19 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface { return $this->queryBuilder->getQuery()->execute(); } + + /** + * {@inheritDoc} + */ + public function getEntitiesByIds($identifier, array $values) + { + $qb = clone ($this->queryBuilder); + $alias = current($qb->getRootAliases()); + $where = $qb->expr()->in($alias.'.'.$identifier, "?1"); + + return $qb->andWhere($where) + ->getQuery() + ->setParameter(1, $values, Connection::PARAM_STR_ARRAY) + ->getResult(); + } } diff --git a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php new file mode 100644 index 0000000000..94ff98359e --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Form\DataTransformer; + +use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\DataTransformerInterface; +use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\ArrayCollection; + +/** + * @author Bernhard Schussek + */ +class CollectionToArrayTransformer implements DataTransformerInterface +{ + /** + * Transforms a collection into an array. + * + * @param Collection|object $collection A collection of entities, a single entity or NULL + * + * @return mixed An array of choice keys, a single key or NULL + */ + public function transform($collection) + { + if (null === $collection) { + return array(); + } + + if (!$collection instanceof Collection) { + throw new UnexpectedTypeException($collection, 'Doctrine\Common\Collections\Collection'); + } + + return $collection->toArray(); + } + + /** + * Transforms choice keys into entities. + * + * @param mixed $keys An array of keys, a single key or NULL + * + * @return Collection|object A collection of entities, a single entity or NULL + */ + public function reverseTransform($array) + { + if ('' === $array || null === $array) { + $array = array(); + } else { + $array = (array) $array; + } + + return new ArrayCollection($array); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/EntitiesToArrayTransformer.php b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/EntitiesToArrayTransformer.php deleted file mode 100644 index 753aa59c31..0000000000 --- a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/EntitiesToArrayTransformer.php +++ /dev/null @@ -1,98 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Doctrine\Form\DataTransformer; - -use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList; -use Symfony\Component\Form\Exception\UnexpectedTypeException; -use Symfony\Component\Form\Exception\TransformationFailedException; -use Symfony\Component\Form\DataTransformerInterface; -use Doctrine\Common\Collections\Collection; -use Doctrine\Common\Collections\ArrayCollection; - -class EntitiesToArrayTransformer implements DataTransformerInterface -{ - private $choiceList; - - public function __construct(EntityChoiceList $choiceList) - { - $this->choiceList = $choiceList; - } - - /** - * Transforms entities into choice keys. - * - * @param Collection|object $collection A collection of entities, a single entity or NULL - * - * @return mixed An array of choice keys, a single key or NULL - */ - public function transform($collection) - { - if (null === $collection) { - return array(); - } - - if (!($collection instanceof Collection)) { - throw new UnexpectedTypeException($collection, 'Doctrine\Common\Collections\Collection'); - } - - $array = array(); - - if (count($this->choiceList->getIdentifier()) > 1) { - // load all choices - $availableEntities = $this->choiceList->getEntities(); - - foreach ($collection as $entity) { - // identify choices by their collection key - $key = array_search($entity, $availableEntities, true); - $array[] = $key; - } - } else { - foreach ($collection as $entity) { - $value = current($this->choiceList->getIdentifierValues($entity)); - $array[] = is_numeric($value) ? (int) $value : $value; - } - } - - return $array; - } - - /** - * Transforms choice keys into entities. - * - * @param mixed $keys An array of keys, a single key or NULL - * - * @return Collection|object A collection of entities, a single entity or NULL - */ - public function reverseTransform($keys) - { - $collection = new ArrayCollection(); - - if ('' === $keys || null === $keys) { - return $collection; - } - - if (!is_array($keys)) { - throw new UnexpectedTypeException($keys, 'array'); - } - - $entities = $this->choiceList->getEntitiesByKeys($keys); - if (count($keys) !== count($entities)) { - throw new TransformationFailedException('Not all entities matching the keys were found.'); - } - - foreach ($entities as $entity) { - $collection->add($entity); - } - - return $collection; - } -} diff --git a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/EntityToIdTransformer.php b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/EntityToIdTransformer.php deleted file mode 100644 index 0de01caca4..0000000000 --- a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/EntityToIdTransformer.php +++ /dev/null @@ -1,83 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Doctrine\Form\DataTransformer; - -use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList; -use Symfony\Component\Form\DataTransformerInterface; -use Symfony\Component\Form\Exception\UnexpectedTypeException; -use Symfony\Component\Form\Exception\TransformationFailedException; -use Doctrine\Common\Collections\Collection; - -class EntityToIdTransformer implements DataTransformerInterface -{ - private $choiceList; - - public function __construct(EntityChoiceList $choiceList) - { - $this->choiceList = $choiceList; - } - - /** - * Transforms entities into choice keys. - * - * @param Collection|object $entity A collection of entities, a single entity or NULL - * - * @return mixed An array of choice keys, a single key or NULL - */ - public function transform($entity) - { - if (null === $entity || '' === $entity) { - return ''; - } - - if (!is_object($entity)) { - throw new UnexpectedTypeException($entity, 'object'); - } - - if ($entity instanceof Collection) { - throw new \InvalidArgumentException('Expected an object, but got a collection. Did you forget to pass "multiple=true" to an entity field?'); - } - - if (count($this->choiceList->getIdentifier()) > 1) { - // load all choices - $availableEntities = $this->choiceList->getEntities(); - - return array_search($entity, $availableEntities); - } - - return current($this->choiceList->getIdentifierValues($entity)); - } - - /** - * Transforms choice keys into entities. - * - * @param mixed $key An array of keys, a single key or NULL - * - * @return Collection|object A collection of entities, a single entity or NULL - */ - public function reverseTransform($key) - { - if ('' === $key || null === $key) { - return null; - } - - if (count($this->choiceList->getIdentifier()) > 1 && !is_numeric($key)) { - throw new UnexpectedTypeException($key, 'numeric'); - } - - if (!($entities = $this->choiceList->getEntitiesByKeys(array($key)))) { - throw new TransformationFailedException(sprintf('The entity with key "%s" could not be found', $key)); - } - - return $entities[0]; - } -} diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 7a4c67f3a1..0937c1c001 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -17,8 +17,7 @@ use Symfony\Component\Form\FormBuilder; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; use Symfony\Bridge\Doctrine\Form\EventListener\MergeCollectionListener; -use Symfony\Bridge\Doctrine\Form\DataTransformer\EntitiesToArrayTransformer; -use Symfony\Bridge\Doctrine\Form\DataTransformer\EntityToIdTransformer; +use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; use Symfony\Component\Form\AbstractType; abstract class DoctrineType extends AbstractType @@ -38,10 +37,8 @@ abstract class DoctrineType extends AbstractType if ($options['multiple']) { $builder ->addEventSubscriber(new MergeCollectionListener()) - ->prependClientTransformer(new EntitiesToArrayTransformer($options['choice_list'])) + ->prependClientTransformer(new CollectionToArrayTransformer()) ; - } else { - $builder->prependClientTransformer(new EntityToIdTransformer($options['choice_list'])); } } @@ -61,6 +58,7 @@ abstract class DoctrineType extends AbstractType if (!isset($options['choice_list'])) { $manager = $this->registry->getManager($options['em']); + if (isset($options['query_builder'])) { $options['loader'] = $this->getLoader($manager, $options); } diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index 3ba5fc5f4b..4639a1c408 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -26,15 +26,15 @@ {% block widget_choice_options %} {% spaceless %} - {% for choice, label in options %} - {% if _form_is_choice_group(label) %} - - {% for nestedChoice, nestedLabel in label %} - + {% for index, choice in options %} + {% if _form_is_choice_group(choice) %} + + {% for nested_index, nested_choice in choice %} + {% endfor %} {% else %} - + {% endif %} {% endfor %} {% endspaceless %} diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php index cb95136e63..12281606d4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php @@ -1,11 +1,11 @@ - $label): ?> - isChoiceGroup($label)): ?> - - $nestedLabel): ?> - + $choice): ?> + isChoiceGroup($choice)): ?> + + $nested_choice): ?> + - + diff --git a/src/Symfony/Component/Form/Exception/StringCastException.php b/src/Symfony/Component/Form/Exception/StringCastException.php new file mode 100644 index 0000000000..ff882ae8f6 --- /dev/null +++ b/src/Symfony/Component/Form/Exception/StringCastException.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 StringCastException extends FormException +{ +} diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ArrayChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ArrayChoiceList.php deleted file mode 100644 index 0ab969afe7..0000000000 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ArrayChoiceList.php +++ /dev/null @@ -1,69 +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\Extension\Core\ChoiceList; - -use Symfony\Component\Form\Exception\UnexpectedTypeException; - -class ArrayChoiceList implements ChoiceListInterface -{ - protected $choices; - - protected $loaded = false; - - /** - * Constructor. - * - * @param array|\Closure $choices An array of choices or a function returning an array - * - * @throws UnexpectedTypeException if the type of the choices parameter is not supported - */ - public function __construct($choices) - { - if (!is_array($choices) && !$choices instanceof \Closure) { - throw new UnexpectedTypeException($choices, 'array or \Closure'); - } - - $this->choices = $choices; - } - - /** - * Returns a list of choices - * - * @return array - */ - public function getChoices() - { - if (!$this->loaded) { - $this->load(); - } - - return $this->choices; - } - - /** - * Initializes the list of choices. - * - * @throws UnexpectedTypeException if the function does not return an array - */ - protected function load() - { - $this->loaded = true; - - if ($this->choices instanceof \Closure) { - $this->choices = call_user_func($this->choices); - - if (!is_array($this->choices)) { - throw new UnexpectedTypeException($this->choices, 'array'); - } - } - } -} diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php new file mode 100644 index 0000000000..f77eb205be --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -0,0 +1,649 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\ChoiceList; + +use Symfony\Component\Form\Util\FormUtil; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * Base class for choice list implementations. + * + * @author Bernhard Schussek + */ +abstract class ChoiceList implements ChoiceListInterface +{ + /** + * Strategy creating new indices/values by creating a copy of the choice. + * + * This strategy can only be used for index creation if choices are + * guaranteed to only contain ASCII letters, digits and underscores. + * + * It can be used for value creation if choices can safely be cast into + * a (unique) string. + * + * @var integer + */ + const COPY_CHOICE = 0; + + /** + * Strategy creating new indices/values by generating a new integer. + * + * This strategy can always be applied, but leads to loss of information + * in the HTML source code. + * + * @var integer + */ + const GENERATE = 1; + + /** + * The choices with their indices as keys. + * + * @var array + */ + private $choices = array(); + + /** + * The choice values with the indices of the matching choices as keys. + * + * @var array + */ + private $values = array(); + + /** + * The choice labels with the indices of the matching choices as keys. + * + * @var array + */ + private $labels = array(); + + /** + * The preferred values as flat array with the indices of the matching + * choices as keys. + * + * @var array + */ + private $preferredValues = array(); + + /** + * The non-preferred values as flat array with the indices of the matching + * choices as keys. + * + * @var array + */ + private $remainingValues = array(); + + /** + * The preferred values as hierarchy containing also the choice groups + * with the indices of the matching choices as bottom-level keys. + * + * @var array + */ + private $preferredValueHierarchy = array(); + + /** + * The non-preferred values as hierarchy containing also the choice groups + * with the indices of the matching choices as bottom-level keys. + * + * @var array + */ + private $remainingValueHierarchy = array(); + + /** + * The strategy used for creating choice indices. + * + * @var integer + * @see COPY_CHOICE + * @see GENERATE + */ + private $indexStrategy; + + /** + * The strategy used for creating choice values. + * + * @var integer + * @see COPY_CHOICE + * @see GENERATE + */ + private $valueStrategy; + + /** + * Creates a new choice list. + * + * @param array|\Traversable $choices The array of choices. Choices may also be given + * as hierarchy of unlimited depth. Hierarchies are + * created by creating nested arrays. The title of + * the sub-hierarchy can be stored in the array + * key pointing to the nested array. + * @param array $labels The array of labels. The structure of this array + * should match the structure of $choices. + * @param array $preferredChoices A flat array of choices that should be + * presented to the user with priority. + * @param integer $valueStrategy The strategy used to create choice values. + * One of COPY_CHOICE and GENERATE. + * @param integer $indexStrategy The strategy used to create choice indices. + * One of COPY_CHOICE and GENERATE. + */ + public function __construct($choices, array $labels, + array $preferredChoices = array(), $valueStrategy, $indexStrategy) + { + $this->valueStrategy = $valueStrategy; + $this->indexStrategy = $indexStrategy; + + $this->initialize($choices, $labels, $preferredChoices); + } + + /** + * Initializes the list with choices. + * + * Safe to be called multiple times. The list is cleared on every call. + * + * @param array|\Traversable $choices The choices to write into the list. + * @param array $labels The labels belonging to the choices. + * @param array $preferredChoices The choices to display with priority. + */ + protected function initialize($choices, array $labels, array $preferredChoices) + { + if (!is_array($choices) && !$choices instanceof \Traversable) { + throw new UnexpectedTypeException($choices, 'array or \Traversable'); + } + + $this->choices = array(); + $this->values = array(); + $this->labels = array(); + $this->preferredValues = array(); + $this->preferredValueHierarchy = array(); + $this->remainingValues = array(); + $this->remainingValueHierarchy = array(); + + $this->addChoices( + $this->preferredValueHierarchy, + $this->remainingValueHierarchy, + $choices, + $labels, + $preferredChoices + ); + } + + /** + * Returns the list of choices + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getChoices() + { + return $this->choices; + } + + /** + * Returns the labels for the choices + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getLabels() + { + return $this->labels; + } + + /** + * Returns the values for the choices + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getValues() + { + return $this->values; + } + + /** + * Returns the values of the choices that should be presented to the user + * with priority. + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getPreferredValues() + { + return $this->preferredValues; + } + + /** + * Returns the values of the choices that should be presented to the user + * with priority as nested array with the choice groups as top-level keys. + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getPreferredValueHierarchy() + { + return $this->preferredValueHierarchy; + } + + /** + * Returns the values of the choices that are not preferred. + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getRemainingValues() + { + return $this->remainingValues; + } + + /** + * Returns the values of the choices that are not preferred as nested array + * with the choice groups as top-level keys. + * + * @return array A nested array containing the values with the corresponding + * choice indices as keys on the lower levels and the choice + * group names in the keys of the topmost level. + * + * @see getPreferredValueHierarchy + */ + public function getRemainingValueHierarchy() + { + return $this->remainingValueHierarchy; + } + + /** + * Returns the choices corresponding to the given values. + * + * @param array $values + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getChoicesForValues(array $values) + { + $values = $this->fixValues($values); + + // If the values are identical to the choices, we can just return them + // to improve performance a little bit + if ($this->valueStrategy === self::COPY_CHOICE) { + return $this->fixChoices(array_intersect($values, $this->values)); + } + + $choices = array(); + + foreach ($this->values as $i => $value) { + foreach ($values as $j => $givenValue) { + if ($value === $givenValue) { + $choices[] = $this->choices[$i]; + unset($values[$j]); + + if (count($values) === 0) { + break 2; + } + } + } + } + + return $choices; + } + + /** + * Returns the values corresponding to the given choices. + * + * @param array $choices + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getValuesForChoices(array $choices) + { + $choices = $this->fixChoices($choices); + + // If the values are identical to the choices, we can just return them + // to improve performance a little bit + if ($this->valueStrategy === self::COPY_CHOICE) { + return $this->fixValues(array_intersect($choices, $this->choices)); + } + + $values = array(); + + foreach ($this->choices as $i => $choice) { + foreach ($choices as $j => $givenChoice) { + if ($choice === $givenChoice) { + $values[] = $this->values[$i]; + unset($choices[$j]); + + if (count($choices) === 0) { + break 2; + } + } + } + } + + return $values; + } + + /** + * Returns the indices corresponding to the given choices. + * + * @param array $choices + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getIndicesForChoices(array $choices) + { + $choices = $this->fixChoices($choices); + $indices = array(); + + foreach ($this->choices as $i => $choice) { + foreach ($choices as $j => $givenChoice) { + if ($choice === $this->fixChoice($givenChoice)) { + $indices[] = $i; + unset($choices[$j]); + + if (count($choices) === 0) { + break 2; + } + } + } + } + + return $indices; + } + + /** + * Returns the indices corresponding to the given values. + * + * @param array $values + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getIndicesForValues(array $values) + { + $values = $this->fixValues($values); + $indices = array(); + + foreach ($this->values as $i => $value) { + foreach ($values as $j => $givenValue) { + if ($value === $givenValue) { + $indices[] = $i; + unset($values[$j]); + + if (count($values) === 0) { + break 2; + } + } + } + } + + return $indices; + } + + /** + * Recursively adds the given choices to the list. + * + * @param array $bucketForPreferred The bucket where to store the preferred + * values. + * @param array $bucketForRemaining The bucket where to store the + * non-preferred values. + * @param array $choices The list of choices. + * @param array $labels The labels corresponding to the choices. + * @param array $preferredChoices The preferred choices. + * + * @throws UnexpectedTypeException If the structure of the $labels array + * does not match the structure of the + * $choices array. + */ + protected function addChoices(&$bucketForPreferred, &$bucketForRemaining, $choices, $labels, array $preferredChoices) + { + // Add choices to the nested buckets + foreach ($choices as $group => $choice) { + if (is_array($choice)) { + if (!is_array($labels)) { + throw new UnexpectedTypeException($labels, 'array'); + } + + // Don't do the work if the array is empty + if (count($choice) > 0) { + $this->addChoiceGroup( + $group, + $bucketForPreferred, + $bucketForRemaining, + $choice, + $labels[$group], + $preferredChoices + ); + } + } else { + $this->addChoice( + $bucketForPreferred, + $bucketForRemaining, + $choice, + $labels[$group], + $preferredChoices + ); + } + } + } + + /** + * Recursively adds a choice group. + * + * @param string $group The name of the group. + * @param array $bucketForPreferred The bucket where to store the preferred + * values. + * @param array $bucketForRemaining The bucket where to store the + * non-preferred values. + * @param array $choices The list of choices in the group. + * @param array $labels The labels corresponding to the choices in the group. + * @param array $preferredChoices The preferred choices. + */ + protected function addChoiceGroup($group, &$bucketForPreferred, &$bucketForRemaining, $choices, $labels, array $preferredChoices) + { + // If this is a choice group, create a new level in the choice + // key hierarchy + $bucketForPreferred[$group] = array(); + $bucketForRemaining[$group] = array(); + + $this->addChoices( + $bucketForPreferred[$group], + $bucketForRemaining[$group], + $choices, + $labels, + $preferredChoices + ); + + // Remove child levels if empty + if (empty($bucketForPreferred[$group])) { + unset($bucketForPreferred[$group]); + } + if (empty($bucketForRemaining[$group])) { + unset($bucketForRemaining[$group]); + } + } + + /** + * Adds a new choice. + * + * @param array $bucketForPreferred The bucket where to store the preferred + * values. + * @param array $bucketForRemaining The bucket where to store the + * non-preferred values. + * @param mixed $choice The choice to add. + * @param string $label The label for the choice. + * @param array $preferredChoices The preferred choices. + */ + protected function addChoice(&$bucketForPreferred, &$bucketForRemaining, $choice, $label, array $preferredChoices) + { + $index = $this->createIndex($choice); + + // Always store values as strings to facilitate comparisons + $value = $this->fixValue($this->createValue($choice)); + + $this->choices[$index] = $this->fixChoice($choice); + $this->labels[$index] = $label; + $this->values[$index] = $value; + + if ($this->isPreferred($choice, $preferredChoices)) { + $bucketForPreferred[$index] = $value; + $this->preferredValues[$index] = $value; + } else { + $bucketForRemaining[$index] = $value; + $this->remainingValues[$index] = $value; + } + } + + /** + * Returns whether the given choice should be preferred judging by the + * given array of preferred choices. + * + * Extension point to optimize performance by changing the structure of the + * $preferredChoices array. + * + * @param mixed $choice The choice to test. + * @param array $preferredChoices An array of preferred choices. + */ + protected function isPreferred($choice, $preferredChoices) + { + return false !== array_search($choice, $preferredChoices, true); + } + + /** + * Creates a new unique index for this choice. + * + * Extension point to change the indexing strategy. + * + * @param mixed $choice The choice to create an index for + * + * @return integer|string A unique index containing only ASCII letters, + * digits and underscores. + */ + protected function createIndex($choice) + { + if ($this->indexStrategy === self::COPY_CHOICE) { + return $choice; + } + + return count($this->choices); + } + + /** + * Creates a new unique value for this choice. + * + * Extension point to change the value strategy. + * + * @param mixed $choice The choice to create a value for + * + * @return integer|string A unique value without character limitations. + */ + protected function createValue($choice) + { + if ($this->valueStrategy === self::COPY_CHOICE) { + return $choice; + } + + return count($this->values); + } + + /** + * Fixes the data type of the given choice value to avoid comparison + * problems. + * + * @param mixed $value The choice value. + * + * @return string The value as string. + */ + protected function fixValue($value) + { + return (string) $value; + } + + /** + * Fixes the data types of the given choice values to avoid comparison + * problems. + * + * @param array $values The choice values. + * + * @return array The values as strings. + */ + protected function fixValues(array $values) + { + return array_map(array($this, 'fixValue'), $values); + } + + /** + * Fixes the data type of the given choice index to avoid comparison + * problems. + * + * @param mixed $index The choice index. + * + * @return integer|string The index as PHP array key. + */ + protected function fixIndex($index) + { + if (is_bool($index) || (string) (int) $index === (string) $index) { + return (int) $index; + } + + return (string) $index; + } + + /** + * Fixes the data types of the given choice indices to avoid comparison + * problems. + * + * @param array $indices The choice indices. + * + * @return array The indices as strings. + */ + protected function fixIndices(array $indices) + { + return array_map(array($this, 'fixIndex'), $indices); + } + + /** + * Fixes the data type of the given choice to avoid comparison problems. + * + * Extension point. In this implementation, choices are guaranteed to + * always maintain their type and thus can be typesafely compared. + * + * @param mixed $choice The choice. + * + * @return mixed The fixed choice. + */ + protected function fixChoice($choice) + { + return $choice; + } + + /** + * Fixes the data type of the given choices to avoid comparison problems. + * + * @param array $choice The choices. + * + * @return array The fixed choices. + * + * @see fixChoice + */ + protected function fixChoices(array $choices) + { + return $choices; + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php index f9d0ab877e..85df8d8d88 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php @@ -11,12 +11,126 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; +/** + * Contains choices that can be selected in a form field. + * + * Each choice has four different properties: + * + * - Choice: The choice that should be returned to the application by the + * choice field. Can be any scalar value or an object, but no + * array. + * - Label: A text representing the choice that is displayed to the user. + * - Index: A uniquely identifying index that should only contain ASCII + * characters, digits and underscores. This index is used to + * identify the choice in the HTML "id" and "name" attributes. + * It is also used as index of the arrays returned by the various + * getters of this class. + * - Value: A uniquely identifying value that can contain arbitrary + * characters, but no arrays or objects. This value is displayed + * in the HTML "value" attribute. + * + * @author Bernhard Schussek + */ interface ChoiceListInterface { /** - * Returns a list of choices + * Returns the list of choices * - * @return array + * @return array The choices with their indices as keys. */ function getChoices(); + + /** + * Returns the labels for the choices + * + * @return array The labels with the corresponding choice indices as keys. + */ + function getLabels(); + + /** + * Returns the values for the choices + * + * @return array The values with the corresponding choice indices as keys. + */ + function getValues(); + + /** + * Returns the values of the choices that should be presented to the user + * with priority. + * + * @return array The values with the corresponding choice indices as keys. + */ + function getPreferredValues(); + + /** + * Returns the values of the choices that should be presented to the user + * with priority as nested array with the choice groups as top-level keys. + * + * @return array A nested array containing the values with the corresponding + * choice indices as keys on the lower levels and the choice + * group names in the keys of the topmost level. + */ + function getPreferredValueHierarchy(); + + /** + * Returns the values of the choices that are not preferred. + * + * @return array The values with the corresponding choice indices as keys. + * + * @see getPreferredValues + */ + function getRemainingValues(); + + /** + * Returns the values of the choices that are not preferred as nested array + * with the choice groups as top-level keys. + * + * @return array A nested array containing the values with the corresponding + * choice indices as keys on the lower levels and the choice + * group names in the keys of the topmost level. + * + * @see getPreferredValueHierarchy + */ + function getRemainingValueHierarchy(); + + /** + * Returns the choices corresponding to the given values. + * + * @param array $values An array of choice values. Not existing values in + * this array are ignored. + * + * @return array An array of choices with ascending, 0-based numeric keys + */ + function getChoicesForValues(array $values); + + /** + * Returns the values corresponding to the given choices. + * + * @param array $choices An array of choices. Not existing choices in this + * array are ignored. + * + * @return array An array of choice values with ascending, 0-based numeric + * keys + */ + function getValuesForChoices(array $choices); + + /** + * Returns the indices corresponding to the given choices. + * + * @param array $choices An array of choices. Not existing choices in this + * array are ignored. + * + * @return array An array of indices with ascending, 0-based numeric keys + */ + function getIndicesForChoices(array $choices); + + /** + * Returns the indices corresponding to the given values. + * + * @param array $values An array of choice values. Not existing values in + * this array are ignored. + * + * @return array An array of indices with ascending, 0-based numeric keys + */ + function getIndicesForValues(array $values); } diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ComplexChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ComplexChoiceList.php new file mode 100644 index 0000000000..3528042387 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ComplexChoiceList.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\ChoiceList; + +use Symfony\Component\Form\Util\FormUtil; + +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * A choice list that can store arbitrary scalar and object choices. + * + * Arrays as choices are not supported. + * + * @author Bernhard Schussek + */ +class ComplexChoiceList extends ChoiceList +{ + /** + * Creates a new complex choice list. + * + * @param array $choices The array of choices. Choices may also be given + * as hierarchy of unlimited depth. Hierarchies are + * created by creating nested arrays. The title of + * the sub-hierarchy can be stored in the array + * key pointing to the nested array. + * @param array $labels The array of labels. The structure of this array + * should match the structure of $choices. + * @param array $preferredChoices A flat array of choices that should be + * presented to the user with priority. + */ + public function __construct(array $choices, array $labels, array $preferredChoices = array()) + { + parent::__construct($choices, $labels, $preferredChoices, self::GENERATE, self::GENERATE); + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/MonthChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/MonthChoiceList.php deleted file mode 100644 index fc2f73a588..0000000000 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/MonthChoiceList.php +++ /dev/null @@ -1,58 +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\Extension\Core\ChoiceList; - -class MonthChoiceList extends PaddedChoiceList -{ - private $formatter; - - /** - * Generates an array of localized month choices. - * - * @param IntlDateFormatter $formatter An IntlDateFormatter instance - * @param array $months The month numbers to generate - */ - public function __construct(\IntlDateFormatter $formatter, array $months) - { - parent::__construct(array_combine($months, $months), 2, '0', STR_PAD_LEFT); - $this->formatter = $formatter; - } - - /** - * Initializes the list of months. - * - * @throws UnexpectedTypeException if the function does not return an array - */ - protected function load() - { - parent::load(); - - $pattern = $this->formatter->getPattern(); - $timezone = $this->formatter->getTimezoneId(); - - $this->formatter->setTimezoneId(\DateTimeZone::UTC); - - if (preg_match('/M+/', $pattern, $matches)) { - $this->formatter->setPattern($matches[0]); - - foreach ($this->choices as $choice => $value) { - $this->choices[$choice] = $this->formatter->format(gmmktime(0, 0, 0, $value, 15)); - } - - // I'd like to clone the formatter above, but then we get a - // segmentation fault, so let's restore the old state instead - $this->formatter->setPattern($pattern); - } - - $this->formatter->setTimezoneId($timezone); - } -} diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php new file mode 100644 index 0000000000..08225ba6a0 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php @@ -0,0 +1,201 @@ + +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +namespace Symfony\Component\Form\Extension\Core\ChoiceList; + +use Symfony\Component\Form\Util\PropertyPath; +use Symfony\Component\Form\Exception\StringCastException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Exception\InvalidPropertyException; + +/** + * A choice list that can store object choices. + * + * Supports generation of choice labels, choice groups, choice values and + * choice indices by introspecting the properties of the object (or + * associated objects). + * + * @author Bernhard Schussek + */ +class ObjectChoiceList extends ChoiceList +{ + /** + * The property path used to obtain the choice label. + * + * @var PropertyPath + */ + private $labelPath; + + /** + * The property path used for object grouping. + * + * @var PropertyPath + */ + private $groupPath; + + /** + * The property path used to obtain the choice value. + * + * @var PropertyPath + */ + private $valuePath; + + /** + * The property path used to obtain the choice index. + * + * @var PropertyPath + */ + private $indexPath; + + /** + * Creates a new object choice list. + * + * @param array $choices The array of choices. Choices may also be given + * as hierarchy of unlimited depth. Hierarchies are + * created by creating nested arrays. The title of + * the sub-hierarchy can be stored in the array + * key pointing to the nested array. + * @param string $labelPath A property path pointing to the property used + * for the choice labels. The value is obtained + * by calling the getter on the object. If the + * path is NULL, the object's __toString() method + * is used instead. + * @param array $preferredChoices A flat array of choices that should be + * presented to the user with priority. + * @param string $groupPath A property path pointing to the property used + * to group the choices. Only allowed if + * the choices are given as flat array. + * @param string $valuePath A property path pointing to the property used + * for the choice values. If not given, integers + * are generated instead. + * @param string $indexPath A property path pointing to the property used + * for the choice indices. If not given, integers + * are generated instead. + */ + public function __construct($choices, $labelPath = null, array $preferredChoices = array(), $groupPath = null, $valuePath = null, $indexPath = null) + { + $this->labelPath = $labelPath ? new PropertyPath($labelPath) : null; + $this->groupPath = $groupPath ? new PropertyPath($groupPath) : null; + $this->valuePath = $valuePath ? new PropertyPath($valuePath) : null; + $this->indexPath = $indexPath ? new PropertyPath($indexPath) : null; + + parent::__construct($choices, array(), $preferredChoices, self::GENERATE, self::GENERATE); + } + + /** + * Initializes the list with choices. + * + * Safe to be called multiple times. The list is cleared on every call. + * + * @param array|\Traversable $choices The choices to write into the list. + * @param array $labels Ignored. + * @param array $preferredChoices The choices to display with priority. + */ + protected function initialize($choices, array $labels, array $preferredChoices) + { + if (!is_array($choices) && !$choices instanceof \Traversable) { + throw new UnexpectedTypeException($choices, 'array or \Traversable'); + } + + if ($this->groupPath !== null) { + $groupedChoices = array(); + + foreach ($choices as $i => $choice) { + if (is_array($choice)) { + throw new \InvalidArgumentException('You should pass a plain object array (without groups, $code, $previous) when using the "groupPath" option'); + } + + try { + $group = $this->groupPath->getValue($choice); + } catch (InvalidPropertyException $e) { + // Don't group items whose group property does not exist + // see https://github.com/symfony/symfony/commit/d9b7abb7c7a0f28e0ce970afc5e305dce5dccddf + $group = null; + } + + if ($group === null) { + $groupedChoices[$i] = $choice; + } else { + if (!isset($groupedChoices[$group])) { + $groupedChoices[$group] = array(); + } + + $groupedChoices[$group][$i] = $choice; + } + } + + $choices = $groupedChoices; + } + + $labels = array(); + + $this->extractLabels($choices, $labels); + + parent::initialize($choices, $labels, $preferredChoices); + } + + /** + * Creates a new unique index for this choice. + * + * If a property path for the index was given at object creation, + * the getter behind that path is now called to obtain a new value. + * + * Otherwise a new integer is generated. + * + * @param mixed $choice The choice to create an index for + * @return integer|string A unique index containing only ASCII letters, + * digits and underscores. + */ + protected function createIndex($choice) + { + if ($this->indexPath) { + return $this->indexPath->getValue($choice); + } + + return parent::createIndex($choice); + } + + /** + * Creates a new unique value for this choice. + * + * If a property path for the value was given at object creation, + * the getter behind that path is now called to obtain a new value. + * + * Otherwise a new integer is generated. + * + * @param mixed $choice The choice to create a value for + * @return integer|string A unique value without character limitations. + */ + protected function createValue($choice) + { + if ($this->valuePath) { + return $this->valuePath->getValue($choice); + } + + return parent::createValue($choice); + } + + private function extractLabels($choices, array &$labels) + { + foreach ($choices as $i => $choice) { + if (is_array($choice) || $choice instanceof \Traversable) { + $labels[$i] = array(); + $this->extractLabels($choice, $labels[$i]); + } elseif ($this->labelPath) { + $labels[$i] = $this->labelPath->getValue($choice); + } elseif (method_exists($choice, '__toString')) { + $labels[$i] = (string) $choice; + } else { + throw new StringCastException('Objects passed to the choice field must have a "__toString()" method defined. Alternatively you can set the $labelPath argument to choose the property used as label.'); + } + } + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/PaddedChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/PaddedChoiceList.php deleted file mode 100644 index fd1f9d1b33..0000000000 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/PaddedChoiceList.php +++ /dev/null @@ -1,59 +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\Extension\Core\ChoiceList; - -class PaddedChoiceList extends ArrayChoiceList -{ - private $padLength; - - private $padString; - - private $padType; - - /** - * Generates an array of choices for the given values - * - * If the values are shorter than $padLength characters, they are padded with - * zeros on the left side. - * - * @param array|\Closure $values The available choices - * @param integer $padLength The length to pad the choices - * @param string $padString The padding character - * @param integer $padType The direction of padding - * - * @throws UnexpectedTypeException if the type of the values parameter is not supported - */ - public function __construct($values, $padLength, $padString, $padType = STR_PAD_LEFT) - { - parent::__construct($values); - - $this->padLength = $padLength; - $this->padString = $padString; - $this->padType = $padType; - } - - /** - * Initializes the list of choices. - * - * Each choices is padded according to the format given in the constructor - * - * @throws UnexpectedTypeException if the function does not return an array - */ - protected function load() - { - parent::load(); - - foreach ($this->choices as $key => $choice) { - $this->choices[$key] = str_pad($choice, $this->padLength, $this->padString, $this->padType); - } - } -} diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php new file mode 100644 index 0000000000..16845d648d --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\ChoiceList; + +use Symfony\Component\Form\Util\FormUtil; + +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * A choice list that can store any choices that are allowed as PHP array keys. + * + * The value strategy of simple choice lists is fixed to ChoiceList::COPY_CHOICE, + * since array keys are always valid choice values. + * + * @author Bernhard Schussek + */ +class SimpleChoiceList extends ChoiceList +{ + /** + * Creates a new simple choice list. + * + * @param array $choices The array of choices with the choices as keys and + * the labels as values. Choices may also be given + * as hierarchy of unlimited depth. Hierarchies are + * created by creating nested arrays. The title of + * the sub-hierarchy is stored in the array + * key pointing to the nested array. + * @param array $preferredChoices A flat array of choices that should be + * presented to the user with priority. + * @param integer $indexStrategy The strategy used to create choice indices. + * One of COPY_CHOICE and GENERATE. + */ + public function __construct(array $choices, array $preferredChoices = array(), + $valueStrategy = self::COPY_CHOICE, $indexStrategy = self::GENERATE) + { + // Flip preferred choices to speed up lookup + parent::__construct($choices, $choices, array_flip($preferredChoices), $valueStrategy, $indexStrategy); + } + + /** + * Recursively adds the given choices to the list. + * + * Takes care of splitting the single $choices array passed in the + * constructor into choices and labels. + * + * @param array $bucketForPreferred + * @param array $bucketForRemaining + * @param array $choices + * @param array $labels + * @param array $preferredChoices + * + * @throws UnexpectedTypeException + * + * @see parent::addChoices + */ + protected function addChoices(&$bucketForPreferred, &$bucketForRemaining, $choices, $labels, array $preferredChoices) + { + // Add choices to the nested buckets + foreach ($choices as $choice => $label) { + if (is_array($label)) { + // Don't do the work if the array is empty + if (count($label) > 0) { + $this->addChoiceGroup( + $choice, + $bucketForPreferred, + $bucketForRemaining, + $label, + $label, + $preferredChoices + ); + } + } else { + $this->addChoice( + $bucketForPreferred, + $bucketForRemaining, + $choice, + $label, + $preferredChoices + ); + } + } + } + + /** + * Returns whether the given choice should be preferred judging by the + * given array of preferred choices. + * + * Optimized for performance by treating the preferred choices as array + * where choices are stored in the keys. + * + * @param mixed $choice The choice to test. + * @param array $preferredChoices An array of preferred choices. + */ + protected function isPreferred($choice, $preferredChoices) + { + // Optimize performance over the default implementation + return isset($preferredChoices[$choice]); + } + + /** + * Converts the choice to a valid PHP array key. + * + * @param mixed $choice The choice. + * + * @return string|integer A valid PHP array key. + */ + protected function fixChoice($choice) + { + return $this->fixIndex($choice); + } + + + /** + * Converts the choices to a valid PHP array keys. + * + * @param array $choices The choices. + * + * @return array Valid PHP array keys. + */ + protected function fixChoices(array $choices) + { + return $this->fixIndices($choices); + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/TimezoneChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/TimezoneChoiceList.php deleted file mode 100644 index 57e7bb5863..0000000000 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/TimezoneChoiceList.php +++ /dev/null @@ -1,63 +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\Extension\Core\ChoiceList; - -/** - * Represents a choice list where each timezone is broken down by continent. - * - * @author Bernhard Schussek - */ -class TimezoneChoiceList implements ChoiceListInterface -{ - /** - * Stores the available timezone choices - * @var array - */ - static protected $timezones; - - /** - * Returns the timezone choices. - * - * The choices are generated from the ICU function - * \DateTimeZone::listIdentifiers(). They are cached during a single request, - * so multiple timezone fields on the same page don't lead to unnecessary - * overhead. - * - * @return array The timezone choices - */ - public function getChoices() - { - if (null !== static::$timezones) { - return static::$timezones; - } - - static::$timezones = array(); - foreach (\DateTimeZone::listIdentifiers() as $timezone) { - $parts = explode('/', $timezone); - - if (count($parts) > 2) { - $region = $parts[0]; - $name = $parts[1].' - '.$parts[2]; - } elseif (count($parts) > 1) { - $region = $parts[0]; - $name = $parts[1]; - } else { - $region = 'Other'; - $name = $parts[0]; - } - - static::$timezones[$region][$timezone] = str_replace('_', ' ', $name); - } - - return static::$timezones; - } -} diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ScalarToBooleanChoicesTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php similarity index 69% rename from src/Symfony/Component/Form/Extension/Core/DataTransformer/ScalarToBooleanChoicesTransformer.php rename to src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php index 439acfb0a6..4b92f0d788 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ScalarToBooleanChoicesTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php @@ -17,7 +17,10 @@ use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Util\FormUtil; -class ScalarToBooleanChoicesTransformer implements DataTransformerInterface +/** + * @author Bernhard Schussek + */ +class ChoiceToBooleanArrayTransformer implements DataTransformerInterface { private $choiceList; @@ -47,24 +50,21 @@ class ScalarToBooleanChoicesTransformer implements DataTransformerInterface * @throws UnexpectedTypeException if the given value is not scalar * @throws TransformationFailedException if the choices can not be retrieved */ - public function transform($value) + public function transform($choice) { - if (!is_scalar($value) && null !== $value) { - throw new UnexpectedTypeException($value, 'scalar'); - } - try { - $choices = $this->choiceList->getChoices(); + $values = $this->choiceList->getValues(); } catch (\Exception $e) { throw new TransformationFailedException('Can not get the choice list', $e->getCode(), $e); } - $value = FormUtil::toArrayKey($value); - foreach (array_keys($choices) as $key) { - $choices[$key] = $key === $value; + $index = current($this->choiceList->getIndicesForChoices(array($choice))); + + foreach ($values as $i => $value) { + $values[$i] = $i === $index; } - return $choices; + return $values; } /** @@ -80,15 +80,25 @@ class ScalarToBooleanChoicesTransformer implements DataTransformerInterface * * @throws new UnexpectedTypeException if the given value is not an array */ - public function reverseTransform($value) + public function reverseTransform($values) { - if (!is_array($value)) { - throw new UnexpectedTypeException($value, 'array'); + if (!is_array($values)) { + throw new UnexpectedTypeException($values, 'array'); } - foreach ($value as $choice => $selected) { + try { + $choices = $this->choiceList->getChoices(); + } catch (\Exception $e) { + throw new TransformationFailedException('Can not get the choice list', $e->getCode(), $e); + } + + foreach ($values as $i => $selected) { if ($selected) { - return (string) $choice; + if (isset($choices[$i])) { + return $choices[$i] === '' ? null : $choices[$i]; + } else { + throw new TransformationFailedException('The choice "' . $i . '" does not exist'); + } } } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php new file mode 100644 index 0000000000..e63e0f1941 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; + +/** + * @author Bernhard Schussek + */ +class ChoiceToValueTransformer implements DataTransformerInterface +{ + private $choiceList; + + /** + * Constructor. + * + * @param ChoiceListInterface $choiceList + */ + public function __construct(ChoiceListInterface $choiceList) + { + $this->choiceList = $choiceList; + } + + public function transform($choice) + { + return (string) current($this->choiceList->getValuesForChoices(array($choice))); + } + + public function reverseTransform($value) + { + if (null !== $value && !is_scalar($value)) { + throw new UnexpectedTypeException($value, 'scalar'); + } + + // These are now valid ChoiceList values, so we can return null + // right away + if ($value === '' || $value === null) { + return null; + } + + $choices = $this->choiceList->getChoicesForValues(array($value)); + + if (count($choices) !== 1) { + throw new TransformationFailedException('The choice "' . $value . '" does not exist'); + } + + $choice = current($choices); + + return $choice === '' ? null : $choice; + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ArrayToBooleanChoicesTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php similarity index 66% rename from src/Symfony/Component/Form/Extension/Core/DataTransformer/ArrayToBooleanChoicesTransformer.php rename to src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php index 11b00d94a6..eafc6c3f82 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ArrayToBooleanChoicesTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php @@ -16,7 +16,10 @@ use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Exception\UnexpectedTypeException; -class ArrayToBooleanChoicesTransformer implements DataTransformerInterface +/** + * @author Bernhard Schussek + */ +class ChoicesToBooleanArrayTransformer implements DataTransformerInterface { private $choiceList; @@ -51,16 +54,18 @@ class ArrayToBooleanChoicesTransformer implements DataTransformerInterface } try { - $choices = $this->choiceList->getChoices(); + $values = $this->choiceList->getValues(); } catch (\Exception $e) { throw new TransformationFailedException('Can not get the choice list', $e->getCode(), $e); } - foreach (array_keys($choices) as $key) { - $choices[$key] = in_array($key, $array, true); + $indexMap = array_flip($this->choiceList->getIndicesForChoices($array)); + + foreach ($values as $i => $value) { + $values[$i] = isset($indexMap[$i]); } - return $choices; + return $values; } /** @@ -76,20 +81,35 @@ class ArrayToBooleanChoicesTransformer implements DataTransformerInterface * * @throws UnexpectedTypeException if the given value is not an array */ - public function reverseTransform($value) + public function reverseTransform($values) { - if (!is_array($value)) { - throw new UnexpectedTypeException($value, 'array'); + if (!is_array($values)) { + throw new UnexpectedTypeException($values, 'array'); } - $choices = array(); + try { + $choices = $this->choiceList->getChoices(); + } catch (\Exception $e) { + throw new TransformationFailedException('Can not get the choice list', $e->getCode(), $e); + } - foreach ($value as $choice => $selected) { + $result = array(); + $unknown = array(); + + foreach ($values as $i => $selected) { if ($selected) { - $choices[] = $choice; + if (isset($choices[$i])) { + $result[] = $choices[$i]; + } else { + $unknown[] = $i; + } } } - return $choices; + if (count($unknown) > 0) { + throw new TransformationFailedException('The choices "' . implode('", "', $unknown, $code, $previous) . '" where not found'); + } + + return $result; } } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ArrayToChoicesTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php similarity index 59% rename from src/Symfony/Component/Form/Extension/Core/DataTransformer/ArrayToChoicesTransformer.php rename to src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php index 49e8f088df..e722c47065 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ArrayToChoicesTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php @@ -11,12 +11,30 @@ namespace Symfony\Component\Form\Extension\Core\DataTransformer; +use Symfony\Component\Form\Exception\TransformationFailedException; + use Symfony\Component\Form\Util\FormUtil; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; -class ArrayToChoicesTransformer implements DataTransformerInterface +/** + * @author Bernhard Schussek + */ +class ChoicesToValuesTransformer implements DataTransformerInterface { + private $choiceList; + + /** + * Constructor. + * + * @param ChoiceListInterface $choiceList + */ + public function __construct(ChoiceListInterface $choiceList) + { + $this->choiceList = $choiceList; + } + /** * @param array $array * @@ -34,7 +52,7 @@ class ArrayToChoicesTransformer implements DataTransformerInterface throw new UnexpectedTypeException($array, 'array'); } - return FormUtil::toArrayKeys($array); + return $this->choiceList->getValuesForChoices($array); } /** @@ -54,6 +72,12 @@ class ArrayToChoicesTransformer implements DataTransformerInterface throw new UnexpectedTypeException($array, 'array'); } - return $array; + $choices = $this->choiceList->getChoicesForValues($array); + + if (count($choices) !== count($array)) { + throw new TransformationFailedException('Could not find all matching choices for the given values'); + } + + return $choices; } } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ScalarToChoiceTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ScalarToChoiceTransformer.php deleted file mode 100644 index c6c6953586..0000000000 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ScalarToChoiceTransformer.php +++ /dev/null @@ -1,37 +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\Extension\Core\DataTransformer; - -use Symfony\Component\Form\Util\FormUtil; -use Symfony\Component\Form\DataTransformerInterface; -use Symfony\Component\Form\Exception\UnexpectedTypeException; - -class ScalarToChoiceTransformer implements DataTransformerInterface -{ - public function transform($value) - { - if (null !== $value && !is_scalar($value)) { - throw new UnexpectedTypeException($value, 'scalar'); - } - - return FormUtil::toArrayKey($value); - } - - public function reverseTransform($value) - { - if (null !== $value && !is_scalar($value)) { - throw new UnexpectedTypeException($value, 'scalar'); - } - - return $value; - } -} diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php index ddf474781c..e094a77a38 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Form\Extension\Core\EventListener; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\Event\FilterDataEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; /** * Takes care of converting the input from a single radio button @@ -23,11 +24,24 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; */ class FixRadioInputListener implements EventSubscriberInterface { + private $choiceList; + + /** + * Constructor. + * + * @param ChoiceListInterface $choiceList + */ + public function __construct(ChoiceListInterface $choiceList) + { + $this->choiceList = $choiceList; + } + public function onBindClientData(FilterDataEvent $event) { - $data = $event->getData(); + $value = $event->getData(); + $index = current($this->choiceList->getIndicesForValues(array($value))); - $event->setData(strlen($data) < 1 ? array() : array($data => true)); + $event->setData($index !== false ? array($index => $value) : array()); } static public function getSubscribedEvents() diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index bcc119fee8..378a795e6b 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -15,14 +15,17 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\Exception\FormException; -use Symfony\Component\Form\Extension\Core\ChoiceList\ArrayChoiceList; +use Symfony\Component\Form\Extension\Core\ChoiceList\Loader\ChoiceListLoaderInterface; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; +use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; +use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList; use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener; use Symfony\Component\Form\FormView; -use Symfony\Component\Form\Extension\Core\DataTransformer\ScalarToChoiceTransformer; -use Symfony\Component\Form\Extension\Core\DataTransformer\ScalarToBooleanChoicesTransformer; -use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToChoicesTransformer; -use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToBooleanChoicesTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToBooleanArrayTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToBooleanArrayTransformer; class ChoiceType extends AbstractType { @@ -35,40 +38,38 @@ class ChoiceType extends AbstractType throw new FormException('The "choice_list" must be an instance of "Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface".'); } + if (!$options['choice_list'] && !$options['choices']) { + throw new FormException('Either the option "choices" or "choice_list" must be set.'); + } + if (!$options['choice_list']) { - $options['choice_list'] = new ArrayChoiceList($options['choices']); + $options['choice_list'] = new SimpleChoiceList( + $options['choices'], + $options['preferred_choices'], + $options['value_strategy'], + $options['index_strategy'] + ); } if ($options['expanded']) { // Load choices already if expanded - $choices = $options['choice_list']->getChoices(); + $values = $options['choice_list']->getValues(); + $labels = $options['choice_list']->getLabels(); - // Flatten choices - $flattened = array(); - foreach ($choices as $value => $choice) { - if (is_array($choice)) { - $flattened = array_replace($flattened, $choice); - } else { - $flattened[$value] = $choice; - } - } - - $options['choices'] = $flattened; - - foreach ($options['choices'] as $choice => $value) { + foreach ($values as $i => $value) { if ($options['multiple']) { - $builder->add((string) $choice, 'checkbox', array( - 'value' => $choice, - 'label' => $value, + $builder->add((string) $i, 'checkbox', array( + 'value' => $value, + 'label' => $labels[$i], // The user can check 0 or more checkboxes. If required // is true, he is required to check all of them. 'required' => false, 'translation_domain' => $options['translation_domain'], )); } else { - $builder->add((string) $choice, 'radio', array( - 'value' => $choice, - 'label' => $value, + $builder->add((string) $i, 'radio', array( + 'value' => $value, + 'label' => $labels[$i], 'translation_domain' => $options['translation_domain'], )); } @@ -101,18 +102,18 @@ class ChoiceType extends AbstractType if ($options['expanded']) { if ($options['multiple']) { - $builder->appendClientTransformer(new ArrayToBooleanChoicesTransformer($options['choice_list'])); + $builder->appendClientTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list'])); } else { $builder - ->appendClientTransformer(new ScalarToBooleanChoicesTransformer($options['choice_list'])) - ->addEventSubscriber(new FixRadioInputListener(), 10) + ->appendClientTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list'])) + ->addEventSubscriber(new FixRadioInputListener($options['choice_list']), 10) ; } } else { if ($options['multiple']) { - $builder->appendClientTransformer(new ArrayToChoicesTransformer()); + $builder->appendClientTransformer(new ChoicesToValuesTransformer($options['choice_list'])); } else { - $builder->appendClientTransformer(new ScalarToChoiceTransformer()); + $builder->appendClientTransformer(new ChoiceToValueTransformer($options['choice_list'])); } } @@ -123,14 +124,14 @@ class ChoiceType extends AbstractType */ public function buildView(FormView $view, FormInterface $form) { - $choices = $form->getAttribute('choice_list')->getChoices(); - $preferred = array_flip($form->getAttribute('preferred_choices')); + $choiceList = $form->getAttribute('choice_list'); $view ->set('multiple', $form->getAttribute('multiple')) ->set('expanded', $form->getAttribute('expanded')) - ->set('preferred_choices', array_intersect_key($choices, $preferred)) - ->set('choices', array_diff_key($choices, $preferred)) + ->set('preferred_choices', $choiceList->getPreferredValueHierarchy()) + ->set('choices', $choiceList->getRemainingValueHierarchy()) + ->set('choice_labels', $choiceList->getLabels()) ->set('separator', '-------------------') ->set('empty_value', $form->getAttribute('empty_value')) ; @@ -157,6 +158,8 @@ class ChoiceType extends AbstractType 'choice_list' => null, 'choices' => array(), 'preferred_choices' => array(), + 'value_strategy' => ChoiceList::GENERATE, + 'index_strategy' => ChoiceList::GENERATE, 'empty_data' => $multiple || $expanded ? array() : '', 'empty_value' => $multiple || $expanded || !isset($options['empty_value']) ? null : '', 'error_bubbling' => false, diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php b/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php index cea4ab2914..f800678d87 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; use Symfony\Component\Locale\Locale; class CountryType extends AbstractType @@ -23,6 +24,8 @@ class CountryType extends AbstractType { return array( 'choices' => Locale::getDisplayCountries(\Locale::getDefault()), + 'value_strategy' => ChoiceList::COPY_CHOICE, + 'index_strategy' => ChoiceList::COPY_CHOICE, ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php index ba72257768..9e878d6535 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php @@ -11,12 +11,13 @@ namespace Symfony\Component\Form\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; + use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\Exception\CreationException; -use Symfony\Component\Form\Extension\Core\ChoiceList\PaddedChoiceList; -use Symfony\Component\Form\Extension\Core\ChoiceList\MonthChoiceList; +use Symfony\Component\Form\Extension\Core\ChoiceList\Loader\MonthChoiceListLoader; use Symfony\Component\Form\FormView; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer; @@ -74,23 +75,35 @@ class DateType extends AbstractType $options['empty_value'] = array('year' => $options['empty_value'], 'month' => $options['empty_value'], 'day' => $options['empty_value']); } + $years = $months = $days = array(); + + foreach ($options['years'] as $year) { + $years[$year] = str_pad($year, 4, '0', STR_PAD_LEFT); + } + foreach ($options['months'] as $month) { + $months[$month] = str_pad($month, 2, '0', STR_PAD_LEFT); + } + foreach ($options['days'] as $day) { + $days[$day] = str_pad($day, 2, '0', STR_PAD_LEFT); + } + // Only pass a subset of the options to children $yearOptions = array( - 'choice_list' => new PaddedChoiceList( - array_combine($options['years'], $options['years']), 4, '0', STR_PAD_LEFT - ), + 'choices' => $years, + 'value_strategy' => ChoiceList::COPY_CHOICE, + 'index_strategy' => ChoiceList::COPY_CHOICE, 'empty_value' => $options['empty_value']['year'], ); $monthOptions = array( - 'choice_list' => new MonthChoiceList( - $formatter, $options['months'] - ), + 'choices' => $this->formatMonths($formatter, $months), + 'value_strategy' => ChoiceList::COPY_CHOICE, + 'index_strategy' => ChoiceList::COPY_CHOICE, 'empty_value' => $options['empty_value']['month'], ); $dayOptions = array( - 'choice_list' => new PaddedChoiceList( - array_combine($options['days'], $options['days']), 2, '0', STR_PAD_LEFT - ), + 'choices' => $days, + 'value_strategy' => ChoiceList::COPY_CHOICE, + 'index_strategy' => ChoiceList::COPY_CHOICE, 'empty_value' => $options['empty_value']['day'], ); @@ -209,4 +222,28 @@ class DateType extends AbstractType { return 'date'; } + + private function formatMonths(\IntlDateFormatter $formatter, array $months) + { + $pattern = $formatter->getPattern(); + $timezone = $formatter->getTimezoneId(); + + $formatter->setTimezoneId(\DateTimeZone::UTC); + + if (preg_match('/M+/', $pattern, $matches)) { + $formatter->setPattern($matches[0]); + + foreach ($months as $key => $value) { + $months[$key] = $formatter->format(gmmktime(0, 0, 0, $key, 15)); + } + + // I'd like to clone the formatter above, but then we get a + // segmentation fault, so let's restore the old state instead + $formatter->setPattern($pattern); + } + + $formatter->setTimezoneId($timezone); + + return $months; + } } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php b/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php index 46cacfd1bc..da2e315c13 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; use Symfony\Component\Locale\Locale; class LanguageType extends AbstractType @@ -23,6 +24,7 @@ class LanguageType extends AbstractType { return array( 'choices' => Locale::getDisplayLanguages(\Locale::getDefault()), + 'value_strategy' => ChoiceList::COPY_CHOICE, ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php b/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php index 4f3fa08760..c4841e3184 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; use Symfony\Component\Locale\Locale; class LocaleType extends AbstractType @@ -23,6 +24,7 @@ class LocaleType extends AbstractType { return array( 'choices' => Locale::getDisplayLocales(\Locale::getDefault()), + 'value_strategy' => ChoiceList::COPY_CHOICE, ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/RadioType.php b/src/Symfony/Component/Form/Extension/Core/Type/RadioType.php index 97a5d03948..233db68f51 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/RadioType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/RadioType.php @@ -19,48 +19,22 @@ use Symfony\Component\Form\FormView; class RadioType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilder $builder, array $options) - { - $builder - ->appendClientTransformer(new BooleanToStringTransformer()) - ->setAttribute('value', $options['value']) - ; - } - /** * {@inheritdoc} */ public function buildView(FormView $view, FormInterface $form) { - $view - ->set('value', $form->getAttribute('value')) - ->set('checked', (Boolean) $form->getClientData()) - ; - if ($view->hasParent()) { $view->set('full_name', $view->getParent()->get('full_name')); } } - /** - * {@inheritdoc} - */ - public function getDefaultOptions(array $options) - { - return array( - 'value' => null, - ); - } - /** * {@inheritdoc} */ public function getParent(array $options) { - return 'field'; + return 'checkbox'; } /** diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php index 71c19b408a..1eeaa40ec5 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php @@ -14,8 +14,8 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormBuilder; -use Symfony\Component\Form\Extension\Core\ChoiceList\PaddedChoiceList; use Symfony\Component\Form\ReversedTransformer; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer; @@ -47,25 +47,40 @@ class TimeType extends AbstractType $options['empty_value'] = array('hour' => $options['empty_value'], 'minute' => $options['empty_value'], 'second' => $options['empty_value']); } + $hours = $minutes = array(); + + foreach ($options['hours'] as $hour) { + $hours[$hour] = str_pad($hour, 2, '0', STR_PAD_LEFT); + } + foreach ($options['minutes'] as $minute) { + $minutes[$minute] = str_pad($minute, 2, '0', STR_PAD_LEFT); + } + // Only pass a subset of the options to children $hourOptions = array( - 'choice_list' => new PaddedChoiceList( - array_combine($options['hours'], $options['hours']), 2, '0', STR_PAD_LEFT - ), + 'choices' => $hours, + 'value_strategy' => ChoiceList::COPY_CHOICE, + 'index_strategy' => ChoiceList::COPY_CHOICE, 'empty_value' => $options['empty_value']['hour'], ); $minuteOptions = array( - 'choice_list' => new PaddedChoiceList( - array_combine($options['minutes'], $options['minutes']), 2, '0', STR_PAD_LEFT - ), + 'choices' => $minutes, + 'value_strategy' => ChoiceList::COPY_CHOICE, + 'index_strategy' => ChoiceList::COPY_CHOICE, 'empty_value' => $options['empty_value']['minute'], ); if ($options['with_seconds']) { + $seconds = array(); + + foreach ($options['seconds'] as $second) { + $seconds[$second] = str_pad($second, 2, '0', STR_PAD_LEFT); + } + $secondOptions = array( - 'choice_list' => new PaddedChoiceList( - array_combine($options['seconds'], $options['seconds']), 2, '0', STR_PAD_LEFT - ), + 'choices' => $seconds, + 'value_strategy' => ChoiceList::COPY_CHOICE, + 'index_strategy' => ChoiceList::COPY_CHOICE, 'empty_value' => $options['empty_value']['second'], ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php index 2b22523af7..ef308faeeb 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php @@ -12,18 +12,30 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\ChoiceList\TimezoneChoiceList; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; class TimezoneType extends AbstractType { + /** + * Stores the available timezone choices + * @var array + */ + static protected $timezones; + /** * {@inheritdoc} */ public function getDefaultOptions(array $options) { - return array( - 'choice_list' => new TimezoneChoiceList(), + $defaultOptions = array( + 'value_strategy' => ChoiceList::COPY_CHOICE, ); + + if (!isset($options['choice_list']) && !isset($options['choices'])) { + $defaultOptions['choices'] = self::getTimezones(); + } + + return $defaultOptions; } /** @@ -41,4 +53,40 @@ class TimezoneType extends AbstractType { return 'timezone'; } + + /** + * Returns the timezone choices. + * + * The choices are generated from the ICU function + * \DateTimeZone::listIdentifiers(). They are cached during a single request, + * so multiple timezone fields on the same page don't lead to unnecessary + * overhead. + * + * @return array The timezone choices + */ + static private function getTimezones() + { + if (null === static::$timezones) { + static::$timezones = array(); + + foreach (\DateTimeZone::listIdentifiers() as $timezone) { + $parts = explode('/', $timezone); + + if (count($parts) > 2) { + $region = $parts[0]; + $name = $parts[1].' - '.$parts[2]; + } elseif (count($parts) > 1) { + $region = $parts[0]; + $name = $parts[1]; + } else { + $region = 'Other'; + $name = $parts[0]; + } + + static::$timezones[$region][$timezone] = str_replace('_', ' ', $name); + } + } + + return static::$timezones; + } } diff --git a/src/Symfony/Component/Form/Util/FormUtil.php b/src/Symfony/Component/Form/Util/FormUtil.php index bc521efff2..4ddacd25c5 100644 --- a/src/Symfony/Component/Form/Util/FormUtil.php +++ b/src/Symfony/Component/Form/Util/FormUtil.php @@ -13,20 +13,6 @@ namespace Symfony\Component\Form\Util; abstract class FormUtil { - static public function toArrayKey($value) - { - if (is_bool($value) || (string) (int) $value === (string) $value) { - return (int) $value; - } - - return (string) $value; - } - - static public function toArrayKeys(array $array) - { - return array_map(array(__CLASS__, 'toArrayKey'), $array); - } - /** * Returns whether the given choice is a group. * @@ -49,10 +35,6 @@ abstract class FormUtil */ static public function isChoiceSelected($choice, $value) { - $choice = static::toArrayKey($choice); - - // The value should already have been converted by value transformers, - // otherwise we had to do the conversion on every call of this method if (is_array($value)) { return false !== array_search($choice, $value, true); } diff --git a/tests/Symfony/Tests/Bridge/Doctrine/Form/ChoiceList/EntityChoiceListTest.php b/tests/Symfony/Tests/Bridge/Doctrine/Form/ChoiceList/EntityChoiceListTest.php index 74feb80d4b..66e2121b7f 100644 --- a/tests/Symfony/Tests/Bridge/Doctrine/Form/ChoiceList/EntityChoiceListTest.php +++ b/tests/Symfony/Tests/Bridge/Doctrine/Form/ChoiceList/EntityChoiceListTest.php @@ -89,7 +89,7 @@ class EntityChoiceListTest extends DoctrineOrmTestCase ) ); - $this->assertSame(array(1 => 'Foo', 2 => 'Bar'), $choiceList->getChoices()); + $this->assertSame(array(1 => $entity1, 2 => $entity2), $choiceList->getChoices()); } public function testEmptyChoicesAreManaged() @@ -132,10 +132,11 @@ class EntityChoiceListTest extends DoctrineOrmTestCase ) ); + $this->assertSame(array(1 => $entity1, 2 => $entity2), $choiceList->getChoices()); $this->assertSame(array( - 'group1' => array(1 => 'Foo'), - 'group2' => array(2 => 'Bar') - ), $choiceList->getChoices()); + 'group1' => array(1 => '1'), + 'group2' => array(2 => '2') + ), $choiceList->getRemainingValueHierarchy()); } public function testGroupBySupportsString() @@ -164,11 +165,12 @@ class EntityChoiceListTest extends DoctrineOrmTestCase 'groupName' ); + $this->assertEquals(array(1 => $item1, 2 => $item2, 3 => $item3, 4 => $item4), $choiceList->getChoices()); $this->assertEquals(array( - 'Group1' => array(1 => 'Foo', '2' => 'Bar'), - 'Group2' => array(3 => 'Baz'), - '4' => 'Boo!' - ), $choiceList->getChoices('choices')); + 'Group1' => array(1 => '1', 2 => '2'), + 'Group2' => array(3 => '3'), + 4 => '4' + ), $choiceList->getRemainingValueHierarchy()); } public function testGroupByInvalidPropertyPathReturnsFlatChoices() @@ -188,13 +190,13 @@ class EntityChoiceListTest extends DoctrineOrmTestCase $item1, $item2, ), - 'groupName.child.that.does.not.exist' + 'child.that.does.not.exist' ); $this->assertEquals(array( - 1 => 'Foo', - 2 => 'Bar' - ), $choiceList->getChoices('choices')); + 1 => $item1, + 2 => $item2 + ), $choiceList->getChoices()); } public function testPossibleToProvideShorthandEntityName() diff --git a/tests/Symfony/Tests/Bridge/Doctrine/Form/Type/EntityTypeTest.php b/tests/Symfony/Tests/Bridge/Doctrine/Form/Type/EntityTypeTest.php index 5f77dd5a37..5ff457f879 100644 --- a/tests/Symfony/Tests/Bridge/Doctrine/Form/Type/EntityTypeTest.php +++ b/tests/Symfony/Tests/Bridge/Doctrine/Form/Type/EntityTypeTest.php @@ -109,7 +109,8 @@ class EntityTypeTest extends TypeTestCase 'property' => 'name' )); - $this->assertEquals(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choices')); + $this->assertSame(array(1 => '1', 2 => '2'), $field->createView()->get('choices')); + $this->assertSame(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choice_labels')); } public function testSetDataToUninitializedEntityWithNonRequiredToString() @@ -125,7 +126,8 @@ class EntityTypeTest extends TypeTestCase 'required' => false, )); - $this->assertEquals(array("1" => 'Foo', "2" => 'Bar'), $field->createView()->get('choices')); + $this->assertSame(array(1 => '1', 2 => '2'), $field->createView()->get('choices')); + $this->assertSame(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choice_labels')); } public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder() @@ -144,7 +146,8 @@ class EntityTypeTest extends TypeTestCase 'query_builder' => $qb )); - $this->assertEquals(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choices')); + $this->assertSame(array(1 => '1', 2 => '2'), $field->createView()->get('choices')); + $this->assertSame(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choice_labels')); } /** @@ -185,7 +188,7 @@ class EntityTypeTest extends TypeTestCase $field->setData(null); $this->assertNull($field->getData()); - $this->assertEquals('', $field->getClientData()); + $this->assertSame('', $field->getClientData()); } public function testSetDataMultipleExpandedNull() @@ -199,7 +202,7 @@ class EntityTypeTest extends TypeTestCase $field->setData(null); $this->assertNull($field->getData()); - $this->assertEquals(array(), $field->getClientData()); + $this->assertSame(array(), $field->getClientData()); } public function testSetDataMultipleNonExpandedNull() @@ -213,7 +216,7 @@ class EntityTypeTest extends TypeTestCase $field->setData(null); $this->assertNull($field->getData()); - $this->assertEquals(array(), $field->getClientData()); + $this->assertSame(array(), $field->getClientData()); } public function testSubmitSingleExpandedNull() @@ -227,7 +230,7 @@ class EntityTypeTest extends TypeTestCase $field->bind(null); $this->assertNull($field->getData()); - $this->assertEquals(array(), $field->getClientData()); + $this->assertSame(array(), $field->getClientData()); } public function testSubmitSingleNonExpandedNull() @@ -241,7 +244,7 @@ class EntityTypeTest extends TypeTestCase $field->bind(null); $this->assertNull($field->getData()); - $this->assertEquals('', $field->getClientData()); + $this->assertSame('', $field->getClientData()); } public function testSubmitMultipleNull() @@ -254,7 +257,7 @@ class EntityTypeTest extends TypeTestCase $field->bind(null); $this->assertEquals(new ArrayCollection(), $field->getData()); - $this->assertEquals(array(), $field->getClientData()); + $this->assertSame(array(), $field->getClientData()); } public function testSubmitSingleNonExpandedSingleIdentifier() @@ -275,8 +278,8 @@ class EntityTypeTest extends TypeTestCase $field->bind('2'); $this->assertTrue($field->isSynchronized()); - $this->assertEquals($entity2, $field->getData()); - $this->assertEquals(2, $field->getClientData()); + $this->assertSame($entity2, $field->getData()); + $this->assertSame('2', $field->getClientData()); } public function testSubmitSingleNonExpandedCompositeIdentifier() @@ -298,8 +301,8 @@ class EntityTypeTest extends TypeTestCase $field->bind('1'); $this->assertTrue($field->isSynchronized()); - $this->assertEquals($entity2, $field->getData()); - $this->assertEquals(1, $field->getClientData()); + $this->assertSame($entity2, $field->getData()); + $this->assertSame('1', $field->getClientData()); } public function testSubmitMultipleNonExpandedSingleIdentifier() @@ -324,7 +327,7 @@ class EntityTypeTest extends TypeTestCase $this->assertTrue($field->isSynchronized()); $this->assertEquals($expected, $field->getData()); - $this->assertEquals(array(1, 3), $field->getClientData()); + $this->assertSame(array('1', '3'), $field->getClientData()); } public function testSubmitMultipleNonExpandedSingleIdentifier_existingData() @@ -355,7 +358,7 @@ class EntityTypeTest extends TypeTestCase $this->assertEquals($expected, $field->getData()); // same object still, useful if it is a PersistentCollection $this->assertSame($existing, $field->getData()); - $this->assertEquals(array(1, 3), $field->getClientData()); + $this->assertSame(array('1', '3'), $field->getClientData()); } public function testSubmitMultipleNonExpandedCompositeIdentifier() @@ -381,7 +384,7 @@ class EntityTypeTest extends TypeTestCase $this->assertTrue($field->isSynchronized()); $this->assertEquals($expected, $field->getData()); - $this->assertEquals(array(0, 2), $field->getClientData()); + $this->assertSame(array('0', '2'), $field->getClientData()); } public function testSubmitMultipleNonExpandedCompositeIdentifier_existingData() @@ -412,7 +415,7 @@ class EntityTypeTest extends TypeTestCase $this->assertEquals($expected, $field->getData()); // same object still, useful if it is a PersistentCollection $this->assertSame($existing, $field->getData()); - $this->assertEquals(array(0, 2), $field->getClientData()); + $this->assertSame(array('0', '2'), $field->getClientData()); } public function testSubmitSingleExpanded() @@ -433,7 +436,7 @@ class EntityTypeTest extends TypeTestCase $field->bind('2'); $this->assertTrue($field->isSynchronized()); - $this->assertEquals($entity2, $field->getData()); + $this->assertSame($entity2, $field->getData()); $this->assertFalse($field['1']->getData()); $this->assertTrue($field['2']->getData()); $this->assertSame('', $field['1']->getClientData()); @@ -488,10 +491,11 @@ class EntityTypeTest extends TypeTestCase $field->bind('2'); - $this->assertEquals(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choices')); + $this->assertSame(array(1 => '1', 2 => '2'), $field->createView()->get('choices')); + $this->assertSame(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choice_labels')); $this->assertTrue($field->isSynchronized()); - $this->assertEquals($entity2, $field->getData()); - $this->assertEquals(2, $field->getClientData()); + $this->assertSame($entity2, $field->getData()); + $this->assertSame('2', $field->getClientData()); } public function testGroupByChoices() @@ -513,12 +517,13 @@ class EntityTypeTest extends TypeTestCase $field->bind('2'); - $this->assertEquals(2, $field->getClientData()); - $this->assertEquals(array( - 'Group1' => array(1 => 'Foo', '2' => 'Bar'), - 'Group2' => array(3 => 'Baz'), - '4' => 'Boo!' + $this->assertSame('2', $field->getClientData()); + $this->assertSame(array( + 'Group1' => array(1 => '1', 2 => '2'), + 'Group2' => array(3 => '3'), + '4' => '4' ), $field->createView()->get('choices')); + $this->assertSame(array(1 => 'Foo', 2 => 'Bar', 3 => 'Baz', 4 => 'Boo!'), $field->createView()->get('choice_labels')); } public function testDisallowChoicesThatAreNotIncluded_choicesSingleIdentifier() @@ -652,8 +657,8 @@ class EntityTypeTest extends TypeTestCase $field->bind('foo'); $this->assertTrue($field->isSynchronized()); - $this->assertEquals($entity1, $field->getData()); - $this->assertEquals('foo', $field->getClientData()); + $this->assertSame($entity1, $field->getData()); + $this->assertSame('foo', $field->getClientData()); } public function testSubmitCompositeStringIdentifier() @@ -674,8 +679,8 @@ class EntityTypeTest extends TypeTestCase $field->bind('0'); $this->assertTrue($field->isSynchronized()); - $this->assertEquals($entity1, $field->getData()); - $this->assertEquals(0, $field->getClientData()); + $this->assertSame($entity1, $field->getData()); + $this->assertSame('0', $field->getClientData()); } protected function createRegistryMock($name, $em) diff --git a/tests/Symfony/Tests/Component/Form/AbstractLayoutTest.php b/tests/Symfony/Tests/Component/Form/AbstractLayoutTest.php index 877bda7c0e..a5ce3b0695 100644 --- a/tests/Symfony/Tests/Component/Form/AbstractLayoutTest.php +++ b/tests/Symfony/Tests/Component/Form/AbstractLayoutTest.php @@ -309,23 +309,6 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase ); } - public function testCheckedCheckboxWithValue() - { - $form = $this->factory->createNamed('checkbox', 'na&me', true, array( - 'property_path' => 'name', - 'value' => 'foo&bar', - )); - - $this->assertWidgetMatchesXpath($form->createView(), array(), -'/input - [@type="checkbox"] - [@name="na&me"] - [@checked="checked"] - [@value="foo&bar"] -' - ); - } - public function testUncheckedCheckbox() { $form = $this->factory->createNamed('checkbox', 'na&me', false, array( @@ -341,6 +324,22 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase ); } + public function testCheckboxWithValue() + { + $form = $this->factory->createNamed('checkbox', 'na&me', false, array( + 'property_path' => 'name', + 'value' => 'foo&bar', + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/input + [@type="checkbox"] + [@name="na&me"] + [@value="foo&bar"] +' + ); + } + public function testSingleChoice() { $form = $this->factory->createNamed('choice', 'na&me', '&a', array( @@ -355,8 +354,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@name="na&me"] [@required="required"] [ - ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] ] [count(./option)=2] ' @@ -378,9 +377,9 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@name="na&me"] [@required="required"] [ - ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ./option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] /following-sibling::option[@disabled="disabled"][not(@selected)][.="-- sep --"] - /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] ] [count(./option)=3] ' @@ -403,8 +402,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@name="na&me"] [@required="required"] [ - ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] - /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + ./option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] + /following-sibling::option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] ] [count(./option)=2] ' @@ -426,9 +425,9 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@name="na&me"] [@required="required"] [ - ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ./option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] /following-sibling::option[@disabled="disabled"][not(@selected)][.=""] - /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] ] [count(./option)=3] ' @@ -468,8 +467,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [not(@required)] [ ./option[@value=""][.="[trans][/trans]"] - /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + /following-sibling::option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] ] [count(./option)=3] ' @@ -492,8 +491,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [not(@required)] [ ./option[@value=""][.="[trans][/trans]"] - /following-sibling::option[@value="&a"][not(@selected)][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + /following-sibling::option[@value="0"][not(@selected)][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] ] [count(./option)=3] ' @@ -517,8 +516,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [not(@required)] [ ./option[@value=""][not(@selected)][.="[trans]Select&Anything&Not&Me[/trans]"] - /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + /following-sibling::option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] ] [count(./option)=3] ' @@ -542,8 +541,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@required="required"] [ ./option[@value=""][.="[trans]Test&Me[/trans]"] - /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + /following-sibling::option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] ] [count(./option)=3] ' @@ -566,8 +565,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@required="required"] [ ./option[@value=""][.="[trans][/trans]"] - /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + /following-sibling::option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] ] [count(./option)=3] ' @@ -591,13 +590,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@name="na&me"] [./optgroup[@label="[trans]Group&1[/trans]"] [ - ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] ] [count(./option)=2] ] [./optgroup[@label="[trans]Group&2[/trans]"] - [./option[@value="&c"][not(@selected)][.="[trans]Choice&C[/trans]"]] + [./option[@value="2"][not(@selected)][.="[trans]Choice&C[/trans]"]] [count(./option)=1] ] [count(./optgroup)=2] @@ -619,8 +618,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@name="na&me[]"] [@multiple="multiple"] [ - ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] ] [count(./option)=2] ' @@ -642,8 +641,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@name="na&me[]"] [@multiple="multiple"] [ - ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] ] [count(./option)=2] ' @@ -665,8 +664,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@name="na&me[]"] [@multiple="multiple"] [ - ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] ] [count(./option)=2] ' @@ -685,10 +684,10 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/div [ - ./input[@type="radio"][@name="na&me"][@id="na&me_&a"][@checked] - /following-sibling::label[@for="na&me_&a"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_&b"][not(@checked)] - /following-sibling::label[@for="na&me_&b"][.="[trans]Choice&B[/trans]"] + ./input[@type="radio"][@name="na&me"][@id="na&me_0"][@value="0"][@checked] + /following-sibling::label[@for="na&me_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_1"][@value="1"][not(@checked)] + /following-sibling::label[@for="na&me_1"][.="[trans]Choice&B[/trans]"] ] [count(./input)=2] ' @@ -708,10 +707,10 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/div [ - ./input[@type="radio"][@name="na&me"][@id="na&me_&a"][@checked] - /following-sibling::label[@for="na&me_&a"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_&b"][not(@checked)] - /following-sibling::label[@for="na&me_&b"][.="[trans]Choice&B[/trans]"] + ./input[@type="radio"][@name="na&me"][@id="na&me_0"][@checked] + /following-sibling::label[@for="na&me_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_1"][not(@checked)] + /following-sibling::label[@for="na&me_1"][.="[trans]Choice&B[/trans]"] ] [count(./input)=2] ' @@ -730,10 +729,10 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/div [ - ./input[@type="radio"][@name="na&me"][@id="na&me_1"][@checked] - /following-sibling::label[@for="na&me_1"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_0"][not(@checked)] - /following-sibling::label[@for="na&me_0"][.="[trans]Choice&B[/trans]"] + ./input[@type="radio"][@name="na&me"][@id="na&me_0"][@checked] + /following-sibling::label[@for="na&me_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_1"][not(@checked)] + /following-sibling::label[@for="na&me_1"][.="[trans]Choice&B[/trans]"] ] [count(./input)=2] ' @@ -753,12 +752,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/div [ - ./input[@type="checkbox"][@name="na&me[&a]"][@id="na&me_&a"][@checked][not(@required)] - /following-sibling::label[@for="na&me_&a"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="checkbox"][@name="na&me[&b]"][@id="na&me_&b"][not(@checked)][not(@required)] - /following-sibling::label[@for="na&me_&b"][.="[trans]Choice&B[/trans]"] - /following-sibling::input[@type="checkbox"][@name="na&me[&c]"][@id="na&me_&c"][@checked][not(@required)] - /following-sibling::label[@for="na&me_&c"][.="[trans]Choice&C[/trans]"] + ./input[@type="checkbox"][@name="na&me[0]"][@id="na&me_0"][@checked][not(@required)] + /following-sibling::label[@for="na&me_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="checkbox"][@name="na&me[1]"][@id="na&me_1"][not(@checked)][not(@required)] + /following-sibling::label[@for="na&me_1"][.="[trans]Choice&B[/trans]"] + /following-sibling::input[@type="checkbox"][@name="na&me[2]"][@id="na&me_2"][@checked][not(@required)] + /following-sibling::label[@for="na&me_2"][.="[trans]Choice&C[/trans]"] ] [count(./input)=3] ' @@ -1450,24 +1449,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@type="radio"] [@name="na&me"] [@checked="checked"] - [@value=""] -' - ); - } - - public function testCheckedRadioWithValue() - { - $form = $this->factory->createNamed('radio', 'na&me', true, array( - 'property_path' => 'name', - 'value' => 'foo&bar', - )); - - $this->assertWidgetMatchesXpath($form->createView(), array(), -'/input - [@type="radio"] - [@name="na&me"] - [@checked="checked"] - [@value="foo&bar"] + [@value="1"] ' ); } @@ -1487,6 +1469,22 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase ); } + public function testRadioWithValue() + { + $form = $this->factory->createNamed('radio', 'na&me', false, array( + 'property_path' => 'name', + 'value' => 'foo&bar', + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/input + [@type="radio"] + [@name="na&me"] + [@value="foo&bar"] +' + ); + } + public function testTextarea() { $form = $this->factory->createNamed('textarea', 'na&me', 'foo&bar', array( diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ArrayChoiceListTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ArrayChoiceListTest.php deleted file mode 100644 index ccf4b7a6dd..0000000000 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ArrayChoiceListTest.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Tests\Component\Form\Extension\Core\ChoiceList; - -use Symfony\Component\Form\Extension\Core\ChoiceList\ArrayChoiceList; - -class ArrayChoiceListTest extends \PHPUnit_Framework_TestCase -{ - /** - * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testConstructorExpectsArrayOrClosure() - { - new ArrayChoiceList('foobar'); - } - - public function testGetChoices() - { - $choices = array('a' => 'A', 'b' => 'B'); - $list = new ArrayChoiceList($choices); - - $this->assertSame($choices, $list->getChoices()); - } - - public function testGetChoicesFromClosure() - { - $choices = array('a' => 'A', 'b' => 'B'); - $closure = function () use ($choices) { return $choices; }; - $list = new ArrayChoiceList($closure); - - $this->assertSame($choices, $list->getChoices()); - } - - /** - * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testClosureShouldReturnArray() - { - $closure = function () { return 'foobar'; }; - $list = new ArrayChoiceList($closure); - - $list->getChoices(); - } -} diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ComplexChoiceListTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ComplexChoiceListTest.php new file mode 100644 index 0000000000..4b61c16df3 --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ComplexChoiceListTest.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\Form\Extension\Core\ChoiceList; + +use Symfony\Component\Form\Extension\Core\ChoiceList\ComplexChoiceList; + +class ComplexChoiceListTest extends \PHPUnit_Framework_TestCase +{ + private $obj1; + + private $obj2; + + private $obj3; + + private $obj4; + + private $list; + + protected function setUp() + { + parent::setUp(); + + $this->obj1 = new \stdClass(); + $this->obj2 = new \stdClass(); + $this->obj3 = new \stdClass(); + $this->obj4 = new \stdClass(); + + $this->list = new ComplexChoiceList( + array( + 'Group 1' => array($this->obj1, $this->obj2), + 'Group 2' => array($this->obj3, $this->obj4), + ), + array( + 'Group 1' => array('A', 'B'), + 'Group 2' => array('C', 'D'), + ), + array($this->obj2, $this->obj3) + ); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->obj1 = null; + $this->obj2 = null; + $this->obj3 = null; + $this->obj4 = null; + $this->list = null; + } + + public function testInitArray() + { + $this->list = new ComplexChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4), + array('A', 'B', 'C', 'D'), + array($this->obj2) + ); + + $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); + $this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels()); + + $this->assertSame(array(1 => '1'), $this->list->getPreferredValues()); + $this->assertSame(array(1 => '1'), $this->list->getPreferredValueHierarchy()); + $this->assertSame(array(0 => '0', 2 => '2', 3 => '3'), $this->list->getRemainingValues()); + $this->assertSame(array(0 => '0', 2 => '2', 3 => '3'), $this->list->getRemainingValueHierarchy()); + } + + public function testInitNestedArray() + { + $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); + $this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels()); + + $this->assertSame(array(1 => '1', 2 => '2'), $this->list->getPreferredValues()); + $this->assertSame(array( + 'Group 1' => array(1 => '1'), + 'Group 2' => array(2 => '2') + ), $this->list->getPreferredValueHierarchy()); + $this->assertSame(array(0 => '0', 3 => '3'), $this->list->getRemainingValues()); + $this->assertSame(array( + 'Group 1' => array(0 => '0'), + 'Group 2' => array(3 => '3') + ), $this->list->getRemainingValueHierarchy()); + } + + public function testGetIndicesForChoices() + { + $choices = array($this->obj2, $this->obj3); + $this->assertSame(array(1, 2), $this->list->getIndicesForChoices($choices)); + } + + public function testGetIndicesForChoicesIgnoresNonExistingChoices() + { + $choices = array($this->obj2, $this->obj3, 'foobar'); + $this->assertSame(array(1, 2), $this->list->getIndicesForChoices($choices)); + } + + public function testGetIndicesForValues() + { + // values and indices are always the same + $values = array('1', '2'); + $this->assertSame(array(1, 2), $this->list->getIndicesForValues($values)); + } + + public function testGetIndicesForValuesIgnoresNonExistingValues() + { + $values = array('1', '2', '5'); + $this->assertSame(array(1, 2), $this->list->getIndicesForValues($values)); + } + + public function testGetChoicesForValues() + { + $values = array('1', '2'); + $this->assertSame(array($this->obj2, $this->obj3), $this->list->getChoicesForValues($values)); + } + + public function testGetChoicesForValuesIgnoresNonExistingValues() + { + $values = array('1', '2', '5'); + $this->assertSame(array($this->obj2, $this->obj3), $this->list->getChoicesForValues($values)); + } + + public function testGetValuesForChoices() + { + $choices = array($this->obj2, $this->obj3); + $this->assertSame(array('1', '2'), $this->list->getValuesForChoices($choices)); + } + + public function testGetValuesForChoicesIgnoresNonExistingChoices() + { + $choices = array($this->obj2, $this->obj3, 'foobar'); + $this->assertSame(array('1', '2'), $this->list->getValuesForChoices($choices)); + } +} diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/MonthChoiceListTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/MonthChoiceListTest.php deleted file mode 100644 index 5fd37fdea4..0000000000 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/MonthChoiceListTest.php +++ /dev/null @@ -1,95 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Tests\Component\Form\Extension\Core\ChoiceList; - -use Symfony\Component\Form\Extension\Core\ChoiceList\MonthChoiceList; - -class MonthChoiceListTest extends \PHPUnit_Framework_TestCase -{ - private $formatter; - - protected function setUp() - { - if (!extension_loaded('intl')) { - $this->markTestSkipped('The "intl" extension is not available'); - } - - \Locale::setDefault('en'); - - // I would prefer to mock the formatter, but this leads to weird bugs - // with the current version of PHPUnit - $this->formatter = new \IntlDateFormatter( - \Locale::getDefault(), - \IntlDateFormatter::SHORT, - \IntlDateFormatter::NONE, - \DateTimeZone::UTC - ); - } - - protected function tearDown() - { - $this->formatter = null; - } - - public function testNumericMonthsIfPatternContainsNoMonth() - { - $this->formatter->setPattern('yy'); - - $months = array(1, 4); - $list = new MonthChoiceList($this->formatter, $months); - - $names = array(1 => '01', 4 => '04'); - $this->assertSame($names, $list->getChoices()); - } - - public function testFormattedMonthsShort() - { - $this->formatter->setPattern('dd.MMM.yy'); - - $months = array(1, 4); - $list = new MonthChoiceList($this->formatter, $months); - - $names = array(1 => 'Jan', 4 => 'Apr'); - $this->assertSame($names, $list->getChoices()); - } - - public function testFormattedMonthsLong() - { - $this->formatter->setPattern('dd.MMMM.yy'); - - $months = array(1, 4); - $list = new MonthChoiceList($this->formatter, $months); - - $names = array(1 => 'January', 4 => 'April'); - $this->assertSame($names, $list->getChoices()); - } - - public function testFormattedMonthsLongWithDifferentTimezone() - { - $this->formatter = new \IntlDateFormatter( - \Locale::getDefault(), - \IntlDateFormatter::SHORT, - \IntlDateFormatter::NONE, - 'PST' - ); - - $this->formatter->setPattern('dd.MMMM.yy'); - - $months = array(1, 4); - $list = new MonthChoiceList($this->formatter, $months); - - $names = array(1 => 'January', 4 => 'April'); - // uses UTC internally - $this->assertSame($names, $list->getChoices()); - $this->assertSame('PST', $this->formatter->getTimezoneId()); - } -} diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ObjectChoiceListTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ObjectChoiceListTest.php new file mode 100644 index 0000000000..8e807a86b0 --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ObjectChoiceListTest.php @@ -0,0 +1,249 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\Form\Extension\Core\ChoiceList; + +use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; + +class ObjectChoiceListTest_EntityWithToString +{ + private $property; + + public function __construct($property) + { + $this->property = $property; + } + + public function __toString() + { + return $this->property; + } +} + +class ObjectChoiceListTest extends \PHPUnit_Framework_TestCase +{ + private $obj1; + + private $obj2; + + private $obj3; + + private $obj4; + + private $list; + + protected function setUp() + { + parent::setUp(); + + $this->obj1 = (object) array('name' => 'A'); + $this->obj2 = (object) array('name' => 'B'); + $this->obj3 = (object) array('name' => 'C'); + $this->obj4 = (object) array('name' => 'D'); + + $this->list = new ObjectChoiceList( + array( + 'Group 1' => array($this->obj1, $this->obj2), + 'Group 2' => array($this->obj3, $this->obj4), + ), + 'name', + array($this->obj2, $this->obj3) + ); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->obj1 = null; + $this->obj2 = null; + $this->obj3 = null; + $this->obj4 = null; + $this->list = null; + } + + public function testInitArray() + { + $this->list = new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4), + 'name', + array($this->obj2) + ); + + $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); + $this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels()); + + $this->assertSame(array(1 => '1'), $this->list->getPreferredValues()); + $this->assertSame(array(1 => '1'), $this->list->getPreferredValueHierarchy()); + $this->assertSame(array(0 => '0', 2 => '2', 3 => '3'), $this->list->getRemainingValues()); + $this->assertSame(array(0 => '0', 2 => '2', 3 => '3'), $this->list->getRemainingValueHierarchy()); + } + + public function testInitNestedArray() + { + $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); + $this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels()); + + $this->assertSame(array(1 => '1', 2 => '2'), $this->list->getPreferredValues()); + $this->assertSame(array( + 'Group 1' => array(1 => '1'), + 'Group 2' => array(2 => '2') + ), $this->list->getPreferredValueHierarchy()); + $this->assertSame(array(0 => '0', 3 => '3'), $this->list->getRemainingValues()); + $this->assertSame(array( + 'Group 1' => array(0 => '0'), + 'Group 2' => array(3 => '3') + ), $this->list->getRemainingValueHierarchy()); + } + + public function testInitArrayWithGroupPath() + { + $this->obj1 = (object) array('name' => 'A', 'category' => 'Group 1'); + $this->obj2 = (object) array('name' => 'B', 'category' => 'Group 1'); + $this->obj3 = (object) array('name' => 'C', 'category' => 'Group 2'); + $this->obj4 = (object) array('name' => 'D', 'category' => 'Group 2'); + + // Objects with NULL groups are not grouped + $obj5 = (object) array('name' => 'E', 'category' => null); + + // Objects without the group property are not grouped either + // see https://github.com/symfony/symfony/commit/d9b7abb7c7a0f28e0ce970afc5e305dce5dccddf + $obj6 = (object) array('name' => 'F'); + + $this->list = new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4, $obj5, $obj6), + 'name', + array($this->obj2, $this->obj3), + 'category' + ); + + $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4, $obj5, $obj6), $this->list->getChoices()); + $this->assertSame(array('A', 'B', 'C', 'D', 'E', 'F'), $this->list->getLabels()); + + $this->assertSame(array(1 => '1', 2 => '2'), $this->list->getPreferredValues()); + $this->assertSame(array( + 'Group 1' => array(1 => '1'), + 'Group 2' => array(2 => '2') + ), $this->list->getPreferredValueHierarchy()); + $this->assertSame(array(0 => '0', 3 => '3', 4 => '4', 5 => '5'), $this->list->getRemainingValues()); + $this->assertSame(array( + 'Group 1' => array(0 => '0'), + 'Group 2' => array(3 => '3'), + 4 => '4', + 5 => '5', + ), $this->list->getRemainingValueHierarchy()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testInitArrayWithGroupPathThrowsExceptionIfNestedArray() + { + $this->obj1 = (object) array('name' => 'A', 'category' => 'Group 1'); + $this->obj2 = (object) array('name' => 'B', 'category' => 'Group 1'); + $this->obj3 = (object) array('name' => 'C', 'category' => 'Group 2'); + $this->obj4 = (object) array('name' => 'D', 'category' => 'Group 2'); + + new ObjectChoiceList( + array( + 'Group 1' => array($this->obj1, $this->obj2), + 'Group 2' => array($this->obj3, $this->obj4), + ), + 'name', + array($this->obj2, $this->obj3), + 'category' + ); + } + + public function testInitArrayWithValuePath() + { + $this->obj1 = (object) array('name' => 'A', 'id' => 10); + $this->obj2 = (object) array('name' => 'B', 'id' => 20); + $this->obj3 = (object) array('name' => 'C', 'id' => 30); + $this->obj4 = (object) array('name' => 'D', 'id' => 40); + + $this->list = new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4), + 'name', + array($this->obj2, $this->obj3), + null, + 'id' + ); + + $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); + $this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels()); + + // Values are always converted to strings to avoid problems with + // comparisons + $this->assertSame(array(1 => '20', 2 => '30'), $this->list->getPreferredValues()); + $this->assertSame(array(1 => '20', 2 => '30'), $this->list->getPreferredValueHierarchy()); + $this->assertSame(array(0 => '10', 3 => '40'), $this->list->getRemainingValues()); + $this->assertSame(array(0 => '10', 3 => '40'), $this->list->getRemainingValueHierarchy()); + } + + public function testInitArrayWithIndexPath() + { + $this->obj1 = (object) array('name' => 'A', 'id' => 10); + $this->obj2 = (object) array('name' => 'B', 'id' => 20); + $this->obj3 = (object) array('name' => 'C', 'id' => 30); + $this->obj4 = (object) array('name' => 'D', 'id' => 40); + + $this->list = new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4), + 'name', + array($this->obj2, $this->obj3), + null, + null, + 'id' + ); + + $this->assertSame(array(10 => $this->obj1, 20 => $this->obj2, 30 => $this->obj3, 40 => $this->obj4), $this->list->getChoices()); + $this->assertSame(array(10 => 'A', 20 => 'B', 30 => 'C', 40 => 'D'), $this->list->getLabels()); + + $this->assertSame(array(20 => '1', 30 => '2'), $this->list->getPreferredValues()); + $this->assertSame(array(20 => '1', 30 => '2'), $this->list->getPreferredValueHierarchy()); + $this->assertSame(array(10 => '0', 40 => '3'), $this->list->getRemainingValues()); + $this->assertSame(array(10 => '0', 40 => '3'), $this->list->getRemainingValueHierarchy()); + } + + public function testInitArrayUsesToString() + { + $this->obj1 = new ObjectChoiceListTest_EntityWithToString('A'); + $this->obj2 = new ObjectChoiceListTest_EntityWithToString('B'); + $this->obj3 = new ObjectChoiceListTest_EntityWithToString('C'); + $this->obj4 = new ObjectChoiceListTest_EntityWithToString('D'); + + $this->list = new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4) + ); + + $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); + $this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels()); + } + + /** + * @expectedException Symfony\Component\Form\Exception\FormException + */ + public function testInitArrayThrowsExceptionIfToStringNotFound() + { + $this->obj1 = new ObjectChoiceListTest_EntityWithToString('A'); + $this->obj2 = new ObjectChoiceListTest_EntityWithToString('B'); + $this->obj3 = (object) array('name' => 'C'); + $this->obj4 = new ObjectChoiceListTest_EntityWithToString('D'); + + $this->list = new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4) + ); + + $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); + $this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels()); + } +} diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/PaddedChoiceListTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/PaddedChoiceListTest.php deleted file mode 100644 index 6bc115012a..0000000000 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/PaddedChoiceListTest.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Tests\Component\Form\Extension\Core\ChoiceList; - -use Symfony\Component\Form\Extension\Core\ChoiceList\PaddedChoiceList; - -class PaddedChoiceListTest extends \PHPUnit_Framework_TestCase -{ - /** - * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testConstructorExpectsArrayOrClosure() - { - $list = new PaddedChoiceList('foobar', 3, '-', STR_PAD_RIGHT); - } - - public function testPaddingDirections() - { - $list = new PaddedChoiceList(array('a' => 'C', 'b' => 'D'), 3, '-', STR_PAD_RIGHT); - $this->assertSame(array('a' => 'C--', 'b' => 'D--'), $list->getChoices()); - $list = new PaddedChoiceList(array('a' => 'C', 'b' => 'D'), 3, '-', STR_PAD_LEFT); - $this->assertSame(array('a' => '--C', 'b' => '--D'), $list->getChoices()); - $list = new PaddedChoiceList(array('a' => 'C', 'b' => 'D'), 3, '-', STR_PAD_BOTH); - $this->assertSame(array('a' => '-C-', 'b' => '-D-'), $list->getChoices()); - } - - public function testGetChoicesFromClosure() - { - $closure = function () { return array('a' => 'C', 'b' => 'D'); }; - $list = new PaddedChoiceList($closure, 3, '-', STR_PAD_RIGHT); - - $this->assertSame(array('a' => 'C--', 'b' => 'D--'), $list->getChoices()); - } - - /** - * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testClosureShouldReturnArray() - { - $closure = function () { return 'foobar'; }; - $list = new PaddedChoiceList($closure, 3, '-', STR_PAD_RIGHT); - - $list->getChoices(); - } -} diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/SimpleChoiceListTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/SimpleChoiceListTest.php new file mode 100644 index 0000000000..508695cfda --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/SimpleChoiceListTest.php @@ -0,0 +1,222 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\Form\Extension\Core\ChoiceList; + +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; + +use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; + +class SimpleChoiceListTest extends \PHPUnit_Framework_TestCase +{ + private $list; + + private $numericList; + + protected function setUp() + { + parent::setUp(); + + $choices = array( + 'Group 1' => array('a' => 'A', 'b' => 'B'), + 'Group 2' => array('c' => 'C', 'd' => 'D'), + ); + $numericChoices = array( + 'Group 1' => array(0 => 'A', 1 => 'B'), + 'Group 2' => array(2 => 'C', 3 => 'D'), + ); + + $this->list = new SimpleChoiceList($choices, array('b', 'c'), ChoiceList::GENERATE, ChoiceList::GENERATE); + + // Use COPY_CHOICE strategy to test for the various associated problems + $this->numericList = new SimpleChoiceList($numericChoices, array(1, 2), ChoiceList::COPY_CHOICE, ChoiceList::GENERATE); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->list = null; + $this->numericList = null; + } + + public function testInitArray() + { + $choices = array('a' => 'A', 'b' => 'B', 'c' => 'C'); + $this->list = new SimpleChoiceList($choices, array('b'), ChoiceList::GENERATE, ChoiceList::GENERATE); + + $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getChoices()); + $this->assertSame(array(0 => 'A', 1 => 'B', 2 => 'C'), $this->list->getLabels()); + + $this->assertSame(array(1 => '1'), $this->list->getPreferredValues()); + $this->assertSame(array(1 => '1'), $this->list->getPreferredValueHierarchy()); + $this->assertSame(array(0 => '0', 2 => '2'), $this->list->getRemainingValues()); + $this->assertSame(array(0 => '0', 2 => '2'), $this->list->getRemainingValueHierarchy()); + } + + public function testInitArrayValueCopyChoice() + { + $choices = array('a' => 'A', 'b' => 'B', 'c' => 'C'); + $this->list = new SimpleChoiceList($choices, array('b'), ChoiceList::COPY_CHOICE, ChoiceList::GENERATE); + + $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getChoices()); + $this->assertSame(array(0 => 'A', 1 => 'B', 2 => 'C'), $this->list->getLabels()); + + $this->assertSame(array(1 => 'b'), $this->list->getPreferredValues()); + $this->assertSame(array(1 => 'b'), $this->list->getPreferredValueHierarchy()); + $this->assertSame(array(0 => 'a', 2 => 'c'), $this->list->getRemainingValues()); + $this->assertSame(array(0 => 'a', 2 => 'c'), $this->list->getRemainingValueHierarchy()); + } + + public function testInitArrayIndexCopyChoice() + { + $choices = array('a' => 'A', 'b' => 'B', 'c' => 'C'); + $this->list = new SimpleChoiceList($choices, array('b'), ChoiceList::GENERATE, ChoiceList::COPY_CHOICE); + + $this->assertSame(array('a' => 'a', 'b' => 'b', 'c' => 'c'), $this->list->getChoices()); + $this->assertSame(array('a' => 'A', 'b' => 'B', 'c' => 'C'), $this->list->getLabels()); + + $this->assertSame(array('b' => '1'), $this->list->getPreferredValues()); + $this->assertSame(array('b' => '1'), $this->list->getPreferredValueHierarchy()); + $this->assertSame(array('a' => '0', 'c' => '2'), $this->list->getRemainingValues()); + $this->assertSame(array('a' => '0', 'c' => '2'), $this->list->getRemainingValueHierarchy()); + } + + public function testInitNestedArray() + { + $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd'), $this->list->getChoices()); + $this->assertSame(array(0 => 'A', 1 => 'B', 2 => 'C', 3 => 'D'), $this->list->getLabels()); + + $this->assertSame(array(1 => '1', 2 => '2'), $this->list->getPreferredValues()); + $this->assertSame(array( + 'Group 1' => array(1 => '1'), + 'Group 2' => array(2 => '2') + ), $this->list->getPreferredValueHierarchy()); + $this->assertSame(array(0 => '0', 3 => '3'), $this->list->getRemainingValues()); + $this->assertSame(array( + 'Group 1' => array(0 => '0'), + 'Group 2' => array(3 => '3') + ), $this->list->getRemainingValueHierarchy()); + } + + public function testGetIndicesForChoices() + { + $choices = array('b', 'c'); + $this->assertSame(array(1, 2), $this->list->getIndicesForChoices($choices)); + } + + public function testGetIndicesForChoicesIgnoresNonExistingChoices() + { + $choices = array('b', 'c', 'foobar'); + $this->assertSame(array(1, 2), $this->list->getIndicesForChoices($choices)); + } + + public function testGetIndicesForChoicesDealsWithNumericChoices() + { + // Pass choices as strings although they are integers + $choices = array('0', '1'); + $this->assertSame(array(0, 1), $this->numericList->getIndicesForChoices($choices)); + } + + public function testGetIndicesForValues() + { + $values = array('1', '2'); + $this->assertSame(array(1, 2), $this->list->getIndicesForValues($values)); + } + + public function testGetIndicesForValuesIgnoresNonExistingValues() + { + $values = array('1', '2', '100'); + $this->assertSame(array(1, 2), $this->list->getIndicesForValues($values)); + } + + public function testGetIndicesForValuesDealsWithNumericValues() + { + // Pass values as strings although they are integers + $values = array('0', '1'); + $this->assertSame(array(0, 1), $this->numericList->getIndicesForValues($values)); + } + + public function testGetChoicesForValues() + { + $values = array('1', '2'); + $this->assertSame(array('b', 'c'), $this->list->getChoicesForValues($values)); + } + + public function testGetChoicesForValuesIgnoresNonExistingValues() + { + $values = array('1', '2', '100'); + $this->assertSame(array('b', 'c'), $this->list->getChoicesForValues($values)); + } + + public function testGetChoicesForValuesDealsWithNumericValues() + { + // Pass values as strings although they are integers + $values = array('0', '1'); + $this->assertSame(array(0, 1), $this->numericList->getChoicesForValues($values)); + } + + public function testGetValuesForChoices() + { + $choices = array('b', 'c'); + $this->assertSame(array('1', '2'), $this->list->getValuesForChoices($choices)); + } + + public function testGetValuesForChoicesIgnoresNonExistingValues() + { + $choices = array('b', 'c', 'foobar'); + $this->assertSame(array('1', '2'), $this->list->getValuesForChoices($choices)); + } + + public function testGetValuesForChoicesDealsWithNumericValues() + { + // Pass values as strings although they are integers + $values = array('0', '1'); + + $this->assertSame(array('0', '1'), $this->numericList->getValuesForChoices($values)); + } + + /** + * @dataProvider dirtyValuesProvider + */ + public function testGetValuesForChoicesDealsWithDirtyValues($choice, $value) + { + $choices = array( + '0' => 'Zero', + '1' => 'One', + '' => 'Empty', + '1.23' => 'Float', + 'foo' => 'Foo', + 'foo10' => 'Foo 10', + ); + + // use COPY_CHOICE strategy to test the problems + $this->list = new SimpleChoiceList($choices, array(), ChoiceList::COPY_CHOICE, ChoiceList::GENERATE); + + $this->assertSame(array($value), $this->list->getValuesForChoices(array($choice))); + } + + public function dirtyValuesProvider() + { + return array( + array(0, '0'), + array('0', '0'), + array('1', '1'), + array(false, '0'), + array(true, '1'), + array('', ''), + array(null, ''), + array('1.23', '1.23'), + array('foo', 'foo'), + array('foo10', 'foo10'), + ); + } +} diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/DataTransformer/ScalarToChoiceTransformerTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php similarity index 69% rename from tests/Symfony/Tests/Component/Form/Extension/Core/DataTransformer/ScalarToChoiceTransformerTest.php rename to tests/Symfony/Tests/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php index b723419303..2885121534 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/DataTransformer/ScalarToChoiceTransformerTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php @@ -11,15 +11,17 @@ namespace Symfony\Tests\Component\Form\Extension\Core\DataTransformer; -use Symfony\Component\Form\Extension\Core\DataTransformer\ScalarToChoiceTransformer; +use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; +use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer; -class ScalarToChoiceTransformerTest extends \PHPUnit_Framework_TestCase +class ChoiceToValueTransformerTest extends \PHPUnit_Framework_TestCase { protected $transformer; protected function setUp() { - $this->transformer = new ScalarToChoiceTransformer(); + $list = new SimpleChoiceList(array('' => 'A', 0 => 'B', 1 => 'C')); + $this->transformer = new ChoiceToValueTransformer($list); } protected function tearDown() @@ -31,8 +33,8 @@ class ScalarToChoiceTransformerTest extends \PHPUnit_Framework_TestCase { return array( // more extensive test set can be found in FormUtilTest - array(0, 0), - array(false, 0), + array(0, '0'), + array(false, '0'), array('', ''), ); } @@ -50,8 +52,9 @@ class ScalarToChoiceTransformerTest extends \PHPUnit_Framework_TestCase return array( // values are expected to be valid choice keys already and stay // the same - array(0, 0), - array('', ''), + array('0', 0), + array('', null), + array(null, null), ); } @@ -60,15 +63,7 @@ class ScalarToChoiceTransformerTest extends \PHPUnit_Framework_TestCase */ public function testReverseTransform($in, $out) { - $this->assertSame($out, $this->transformer->transform($in)); - } - - /** - * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testTransformExpectsScalar() - { - $this->transformer->transform(array()); + $this->assertSame($out, $this->transformer->reverseTransform($in)); } /** diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/DataTransformer/ArrayToChoicesTransformerTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php similarity index 68% rename from tests/Symfony/Tests/Component/Form/Extension/Core/DataTransformer/ArrayToChoicesTransformerTest.php rename to tests/Symfony/Tests/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php index 992dac5776..0166458043 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/DataTransformer/ArrayToChoicesTransformerTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php @@ -11,15 +11,18 @@ namespace Symfony\Tests\Component\Form\Extension\Core\DataTransformer; -use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToChoicesTransformer; +use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; -class ArrayToChoicesTransformerTest extends \PHPUnit_Framework_TestCase +use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer; + +class ChoicesToValuesTransformerTest extends \PHPUnit_Framework_TestCase { protected $transformer; protected function setUp() { - $this->transformer = new ArrayToChoicesTransformer(); + $list = new SimpleChoiceList(array(0 => 'A', 1 => 'B', 2 => 'C')); + $this->transformer = new ChoicesToValuesTransformer($list); } protected function tearDown() @@ -29,8 +32,9 @@ class ArrayToChoicesTransformerTest extends \PHPUnit_Framework_TestCase public function testTransform() { - $in = array(0, false, ''); - $out = array(0, 0, ''); + // Value strategy in SimpleChoiceList is to copy and convert to string + $in = array(0, 1, 2); + $out = array('0', '1', '2'); $this->assertSame($out, $this->transformer->transform($in)); } @@ -51,10 +55,10 @@ class ArrayToChoicesTransformerTest extends \PHPUnit_Framework_TestCase public function testReverseTransform() { // values are expected to be valid choices and stay the same - $in = array(0, 0, ''); - $out = array(0, 0, ''); + $in = array('0', '1', '2'); + $out = array(0, 1, 2); - $this->assertSame($out, $this->transformer->transform($in)); + $this->assertSame($out, $this->transformer->reverseTransform($in)); } public function testReverseTransformNull() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/FixRadioInputListenerTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/FixRadioInputListenerTest.php index 9f9fc25246..41949e9f01 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/FixRadioInputListenerTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/FixRadioInputListenerTest.php @@ -13,19 +13,36 @@ namespace Symfony\Tests\Component\Form\Extension\Core\EventListener; use Symfony\Component\Form\Event\FilterDataEvent; use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener; +use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase { + private $listener; + + protected function setUp() + { + parent::setUp(); + + $list = new SimpleChoiceList(array(0 => 'A', 1 => 'B')); + $this->listener = new FixRadioInputListener($list); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->listener = null; + } + public function testFixRadio() { $data = '1'; $form = $this->getMock('Symfony\Tests\Component\Form\FormInterface'); $event = new FilterDataEvent($form, $data); - $filter = new FixRadioInputListener(); - $filter->onBindClientData($event); + $this->listener->onBindClientData($event); - $this->assertEquals(array('1' => true), $event->getData()); + $this->assertEquals(array(1 => '1'), $event->getData()); } public function testFixZero() @@ -34,10 +51,9 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $form = $this->getMock('Symfony\Tests\Component\Form\FormInterface'); $event = new FilterDataEvent($form, $data); - $filter = new FixRadioInputListener(); - $filter->onBindClientData($event); + $this->listener->onBindClientData($event); - $this->assertEquals(array('0' => true), $event->getData()); + $this->assertEquals(array(0 => '0'), $event->getData()); } public function testIgnoreEmptyString() @@ -46,8 +62,7 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $form = $this->getMock('Symfony\Tests\Component\Form\FormInterface'); $event = new FilterDataEvent($form, $data); - $filter = new FixRadioInputListener(); - $filter->onBindClientData($event); + $this->listener->onBindClientData($event); $this->assertEquals(array(), $event->getData()); } diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/ChoiceTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/ChoiceTypeTest.php index 605fd3f322..8dae89442d 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/ChoiceTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/ChoiceTypeTest.php @@ -11,6 +11,10 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; + +use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; + use Symfony\Component\Form\Exception\UnexpectedTypeException; class ChoiceTypeTest extends TypeTestCase @@ -31,6 +35,8 @@ class ChoiceTypeTest extends TypeTestCase 4 => 'Roman', ); + private $objectChoices; + private $stringButNumericChoices = array( '0' => 'Bernhard', '1' => 'Fabien', @@ -51,8 +57,29 @@ class ChoiceTypeTest extends TypeTestCase ) ); + protected function setUp() + { + parent::setUp(); + + $this->objectChoices = array( + (object) array('id' => 1, 'name' => 'Bernhard'), + (object) array('id' => 2, 'name' => 'Fabien'), + (object) array('id' => 3, 'name' => 'Kris'), + (object) array('id' => 4, 'name' => 'Jon'), + (object) array('id' => 5, 'name' => 'Roman'), + ); + } + + + protected function tearDown() + { + parent::tearDown(); + + $this->objectChoices = null; + } + /** - * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException + * @expectedException \PHPUnit_Framework_Error */ public function testChoicesOptionExpectsArray() { @@ -71,6 +98,15 @@ class ChoiceTypeTest extends TypeTestCase )); } + /** + * @expectedException Symfony\Component\Form\Exception\FormException + */ + public function testEitherChoiceListOrChoicesMustBeSet() + { + $form = $this->factory->create('choice', null, array( + )); + } + public function testExpandedChoicesOptionsTurnIntoFields() { $form = $this->factory->create('choice', null, array( @@ -90,7 +126,7 @@ class ChoiceTypeTest extends TypeTestCase $flattened = array(); foreach ($this->groupedChoices as $choices) { - $flattened = array_replace($flattened, $choices); + $flattened = array_merge($flattened, array_keys($choices)); } $this->assertCount($form->count(), $flattened, 'Each nested choice should become a new field, not the groups'); @@ -150,10 +186,33 @@ class ChoiceTypeTest extends TypeTestCase 'choices' => $this->choices, )); - $form->bind('b'); + $form->bind('1'); $this->assertEquals('b', $form->getData()); - $this->assertEquals('b', $form->getClientData()); + $this->assertEquals('1', $form->getClientData()); + } + + public function testBindSingleNonExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choice_list' => new ObjectChoiceList( + $this->objectChoices, + // label path + 'name', + array(), + null, + // value path + 'id' + ), + )); + + // "id" value of the second entry + $form->bind('2'); + + $this->assertEquals($this->objectChoices[1], $form->getData()); + $this->assertEquals('2', $form->getClientData()); } public function testBindMultipleNonExpanded() @@ -164,10 +223,32 @@ class ChoiceTypeTest extends TypeTestCase 'choices' => $this->choices, )); - $form->bind(array('a', 'b')); + $form->bind(array('0', '1')); $this->assertEquals(array('a', 'b'), $form->getData()); - $this->assertEquals(array('a', 'b'), $form->getClientData()); + $this->assertEquals(array('0', '1'), $form->getClientData()); + } + + public function testBindMultipleNonExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => false, + 'choice_list' => new ObjectChoiceList( + $this->objectChoices, + // label path + 'name', + array(), + null, + // value path + 'id' + ), + )); + + $form->bind(array('2', '3')); + + $this->assertEquals(array($this->objectChoices[1], $this->objectChoices[2]), $form->getData()); + $this->assertEquals(array('2', '3'), $form->getClientData()); } public function testBindSingleExpanded() @@ -178,19 +259,19 @@ class ChoiceTypeTest extends TypeTestCase 'choices' => $this->choices, )); - $form->bind('b'); + $form->bind('1'); $this->assertSame('b', $form->getData()); - $this->assertFalse($form['a']->getData()); - $this->assertTrue($form['b']->getData()); - $this->assertFalse($form['c']->getData()); - $this->assertFalse($form['d']->getData()); - $this->assertFalse($form['e']->getData()); - $this->assertSame('', $form['a']->getClientData()); - $this->assertSame('1', $form['b']->getClientData()); - $this->assertSame('', $form['c']->getClientData()); - $this->assertSame('', $form['d']->getClientData()); - $this->assertSame('', $form['e']->getClientData()); + $this->assertFalse($form[0]->getData()); + $this->assertTrue($form[1]->getData()); + $this->assertFalse($form[2]->getData()); + $this->assertFalse($form[3]->getData()); + $this->assertFalse($form[4]->getData()); + $this->assertSame('', $form[0]->getClientData()); + $this->assertSame('1', $form[1]->getClientData()); + $this->assertSame('', $form[2]->getClientData()); + $this->assertSame('', $form[3]->getClientData()); + $this->assertSame('', $form[4]->getClientData()); } public function testBindSingleExpandedWithFalseDoesNotHaveExtraFields() @@ -207,6 +288,57 @@ class ChoiceTypeTest extends TypeTestCase $this->assertNull($form->getData()); } + public function testBindSingleExpandedWithEmptyField() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'choices' => array( + '' => 'Empty', + '1' => 'Not empty', + ), + )); + + $form->bind('0'); + + $this->assertNull($form->getData()); + $this->assertTrue($form[0]->getData()); + $this->assertFalse($form[1]->getData()); + $this->assertSame('1', $form[0]->getClientData()); + $this->assertSame('', $form[1]->getClientData()); + } + + public function testBindSingleExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'choice_list' => new ObjectChoiceList( + $this->objectChoices, + // label path + 'name', + array(), + null, + // value path + 'id' + ), + )); + + $form->bind('2'); + + $this->assertSame($this->objectChoices[1], $form->getData()); + $this->assertFalse($form[0]->getData()); + $this->assertTrue($form[1]->getData()); + $this->assertFalse($form[2]->getData()); + $this->assertFalse($form[3]->getData()); + $this->assertFalse($form[4]->getData()); + $this->assertSame('', $form[0]->getClientData()); + $this->assertSame('1', $form[1]->getClientData()); + $this->assertSame('', $form[2]->getClientData()); + $this->assertSame('', $form[3]->getClientData()); + $this->assertSame('', $form[4]->getClientData()); + } + public function testBindSingleExpandedNumericChoices() { $form = $this->factory->create('choice', null, array( @@ -217,7 +349,7 @@ class ChoiceTypeTest extends TypeTestCase $form->bind('1'); - $this->assertSame('1', $form->getData()); + $this->assertSame(1, $form->getData()); $this->assertFalse($form[0]->getData()); $this->assertTrue($form[1]->getData()); $this->assertFalse($form[2]->getData()); @@ -240,7 +372,7 @@ class ChoiceTypeTest extends TypeTestCase $form->bind('1'); - $this->assertSame('1', $form->getData()); + $this->assertSame(1, $form->getData()); $this->assertFalse($form[0]->getData()); $this->assertTrue($form[1]->getData()); $this->assertFalse($form[2]->getData()); @@ -261,19 +393,50 @@ class ChoiceTypeTest extends TypeTestCase 'choices' => $this->choices, )); - $form->bind(array('a' => 'a', 'b' => 'b')); + $form->bind(array(0 => 'a', 1 => 'b')); - $this->assertSame(array('a', 'b'), $form->getData()); - $this->assertTrue($form['a']->getData()); - $this->assertTrue($form['b']->getData()); - $this->assertFalse($form['c']->getData()); - $this->assertFalse($form['d']->getData()); - $this->assertFalse($form['e']->getData()); - $this->assertSame('1', $form['a']->getClientData()); - $this->assertSame('1', $form['b']->getClientData()); - $this->assertSame('', $form['c']->getClientData()); - $this->assertSame('', $form['d']->getClientData()); - $this->assertSame('', $form['e']->getClientData()); + $this->assertSame(array(0 => 'a', 1 => 'b'), $form->getData()); + $this->assertTrue($form[0]->getData()); + $this->assertTrue($form[1]->getData()); + $this->assertFalse($form[2]->getData()); + $this->assertFalse($form[3]->getData()); + $this->assertFalse($form[4]->getData()); + $this->assertSame('1', $form[0]->getClientData()); + $this->assertSame('1', $form[1]->getClientData()); + $this->assertSame('', $form[2]->getClientData()); + $this->assertSame('', $form[3]->getClientData()); + $this->assertSame('', $form[4]->getClientData()); + } + + public function testBindMultipleExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => true, + 'choice_list' => new ObjectChoiceList( + $this->objectChoices, + // label path + 'name', + array(), + null, + // value path + 'id' + ), + )); + + $form->bind(array(0 => '1', 1 => '2')); + + $this->assertSame(array($this->objectChoices[0], $this->objectChoices[1]), $form->getData()); + $this->assertTrue($form[0]->getData()); + $this->assertTrue($form[1]->getData()); + $this->assertFalse($form[2]->getData()); + $this->assertFalse($form[3]->getData()); + $this->assertFalse($form[4]->getData()); + $this->assertSame('1', $form[0]->getClientData()); + $this->assertSame('1', $form[1]->getClientData()); + $this->assertSame('', $form[2]->getClientData()); + $this->assertSame('', $form[3]->getClientData()); + $this->assertSame('', $form[4]->getClientData()); } public function testBindMultipleExpandedNumericChoices() @@ -284,7 +447,7 @@ class ChoiceTypeTest extends TypeTestCase 'choices' => $this->numericChoices, )); - $form->bind(array(1 => 1, 2 => 2)); + $form->bind(array(1 => '1', 2 => '2')); $this->assertSame(array(1, 2), $form->getData()); $this->assertFalse($form[0]->getData()); @@ -437,7 +600,8 @@ class ChoiceTypeTest extends TypeTestCase )); $view = $form->createView(); - $this->assertSame($choices, $view->get('choices')); + $this->assertSame(array('0', '1', '2', '3'), $view->get('choices')); + $this->assertSame(array('A', 'B', 'C', 'D'), $view->get('choice_labels')); } public function testPassPreferredChoicesToView() @@ -449,8 +613,22 @@ class ChoiceTypeTest extends TypeTestCase )); $view = $form->createView(); - $this->assertSame(array('a' => 'A', 'c' => 'C'), $view->get('choices')); - $this->assertSame(array('b' => 'B', 'd' => 'D'), $view->get('preferred_choices')); + $this->assertSame(array(0 => '0', 2 => '2'), $view->get('choices')); + $this->assertSame(array(1 => '1', 3 => '3'), $view->get('preferred_choices')); + $this->assertSame(array('A', 'B', 'C', 'D'), $view->get('choice_labels')); + } + + public function testPassHierarchicalChoicesToView() + { + $form = $this->factory->create('choice', null, array( + 'choices' => $this->groupedChoices, + 'preferred_choices' => array('b', 'd'), + )); + $view = $form->createView(); + + $this->assertSame(array('Symfony' => array(0 => '0', 2 => '2'), 'Doctrine' => array(4 => '4')), $view->get('choices')); + $this->assertSame(array('Symfony' => array(1 => '1'), 'Doctrine' => array(3 => '3')), $view->get('preferred_choices')); + $this->assertSame(array(0 => 'Bernhard', 1 => 'Fabien', 2 => 'Kris', 3 => 'Jon', 4 => 'Roman'), $view->get('choice_labels')); } public function testAdjustFullNameForMultipleNonExpanded() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CountryTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CountryTypeTest.php index ff79433e0e..a6a150a490 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CountryTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CountryTypeTest.php @@ -21,17 +21,18 @@ class CountryTypeTest extends LocalizedTestCase $form = $this->factory->create('country'); $view = $form->createView(); $choices = $view->get('choices'); + $labels = $view->get('choice_labels'); - $this->assertArrayHasKey('DE', $choices); - $this->assertEquals('Deutschland', $choices['DE']); - $this->assertArrayHasKey('GB', $choices); - $this->assertEquals('Vereinigtes Königreich', $choices['GB']); - $this->assertArrayHasKey('US', $choices); - $this->assertEquals('Vereinigte Staaten', $choices['US']); - $this->assertArrayHasKey('FR', $choices); - $this->assertEquals('Frankreich', $choices['FR']); - $this->assertArrayHasKey('MY', $choices); - $this->assertEquals('Malaysia', $choices['MY']); + $this->assertContains('DE', $choices); + $this->assertEquals('Deutschland', $labels['DE']); + $this->assertContains('GB', $choices); + $this->assertEquals('Vereinigtes Königreich', $labels['GB']); + $this->assertContains('US', $choices); + $this->assertEquals('Vereinigte Staaten', $labels['US']); + $this->assertContains('FR', $choices); + $this->assertEquals('Frankreich', $labels['FR']); + $this->assertContains('MY', $choices); + $this->assertEquals('Malaysia', $labels['MY']); } public function testUnknownCountryIsNotIncluded() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/DateTimeTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/DateTimeTypeTest.php index e6046c987b..27f3108d9a 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/DateTimeTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/DateTimeTypeTest.php @@ -237,6 +237,9 @@ class DateTimeTypeTest extends LocalizedTestCase { $form = $this->factory->create('datetime', null, array( 'invalid_message' => 'Customized invalid message', + // Only possible with the "text" widget, because the "choice" + // widget automatically fields invalid values + 'widget' => 'text', )); $form->bind(array( diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/DateTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/DateTypeTest.php index 71e8bc38f1..d1b9c45023 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/DateTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/DateTypeTest.php @@ -325,7 +325,7 @@ class DateTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(2010 => '2010', 2011 => '2011'), $view->getChild('year')->get('choices')); + $this->assertSame(array(2010 => '2010', 2011 => '2011'), $view->getChild('year')->get('choice_labels')); } public function testMonthsOption() @@ -336,7 +336,55 @@ class DateTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('month')->get('choices')); + $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('month')->get('choice_labels')); + } + + public function testMonthsOptionNumericIfFormatContainsNoMonth() + { + $form = $this->factory->create('date', null, array( + 'months' => array(6, 7), + 'format' => 'yy', + )); + + $view = $form->createView(); + + $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('month')->get('choice_labels')); + } + + public function testMonthsOptionShortFormat() + { + $form = $this->factory->create('date', null, array( + 'months' => array(1, 4), + 'format' => 'dd.MMM.yy', + )); + + $view = $form->createView(); + + $this->assertSame(array(1 => 'Jän', 4 => 'Apr'), $view->getChild('month')->get('choice_labels')); + } + + public function testMonthsOptionLongFormat() + { + $form = $this->factory->create('date', null, array( + 'months' => array(1, 4), + 'format' => 'dd.MMMM.yy', + )); + + $view = $form->createView(); + + $this->assertSame(array(1 => 'Jänner', 4 => 'April'), $view->getChild('month')->get('choice_labels')); + } + + public function testMonthsOptionLongFormatWithDifferentTimezone() + { + $form = $this->factory->create('date', null, array( + 'months' => array(1, 4), + 'format' => 'dd.MMMM.yy', + )); + + $view = $form->createView(); + + $this->assertSame(array(1 => 'Jänner', 4 => 'April'), $view->getChild('month')->get('choice_labels')); } public function testIsDayWithinRangeReturnsTrueIfWithin() @@ -347,7 +395,7 @@ class DateTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('day')->get('choices')); + $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('day')->get('choice_labels')); } public function testIsPartiallyFilledReturnsFalseIfSingleText() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LanguageTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LanguageTypeTest.php index 096b568edd..b8a1595bb9 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LanguageTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LanguageTypeTest.php @@ -21,17 +21,18 @@ class LanguageTypeTest extends LocalizedTestCase $form = $this->factory->create('language'); $view = $form->createView(); $choices = $view->get('choices'); + $labels = $view->get('choice_labels'); - $this->assertArrayHasKey('en', $choices); - $this->assertEquals('Englisch', $choices['en']); - $this->assertArrayHasKey('en_GB', $choices); - $this->assertEquals('Britisches Englisch', $choices['en_GB']); - $this->assertArrayHasKey('en_US', $choices); - $this->assertEquals('Amerikanisches Englisch', $choices['en_US']); - $this->assertArrayHasKey('fr', $choices); - $this->assertEquals('Französisch', $choices['fr']); - $this->assertArrayHasKey('my', $choices); - $this->assertEquals('Birmanisch', $choices['my']); + $this->assertContains('en', $choices); + $this->assertEquals('Englisch', $labels[array_search('en', $choices)]); + $this->assertContains('en_GB', $choices); + $this->assertEquals('Britisches Englisch', $labels[array_search('en_GB', $choices)]); + $this->assertContains('en_US', $choices); + $this->assertEquals('Amerikanisches Englisch', $labels[array_search('en_US', $choices)]); + $this->assertContains('fr', $choices); + $this->assertEquals('Französisch', $labels[array_search('fr', $choices)]); + $this->assertContains('my', $choices); + $this->assertEquals('Birmanisch', $labels[array_search('my', $choices)]); } public function testMultipleLanguagesIsNotIncluded() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LocaleTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LocaleTypeTest.php index c813c6c3cc..ebd89940de 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LocaleTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LocaleTypeTest.php @@ -21,12 +21,13 @@ class LocaleTypeTest extends LocalizedTestCase $form = $this->factory->create('locale'); $view = $form->createView(); $choices = $view->get('choices'); + $labels = $view->get('choice_labels'); - $this->assertArrayHasKey('en', $choices); - $this->assertEquals('Englisch', $choices['en']); - $this->assertArrayHasKey('en_GB', $choices); - $this->assertEquals('Englisch (Vereinigtes Königreich)', $choices['en_GB']); - $this->assertArrayHasKey('zh_Hant_MO', $choices); - $this->assertEquals('Chinesisch (traditionell, Sonderverwaltungszone Macao)', $choices['zh_Hant_MO']); + $this->assertContains('en', $choices); + $this->assertEquals('Englisch', $labels[array_search('en', $choices)]); + $this->assertContains('en_GB', $choices); + $this->assertEquals('Englisch (Vereinigtes Königreich)', $labels[array_search('en_GB', $choices)]); + $this->assertContains('zh_Hant_MO', $choices); + $this->assertEquals('Chinesisch (traditionell, Sonderverwaltungszone Macao)', $labels[array_search('zh_Hant_MO', $choices)]); } } diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/RadioTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/RadioTypeTest.php index 869a33b02e..0d3b9d2e69 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/RadioTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/RadioTypeTest.php @@ -13,14 +13,6 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; class RadioTypeTest extends TypeTestCase { - public function testPassValueToView() - { - $form = $this->factory->create('radio', null, array('value' => 'foobar')); - $view = $form->createView(); - - $this->assertEquals('foobar', $view->get('value')); - } - public function testPassParentFullNameToView() { $parent = $this->factory->createNamed('field', 'parent'); @@ -29,22 +21,4 @@ class RadioTypeTest extends TypeTestCase $this->assertEquals('parent', $view['child']->get('full_name')); } - - public function testCheckedIfDataTrue() - { - $form = $this->factory->create('radio'); - $form->setData(true); - $view = $form->createView(); - - $this->assertTrue($view->get('checked')); - } - - public function testNotCheckedIfDataFalse() - { - $form = $this->factory->create('radio'); - $form->setData(false); - $view = $form->createView(); - - $this->assertFalse($view->get('checked')); - } } diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimeTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimeTypeTest.php index c9ba9a02d0..1b46c75207 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimeTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimeTypeTest.php @@ -243,7 +243,7 @@ class TimeTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('hour')->get('choices')); + $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('hour')->get('choice_labels')); } public function testIsMinuteWithinRange_returnsTrueIfWithin() @@ -254,7 +254,7 @@ class TimeTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('minute')->get('choices')); + $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('minute')->get('choice_labels')); } public function testIsSecondWithinRange_returnsTrueIfWithin() @@ -266,7 +266,7 @@ class TimeTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('second')->get('choices')); + $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('second')->get('choice_labels')); } public function testIsPartiallyFilled_returnsFalseIfCompletelyEmpty() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimezoneTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimezoneTypeTest.php index 94e32f9a87..be7db448fe 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimezoneTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimezoneTypeTest.php @@ -19,13 +19,14 @@ class TimezoneTypeTest extends TypeTestCase $form = $this->factory->create('timezone'); $view = $form->createView(); $choices = $view->get('choices'); + $labels = $view->get('choice_labels'); $this->assertArrayHasKey('Africa', $choices); - $this->assertArrayHasKey('Africa/Kinshasa', $choices['Africa']); - $this->assertEquals('Kinshasa', $choices['Africa']['Africa/Kinshasa']); + $this->assertContains('Africa/Kinshasa', $choices['Africa']); + $this->assertEquals('Kinshasa', $labels[array_search('Africa/Kinshasa', $choices['Africa'])]); $this->assertArrayHasKey('America', $choices); - $this->assertArrayHasKey('America/New_York', $choices['America']); - $this->assertEquals('New York', $choices['America']['America/New_York']); + $this->assertContains('America/New_York', $choices['America']); + $this->assertEquals('New York', $labels[array_search('America/New_York', $choices['America'])]); } } diff --git a/tests/Symfony/Tests/Component/Form/Util/FormUtilTest.php b/tests/Symfony/Tests/Component/Form/Util/FormUtilTest.php index 3938e13117..20e110a791 100644 --- a/tests/Symfony/Tests/Component/Form/Util/FormUtilTest.php +++ b/tests/Symfony/Tests/Component/Form/Util/FormUtilTest.php @@ -15,42 +15,6 @@ use Symfony\Component\Form\Util\FormUtil; class FormUtilTest extends \PHPUnit_Framework_TestCase { - public function toArrayKeyProvider() - { - return array( - array(0, 0), - array('0', 0), - array('1', 1), - array(false, 0), - array(true, 1), - array('', ''), - array(null, ''), - array('1.23', '1.23'), - array('foo', 'foo'), - array('foo10', 'foo10'), - ); - } - - /** - * @dataProvider toArrayKeyProvider - */ - public function testToArrayKey($in, $out) - { - $this->assertSame($out, FormUtil::toArrayKey($in)); - } - - public function testToArrayKeys() - { - $in = $out = array(); - - foreach ($this->toArrayKeyProvider() as $call) { - $in[] = $call[0]; - $out[] = $call[1]; - } - - $this->assertSame($out, FormUtil::toArrayKeys($in)); - } - public function isChoiceGroupProvider() { return array( @@ -85,14 +49,17 @@ class FormUtilTest extends \PHPUnit_Framework_TestCase public function isChoiceSelectedProvider() { + // The commented cases should not be necessary anymore, because the + // choice lists should assure that both values passed here are always + // strings return array( - array(true, 0, 0), - array(true, '0', 0), - array(true, '1', 1), - array(true, false, 0), - array(true, true, 1), +// array(true, 0, 0), + array(true, '0', '0'), + array(true, '1', '1'), +// array(true, false, 0), +// array(true, true, 1), array(true, '', ''), - array(true, null, ''), +// array(true, null, ''), array(true, '1.23', '1.23'), array(true, 'foo', 'foo'), array(true, 'foo10', 'foo10'), From e1fc5a5c8c9496cd006a421f880b573ba57ea356 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 17 Jan 2012 00:02:58 +0100 Subject: [PATCH 02/13] [Form] Restricted form names to specific characters to (1) fix generation of HTML IDs and to (2) avoid problems with property paths. ad (1): HTML4 "id" attributes are limited to strings starting with a letter and containing only letters, digits, underscores, hyphens, periods and colons. ad (2): Property paths contain three special characters needed for correct parsing: left/right bracket and period. The rules for form naming are: * Names may start with a letter, a digit or an underscore. Leading digits or underscores will be stripped from the "id" attributes. * Names must only contain letters, digits, underscores, hyphens and colons. * Root forms may have an empty name. Solves #1919 and #3021 on a wider scope. --- .../Extension/Core/Type/CollectionType.php | 4 +- .../Form/Extension/Core/Type/FieldType.php | 5 + src/Symfony/Component/Form/Form.php | 34 +- src/Symfony/Component/Form/FormBuilder.php | 4 + src/Symfony/Component/Form/Util/FormUtil.php | 2 + .../Component/Form/AbstractLayoutTest.php | 530 +++++++----------- .../Core/Type/CollectionTypeTest.php | 12 +- .../Extension/Core/Type/FieldTypeTest.php | 10 + .../Tests/Component/Form/FormBuilderTest.php | 31 + .../Symfony/Tests/Component/Form/FormTest.php | 56 ++ 10 files changed, 361 insertions(+), 327 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php b/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php index 66d49c747c..20ac8345f1 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php @@ -25,7 +25,7 @@ class CollectionType extends AbstractType public function buildForm(FormBuilder $builder, array $options) { if ($options['allow_add'] && $options['prototype']) { - $prototype = $builder->create('$$' . $options['prototype_name'] . '$$', $options['type'], $options['options']); + $prototype = $builder->create($options['prototype_name'], $options['type'], $options['options']); $builder->setAttribute('prototype', $prototype->getForm()); } @@ -78,7 +78,7 @@ class CollectionType extends AbstractType 'allow_add' => false, 'allow_delete' => false, 'prototype' => true, - 'prototype_name' => 'name', + 'prototype_name' => '__name__', 'type' => 'text', 'options' => array(), ); diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FieldType.php b/src/Symfony/Component/Form/Extension/Core/Type/FieldType.php index 05aaf230f1..4de3fe16f2 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FieldType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FieldType.php @@ -88,6 +88,11 @@ class FieldType extends AbstractType } else { $id = $name; $fullName = $name; + + // Strip leading underscores and digits. These are allowed in + // form names, but not in HTML4 ID attributes. + // http://www.w3.org/TR/html401/struct/global.html#adef-id + $id = ltrim($id, '_0123456789'); } $types = array(); diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index aa26db6bf6..9a9b58f2f2 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -193,6 +193,10 @@ class Form implements \IteratorAggregate, FormInterface $required = false, $readOnly = false, $errorBubbling = false, $emptyData = null, array $attributes = array()) { + $name = (string) $name; + + self::validateName($name); + foreach ($clientTransformers as $transformer) { if (!$transformer instanceof DataTransformerInterface) { throw new UnexpectedTypeException($transformer, 'Symfony\Component\Form\DataTransformerInterface'); @@ -211,7 +215,7 @@ class Form implements \IteratorAggregate, FormInterface } } - $this->name = (string) $name; + $this->name = $name; $this->dispatcher = $dispatcher; $this->types = $types; $this->clientTransformers = $clientTransformers; @@ -1055,4 +1059,32 @@ class Form implements \IteratorAggregate, FormInterface return $value; } + + /** + * Validates whether the given variable is a valid form name. + * + * A name is accepted if it + * + * * is empty + * * starts with a letter, digit or underscore + * * contains only letters, digits, numbers, underscores ("_"), + * hyphens ("-") and colons (":") + * + * @param string $name The tested form name. + * @throws UnexpectedTypeException If the name is not a string. + * @throws \InvalidArgumentException If the name contains invalid characters. + */ + static public function validateName($name) + { + if (!is_string($name)) { + throw new UnexpectedTypeException($name, 'string'); + } + + if ($name !== '' && !preg_match('/^[a-zA-Z0-9_][a-zA-Z0-9_\-:]*$/D', $name)) { + throw new \InvalidArgumentException(sprintf( + 'The name "%s" contains illegal characters. Names should start with a letter, digit or underscore and only contains letters, digits, numbers, underscores ("_"), hyphens ("-") and colons (":").', + $name + )); + } + } } diff --git a/src/Symfony/Component/Form/FormBuilder.php b/src/Symfony/Component/Form/FormBuilder.php index b3fcfb41fd..9aaf474141 100644 --- a/src/Symfony/Component/Form/FormBuilder.php +++ b/src/Symfony/Component/Form/FormBuilder.php @@ -123,6 +123,10 @@ class FormBuilder */ public function __construct($name, FormFactoryInterface $factory, EventDispatcherInterface $dispatcher, $dataClass = null) { + $name = (string) $name; + + Form::validateName($name); + $this->name = $name; $this->factory = $factory; $this->dispatcher = $dispatcher; diff --git a/src/Symfony/Component/Form/Util/FormUtil.php b/src/Symfony/Component/Form/Util/FormUtil.php index 4ddacd25c5..9a4eec8b50 100644 --- a/src/Symfony/Component/Form/Util/FormUtil.php +++ b/src/Symfony/Component/Form/Util/FormUtil.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Form\Util; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + abstract class FormUtil { /** diff --git a/tests/Symfony/Tests/Component/Form/AbstractLayoutTest.php b/tests/Symfony/Tests/Component/Form/AbstractLayoutTest.php index a5ce3b0695..3693afc013 100644 --- a/tests/Symfony/Tests/Component/Form/AbstractLayoutTest.php +++ b/tests/Symfony/Tests/Component/Form/AbstractLayoutTest.php @@ -115,9 +115,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testEnctype() { - $form = $this->factory->createNamedBuilder('form', 'na&me', null, array( - 'property_path' => 'name', - )) + $form = $this->factory->createNamedBuilder('form', 'name') ->add('file', 'file') ->getForm(); @@ -126,9 +124,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testNoEnctype() { - $form = $this->factory->createNamedBuilder('form', 'na&me', null, array( - 'property_path' => 'name', - )) + $form = $this->factory->createNamedBuilder('form', 'name') ->add('text', 'text') ->getForm(); @@ -137,26 +133,22 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testLabel() { - $form = $this->factory->createNamed('text', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('text', 'name'); $view = $form->createView(); $this->renderWidget($view, array('label' => 'foo')); $html = $this->renderLabel($view); $this->assertMatchesXpath($html, '/label - [@for="na&me"] - [.="[trans]Na&me[/trans]"] + [@for="name"] + [.="[trans]Name[/trans]"] ' ); } public function testLabelOnForm() { - $form = $this->factory->createNamed('date', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('date', 'name'); $view = $form->createView(); $this->renderWidget($view, array('label' => 'foo')); $html = $this->renderLabel($view); @@ -164,22 +156,21 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertMatchesXpath($html, '/label [@class=" required"] - [.="[trans]Na&me[/trans]"] + [.="[trans]Name[/trans]"] ' ); } public function testLabelWithCustomTextPassedAsOption() { - $form = $this->factory->createNamed('text', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('text', 'name', null, array( 'label' => 'Custom label', )); $html = $this->renderLabel($form->createView()); $this->assertMatchesXpath($html, '/label - [@for="na&me"] + [@for="name"] [.="[trans]Custom label[/trans]"] ' ); @@ -187,14 +178,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testLabelWithCustomTextPassedDirectly() { - $form = $this->factory->createNamed('text', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('text', 'name'); $html = $this->renderLabel($form->createView(), 'Custom label'); $this->assertMatchesXpath($html, '/label - [@for="na&me"] + [@for="name"] [.="[trans]Custom label[/trans]"] ' ); @@ -202,15 +191,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testLabelWithCustomTextPassedAsOptionAndDirectly() { - $form = $this->factory->createNamed('text', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('text', 'name', null, array( 'label' => 'Custom label', )); $html = $this->renderLabel($form->createView(), 'Overridden label'); $this->assertMatchesXpath($html, '/label - [@for="na&me"] + [@for="name"] [.="[trans]Overridden label[/trans]"] ' ); @@ -218,9 +206,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testLabelWithCustomOptionsPassedDirectly() { - $form = $this->factory->createNamed('text', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('text', 'name'); $html = $this->renderLabel($form->createView(), null, array( 'attr' => array( 'class' => 'my&class' @@ -229,7 +215,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertMatchesXpath($html, '/label - [@for="na&me"] + [@for="name"] [@class="my&class required"] ' ); @@ -237,9 +223,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testLabelWithCustomTextAndCustomOptionsPassedDirectly() { - $form = $this->factory->createNamed('text', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('text', 'name'); $html = $this->renderLabel($form->createView(), 'Custom label', array( 'attr' => array( 'class' => 'my&class' @@ -248,7 +232,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertMatchesXpath($html, '/label - [@for="na&me"] + [@for="name"] [@class="my&class required"] [.="[trans]Custom label[/trans]"] ' @@ -257,9 +241,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testErrors() { - $form = $this->factory->createNamed('text', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('text', 'name'); $form->addError(new FormError('Error 1')); $form->addError(new FormError('Error 2')); $view = $form->createView(); @@ -295,14 +277,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testCheckedCheckbox() { - $form = $this->factory->createNamed('checkbox', 'na&me', true, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('checkbox', 'name', true); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="checkbox"] - [@name="na&me"] + [@name="name"] [@checked="checked"] [@value="1"] ' @@ -311,14 +291,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testUncheckedCheckbox() { - $form = $this->factory->createNamed('checkbox', 'na&me', false, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('checkbox', 'name', false); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="checkbox"] - [@name="na&me"] + [@name="name"] [not(@checked)] ' ); @@ -326,15 +304,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testCheckboxWithValue() { - $form = $this->factory->createNamed('checkbox', 'na&me', false, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('checkbox', 'name', false, array( 'value' => 'foo&bar', )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="checkbox"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] ' ); @@ -342,8 +319,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoice() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'multiple' => false, 'expanded' => false, @@ -351,7 +327,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [@required="required"] [ ./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] @@ -364,8 +340,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceWithPreferred() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'preferred_choices' => array('&b'), 'multiple' => false, @@ -374,7 +349,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array('separator' => '-- sep --'), '/select - [@name="na&me"] + [@name="name"] [@required="required"] [ ./option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] @@ -389,8 +364,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceWithPreferredAndNoSeparator() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'preferred_choices' => array('&b'), 'multiple' => false, @@ -399,7 +373,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array('separator' => null), '/select - [@name="na&me"] + [@name="name"] [@required="required"] [ ./option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] @@ -412,8 +386,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceWithPreferredAndBlankSeparator() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'preferred_choices' => array('&b'), 'multiple' => false, @@ -422,7 +395,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array('separator' => ''), '/select - [@name="na&me"] + [@name="name"] [@required="required"] [ ./option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"] @@ -436,8 +409,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testChoiceWithOnlyPreferred() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'preferred_choices' => array('&a', '&b'), 'multiple' => false, @@ -453,8 +425,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceNonRequired() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'required' => false, 'multiple' => false, @@ -463,7 +434,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [not(@required)] [ ./option[@value=""][.="[trans][/trans]"] @@ -477,8 +448,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceNonRequiredNoneSelected() { - $form = $this->factory->createNamed('choice', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', null, array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'required' => false, 'multiple' => false, @@ -487,7 +457,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [not(@required)] [ ./option[@value=""][.="[trans][/trans]"] @@ -501,8 +471,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceWithNonRequiredEmptyValue() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'multiple' => false, 'expanded' => false, @@ -512,7 +481,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [not(@required)] [ ./option[@value=""][not(@selected)][.="[trans]Select&Anything&Not&Me[/trans]"] @@ -526,8 +495,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceRequiredWithEmptyValue() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'required' => true, 'multiple' => false, @@ -537,7 +505,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [@required="required"] [ ./option[@value=""][.="[trans]Test&Me[/trans]"] @@ -551,8 +519,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceRequiredWithEmptyValueViaView() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'required' => true, 'multiple' => false, @@ -561,7 +528,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array('empty_value' => ''), '/select - [@name="na&me"] + [@name="name"] [@required="required"] [ ./option[@value=""][.="[trans][/trans]"] @@ -575,8 +542,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceGrouped() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array( 'Group&1' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'Group&2' => array('&c' => 'Choice&C'), @@ -587,7 +553,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [./optgroup[@label="[trans]Group&1[/trans]"] [ ./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] @@ -606,8 +572,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testMultipleChoice() { - $form = $this->factory->createNamed('choice', 'na&me', array('&a'), array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', array('&a'), array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'multiple' => true, 'expanded' => false, @@ -615,7 +580,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me[]"] + [@name="name[]"] [@multiple="multiple"] [ ./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] @@ -628,8 +593,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testMultipleChoiceSkipEmptyValue() { - $form = $this->factory->createNamed('choice', 'na&me', array('&a'), array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', array('&a'), array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'multiple' => true, 'expanded' => false, @@ -638,7 +602,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me[]"] + [@name="name[]"] [@multiple="multiple"] [ ./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] @@ -651,8 +615,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testMultipleChoiceNonRequired() { - $form = $this->factory->createNamed('choice', 'na&me', array('&a'), array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', array('&a'), array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'required' => false, 'multiple' => true, @@ -661,7 +624,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me[]"] + [@name="name[]"] [@multiple="multiple"] [ ./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"] @@ -674,8 +637,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceExpanded() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'multiple' => false, 'expanded' => true, @@ -684,10 +646,10 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/div [ - ./input[@type="radio"][@name="na&me"][@id="na&me_0"][@value="0"][@checked] - /following-sibling::label[@for="na&me_0"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_1"][@value="1"][not(@checked)] - /following-sibling::label[@for="na&me_1"][.="[trans]Choice&B[/trans]"] + ./input[@type="radio"][@name="name"][@id="name_0"][@value="0"][@checked] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="1"][not(@checked)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] ] [count(./input)=2] ' @@ -696,8 +658,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceExpandedSkipEmptyValue() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'multiple' => false, 'expanded' => true, @@ -707,10 +668,10 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/div [ - ./input[@type="radio"][@name="na&me"][@id="na&me_0"][@checked] - /following-sibling::label[@for="na&me_0"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_1"][not(@checked)] - /following-sibling::label[@for="na&me_1"][.="[trans]Choice&B[/trans]"] + ./input[@type="radio"][@name="name"][@id="name_0"][@checked] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][not(@checked)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] ] [count(./input)=2] ' @@ -719,8 +680,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceExpandedWithBooleanValue() { - $form = $this->factory->createNamed('choice', 'na&me', true, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', true, array( 'choices' => array('1' => 'Choice&A', '0' => 'Choice&B'), 'multiple' => false, 'expanded' => true, @@ -729,10 +689,10 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/div [ - ./input[@type="radio"][@name="na&me"][@id="na&me_0"][@checked] - /following-sibling::label[@for="na&me_0"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_1"][not(@checked)] - /following-sibling::label[@for="na&me_1"][.="[trans]Choice&B[/trans]"] + ./input[@type="radio"][@name="name"][@id="name_0"][@checked] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][not(@checked)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] ] [count(./input)=2] ' @@ -741,8 +701,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testMultipleChoiceExpanded() { - $form = $this->factory->createNamed('choice', 'na&me', array('&a', '&c'), array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', array('&a', '&c'), array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B', '&c' => 'Choice&C'), 'multiple' => true, 'expanded' => true, @@ -752,12 +711,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/div [ - ./input[@type="checkbox"][@name="na&me[0]"][@id="na&me_0"][@checked][not(@required)] - /following-sibling::label[@for="na&me_0"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="checkbox"][@name="na&me[1]"][@id="na&me_1"][not(@checked)][not(@required)] - /following-sibling::label[@for="na&me_1"][.="[trans]Choice&B[/trans]"] - /following-sibling::input[@type="checkbox"][@name="na&me[2]"][@id="na&me_2"][@checked][not(@required)] - /following-sibling::label[@for="na&me_2"][.="[trans]Choice&C[/trans]"] + ./input[@type="checkbox"][@name="name[0]"][@id="name_0"][@checked][not(@required)] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="checkbox"][@name="name[1]"][@id="name_1"][not(@checked)][not(@required)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] + /following-sibling::input[@type="checkbox"][@name="name[2]"][@id="name_2"][@checked][not(@required)] + /following-sibling::label[@for="name_2"][.="[trans]Choice&C[/trans]"] ] [count(./input)=3] ' @@ -766,13 +725,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testCountry() { - $form = $this->factory->createNamed('country', 'na&me', 'AT', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('country', 'name', 'AT'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [./option[@value="AT"][@selected="selected"][.="[trans]Austria[/trans]"]] [count(./option)>200] ' @@ -781,15 +738,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testCountryWithEmptyValue() { - $form = $this->factory->createNamed('country', 'na&me', 'AT', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('country', 'name', 'AT', array( 'empty_value' => 'Select&Country', 'required' => false, )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [./option[@value=""][not(@selected)][.="[trans]Select&Country[/trans]"]] [./option[@value="AT"][@selected="selected"][.="[trans]Austria[/trans]"]] [count(./option)>201] @@ -803,9 +759,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase ->method('generateCsrfToken') ->will($this->returnValue('foo&bar')); - $form = $this->factory->createNamed('csrf', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('csrf', 'name'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input @@ -817,8 +771,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateTime() { - $form = $this->factory->createNamed('datetime', 'na&me', '2011-02-03 04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('datetime', 'name', '2011-02-03 04:05:06', array( 'input' => 'string', 'with_seconds' => false, )); @@ -827,26 +780,26 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./div - [@id="na&me_date"] + [@id="name_date"] [ ./select - [@id="na&me_date_month"] + [@id="name_date_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select - [@id="na&me_date_day"] + [@id="name_date_day"] [./option[@value="3"][@selected="selected"]] /following-sibling::select - [@id="na&me_date_year"] + [@id="name_date_year"] [./option[@value="2011"][@selected="selected"]] ] /following-sibling::div - [@id="na&me_time"] + [@id="name_time"] [ ./select - [@id="na&me_time_hour"] + [@id="name_time_hour"] [./option[@value="4"][@selected="selected"]] /following-sibling::select - [@id="na&me_time_minute"] + [@id="name_time_minute"] [./option[@value="5"][@selected="selected"]] ] ] @@ -857,8 +810,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateTimeWithEmptyValueGlobal() { - $form = $this->factory->createNamed('datetime', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('datetime', 'name', null, array( 'input' => 'string', 'empty_value' => 'Change&Me', 'required' => false, @@ -868,26 +820,26 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./div - [@id="na&me_date"] + [@id="name_date"] [ ./select - [@id="na&me_date_month"] + [@id="name_date_month"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] /following-sibling::select - [@id="na&me_date_day"] + [@id="name_date_day"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] /following-sibling::select - [@id="na&me_date_year"] + [@id="name_date_year"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] ] /following-sibling::div - [@id="na&me_time"] + [@id="name_time"] [ ./select - [@id="na&me_time_hour"] + [@id="name_time_hour"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] /following-sibling::select - [@id="na&me_time_minute"] + [@id="name_time_minute"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] ] ] @@ -898,8 +850,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateTimeWithEmptyValueOnTime() { - $form = $this->factory->createNamed('datetime', 'na&me', '2011-02-03', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('datetime', 'name', '2011-02-03', array( 'input' => 'string', 'empty_value' => array('hour' => 'Change&Me', 'minute' => 'Change&Me'), 'required' => false, @@ -909,26 +860,26 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./div - [@id="na&me_date"] + [@id="name_date"] [ ./select - [@id="na&me_date_month"] + [@id="name_date_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select - [@id="na&me_date_day"] + [@id="name_date_day"] [./option[@value="3"][@selected="selected"]] /following-sibling::select - [@id="na&me_date_year"] + [@id="name_date_year"] [./option[@value="2011"][@selected="selected"]] ] /following-sibling::div - [@id="na&me_time"] + [@id="name_time"] [ ./select - [@id="na&me_time_hour"] + [@id="name_time_hour"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] /following-sibling::select - [@id="na&me_time_minute"] + [@id="name_time_minute"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] ] ] @@ -939,8 +890,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateTimeWithSeconds() { - $form = $this->factory->createNamed('datetime', 'na&me', '2011-02-03 04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('datetime', 'name', '2011-02-03 04:05:06', array( 'input' => 'string', 'with_seconds' => true, )); @@ -949,29 +899,29 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./div - [@id="na&me_date"] + [@id="name_date"] [ ./select - [@id="na&me_date_month"] + [@id="name_date_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select - [@id="na&me_date_day"] + [@id="name_date_day"] [./option[@value="3"][@selected="selected"]] /following-sibling::select - [@id="na&me_date_year"] + [@id="name_date_year"] [./option[@value="2011"][@selected="selected"]] ] /following-sibling::div - [@id="na&me_time"] + [@id="name_time"] [ ./select - [@id="na&me_time_hour"] + [@id="name_time_hour"] [./option[@value="4"][@selected="selected"]] /following-sibling::select - [@id="na&me_time_minute"] + [@id="name_time_minute"] [./option[@value="5"][@selected="selected"]] /following-sibling::select - [@id="na&me_time_second"] + [@id="name_time_second"] [./option[@value="6"][@selected="selected"]] ] ] @@ -982,8 +932,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateTimeSingleText() { - $form = $this->factory->createNamed('datetime', 'na&me', '2011-02-03 04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('datetime', 'name', '2011-02-03 04:05:06', array( 'input' => 'string', 'date_widget' => 'single_text', 'time_widget' => 'single_text', @@ -994,13 +943,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [ ./input [@type="text"] - [@id="na&me_date"] - [@name="na&me[date]"] + [@id="name_date"] + [@name="name[date]"] [@value="Feb 3, 2011"] /following-sibling::input [@type="text"] - [@id="na&me_time"] - [@name="na&me[time]"] + [@id="name_time"] + [@name="name[time]"] [@value="04:05:00"] ] ' @@ -1010,7 +959,6 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateTimeWithWidgetSingleText() { $form = $this->factory->createNamed('datetime', 'name', '2011-02-03 04:05:06', array( - 'property_path' => 'name', 'input' => 'string', 'widget' => 'single_text', )); @@ -1026,8 +974,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateTimeWithWidgetSingleTextIgnoreDateAndTimeWidgets() { - $form = $this->factory->createNamed('datetime', 'na&me', '2011-02-03 04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('datetime', 'name', '2011-02-03 04:05:06', array( 'input' => 'string', 'date_widget' => 'choice', 'time_widget' => 'choice', @@ -1037,7 +984,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="2011-02-03 04:05:00"] ' ); @@ -1045,8 +992,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateChoice() { - $form = $this->factory->createNamed('date', 'na&me', '2011-02-03', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('date', 'name', '2011-02-03', array( 'input' => 'string', 'widget' => 'choice', )); @@ -1055,13 +1001,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_month"] + [@id="name_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select - [@id="na&me_day"] + [@id="name_day"] [./option[@value="3"][@selected="selected"]] /following-sibling::select - [@id="na&me_year"] + [@id="name_year"] [./option[@value="2011"][@selected="selected"]] ] [count(./select)=3] @@ -1071,8 +1017,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateChoiceWithEmptyValueGlobal() { - $form = $this->factory->createNamed('date', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('date', 'name', null, array( 'input' => 'string', 'widget' => 'choice', 'empty_value' => 'Change&Me', @@ -1083,13 +1028,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_month"] + [@id="name_month"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] /following-sibling::select - [@id="na&me_day"] + [@id="name_day"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] /following-sibling::select - [@id="na&me_year"] + [@id="name_year"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] ] [count(./select)=3] @@ -1099,8 +1044,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateChoiceWithEmptyValueOnYear() { - $form = $this->factory->createNamed('date', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('date', 'name', null, array( 'input' => 'string', 'widget' => 'choice', 'required' => false, @@ -1111,13 +1055,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_month"] + [@id="name_month"] [./option[@value="1"]] /following-sibling::select - [@id="na&me_day"] + [@id="name_day"] [./option[@value="1"]] /following-sibling::select - [@id="na&me_year"] + [@id="name_year"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] ] [count(./select)=3] @@ -1127,8 +1071,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateText() { - $form = $this->factory->createNamed('date', 'na&me', '2011-02-03', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('date', 'name', '2011-02-03', array( 'input' => 'string', 'widget' => 'text', )); @@ -1137,15 +1080,15 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./input - [@id="na&me_month"] + [@id="name_month"] [@type="text"] [@value="2"] /following-sibling::input - [@id="na&me_day"] + [@id="name_day"] [@type="text"] [@value="3"] /following-sibling::input - [@id="na&me_year"] + [@id="name_year"] [@type="text"] [@value="2011"] ] @@ -1156,8 +1099,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateSingleText() { - $form = $this->factory->createNamed('date', 'na&me', '2011-02-03', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('date', 'name', '2011-02-03', array( 'input' => 'string', 'widget' => 'single_text', )); @@ -1165,7 +1107,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="Feb 3, 2011"] ' ); @@ -1184,8 +1126,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testBirthDay() { - $form = $this->factory->createNamed('birthday', 'na&me', '2000-02-03', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('birthday', 'name', '2000-02-03', array( 'input' => 'string', )); @@ -1193,13 +1134,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_month"] + [@id="name_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select - [@id="na&me_day"] + [@id="name_day"] [./option[@value="3"][@selected="selected"]] /following-sibling::select - [@id="na&me_year"] + [@id="name_year"] [./option[@value="2000"][@selected="selected"]] ] [count(./select)=3] @@ -1209,8 +1150,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testBirthDayWithEmptyValue() { - $form = $this->factory->createNamed('birthday', 'na&me', '1950-01-01', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('birthday', 'name', '1950-01-01', array( 'input' => 'string', 'empty_value' => '', 'required' => false, @@ -1220,15 +1160,15 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_month"] + [@id="name_month"] [./option[@value=""][.="[trans][/trans]"]] [./option[@value="1"][@selected="selected"]] /following-sibling::select - [@id="na&me_day"] + [@id="name_day"] [./option[@value=""][.="[trans][/trans]"]] [./option[@value="1"][@selected="selected"]] /following-sibling::select - [@id="na&me_year"] + [@id="name_year"] [./option[@value=""][.="[trans][/trans]"]] [./option[@value="1950"][@selected="selected"]] ] @@ -1239,14 +1179,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testEmail() { - $form = $this->factory->createNamed('email', 'na&me', 'foo&bar', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('email', 'name', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="email"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] [not(@maxlength)] ' @@ -1255,15 +1193,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testEmailWithMaxLength() { - $form = $this->factory->createNamed('email', 'na&me', 'foo&bar', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('email', 'name', 'foo&bar', array( 'max_length' => 123, )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="email"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] [@maxlength="123"] ' @@ -1272,9 +1209,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testFile() { - $form = $this->factory->createNamed('file', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('file', 'name'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input @@ -1285,14 +1220,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testHidden() { - $form = $this->factory->createNamed('hidden', 'na&me', 'foo&bar', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('hidden', 'name', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="hidden"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] ' ); @@ -1300,14 +1233,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testInteger() { - $form = $this->factory->createNamed('integer', 'na&me', 123, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('integer', 'name', 123); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="number"] - [@name="na&me"] + [@name="name"] [@value="123"] ' ); @@ -1315,13 +1246,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testLanguage() { - $form = $this->factory->createNamed('language', 'na&me', 'de', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('language', 'name', 'de'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [./option[@value="de"][@selected="selected"][.="[trans]German[/trans]"]] [count(./option)>200] ' @@ -1330,13 +1259,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testLocale() { - $form = $this->factory->createNamed('locale', 'na&me', 'de_AT', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('locale', 'name', 'de_AT'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [./option[@value="de_AT"][@selected="selected"][.="[trans]German (Austria)[/trans]"]] [count(./option)>200] ' @@ -1345,15 +1272,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testMoney() { - $form = $this->factory->createNamed('money', 'na&me', 1234.56, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('money', 'name', 1234.56, array( 'currency' => 'EUR', )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="1234.56"] [contains(.., "€")] ' @@ -1362,14 +1288,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testNumber() { - $form = $this->factory->createNamed('number', 'na&me', 1234.56, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('number', 'name', 1234.56); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="1234.56"] ' ); @@ -1377,22 +1301,19 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testPassword() { - $form = $this->factory->createNamed('password', 'na&me', 'foo&bar', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('password', 'name', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="password"] - [@name="na&me"] + [@name="name"] ' ); } public function testPasswordBoundNotAlwaysEmpty() { - $form = $this->factory->createNamed('password', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('password', 'name', null, array( 'always_empty' => false, )); $form->bind('foo&bar'); @@ -1400,7 +1321,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="password"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] ' ); @@ -1408,15 +1329,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testPasswordWithMaxLength() { - $form = $this->factory->createNamed('password', 'na&me', 'foo&bar', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('password', 'name', 'foo&bar', array( 'max_length' => 123, )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="password"] - [@name="na&me"] + [@name="name"] [@maxlength="123"] ' ); @@ -1424,14 +1344,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testPercent() { - $form = $this->factory->createNamed('percent', 'na&me', 0.1, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('percent', 'name', 0.1); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="10"] [contains(.., "%")] ' @@ -1440,14 +1358,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testCheckedRadio() { - $form = $this->factory->createNamed('radio', 'na&me', true, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('radio', 'name', true); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="radio"] - [@name="na&me"] + [@name="name"] [@checked="checked"] [@value="1"] ' @@ -1456,14 +1372,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testUncheckedRadio() { - $form = $this->factory->createNamed('radio', 'na&me', false, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('radio', 'name', false); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="radio"] - [@name="na&me"] + [@name="name"] [not(@checked)] ' ); @@ -1471,15 +1385,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testRadioWithValue() { - $form = $this->factory->createNamed('radio', 'na&me', false, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('radio', 'name', false, array( 'value' => 'foo&bar', )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="radio"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] ' ); @@ -1487,14 +1400,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTextarea() { - $form = $this->factory->createNamed('textarea', 'na&me', 'foo&bar', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('textarea', 'name', 'foo&bar', array( 'pattern' => 'foo', )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/textarea - [@name="na&me"] + [@name="name"] [not(@pattern)] [.="foo&bar"] ' @@ -1503,14 +1415,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testText() { - $form = $this->factory->createNamed('text', 'na&me', 'foo&bar', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('text', 'name', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] [not(@maxlength)] ' @@ -1519,15 +1429,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTextWithMaxLength() { - $form = $this->factory->createNamed('text', 'na&me', 'foo&bar', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('text', 'name', 'foo&bar', array( 'max_length' => 123, )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] [@maxlength="123"] ' @@ -1536,14 +1445,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSearch() { - $form = $this->factory->createNamed('search', 'na&me', 'foo&bar', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('search', 'name', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="search"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] [not(@maxlength)] ' @@ -1552,8 +1459,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTime() { - $form = $this->factory->createNamed('time', 'na&me', '04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('time', 'name', '04:05:06', array( 'input' => 'string', 'with_seconds' => false, )); @@ -1562,11 +1468,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_hour"] + [@id="name_hour"] [@size="1"] [./option[@value="4"][@selected="selected"]] /following-sibling::select - [@id="na&me_minute"] + [@id="name_minute"] [@size="1"] [./option[@value="5"][@selected="selected"]] ] @@ -1577,8 +1483,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTimeWithSeconds() { - $form = $this->factory->createNamed('time', 'na&me', '04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('time', 'name', '04:05:06', array( 'input' => 'string', 'with_seconds' => true, )); @@ -1587,17 +1492,17 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_hour"] + [@id="name_hour"] [@size="1"] [./option[@value="4"][@selected="selected"]] [count(./option)>23] /following-sibling::select - [@id="na&me_minute"] + [@id="name_minute"] [@size="1"] [./option[@value="5"][@selected="selected"]] [count(./option)>59] /following-sibling::select - [@id="na&me_second"] + [@id="name_second"] [@size="1"] [./option[@value="6"][@selected="selected"]] [count(./option)>59] @@ -1609,8 +1514,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTimeText() { - $form = $this->factory->createNamed('time', 'na&me', '04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('time', 'name', '04:05:06', array( 'input' => 'string', 'widget' => 'text', )); @@ -1620,15 +1524,15 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [ ./input [@type="text"] - [@id="na&me_hour"] - [@name="na&me[hour]"] + [@id="name_hour"] + [@name="name[hour]"] [@value="04"] [@size="1"] [@required="required"] /following-sibling::input [@type="text"] - [@id="na&me_minute"] - [@name="na&me[minute]"] + [@id="name_minute"] + [@name="name[minute]"] [@value="05"] [@size="1"] [@required="required"] @@ -1640,8 +1544,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTimeSingleText() { - $form = $this->factory->createNamed('time', 'na&me', '04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('time', 'name', '04:05:06', array( 'input' => 'string', 'widget' => 'single_text', )); @@ -1649,7 +1552,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="04:05:00"] ' ); @@ -1657,8 +1560,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTimeWithEmptyValueGlobal() { - $form = $this->factory->createNamed('time', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('time', 'name', null, array( 'input' => 'string', 'empty_value' => 'Change&Me', 'required' => false, @@ -1668,11 +1570,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_hour"] + [@id="name_hour"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] [count(./option)>24] /following-sibling::select - [@id="na&me_minute"] + [@id="name_minute"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] [count(./option)>60] ] @@ -1683,8 +1585,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTimeWithEmptyValueOnYear() { - $form = $this->factory->createNamed('time', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('time', 'name', null, array( 'input' => 'string', 'required' => false, 'empty_value' => array('hour' => 'Change&Me'), @@ -1694,11 +1595,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_hour"] + [@id="name_hour"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] [count(./option)>24] /following-sibling::select - [@id="na&me_minute"] + [@id="name_minute"] [./option[@value="1"]] [count(./option)>59] ] @@ -1720,13 +1621,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTimezone() { - $form = $this->factory->createNamed('timezone', 'na&me', 'Europe/Vienna', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('timezone', 'name', 'Europe/Vienna'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [@required="required"] [./optgroup [@label="[trans]Europe[/trans]"] @@ -1740,8 +1639,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTimezoneWithEmptyValue() { - $form = $this->factory->createNamed('timezone', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('timezone', 'name', null, array( 'empty_value' => 'Select&Timezone', 'required' => false, )); @@ -1758,14 +1656,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testUrl() { $url = 'http://www.google.com?foo1=bar1&foo2=bar2'; - $form = $this->factory->createNamed('url', 'na&me', $url, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('url', 'name', $url); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="url"] - [@name="na&me"] + [@name="name"] [@value="http://www.google.com?foo1=bar1&foo2=bar2"] ' ); @@ -1773,7 +1669,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testCollectionPrototype() { - $form = $this->factory->createNamedBuilder('form', 'na&me', array('items' => array('one', 'two', 'three'))) + $form = $this->factory->createNamedBuilder('form', 'name', array('items' => array('one', 'two', 'three'))) ->add('items', 'collection', array('allow_add' => true)) ->getForm() ->createView(); @@ -1781,9 +1677,9 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $html = $this->renderWidget($form); $this->assertMatchesXpath($html, - '//div[@id="na&me_items"][@data-prototype] + '//div[@id="name_items"][@data-prototype] | - //table[@id="na&me_items"][@data-prototype] + //table[@id="name_items"][@data-prototype] ' ); @@ -1791,9 +1687,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testEmptyRootFormName() { - $form = $this->factory->createNamedBuilder('form', '', '', array( - 'property_path' => 'name', - )) + $form = $this->factory->createNamedBuilder('form', '', '') ->add('child', 'text') ->getForm(); diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CollectionTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CollectionTypeTest.php index 8253ba04fd..615a1d4fdc 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CollectionTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CollectionTypeTest.php @@ -125,7 +125,7 @@ class CollectionFormTest extends TypeTestCase 'prototype' => false, )); - $this->assertFalse($form->has('$$name$$')); + $this->assertFalse($form->has('__name__')); } public function testPrototypeMultipartPropagation() @@ -150,7 +150,7 @@ class CollectionFormTest extends TypeTestCase )); $data = $form->getData(); - $this->assertFalse(isset($data['$$name$$'])); + $this->assertFalse(isset($data['__name__'])); } public function testGetDataDoesNotContainsPrototypeNameAfterDataAreSet() @@ -163,7 +163,7 @@ class CollectionFormTest extends TypeTestCase $form->setData(array('foobar.png')); $data = $form->getData(); - $this->assertFalse(isset($data['$$name$$'])); + $this->assertFalse(isset($data['__name__'])); } public function testPrototypeNameOption() @@ -174,15 +174,15 @@ class CollectionFormTest extends TypeTestCase 'allow_add' => true, )); - $this->assertSame('$$name$$', $form->getAttribute('prototype')->getName(), '$$name$$ is the default'); + $this->assertSame('__name__', $form->getAttribute('prototype')->getName(), '__name__ is the default'); $form = $this->factory->create('collection', null, array( 'type' => 'field', 'prototype' => true, 'allow_add' => true, - 'prototype_name' => 'test', + 'prototype_name' => '__test__', )); - $this->assertSame('$$test$$', $form->getAttribute('prototype')->getName()); + $this->assertSame('__test__', $form->getAttribute('prototype')->getName()); } } diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/FieldTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/FieldTypeTest.php index a308215e15..690167679e 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/FieldTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/FieldTypeTest.php @@ -117,6 +117,16 @@ class FieldTypeTest extends TypeTestCase $this->assertEquals('name', $view->get('full_name')); } + public function testStripLeadingUnderscoresAndDigitsFromId() + { + $form = $this->factory->createNamed('field', '_09name'); + $view = $form->createView(); + + $this->assertEquals('name', $view->get('id')); + $this->assertEquals('_09name', $view->get('name')); + $this->assertEquals('_09name', $view->get('full_name')); + } + public function testPassIdAndNameToViewWithParent() { $parent = $this->factory->createNamed('field', 'parent'); diff --git a/tests/Symfony/Tests/Component/Form/FormBuilderTest.php b/tests/Symfony/Tests/Component/Form/FormBuilderTest.php index 87a94348cd..505022d32a 100644 --- a/tests/Symfony/Tests/Component/Form/FormBuilderTest.php +++ b/tests/Symfony/Tests/Component/Form/FormBuilderTest.php @@ -36,6 +36,37 @@ class FormBuilderTest extends \PHPUnit_Framework_TestCase $this->builder = null; } + public function getHtml4Ids() + { + // The full list is tested in FormTest, since both Form and FormBuilder + // use the same implementation internally + return array( + array('#', false), + array('a ', false), + array("a\t", false), + array("a\n", false), + array('a.', false), + ); + } + + /** + * @dataProvider getHtml4Ids + */ + public function testConstructAcceptsOnlyNamesValidAsIdsInHtml4($name, $accepted) + { + try { + new FormBuilder($name, $this->factory, $this->dispatcher); + if (!$accepted) { + $this->fail(sprintf('The value "%s" should not be accepted', $name)); + } + } catch (\InvalidArgumentException $e) { + // if the value was not accepted, but should be, rethrow exception + if ($accepted) { + throw $e; + } + } + } + /** * Changing the name is not allowed, otherwise the name and property path * are not synchronized anymore diff --git a/tests/Symfony/Tests/Component/Form/FormTest.php b/tests/Symfony/Tests/Component/Form/FormTest.php index e662848a4a..e1eb109e3a 100644 --- a/tests/Symfony/Tests/Component/Form/FormTest.php +++ b/tests/Symfony/Tests/Component/Form/FormTest.php @@ -60,6 +60,62 @@ class FormTest extends \PHPUnit_Framework_TestCase new Form('name', $this->dispatcher, array(), array(), array(), null, $validators); } + public function getHtml4Ids() + { + return array( + array('a0', true), + array('a9', true), + array('z0', true), + array('A0', true), + array('A9', true), + array('Z0', true), + array('#', false), + array('a#', false), + array('a$', false), + array('a%', false), + array('a ', false), + array("a\t", false), + array("a\n", false), + array('a-', true), + array('a_', true), + array('a:', true), + // Periods are allowed by the HTML4 spec, but disallowed by us + // because they break the generated property paths + array('a.', false), + // Contrary to the HTML4 spec, we allow names starting with a + // number, otherwise naming fields by collection indices is not + // possible. + // For root forms, leading digits will be stripped from the + // "id" attribute to produce valid HTML4. + array('0', true), + array('9', true), + // Contrary to the HTML4 spec, we allow names starting with an + // underscore, since this is already a widely used practice in + // Symfony2. + // For root forms, leading underscores will be stripped from the + // "id" attribute to produce valid HTML4. + array('_', true), + ); + } + + /** + * @dataProvider getHtml4Ids + */ + public function testConstructAcceptsOnlyNamesValidAsIdsInHtml4($name, $accepted) + { + try { + new Form($name, $this->dispatcher); + if (!$accepted) { + $this->fail(sprintf('The value "%s" should not be accepted', $name)); + } + } catch (\InvalidArgumentException $e) { + // if the value was not accepted, but should be, rethrow exception + if ($accepted) { + throw $e; + } + } + } + public function testDataIsInitializedEmpty() { $norm = new FixedDataTransformer(array( From 28d2f6d38dcd47c03d5e23f52d059bbe36ef78f3 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 19 Jan 2012 17:59:01 +0100 Subject: [PATCH 03/13] Removed duplicated lines from UPGRADE file --- UPGRADE-2.1.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/UPGRADE-2.1.md b/UPGRADE-2.1.md index 00a86f6833..8d13a18f4b 100644 --- a/UPGRADE-2.1.md +++ b/UPGRADE-2.1.md @@ -116,9 +116,3 @@ UPGRADE FROM 2.0 to 2.1 case when you added it manually or when the field is a single-choice field and is not required), you can restore the old behaviour by setting the option "value_strategy" to `ChoiceList::COPY_CHOICE`. - of choices in a choice field has changed. Instead of appending the choice - value, a generated integer is now appended by default. Take care if your - Javascript relies on that. If you can guarantee that your choice values only - contain ASCII letters, digits, letters, colons and underscores, you can - restore the old behaviour by setting the option "index_strategy" of the - choice field to `ChoiceList::COPY_CHOICE`. From d72900e613a7485976730a88f6de56c9f6abe047 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 23 Jan 2012 18:58:56 +0100 Subject: [PATCH 04/13] [Form] Incorporated changes suggested in PR comments --- UPGRADE-2.1.md | 63 ++++++++++--------- .../Form/ChoiceList/EntityChoiceList.php | 2 +- .../CollectionToArrayTransformer.php | 8 +-- .../Extension/Core/ChoiceList/ChoiceList.php | 29 +++++---- .../Core/ChoiceList/ComplexChoiceList.php | 44 ------------- .../Core/ChoiceList/ObjectChoiceList.php | 4 +- ...xChoiceListTest.php => ChoiceListTest.php} | 8 +-- 7 files changed, 59 insertions(+), 99 deletions(-) delete mode 100644 src/Symfony/Component/Form/Extension/Core/ChoiceList/ComplexChoiceList.php rename tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/{ComplexChoiceListTest.php => ChoiceListTest.php} (95%) diff --git a/UPGRADE-2.1.md b/UPGRADE-2.1.md index 8d13a18f4b..c365f1e978 100644 --- a/UPGRADE-2.1.md +++ b/UPGRADE-2.1.md @@ -79,40 +79,45 @@ UPGRADE FROM 2.0 to 2.1 the parent form. * In the template of the choice type, instead of a single "choices" variable - there are now two variables: "choices" and "choice_labels". - "choices" contains the choices in the values (before they were in the keys) - and "choice_labels" contains the matching labels. Both arrays have - identical keys. + there are now two variables: "choices" and "choice_labels" + + "choices" contains the choices in the values (before they were in the keys) + and "choice_labels" contains the matching labels. Both arrays have + identical keys. - Before: + Before: - {% for choice, label in choices %} - - {% endfor %} + {% for choice, label in choices %} + + {% endfor %} - After: + After: - {% for index, choice in choices %} - - {% endfor %} + {% for index, choice in choices %} + + {% endfor %} * The strategy for generating the HTML attributes "id" and "name" - of choices in a choice field has changed. Instead of appending the choice - value, a generated integer is now appended by default. Take care if your - Javascript relies on that. If you can guarantee that your choice values only - contain ASCII letters, digits, letters, colons and underscores, you can - restore the old behaviour by setting the option "index_strategy" of the - choice field to `ChoiceList::COPY_CHOICE`. + of choices in a choice field has changed + + Instead of appending the choice value, a generated integer is now appended + by default. Take care if your Javascript relies on that. If you can + guarantee that your choice values only contain ASCII letters, digits, + letters, colons and underscores, you can restore the old behaviour by + setting the option "index_strategy" of the choice field to + `ChoiceList::COPY_CHOICE`. * The strategy for generating the HTML attributes "value" of choices in a - choice field has changed. Instead of using the choice value, a generated - integer is now stored. Again, take care if your Javascript reads this value. - If your choice field is a non-expanded single-choice field, or if - the choices are guaranteed not to contain the empty string '' (which is the - case when you added it manually or when the field is a single-choice field - and is not required), you can restore the old behaviour by setting the - option "value_strategy" to `ChoiceList::COPY_CHOICE`. + choice field has changed + + Instead of using the choice value, a generated integer is now stored. + Again, take care if your Javascript reads this value. If your choice field + is a non-expanded single-choice field, or if the choices are guaranteed not + to contain the empty string '' (which is the case when you added it manually + or when the field is a single-choice field and is not required), you can + restore the old behaviour by setting the option "value_strategy" to + `ChoiceList::COPY_CHOICE`. diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index ddda8141a3..34a73d1a03 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -386,7 +386,7 @@ class EntityChoiceList extends ObjectChoiceList // The third parameter $preferredChoices is currently not supported parent::initialize($entities, array(), array()); } catch (StringCastException $e) { - throw new StringCastException('Objects in the entity field must have a "__toString()" method defined. Alternatively you can set the "property" option.', null, $e); + throw new StringCastException(str_replace('argument $labelPath', 'option "property"', $e->getMessage()), null, $e); } $this->loaded = true; diff --git a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php index 94ff98359e..2fa93356e4 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php +++ b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php @@ -26,9 +26,9 @@ class CollectionToArrayTransformer implements DataTransformerInterface /** * Transforms a collection into an array. * - * @param Collection|object $collection A collection of entities, a single entity or NULL + * @param Collection $collection A collection of entities * - * @return mixed An array of choice keys, a single key or NULL + * @return mixed An array of entities */ public function transform($collection) { @@ -46,9 +46,9 @@ class CollectionToArrayTransformer implements DataTransformerInterface /** * Transforms choice keys into entities. * - * @param mixed $keys An array of keys, a single key or NULL + * @param mixed $keys An array of entities * - * @return Collection|object A collection of entities, a single entity or NULL + * @return Collection A collection of entities */ public function reverseTransform($array) { diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php index f77eb205be..9605d211ce 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -19,7 +19,7 @@ use Symfony\Component\Form\Exception\UnexpectedTypeException; * * @author Bernhard Schussek */ -abstract class ChoiceList implements ChoiceListInterface +class ChoiceList implements ChoiceListInterface { /** * Strategy creating new indices/values by creating a copy of the choice. @@ -132,8 +132,7 @@ abstract class ChoiceList implements ChoiceListInterface * @param integer $indexStrategy The strategy used to create choice indices. * One of COPY_CHOICE and GENERATE. */ - public function __construct($choices, array $labels, - array $preferredChoices = array(), $valueStrategy, $indexStrategy) + public function __construct($choices, array $labels, array $preferredChoices = array(), $valueStrategy = self::GENERATE, $indexStrategy = self::GENERATE) { $this->valueStrategy = $valueStrategy; $this->indexStrategy = $indexStrategy; @@ -152,10 +151,6 @@ abstract class ChoiceList implements ChoiceListInterface */ protected function initialize($choices, array $labels, array $preferredChoices) { - if (!is_array($choices) && !$choices instanceof \Traversable) { - throw new UnexpectedTypeException($choices, 'array or \Traversable'); - } - $this->choices = array(); $this->values = array(); $this->labels = array(); @@ -277,7 +272,7 @@ abstract class ChoiceList implements ChoiceListInterface // If the values are identical to the choices, we can just return them // to improve performance a little bit - if ($this->valueStrategy === self::COPY_CHOICE) { + if (self::COPY_CHOICE === $this->valueStrategy) { return $this->fixChoices(array_intersect($values, $this->values)); } @@ -289,7 +284,7 @@ abstract class ChoiceList implements ChoiceListInterface $choices[] = $this->choices[$i]; unset($values[$j]); - if (count($values) === 0) { + if (0 === count($values)) { break 2; } } @@ -314,7 +309,7 @@ abstract class ChoiceList implements ChoiceListInterface // If the values are identical to the choices, we can just return them // to improve performance a little bit - if ($this->valueStrategy === self::COPY_CHOICE) { + if (self::COPY_CHOICE === $this->valueStrategy) { return $this->fixValues(array_intersect($choices, $this->choices)); } @@ -326,7 +321,7 @@ abstract class ChoiceList implements ChoiceListInterface $values[] = $this->values[$i]; unset($choices[$j]); - if (count($choices) === 0) { + if (0 === count($choices)) { break 2; } } @@ -356,7 +351,7 @@ abstract class ChoiceList implements ChoiceListInterface $indices[] = $i; unset($choices[$j]); - if (count($choices) === 0) { + if (0 === count($choices)) { break 2; } } @@ -386,7 +381,7 @@ abstract class ChoiceList implements ChoiceListInterface $indices[] = $i; unset($values[$j]); - if (count($values) === 0) { + if (0 === count($values)) { break 2; } } @@ -413,6 +408,10 @@ abstract class ChoiceList implements ChoiceListInterface */ protected function addChoices(&$bucketForPreferred, &$bucketForRemaining, $choices, $labels, array $preferredChoices) { + if (!is_array($choices) && !$choices instanceof \Traversable) { + throw new UnexpectedTypeException($choices, 'array or \Traversable'); + } + // Add choices to the nested buckets foreach ($choices as $group => $choice) { if (is_array($choice)) { @@ -537,7 +536,7 @@ abstract class ChoiceList implements ChoiceListInterface */ protected function createIndex($choice) { - if ($this->indexStrategy === self::COPY_CHOICE) { + if (self::COPY_CHOICE === $this->indexStrategy) { return $choice; } @@ -555,7 +554,7 @@ abstract class ChoiceList implements ChoiceListInterface */ protected function createValue($choice) { - if ($this->valueStrategy === self::COPY_CHOICE) { + if (self::COPY_CHOICE === $this->valueStrategy) { return $choice; } diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ComplexChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ComplexChoiceList.php deleted file mode 100644 index 3528042387..0000000000 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ComplexChoiceList.php +++ /dev/null @@ -1,44 +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\Extension\Core\ChoiceList; - -use Symfony\Component\Form\Util\FormUtil; - -use Symfony\Component\Form\Exception\UnexpectedTypeException; - -/** - * A choice list that can store arbitrary scalar and object choices. - * - * Arrays as choices are not supported. - * - * @author Bernhard Schussek - */ -class ComplexChoiceList extends ChoiceList -{ - /** - * Creates a new complex choice list. - * - * @param array $choices The array of choices. Choices may also be given - * as hierarchy of unlimited depth. Hierarchies are - * created by creating nested arrays. The title of - * the sub-hierarchy can be stored in the array - * key pointing to the nested array. - * @param array $labels The array of labels. The structure of this array - * should match the structure of $choices. - * @param array $preferredChoices A flat array of choices that should be - * presented to the user with priority. - */ - public function __construct(array $choices, array $labels, array $preferredChoices = array()) - { - parent::__construct($choices, $labels, $preferredChoices, self::GENERATE, self::GENERATE); - } -} diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php index 08225ba6a0..f5ea145437 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php @@ -121,7 +121,7 @@ class ObjectChoiceList extends ChoiceList $group = null; } - if ($group === null) { + if (null === $group) { $groupedChoices[$i] = $choice; } else { if (!isset($groupedChoices[$group])) { @@ -194,7 +194,7 @@ class ObjectChoiceList extends ChoiceList } elseif (method_exists($choice, '__toString')) { $labels[$i] = (string) $choice; } else { - throw new StringCastException('Objects passed to the choice field must have a "__toString()" method defined. Alternatively you can set the $labelPath argument to choose the property used as label.'); + throw new StringCastException('A "__toString()" method was not found on the objects of type "' . get_class($choice) . '" passed to the choice field. To read a custom getter instead, set the argument $labelPath to the desired property path.'); } } } diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ComplexChoiceListTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ChoiceListTest.php similarity index 95% rename from tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ComplexChoiceListTest.php rename to tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ChoiceListTest.php index 4b61c16df3..9dc537a6ee 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ComplexChoiceListTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ChoiceListTest.php @@ -11,9 +11,9 @@ namespace Symfony\Tests\Component\Form\Extension\Core\ChoiceList; -use Symfony\Component\Form\Extension\Core\ChoiceList\ComplexChoiceList; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; -class ComplexChoiceListTest extends \PHPUnit_Framework_TestCase +class ChoiceListTest extends \PHPUnit_Framework_TestCase { private $obj1; @@ -34,7 +34,7 @@ class ComplexChoiceListTest extends \PHPUnit_Framework_TestCase $this->obj3 = new \stdClass(); $this->obj4 = new \stdClass(); - $this->list = new ComplexChoiceList( + $this->list = new ChoiceList( array( 'Group 1' => array($this->obj1, $this->obj2), 'Group 2' => array($this->obj3, $this->obj4), @@ -60,7 +60,7 @@ class ComplexChoiceListTest extends \PHPUnit_Framework_TestCase public function testInitArray() { - $this->list = new ComplexChoiceList( + $this->list = new ChoiceList( array($this->obj1, $this->obj2, $this->obj3, $this->obj4), array('A', 'B', 'C', 'D'), array($this->obj2) From f533ef0e1ba28319e84da7729055ea3a8237fe0a Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 24 Jan 2012 01:07:33 +0100 Subject: [PATCH 05/13] [Form] Added ChoiceView class for passing choice-related data to the view --- .../Form/ChoiceList/EntityChoiceList.php | 65 ++-------- .../Bridge/Twig/Extension/FormExtension.php | 5 +- .../views/Form/form_div_layout.html.twig | 6 +- .../views/Form/choice_options.html.php | 6 +- .../Templating/Helper/FormHelper.php | 5 +- .../Extension/Core/ChoiceList/ChoiceList.php | 120 ++++-------------- .../Core/ChoiceList/ChoiceListInterface.php | 48 ++----- .../Form/Extension/Core/Type/ChoiceType.php | 61 +++++---- .../Form/Extension/Core/View/ChoiceView.php | 66 ++++++++++ .../Form/ChoiceList/EntityChoiceListTest.php | 17 +-- .../Doctrine/Form/Type/EntityTypeTest.php | 22 ++-- .../Core/ChoiceList/ChoiceListTest.php | 31 ++--- .../Core/ChoiceList/ObjectChoiceListTest.php | 84 +++++------- .../Core/ChoiceList/SimpleChoiceListTest.php | 50 +++----- .../Extension/Core/Type/ChoiceTypeTest.php | 42 ++++-- .../Extension/Core/Type/CountryTypeTest.php | 17 +-- .../Form/Extension/Core/Type/DateTypeTest.php | 38 ++++-- .../Extension/Core/Type/LanguageTypeTest.php | 18 +-- .../Extension/Core/Type/LocaleTypeTest.php | 11 +- .../Form/Extension/Core/Type/TimeTypeTest.php | 17 ++- .../Extension/Core/Type/TimezoneTypeTest.php | 7 +- 21 files changed, 340 insertions(+), 396 deletions(-) create mode 100644 src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index 34a73d1a03..c1fe98245a 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -110,22 +110,6 @@ class EntityChoiceList extends ObjectChoiceList return parent::getChoices(); } - /** - * Returns the labels for the entities - * - * @return array - * - * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface - */ - public function getLabels() - { - if (!$this->loaded) { - $this->load(); - } - - return parent::getLabels(); - } - /** * Returns the values for the entities * @@ -143,70 +127,37 @@ class EntityChoiceList extends ObjectChoiceList } /** - * Returns the values of the entities that should be presented to the user - * with priority. + * Returns the choice views of the preferred choices as nested array with + * the choice groups as top-level keys. * * @return array * * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface */ - public function getPreferredValues() + public function getPreferredViews() { if (!$this->loaded) { $this->load(); } - return parent::getPreferredValues(); + return parent::getPreferredViews(); } /** - * Returns the values of the entities that should be presented to the user - * with priority as nested array with the choice groups as top-level keys. + * Returns the choice views of the choices that are not preferred as nested + * array with the choice groups as top-level keys. * * @return array * * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface */ - public function getPreferredValueHierarchy() + public function getRemainingViews() { if (!$this->loaded) { $this->load(); } - return parent::getPreferredValueHierarchy(); - } - - /** - * Returns the values of the entities that are not preferred. - * - * @return array - * - * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface - */ - public function getRemainingValues() - { - if (!$this->loaded) { - $this->load(); - } - - return parent::getRemainingValues(); - } - - /** - * Returns the values of the entities that are not preferred as nested array - * with the choice groups as top-level keys. - * - * @return array - * - * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface - */ - public function getRemainingValueHierarchy() - { - if (!$this->loaded) { - $this->load(); - } - - return parent::getRemainingValueHierarchy(); + return parent::getRemainingViews(); } /** diff --git a/src/Symfony/Bridge/Twig/Extension/FormExtension.php b/src/Symfony/Bridge/Twig/Extension/FormExtension.php index 2de31d73ba..cd9f808a45 100644 --- a/src/Symfony/Bridge/Twig/Extension/FormExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/FormExtension.php @@ -15,6 +15,7 @@ use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser; use Symfony\Component\Form\FormView; use Symfony\Component\Form\Exception\FormException; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Util\FormUtil; /** @@ -95,9 +96,9 @@ class FormExtension extends \Twig_Extension return FormUtil::isChoiceGroup($label); } - public function isChoiceSelected(FormView $view, $choice) + public function isChoiceSelected(FormView $view, ChoiceView $choice) { - return FormUtil::isChoiceSelected($choice, $view->get('value')); + return FormUtil::isChoiceSelected($choice->getValue(), $view->get('value')); } /** diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index 4639a1c408..f07d31f17b 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -29,12 +29,12 @@ {% for index, choice in options %} {% if _form_is_choice_group(choice) %} - {% for nested_index, nested_choice in choice %} - + {% for nested_choice in choice %} + {% endfor %} {% else %} - + {% endif %} {% endfor %} {% endspaceless %} diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php index 12281606d4..66c664fb1d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php @@ -1,11 +1,11 @@ $choice): ?> isChoiceGroup($choice)): ?> - $nested_choice): ?> - + + - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php index 084381aa1e..3fac287cf3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php @@ -16,6 +16,7 @@ use Symfony\Component\Templating\EngineInterface; use Symfony\Component\Form\FormView; use Symfony\Component\Form\Exception\FormException; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Util\FormUtil; /** @@ -63,9 +64,9 @@ class FormHelper extends Helper return FormUtil::isChoiceGroup($label); } - public function isChoiceSelected(FormView $view, $choice) + public function isChoiceSelected(FormView $view, ChoiceView $choice) { - return FormUtil::isChoiceSelected($choice, $view->get('value')); + return FormUtil::isChoiceSelected($choice->getValue(), $view->get('value')); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php index 9605d211ce..5059fb2454 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -13,6 +13,7 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; use Symfony\Component\Form\Util\FormUtil; use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; /** * Base class for choice list implementations. @@ -59,43 +60,20 @@ class ChoiceList implements ChoiceListInterface private $values = array(); /** - * The choice labels with the indices of the matching choices as keys. - * - * @var array - */ - private $labels = array(); - - /** - * The preferred values as flat array with the indices of the matching - * choices as keys. - * - * @var array - */ - private $preferredValues = array(); - - /** - * The non-preferred values as flat array with the indices of the matching - * choices as keys. - * - * @var array - */ - private $remainingValues = array(); - - /** - * The preferred values as hierarchy containing also the choice groups + * The preferred view objects as hierarchy containing also the choice groups * with the indices of the matching choices as bottom-level keys. * * @var array */ - private $preferredValueHierarchy = array(); + private $preferredViews = array(); /** - * The non-preferred values as hierarchy containing also the choice groups - * with the indices of the matching choices as bottom-level keys. + * The non-preferred view objects as hierarchy containing also the choice + * groups with the indices of the matching choices as bottom-level keys. * * @var array */ - private $remainingValueHierarchy = array(); + private $remainingViews = array(); /** * The strategy used for creating choice indices. @@ -153,15 +131,12 @@ class ChoiceList implements ChoiceListInterface { $this->choices = array(); $this->values = array(); - $this->labels = array(); - $this->preferredValues = array(); - $this->preferredValueHierarchy = array(); - $this->remainingValues = array(); - $this->remainingValueHierarchy = array(); + $this->preferredViews = array(); + $this->remainingViews = array(); $this->addChoices( - $this->preferredValueHierarchy, - $this->remainingValueHierarchy, + $this->preferredViews, + $this->remainingViews, $choices, $labels, $preferredChoices @@ -180,18 +155,6 @@ class ChoiceList implements ChoiceListInterface return $this->choices; } - /** - * Returns the labels for the choices - * - * @return array - * - * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface - */ - public function getLabels() - { - return $this->labels; - } - /** * Returns the values for the choices * @@ -205,56 +168,29 @@ class ChoiceList implements ChoiceListInterface } /** - * Returns the values of the choices that should be presented to the user - * with priority. + * Returns the choice views of the preferred choices as nested array with + * the choice groups as top-level keys. * * @return array * * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface */ - public function getPreferredValues() + public function getPreferredViews() { - return $this->preferredValues; + return $this->preferredViews; } /** - * Returns the values of the choices that should be presented to the user - * with priority as nested array with the choice groups as top-level keys. + * Returns the choice views of the choices that are not preferred as nested + * array with the choice groups as top-level keys. * * @return array * * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface */ - public function getPreferredValueHierarchy() + public function getRemainingViews() { - return $this->preferredValueHierarchy; - } - - /** - * Returns the values of the choices that are not preferred. - * - * @return array - * - * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface - */ - public function getRemainingValues() - { - return $this->remainingValues; - } - - /** - * Returns the values of the choices that are not preferred as nested array - * with the choice groups as top-level keys. - * - * @return array A nested array containing the values with the corresponding - * choice indices as keys on the lower levels and the choice - * group names in the keys of the topmost level. - * - * @see getPreferredValueHierarchy - */ - public function getRemainingValueHierarchy() - { - return $this->remainingValueHierarchy; + return $this->remainingViews; } /** @@ -395,9 +331,9 @@ class ChoiceList implements ChoiceListInterface * Recursively adds the given choices to the list. * * @param array $bucketForPreferred The bucket where to store the preferred - * values. + * view objects. * @param array $bucketForRemaining The bucket where to store the - * non-preferred values. + * non-preferred view objects. * @param array $choices The list of choices. * @param array $labels The labels corresponding to the choices. * @param array $preferredChoices The preferred choices. @@ -447,9 +383,9 @@ class ChoiceList implements ChoiceListInterface * * @param string $group The name of the group. * @param array $bucketForPreferred The bucket where to store the preferred - * values. + * view objects. * @param array $bucketForRemaining The bucket where to store the - * non-preferred values. + * non-preferred view objects. * @param array $choices The list of choices in the group. * @param array $labels The labels corresponding to the choices in the group. * @param array $preferredChoices The preferred choices. @@ -482,9 +418,9 @@ class ChoiceList implements ChoiceListInterface * Adds a new choice. * * @param array $bucketForPreferred The bucket where to store the preferred - * values. + * view objects. * @param array $bucketForRemaining The bucket where to store the - * non-preferred values. + * non-preferred view objects. * @param mixed $choice The choice to add. * @param string $label The label for the choice. * @param array $preferredChoices The preferred choices. @@ -495,17 +431,15 @@ class ChoiceList implements ChoiceListInterface // Always store values as strings to facilitate comparisons $value = $this->fixValue($this->createValue($choice)); + $view = new ChoiceView($value, $label); $this->choices[$index] = $this->fixChoice($choice); - $this->labels[$index] = $label; $this->values[$index] = $value; if ($this->isPreferred($choice, $preferredChoices)) { - $bucketForPreferred[$index] = $value; - $this->preferredValues[$index] = $value; + $bucketForPreferred[$index] = $view; } else { - $bucketForRemaining[$index] = $value; - $this->remainingValues[$index] = $value; + $bucketForRemaining[$index] = $view; } } diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php index 85df8d8d88..206727dde3 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php @@ -40,13 +40,6 @@ interface ChoiceListInterface */ function getChoices(); - /** - * Returns the labels for the choices - * - * @return array The labels with the corresponding choice indices as keys. - */ - function getLabels(); - /** * Returns the values for the choices * @@ -55,43 +48,26 @@ interface ChoiceListInterface function getValues(); /** - * Returns the values of the choices that should be presented to the user - * with priority. + * Returns the choice views of the preferred choices as nested array with + * the choice groups as top-level keys. * - * @return array The values with the corresponding choice indices as keys. + * @return array A nested array containing the views with the corresponding + * choice indices as keys on the lowest levels and the choice + * group names in the keys of the higher levels. */ - function getPreferredValues(); + function getPreferredViews(); /** - * Returns the values of the choices that should be presented to the user - * with priority as nested array with the choice groups as top-level keys. + * Returns the choice views of the choices that are not preferred as nested + * array with the choice groups as top-level keys. * - * @return array A nested array containing the values with the corresponding - * choice indices as keys on the lower levels and the choice - * group names in the keys of the topmost level. - */ - function getPreferredValueHierarchy(); - - /** - * Returns the values of the choices that are not preferred. - * - * @return array The values with the corresponding choice indices as keys. + * @return array A nested array containing the views with the corresponding + * choice indices as keys on the lowest levels and the choice + * group names in the keys of the higher levels. * * @see getPreferredValues */ - function getRemainingValues(); - - /** - * Returns the values of the choices that are not preferred as nested array - * with the choice groups as top-level keys. - * - * @return array A nested array containing the values with the corresponding - * choice indices as keys on the lower levels and the choice - * group names in the keys of the topmost level. - * - * @see getPreferredValueHierarchy - */ - function getRemainingValueHierarchy(); + function getRemainingViews(); /** * Returns the choices corresponding to the given values. diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 378a795e6b..358c4317b6 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -52,28 +52,8 @@ class ChoiceType extends AbstractType } if ($options['expanded']) { - // Load choices already if expanded - $values = $options['choice_list']->getValues(); - $labels = $options['choice_list']->getLabels(); - - foreach ($values as $i => $value) { - if ($options['multiple']) { - $builder->add((string) $i, 'checkbox', array( - 'value' => $value, - 'label' => $labels[$i], - // The user can check 0 or more checkboxes. If required - // is true, he is required to check all of them. - 'required' => false, - 'translation_domain' => $options['translation_domain'], - )); - } else { - $builder->add((string) $i, 'radio', array( - 'value' => $value, - 'label' => $labels[$i], - 'translation_domain' => $options['translation_domain'], - )); - } - } + $this->addSubFields($builder, $options['choice_list']->getPreferredViews(), $options); + $this->addSubFields($builder, $options['choice_list']->getRemainingViews(), $options); } // empty value @@ -129,9 +109,8 @@ class ChoiceType extends AbstractType $view ->set('multiple', $form->getAttribute('multiple')) ->set('expanded', $form->getAttribute('expanded')) - ->set('preferred_choices', $choiceList->getPreferredValueHierarchy()) - ->set('choices', $choiceList->getRemainingValueHierarchy()) - ->set('choice_labels', $choiceList->getLabels()) + ->set('preferred_choices', $choiceList->getPreferredViews()) + ->set('choices', $choiceList->getRemainingViews()) ->set('separator', '-------------------') ->set('empty_value', $form->getAttribute('empty_value')) ; @@ -181,4 +160,36 @@ class ChoiceType extends AbstractType { return 'choice'; } + + /** + * Adds the sub fields for an expanded choice field. + * + * @param FormBuilder $builder The form builder. + * @param array $choiceViews The choice view objects. + * @param array $options The build options. + */ + private function addSubFields(FormBuilder $builder, array $choiceViews, array $options) + { + foreach ($choiceViews as $i => $choiceView) { + if (is_array($choiceView)) { + // Flatten groups + $this->addSubFields($builder, $choiceView, $options); + } elseif ($options['multiple']) { + $builder->add((string) $i, 'checkbox', array( + 'value' => $choiceView->getValue(), + 'label' => $choiceView->getLabel(), + // The user can check 0 or more checkboxes. If required + // is true, he is required to check all of them. + 'required' => false, + 'translation_domain' => $options['translation_domain'], + )); + } else { + $builder->add((string) $i, 'radio', array( + 'value' => $choiceView->getValue(), + 'label' => $choiceView->getLabel(), + 'translation_domain' => $options['translation_domain'], + )); + } + } + } } diff --git a/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php new file mode 100644 index 0000000000..4e04247900 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\View; + +/** + * Represents a choice in templates. + * + * @author Bernhard Schussek + */ +class ChoiceView +{ + /** + * The view representation of the choice. + * + * @var string + */ + private $value; + + /** + * The label displayed to humans. + * + * @var string + */ + private $label; + + /** + * Creates a new ChoiceView. + * + * @param string $value The view representation of the choice. + * @param string $label The label displayed to humans. + */ + public function __construct($value, $label) + { + $this->value = $value; + $this->label = $label; + } + + /** + * Returns the choice value. + * + * @return string The view representation of the choice. + */ + public function getValue() + { + return $this->value; + } + + /** + * Returns the choice label. + * + * @return string The label displayed to humans. + */ + public function getLabel() + { + return $this->label; + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Bridge/Doctrine/Form/ChoiceList/EntityChoiceListTest.php b/tests/Symfony/Tests/Bridge/Doctrine/Form/ChoiceList/EntityChoiceListTest.php index 66e2121b7f..2dc3e698ad 100644 --- a/tests/Symfony/Tests/Bridge/Doctrine/Form/ChoiceList/EntityChoiceListTest.php +++ b/tests/Symfony/Tests/Bridge/Doctrine/Form/ChoiceList/EntityChoiceListTest.php @@ -19,6 +19,7 @@ use Symfony\Tests\Bridge\Doctrine\DoctrineOrmTestCase; use Symfony\Tests\Bridge\Doctrine\Fixtures\ItemGroupEntity; use Symfony\Tests\Bridge\Doctrine\Fixtures\SingleIdentEntity; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class EntityChoiceListTest extends DoctrineOrmTestCase { @@ -133,10 +134,10 @@ class EntityChoiceListTest extends DoctrineOrmTestCase ); $this->assertSame(array(1 => $entity1, 2 => $entity2), $choiceList->getChoices()); - $this->assertSame(array( - 'group1' => array(1 => '1'), - 'group2' => array(2 => '2') - ), $choiceList->getRemainingValueHierarchy()); + $this->assertEquals(array( + 'group1' => array(1 => new ChoiceView('1', 'Foo')), + 'group2' => array(2 => new ChoiceView('2', 'Bar')) + ), $choiceList->getRemainingViews()); } public function testGroupBySupportsString() @@ -167,10 +168,10 @@ class EntityChoiceListTest extends DoctrineOrmTestCase $this->assertEquals(array(1 => $item1, 2 => $item2, 3 => $item3, 4 => $item4), $choiceList->getChoices()); $this->assertEquals(array( - 'Group1' => array(1 => '1', 2 => '2'), - 'Group2' => array(3 => '3'), - 4 => '4' - ), $choiceList->getRemainingValueHierarchy()); + 'Group1' => array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), + 'Group2' => array(3 => new ChoiceView('3', 'Baz')), + 4 => new ChoiceView('4', 'Boo!') + ), $choiceList->getRemainingViews()); } public function testGroupByInvalidPropertyPathReturnsFlatChoices() diff --git a/tests/Symfony/Tests/Bridge/Doctrine/Form/Type/EntityTypeTest.php b/tests/Symfony/Tests/Bridge/Doctrine/Form/Type/EntityTypeTest.php index 5ff457f879..8c4f65abb1 100644 --- a/tests/Symfony/Tests/Bridge/Doctrine/Form/Type/EntityTypeTest.php +++ b/tests/Symfony/Tests/Bridge/Doctrine/Form/Type/EntityTypeTest.php @@ -29,6 +29,7 @@ use Symfony\Tests\Bridge\Doctrine\Fixtures\CompositeStringIdentEntity; use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; use Doctrine\ORM\Tools\SchemaTool; use Doctrine\Common\Collections\ArrayCollection; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class EntityTypeTest extends TypeTestCase { @@ -109,8 +110,7 @@ class EntityTypeTest extends TypeTestCase 'property' => 'name' )); - $this->assertSame(array(1 => '1', 2 => '2'), $field->createView()->get('choices')); - $this->assertSame(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choice_labels')); + $this->assertEquals(array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), $field->createView()->get('choices')); } public function testSetDataToUninitializedEntityWithNonRequiredToString() @@ -126,8 +126,7 @@ class EntityTypeTest extends TypeTestCase 'required' => false, )); - $this->assertSame(array(1 => '1', 2 => '2'), $field->createView()->get('choices')); - $this->assertSame(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choice_labels')); + $this->assertEquals(array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), $field->createView()->get('choices')); } public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder() @@ -146,8 +145,7 @@ class EntityTypeTest extends TypeTestCase 'query_builder' => $qb )); - $this->assertSame(array(1 => '1', 2 => '2'), $field->createView()->get('choices')); - $this->assertSame(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choice_labels')); + $this->assertEquals(array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), $field->createView()->get('choices')); } /** @@ -491,8 +489,7 @@ class EntityTypeTest extends TypeTestCase $field->bind('2'); - $this->assertSame(array(1 => '1', 2 => '2'), $field->createView()->get('choices')); - $this->assertSame(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choice_labels')); + $this->assertEquals(array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), $field->createView()->get('choices')); $this->assertTrue($field->isSynchronized()); $this->assertSame($entity2, $field->getData()); $this->assertSame('2', $field->getClientData()); @@ -518,12 +515,11 @@ class EntityTypeTest extends TypeTestCase $field->bind('2'); $this->assertSame('2', $field->getClientData()); - $this->assertSame(array( - 'Group1' => array(1 => '1', 2 => '2'), - 'Group2' => array(3 => '3'), - '4' => '4' + $this->assertEquals(array( + 'Group1' => array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), + 'Group2' => array(3 => new ChoiceView('3', 'Baz')), + '4' => new ChoiceView('4', 'Boo!') ), $field->createView()->get('choices')); - $this->assertSame(array(1 => 'Foo', 2 => 'Bar', 3 => 'Baz', 4 => 'Boo!'), $field->createView()->get('choice_labels')); } public function testDisallowChoicesThatAreNotIncluded_choicesSingleIdentifier() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ChoiceListTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ChoiceListTest.php index 9dc537a6ee..db37a57868 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ChoiceListTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ChoiceListTest.php @@ -12,6 +12,7 @@ namespace Symfony\Tests\Component\Form\Extension\Core\ChoiceList; use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class ChoiceListTest extends \PHPUnit_Framework_TestCase { @@ -67,29 +68,23 @@ class ChoiceListTest extends \PHPUnit_Framework_TestCase ); $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); - $this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels()); - - $this->assertSame(array(1 => '1'), $this->list->getPreferredValues()); - $this->assertSame(array(1 => '1'), $this->list->getPreferredValueHierarchy()); - $this->assertSame(array(0 => '0', 2 => '2', 3 => '3'), $this->list->getRemainingValues()); - $this->assertSame(array(0 => '0', 2 => '2', 3 => '3'), $this->list->getRemainingValueHierarchy()); + $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues()); + $this->assertEquals(array(1 => new ChoiceView('1', 'B')), $this->list->getPreferredViews()); + $this->assertEquals(array(0 => new ChoiceView('0', 'A'), 2 => new ChoiceView('2', 'C'), 3 => new ChoiceView('3', 'D')), $this->list->getRemainingViews()); } public function testInitNestedArray() { $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); - $this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels()); - - $this->assertSame(array(1 => '1', 2 => '2'), $this->list->getPreferredValues()); - $this->assertSame(array( - 'Group 1' => array(1 => '1'), - 'Group 2' => array(2 => '2') - ), $this->list->getPreferredValueHierarchy()); - $this->assertSame(array(0 => '0', 3 => '3'), $this->list->getRemainingValues()); - $this->assertSame(array( - 'Group 1' => array(0 => '0'), - 'Group 2' => array(3 => '3') - ), $this->list->getRemainingValueHierarchy()); + $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues()); + $this->assertEquals(array( + 'Group 1' => array(1 => new ChoiceView('1', 'B')), + 'Group 2' => array(2 => new ChoiceView('2', 'C')) + ), $this->list->getPreferredViews()); + $this->assertEquals(array( + 'Group 1' => array(0 => new ChoiceView('0', 'A')), + 'Group 2' => array(3 => new ChoiceView('3', 'D')) + ), $this->list->getRemainingViews()); } public function testGetIndicesForChoices() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ObjectChoiceListTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ObjectChoiceListTest.php index 8e807a86b0..5635cab57c 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ObjectChoiceListTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ObjectChoiceListTest.php @@ -12,6 +12,7 @@ namespace Symfony\Tests\Component\Form\Extension\Core\ChoiceList; use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class ObjectChoiceListTest_EntityWithToString { @@ -79,29 +80,23 @@ class ObjectChoiceListTest extends \PHPUnit_Framework_TestCase ); $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); - $this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels()); - - $this->assertSame(array(1 => '1'), $this->list->getPreferredValues()); - $this->assertSame(array(1 => '1'), $this->list->getPreferredValueHierarchy()); - $this->assertSame(array(0 => '0', 2 => '2', 3 => '3'), $this->list->getRemainingValues()); - $this->assertSame(array(0 => '0', 2 => '2', 3 => '3'), $this->list->getRemainingValueHierarchy()); + $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues()); + $this->assertEquals(array(1 => new ChoiceView('1', 'B')), $this->list->getPreferredViews()); + $this->assertEquals(array(0 => new ChoiceView('0', 'A'), 2 => new ChoiceView('2', 'C'), 3 => new ChoiceView('3', 'D')), $this->list->getRemainingViews()); } public function testInitNestedArray() { $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); - $this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels()); - - $this->assertSame(array(1 => '1', 2 => '2'), $this->list->getPreferredValues()); - $this->assertSame(array( - 'Group 1' => array(1 => '1'), - 'Group 2' => array(2 => '2') - ), $this->list->getPreferredValueHierarchy()); - $this->assertSame(array(0 => '0', 3 => '3'), $this->list->getRemainingValues()); - $this->assertSame(array( - 'Group 1' => array(0 => '0'), - 'Group 2' => array(3 => '3') - ), $this->list->getRemainingValueHierarchy()); + $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues()); + $this->assertEquals(array( + 'Group 1' => array(1 => new ChoiceView('1', 'B')), + 'Group 2' => array(2 => new ChoiceView('2', 'C')) + ), $this->list->getPreferredViews()); + $this->assertEquals(array( + 'Group 1' => array(0 => new ChoiceView('0', 'A')), + 'Group 2' => array(3 => new ChoiceView('3', 'D')) + ), $this->list->getRemainingViews()); } public function testInitArrayWithGroupPath() @@ -126,20 +121,17 @@ class ObjectChoiceListTest extends \PHPUnit_Framework_TestCase ); $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4, $obj5, $obj6), $this->list->getChoices()); - $this->assertSame(array('A', 'B', 'C', 'D', 'E', 'F'), $this->list->getLabels()); - - $this->assertSame(array(1 => '1', 2 => '2'), $this->list->getPreferredValues()); - $this->assertSame(array( - 'Group 1' => array(1 => '1'), - 'Group 2' => array(2 => '2') - ), $this->list->getPreferredValueHierarchy()); - $this->assertSame(array(0 => '0', 3 => '3', 4 => '4', 5 => '5'), $this->list->getRemainingValues()); - $this->assertSame(array( - 'Group 1' => array(0 => '0'), - 'Group 2' => array(3 => '3'), - 4 => '4', - 5 => '5', - ), $this->list->getRemainingValueHierarchy()); + $this->assertSame(array('0', '1', '2', '3', '4', '5'), $this->list->getValues()); + $this->assertEquals(array( + 'Group 1' => array(1 => new ChoiceView('1', 'B')), + 'Group 2' => array(2 => new ChoiceView('2', 'C')) + ), $this->list->getPreferredViews()); + $this->assertEquals(array( + 'Group 1' => array(0 => new ChoiceView('0', 'A')), + 'Group 2' => array(3 => new ChoiceView('3', 'D')), + 4 => new ChoiceView('4', 'E'), + 5 => new ChoiceView('5', 'F'), + ), $this->list->getRemainingViews()); } /** @@ -179,14 +171,9 @@ class ObjectChoiceListTest extends \PHPUnit_Framework_TestCase ); $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); - $this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels()); - - // Values are always converted to strings to avoid problems with - // comparisons - $this->assertSame(array(1 => '20', 2 => '30'), $this->list->getPreferredValues()); - $this->assertSame(array(1 => '20', 2 => '30'), $this->list->getPreferredValueHierarchy()); - $this->assertSame(array(0 => '10', 3 => '40'), $this->list->getRemainingValues()); - $this->assertSame(array(0 => '10', 3 => '40'), $this->list->getRemainingValueHierarchy()); + $this->assertSame(array('10', '20', '30', '40'), $this->list->getValues()); + $this->assertEquals(array(1 => new ChoiceView('20', 'B'), 2 => new ChoiceView('30', 'C')), $this->list->getPreferredViews()); + $this->assertEquals(array(0 => new ChoiceView('10', 'A'), 3 => new ChoiceView('40', 'D')), $this->list->getRemainingViews()); } public function testInitArrayWithIndexPath() @@ -206,12 +193,9 @@ class ObjectChoiceListTest extends \PHPUnit_Framework_TestCase ); $this->assertSame(array(10 => $this->obj1, 20 => $this->obj2, 30 => $this->obj3, 40 => $this->obj4), $this->list->getChoices()); - $this->assertSame(array(10 => 'A', 20 => 'B', 30 => 'C', 40 => 'D'), $this->list->getLabels()); - - $this->assertSame(array(20 => '1', 30 => '2'), $this->list->getPreferredValues()); - $this->assertSame(array(20 => '1', 30 => '2'), $this->list->getPreferredValueHierarchy()); - $this->assertSame(array(10 => '0', 40 => '3'), $this->list->getRemainingValues()); - $this->assertSame(array(10 => '0', 40 => '3'), $this->list->getRemainingValueHierarchy()); + $this->assertSame(array(10 => '0', 20 => '1', 30 => '2', 40 => '3'), $this->list->getValues()); + $this->assertEquals(array(20 => new ChoiceView('1', 'B'), 30 => new ChoiceView('2', 'C')), $this->list->getPreferredViews()); + $this->assertEquals(array(10 => new ChoiceView('0', 'A'), 40 => new ChoiceView('3', 'D')), $this->list->getRemainingViews()); } public function testInitArrayUsesToString() @@ -226,7 +210,8 @@ class ObjectChoiceListTest extends \PHPUnit_Framework_TestCase ); $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); - $this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels()); + $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues()); + $this->assertEquals(array(0 => new ChoiceView('0', 'A'), 1 => new ChoiceView('1', 'B'), 2 => new ChoiceView('2', 'C'), 3 => new ChoiceView('3', 'D')), $this->list->getRemainingViews()); } /** @@ -239,11 +224,8 @@ class ObjectChoiceListTest extends \PHPUnit_Framework_TestCase $this->obj3 = (object) array('name' => 'C'); $this->obj4 = new ObjectChoiceListTest_EntityWithToString('D'); - $this->list = new ObjectChoiceList( + new ObjectChoiceList( array($this->obj1, $this->obj2, $this->obj3, $this->obj4) ); - - $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); - $this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels()); } } diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/SimpleChoiceListTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/SimpleChoiceListTest.php index 508695cfda..cb6afcd815 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/SimpleChoiceListTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/SimpleChoiceListTest.php @@ -12,8 +12,8 @@ namespace Symfony\Tests\Component\Form\Extension\Core\ChoiceList; use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; - use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class SimpleChoiceListTest extends \PHPUnit_Framework_TestCase { @@ -54,12 +54,9 @@ class SimpleChoiceListTest extends \PHPUnit_Framework_TestCase $this->list = new SimpleChoiceList($choices, array('b'), ChoiceList::GENERATE, ChoiceList::GENERATE); $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getChoices()); - $this->assertSame(array(0 => 'A', 1 => 'B', 2 => 'C'), $this->list->getLabels()); - - $this->assertSame(array(1 => '1'), $this->list->getPreferredValues()); - $this->assertSame(array(1 => '1'), $this->list->getPreferredValueHierarchy()); - $this->assertSame(array(0 => '0', 2 => '2'), $this->list->getRemainingValues()); - $this->assertSame(array(0 => '0', 2 => '2'), $this->list->getRemainingValueHierarchy()); + $this->assertSame(array(0 => '0', 1 => '1', 2 => '2'), $this->list->getValues()); + $this->assertEquals(array(1 => new ChoiceView('1', 'B')), $this->list->getPreferredViews()); + $this->assertEquals(array(0 => new ChoiceView('0', 'A'), 2 => new ChoiceView('2', 'C')), $this->list->getRemainingViews()); } public function testInitArrayValueCopyChoice() @@ -68,12 +65,9 @@ class SimpleChoiceListTest extends \PHPUnit_Framework_TestCase $this->list = new SimpleChoiceList($choices, array('b'), ChoiceList::COPY_CHOICE, ChoiceList::GENERATE); $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getChoices()); - $this->assertSame(array(0 => 'A', 1 => 'B', 2 => 'C'), $this->list->getLabels()); - - $this->assertSame(array(1 => 'b'), $this->list->getPreferredValues()); - $this->assertSame(array(1 => 'b'), $this->list->getPreferredValueHierarchy()); - $this->assertSame(array(0 => 'a', 2 => 'c'), $this->list->getRemainingValues()); - $this->assertSame(array(0 => 'a', 2 => 'c'), $this->list->getRemainingValueHierarchy()); + $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getValues()); + $this->assertEquals(array(1 => new ChoiceView('b', 'B')), $this->list->getPreferredViews()); + $this->assertEquals(array(0 => new ChoiceView('a', 'A'), 2 => new ChoiceView('c', 'C')), $this->list->getRemainingViews()); } public function testInitArrayIndexCopyChoice() @@ -82,29 +76,23 @@ class SimpleChoiceListTest extends \PHPUnit_Framework_TestCase $this->list = new SimpleChoiceList($choices, array('b'), ChoiceList::GENERATE, ChoiceList::COPY_CHOICE); $this->assertSame(array('a' => 'a', 'b' => 'b', 'c' => 'c'), $this->list->getChoices()); - $this->assertSame(array('a' => 'A', 'b' => 'B', 'c' => 'C'), $this->list->getLabels()); - - $this->assertSame(array('b' => '1'), $this->list->getPreferredValues()); - $this->assertSame(array('b' => '1'), $this->list->getPreferredValueHierarchy()); - $this->assertSame(array('a' => '0', 'c' => '2'), $this->list->getRemainingValues()); - $this->assertSame(array('a' => '0', 'c' => '2'), $this->list->getRemainingValueHierarchy()); + $this->assertSame(array('a' => '0', 'b' => '1', 'c' => '2'), $this->list->getValues()); + $this->assertEquals(array('b' => new ChoiceView('1', 'B')), $this->list->getPreferredViews()); + $this->assertEquals(array('a' => new ChoiceView('0', 'A'), 'c' => new ChoiceView('2', 'C')), $this->list->getRemainingViews()); } public function testInitNestedArray() { $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd'), $this->list->getChoices()); - $this->assertSame(array(0 => 'A', 1 => 'B', 2 => 'C', 3 => 'D'), $this->list->getLabels()); - - $this->assertSame(array(1 => '1', 2 => '2'), $this->list->getPreferredValues()); - $this->assertSame(array( - 'Group 1' => array(1 => '1'), - 'Group 2' => array(2 => '2') - ), $this->list->getPreferredValueHierarchy()); - $this->assertSame(array(0 => '0', 3 => '3'), $this->list->getRemainingValues()); - $this->assertSame(array( - 'Group 1' => array(0 => '0'), - 'Group 2' => array(3 => '3') - ), $this->list->getRemainingValueHierarchy()); + $this->assertSame(array(0 => '0', 1 => '1', 2 => '2', 3 => '3'), $this->list->getValues()); + $this->assertEquals(array( + 'Group 1' => array(1 => new ChoiceView('1', 'B')), + 'Group 2' => array(2 => new ChoiceView('2', 'C')) + ), $this->list->getPreferredViews()); + $this->assertEquals(array( + 'Group 1' => array(0 => new ChoiceView('0', 'A')), + 'Group 2' => array(3 => new ChoiceView('3', 'D')) + ), $this->list->getRemainingViews()); } public function testGetIndicesForChoices() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/ChoiceTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/ChoiceTypeTest.php index 8dae89442d..cf26d5ea29 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/ChoiceTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/ChoiceTypeTest.php @@ -12,9 +12,8 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; - use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; - +use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Exception\UnexpectedTypeException; class ChoiceTypeTest extends TypeTestCase @@ -600,8 +599,12 @@ class ChoiceTypeTest extends TypeTestCase )); $view = $form->createView(); - $this->assertSame(array('0', '1', '2', '3'), $view->get('choices')); - $this->assertSame(array('A', 'B', 'C', 'D'), $view->get('choice_labels')); + $this->assertEquals(array( + new ChoiceView('0', 'A'), + new ChoiceView('1', 'B'), + new ChoiceView('2', 'C'), + new ChoiceView('3', 'D'), + ), $view->get('choices')); } public function testPassPreferredChoicesToView() @@ -613,9 +616,14 @@ class ChoiceTypeTest extends TypeTestCase )); $view = $form->createView(); - $this->assertSame(array(0 => '0', 2 => '2'), $view->get('choices')); - $this->assertSame(array(1 => '1', 3 => '3'), $view->get('preferred_choices')); - $this->assertSame(array('A', 'B', 'C', 'D'), $view->get('choice_labels')); + $this->assertEquals(array( + 0 => new ChoiceView('0', 'A'), + 2 => new ChoiceView('2', 'C'), + ), $view->get('choices')); + $this->assertEquals(array( + 1 => new ChoiceView('1', 'B'), + 3 => new ChoiceView('3', 'D'), + ), $view->get('preferred_choices')); } public function testPassHierarchicalChoicesToView() @@ -626,9 +634,23 @@ class ChoiceTypeTest extends TypeTestCase )); $view = $form->createView(); - $this->assertSame(array('Symfony' => array(0 => '0', 2 => '2'), 'Doctrine' => array(4 => '4')), $view->get('choices')); - $this->assertSame(array('Symfony' => array(1 => '1'), 'Doctrine' => array(3 => '3')), $view->get('preferred_choices')); - $this->assertSame(array(0 => 'Bernhard', 1 => 'Fabien', 2 => 'Kris', 3 => 'Jon', 4 => 'Roman'), $view->get('choice_labels')); + $this->assertEquals(array( + 'Symfony' => array( + 0 => new ChoiceView('0', 'Bernhard'), + 2 => new ChoiceView('2', 'Kris'), + ), + 'Doctrine' => array( + 4 => new ChoiceView('4', 'Roman'), + ), + ), $view->get('choices')); + $this->assertEquals(array( + 'Symfony' => array( + 1 => new ChoiceView('1', 'Fabien'), + ), + 'Doctrine' => array( + 3 => new ChoiceView('3', 'Jon'), + ), + ), $view->get('preferred_choices')); } public function testAdjustFullNameForMultipleNonExpanded() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CountryTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CountryTypeTest.php index a6a150a490..5d819af566 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CountryTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CountryTypeTest.php @@ -11,6 +11,7 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class CountryTypeTest extends LocalizedTestCase { @@ -21,18 +22,12 @@ class CountryTypeTest extends LocalizedTestCase $form = $this->factory->create('country'); $view = $form->createView(); $choices = $view->get('choices'); - $labels = $view->get('choice_labels'); - $this->assertContains('DE', $choices); - $this->assertEquals('Deutschland', $labels['DE']); - $this->assertContains('GB', $choices); - $this->assertEquals('Vereinigtes Königreich', $labels['GB']); - $this->assertContains('US', $choices); - $this->assertEquals('Vereinigte Staaten', $labels['US']); - $this->assertContains('FR', $choices); - $this->assertEquals('Frankreich', $labels['FR']); - $this->assertContains('MY', $choices); - $this->assertEquals('Malaysia', $labels['MY']); + $this->assertEquals(new ChoiceView('DE', 'Deutschland'), $choices['DE']); + $this->assertEquals(new ChoiceView('GB', 'Vereinigtes Königreich'), $choices['GB']); + $this->assertEquals(new ChoiceView('US', 'Vereinigte Staaten'), $choices['US']); + $this->assertEquals(new ChoiceView('FR', 'Frankreich'), $choices['FR']); + $this->assertEquals(new ChoiceView('MY', 'Malaysia'), $choices['MY']); } public function testUnknownCountryIsNotIncluded() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/DateTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/DateTypeTest.php index d1b9c45023..1be7affc70 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/DateTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/DateTypeTest.php @@ -11,8 +11,9 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; -require_once __DIR__ . '/LocalizedTestCase.php'; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; +require_once __DIR__ . '/LocalizedTestCase.php'; class DateTypeTest extends LocalizedTestCase { @@ -325,7 +326,10 @@ class DateTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(2010 => '2010', 2011 => '2011'), $view->getChild('year')->get('choice_labels')); + $this->assertEquals(array( + 2010 => new ChoiceView('2010', '2010'), + 2011 => new ChoiceView('2011', '2011'), + ), $view->getChild('year')->get('choices')); } public function testMonthsOption() @@ -336,7 +340,10 @@ class DateTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('month')->get('choice_labels')); + $this->assertEquals(array( + 6 => new ChoiceView('6', '06'), + 7 => new ChoiceView('7', '07'), + ), $view->getChild('month')->get('choices')); } public function testMonthsOptionNumericIfFormatContainsNoMonth() @@ -348,7 +355,10 @@ class DateTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('month')->get('choice_labels')); + $this->assertEquals(array( + 6 => new ChoiceView('6', '06'), + 7 => new ChoiceView('7', '07'), + ), $view->getChild('month')->get('choices')); } public function testMonthsOptionShortFormat() @@ -360,7 +370,10 @@ class DateTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(1 => 'Jän', 4 => 'Apr'), $view->getChild('month')->get('choice_labels')); + $this->assertEquals(array( + 1 => new ChoiceView('1', 'Jän'), + 4 => new ChoiceView('4', 'Apr') + ), $view->getChild('month')->get('choices')); } public function testMonthsOptionLongFormat() @@ -372,7 +385,10 @@ class DateTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(1 => 'Jänner', 4 => 'April'), $view->getChild('month')->get('choice_labels')); + $this->assertEquals(array( + 1 => new ChoiceView('1', 'Jänner'), + 4 => new ChoiceView('4', 'April'), + ), $view->getChild('month')->get('choices')); } public function testMonthsOptionLongFormatWithDifferentTimezone() @@ -384,7 +400,10 @@ class DateTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(1 => 'Jänner', 4 => 'April'), $view->getChild('month')->get('choice_labels')); + $this->assertEquals(array( + 1 => new ChoiceView('1', 'Jänner'), + 4 => new ChoiceView('4', 'April'), + ), $view->getChild('month')->get('choices')); } public function testIsDayWithinRangeReturnsTrueIfWithin() @@ -395,7 +414,10 @@ class DateTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('day')->get('choice_labels')); + $this->assertEquals(array( + 6 => new ChoiceView('6', '06'), + 7 => new ChoiceView('7', '07'), + ), $view->getChild('day')->get('choices')); } public function testIsPartiallyFilledReturnsFalseIfSingleText() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LanguageTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LanguageTypeTest.php index b8a1595bb9..fc80a0e06c 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LanguageTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LanguageTypeTest.php @@ -11,6 +11,7 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class LanguageTypeTest extends LocalizedTestCase { @@ -23,16 +24,11 @@ class LanguageTypeTest extends LocalizedTestCase $choices = $view->get('choices'); $labels = $view->get('choice_labels'); - $this->assertContains('en', $choices); - $this->assertEquals('Englisch', $labels[array_search('en', $choices)]); - $this->assertContains('en_GB', $choices); - $this->assertEquals('Britisches Englisch', $labels[array_search('en_GB', $choices)]); - $this->assertContains('en_US', $choices); - $this->assertEquals('Amerikanisches Englisch', $labels[array_search('en_US', $choices)]); - $this->assertContains('fr', $choices); - $this->assertEquals('Französisch', $labels[array_search('fr', $choices)]); - $this->assertContains('my', $choices); - $this->assertEquals('Birmanisch', $labels[array_search('my', $choices)]); + $this->assertContains(new ChoiceView('en', 'Englisch'), $choices, '', false, false); + $this->assertContains(new ChoiceView('en_GB', 'Britisches Englisch'), $choices, '', false, false); + $this->assertContains(new ChoiceView('en_US', 'Amerikanisches Englisch'), $choices, '', false, false); + $this->assertContains(new ChoiceView('fr', 'Französisch'), $choices, '', false, false); + $this->assertContains(new ChoiceView('my', 'Birmanisch'), $choices, '', false, false); } public function testMultipleLanguagesIsNotIncluded() @@ -41,6 +37,6 @@ class LanguageTypeTest extends LocalizedTestCase $view = $form->createView(); $choices = $view->get('choices'); - $this->assertArrayNotHasKey('mul', $choices); + $this->assertNotContains(new ChoiceView('mul', 'Mehrsprachig'), $choices, '', false, false); } } diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LocaleTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LocaleTypeTest.php index ebd89940de..8f4bd97869 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LocaleTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LocaleTypeTest.php @@ -11,6 +11,7 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class LocaleTypeTest extends LocalizedTestCase { @@ -21,13 +22,9 @@ class LocaleTypeTest extends LocalizedTestCase $form = $this->factory->create('locale'); $view = $form->createView(); $choices = $view->get('choices'); - $labels = $view->get('choice_labels'); - $this->assertContains('en', $choices); - $this->assertEquals('Englisch', $labels[array_search('en', $choices)]); - $this->assertContains('en_GB', $choices); - $this->assertEquals('Englisch (Vereinigtes Königreich)', $labels[array_search('en_GB', $choices)]); - $this->assertContains('zh_Hant_MO', $choices); - $this->assertEquals('Chinesisch (traditionell, Sonderverwaltungszone Macao)', $labels[array_search('zh_Hant_MO', $choices)]); + $this->assertContains(new ChoiceView('en', 'Englisch'), $choices, '', false, false); + $this->assertContains(new ChoiceView('en_GB', 'Englisch (Vereinigtes Königreich)'), $choices, '', false, false); + $this->assertContains(new ChoiceView('zh_Hant_MO', 'Chinesisch (traditionell, Sonderverwaltungszone Macao)'), $choices, '', false, false); } } diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimeTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimeTypeTest.php index 1b46c75207..34676ddf19 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimeTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimeTypeTest.php @@ -11,6 +11,8 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; + require_once __DIR__ . '/LocalizedTestCase.php'; @@ -243,7 +245,10 @@ class TimeTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('hour')->get('choice_labels')); + $this->assertEquals(array( + 6 => new ChoiceView('6', '06'), + 7 => new ChoiceView('7', '07'), + ), $view->getChild('hour')->get('choices')); } public function testIsMinuteWithinRange_returnsTrueIfWithin() @@ -254,7 +259,10 @@ class TimeTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('minute')->get('choice_labels')); + $this->assertEquals(array( + 6 => new ChoiceView('6', '06'), + 7 => new ChoiceView('7', '07'), + ), $view->getChild('minute')->get('choices')); } public function testIsSecondWithinRange_returnsTrueIfWithin() @@ -266,7 +274,10 @@ class TimeTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('second')->get('choice_labels')); + $this->assertEquals(array( + 6 => new ChoiceView('6', '06'), + 7 => new ChoiceView('7', '07'), + ), $view->getChild('second')->get('choices')); } public function testIsPartiallyFilled_returnsFalseIfCompletelyEmpty() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimezoneTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimezoneTypeTest.php index be7db448fe..f8f5126c32 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimezoneTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimezoneTypeTest.php @@ -11,6 +11,7 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class TimezoneTypeTest extends TypeTestCase { @@ -22,11 +23,9 @@ class TimezoneTypeTest extends TypeTestCase $labels = $view->get('choice_labels'); $this->assertArrayHasKey('Africa', $choices); - $this->assertContains('Africa/Kinshasa', $choices['Africa']); - $this->assertEquals('Kinshasa', $labels[array_search('Africa/Kinshasa', $choices['Africa'])]); + $this->assertContains(new ChoiceView('Africa/Kinshasa', 'Kinshasa'), $choices['Africa'], '', false, false); $this->assertArrayHasKey('America', $choices); - $this->assertContains('America/New_York', $choices['America']); - $this->assertEquals('New York', $labels[array_search('America/New_York', $choices['America'])]); + $this->assertContains(new ChoiceView('America/New_York', 'New York'), $choices['America'], '', false, false); } } From 18f92cd331897a21de27cd82170ed45272b6c15c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 24 Jan 2012 01:09:35 +0100 Subject: [PATCH 06/13] [Form] Fixed double choice fixing --- .../Component/Form/Extension/Core/ChoiceList/ChoiceList.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php index 5059fb2454..261a96a663 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -283,7 +283,7 @@ class ChoiceList implements ChoiceListInterface foreach ($this->choices as $i => $choice) { foreach ($choices as $j => $givenChoice) { - if ($choice === $this->fixChoice($givenChoice)) { + if ($choice === $givenChoice) { $indices[] = $i; unset($choices[$j]); From c26b47af8d3642d4936d47cd51ac73b02ec28b6f Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 24 Jan 2012 01:13:50 +0100 Subject: [PATCH 07/13] [Form] Made query parameter name generated by ORMQueryBuilderLoader unique --- .../Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index 5dd51fb10b..2d92b03784 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -71,11 +71,12 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface { $qb = clone ($this->queryBuilder); $alias = current($qb->getRootAliases()); - $where = $qb->expr()->in($alias.'.'.$identifier, "?1"); + $parameter = 'ORMQueryBuilderLoader_getEntitiesByIds_'.$identifier; + $where = $qb->expr()->in($alias.'.'.$identifier, ':'.$parameter); return $qb->andWhere($where) ->getQuery() - ->setParameter(1, $values, Connection::PARAM_STR_ARRAY) + ->setParameter($parameter, $values, Connection::PARAM_STR_ARRAY) ->getResult(); } } From 7c7097675ba185473aa804d427cb7aa9a6b49c24 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 24 Jan 2012 01:23:01 +0100 Subject: [PATCH 08/13] [Form] Fixed text in UPGRADE file --- UPGRADE-2.1.md | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/UPGRADE-2.1.md b/UPGRADE-2.1.md index c365f1e978..601b18b452 100644 --- a/UPGRADE-2.1.md +++ b/UPGRADE-2.1.md @@ -78,29 +78,6 @@ UPGRADE FROM 2.0 to 2.1 enable BC behaviour by setting the option "cascade_validation" to `true` on the parent form. -* In the template of the choice type, instead of a single "choices" variable - there are now two variables: "choices" and "choice_labels" - - "choices" contains the choices in the values (before they were in the keys) - and "choice_labels" contains the matching labels. Both arrays have - identical keys. - - Before: - - {% for choice, label in choices %} - - {% endfor %} - - After: - - {% for index, choice in choices %} - - {% endfor %} - * The strategy for generating the HTML attributes "id" and "name" of choices in a choice field has changed @@ -121,3 +98,27 @@ UPGRADE FROM 2.0 to 2.1 or when the field is a single-choice field and is not required), you can restore the old behaviour by setting the option "value_strategy" to `ChoiceList::COPY_CHOICE`. + +* In the template of the choice type, the structure of the "choices" variable + has changed + + "choices" now contains ChoiceView objects with two getters `getValue()` + and `getLabel()` to access the choice data. The indices of the array + store an index whose generation is controlled by the "index_generation" + option of the choice field. + + Before: + + {% for choice, label in choices %} + + {% endfor %} + + After: + + {% for choice in choices %} + + {% endfor %} From 5f6f75c0263a1bb4535b5fddc3dda62b13cc375a Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 24 Jan 2012 11:59:07 +0100 Subject: [PATCH 09/13] [Form] Fixed outstanding issues mentioned in the PR --- CHANGELOG-2.1.md | 2 +- UPGRADE-2.1.md | 12 ++++---- .../Form/ChoiceList/EntityChoiceList.php | 6 ++-- .../Form/ChoiceList/EntityLoaderInterface.php | 10 ++++--- .../Core/ChoiceList/ChoiceListInterface.php | 28 +++++++++++++++++++ .../Core/ChoiceList/SimpleChoiceList.php | 10 +++---- .../ChoiceToValueTransformer.php | 2 +- 7 files changed, 51 insertions(+), 19 deletions(-) diff --git a/CHANGELOG-2.1.md b/CHANGELOG-2.1.md index 7e1cfe65fc..5dbe07a044 100644 --- a/CHANGELOG-2.1.md +++ b/CHANGELOG-2.1.md @@ -168,7 +168,7 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c * [BC BREAK] greatly improved `ChoiceListInterface` and all of its implementations. `EntityChoiceList` was adapted, the methods `getEntities()`, - `getEntitiesByByKeys()`, `getIdentifier()` and `getIdentifierValues()` were + `getEntitiesByKeys()`, `getIdentifier()` and `getIdentifierValues()` were removed/made private. Instead of the first two you can use `getChoices()` and `getChoicesByValues()`, for the latter two no replacement exists. `ArrayChoiceList` was replaced by `SimpleChoiceList`. diff --git a/UPGRADE-2.1.md b/UPGRADE-2.1.md index 601b18b452..7439f0eb10 100644 --- a/UPGRADE-2.1.md +++ b/UPGRADE-2.1.md @@ -92,12 +92,12 @@ UPGRADE FROM 2.0 to 2.1 choice field has changed Instead of using the choice value, a generated integer is now stored. - Again, take care if your Javascript reads this value. If your choice field - is a non-expanded single-choice field, or if the choices are guaranteed not - to contain the empty string '' (which is the case when you added it manually - or when the field is a single-choice field and is not required), you can - restore the old behaviour by setting the option "value_strategy" to - `ChoiceList::COPY_CHOICE`. + Again, take care if your Javascript reads this value. If your choice field + is a non-expanded single-choice field, or if the choices are guaranteed not + to contain the empty string '' (which is the case when you added it manually + or when the field is a single-choice field and is not required), you can + restore the old behaviour by setting the option "value_strategy" to + `ChoiceList::COPY_CHOICE`. * In the template of the choice type, the structure of the "choices" variable has changed diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index c1fe98245a..ee8019a0fd 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -73,8 +73,10 @@ class EntityChoiceList extends ObjectChoiceList * @param string $class The class name * @param string $labelPath The property path used for the label * @param EntityLoaderInterface $entityLoader An optional query builder - * @param array $entities An array of choices - * @param string $groupPath + * @param array $entities An array of choices + * @param string $groupPath A property path pointing to the property used + * to group the choices. Only allowed if + * the choices are given as flat array. */ public function __construct(ObjectManager $manager, $class, $labelPath = null, EntityLoaderInterface $entityLoader = null, $entities = null, $groupPath = null) { diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php index 4fc8e6ba99..ea459a6487 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php @@ -21,17 +21,19 @@ interface EntityLoaderInterface /** * Returns an array of entities that are valid choices in the corresponding choice list. * - * @return array + * @return array The entities. */ function getEntities(); /** * Returns an array of entities matching the given identifiers. * - * @param string $identifier - * @param array $values + * @param string $identifier The identifier field of the object. This method + * is not applicable for fields with multiple + * identifiers. + * @param array $values The values of the identifiers. * - * @return array + * @return array The entities. */ function getEntitiesByIds($identifier, array $values); } diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php index 206727dde3..122b50e7e5 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php @@ -51,6 +51,20 @@ interface ChoiceListInterface * Returns the choice views of the preferred choices as nested array with * the choice groups as top-level keys. * + * Example: + * + * + * array( + * 'Group 1' => array( + * 10 => ChoiceView object, + * 20 => ChoiceView object, + * ), + * 'Group 2' => array( + * 30 => ChoiceView object, + * ), + * ) + * + * * @return array A nested array containing the views with the corresponding * choice indices as keys on the lowest levels and the choice * group names in the keys of the higher levels. @@ -61,6 +75,20 @@ interface ChoiceListInterface * Returns the choice views of the choices that are not preferred as nested * array with the choice groups as top-level keys. * + * Example: + * + * + * array( + * 'Group 1' => array( + * 10 => ChoiceView object, + * 20 => ChoiceView object, + * ), + * 'Group 2' => array( + * 30 => ChoiceView object, + * ), + * ) + * + * * @return array A nested array containing the views with the corresponding * choice indices as keys on the lowest levels and the choice * group names in the keys of the higher levels. diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php index 16845d648d..f59827e7d8 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php @@ -120,12 +120,12 @@ class SimpleChoiceList extends ChoiceList /** - * Converts the choices to a valid PHP array keys. + * Converts the choices to valid PHP array keys. * - * @param array $choices The choices. - * - * @return array Valid PHP array keys. - */ + * @param array $choices The choices. + * + * @return array Valid PHP array keys. + */ protected function fixChoices(array $choices) { return $this->fixIndices($choices); diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php index e63e0f1941..13d90a9151 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php @@ -53,7 +53,7 @@ class ChoiceToValueTransformer implements DataTransformerInterface $choices = $this->choiceList->getChoicesForValues(array($value)); if (count($choices) !== 1) { - throw new TransformationFailedException('The choice "' . $value . '" does not exist'); + throw new TransformationFailedException('The choice "' . $value . '" does not exist or is not unique'); } $choice = current($choices); From 399af275acdb5f4f881139f88a5b9d1910a8da75 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 24 Jan 2012 12:21:25 +0100 Subject: [PATCH 10/13] [Form] Implemented checks to assert that values and indices generated in choice lists match their requirements --- .../Extension/Core/ChoiceList/ChoiceList.php | 14 ++++++++- src/Symfony/Component/Form/Form.php | 29 ++++++++++++++----- .../Core/ChoiceList/ChoiceListTest.php | 28 ++++++++++++++++++ 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php index 261a96a663..374a65543a 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -11,8 +11,10 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; +use Symfony\Component\Form\Form; use Symfony\Component\Form\Util\FormUtil; use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Exception\InvalidConfigurationException; use Symfony\Component\Form\Extension\Core\View\ChoiceView; /** @@ -429,8 +431,18 @@ class ChoiceList implements ChoiceListInterface { $index = $this->createIndex($choice); + if ('' === $index || null === $index || !Form::isValidName((string)$index)) { + throw new InvalidConfigurationException('The choice list index "' . $index . '" is invalid. Please set the choice field option "index_generation" to ChoiceList::GENERATE.'); + } + + $value = $this->createValue($choice); + + if (!is_scalar($value)) { + throw new InvalidConfigurationException('The choice list value of type "' . gettype($value) . '" should be a scalar. Please set the choice field option "value_generation" to ChoiceList::GENERATE.'); + } + // Always store values as strings to facilitate comparisons - $value = $this->fixValue($this->createValue($choice)); + $value = $this->fixValue($value); $view = new ChoiceView($value, $label); $this->choices[$index] = $this->fixChoice($choice); diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 9a9b58f2f2..5d5d8011ce 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -1063,14 +1063,8 @@ class Form implements \IteratorAggregate, FormInterface /** * Validates whether the given variable is a valid form name. * - * A name is accepted if it - * - * * is empty - * * starts with a letter, digit or underscore - * * contains only letters, digits, numbers, underscores ("_"), - * hyphens ("-") and colons (":") - * * @param string $name The tested form name. + * * @throws UnexpectedTypeException If the name is not a string. * @throws \InvalidArgumentException If the name contains invalid characters. */ @@ -1080,11 +1074,30 @@ class Form implements \IteratorAggregate, FormInterface throw new UnexpectedTypeException($name, 'string'); } - if ($name !== '' && !preg_match('/^[a-zA-Z0-9_][a-zA-Z0-9_\-:]*$/D', $name)) { + if (!self::isValidName($name)) { throw new \InvalidArgumentException(sprintf( 'The name "%s" contains illegal characters. Names should start with a letter, digit or underscore and only contains letters, digits, numbers, underscores ("_"), hyphens ("-") and colons (":").', $name )); } } + + /** + * Returns whether the given variable contains a valid form name. + * + * A name is accepted if it + * + * * is empty + * * starts with a letter, digit or underscore + * * contains only letters, digits, numbers, underscores ("_"), + * hyphens ("-") and colons (":") + * + * @param string $name The tested form name. + * + * @return Boolean Whether the name is valid. + */ + static public function isValidName($name) + { + return $name === '' || preg_match('/^[a-zA-Z0-9_][a-zA-Z0-9_\-:]*$/D', $name); + } } diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ChoiceListTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ChoiceListTest.php index db37a57868..7774b876c1 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ChoiceListTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ChoiceListTest.php @@ -87,6 +87,34 @@ class ChoiceListTest extends \PHPUnit_Framework_TestCase ), $this->list->getRemainingViews()); } + /** + * @expectedException Symfony\Component\Form\Exception\InvalidConfigurationException + */ + public function testInitIndexCopyChoiceWithInvalidIndex() + { + new ChoiceList( + array('a.'), + array('A'), + array(), + ChoiceList::GENERATE, + ChoiceList::COPY_CHOICE + ); + } + + /** + * @expectedException Symfony\Component\Form\Exception\InvalidConfigurationException + */ + public function testInitValueCopyChoiceWithInvalidValue() + { + new ChoiceList( + array($this->obj1), + array('A'), + array(), + ChoiceList::COPY_CHOICE, + ChoiceList::GENERATE + ); + } + public function testGetIndicesForChoices() { $choices = array($this->obj2, $this->obj3); From b154f7cb92729259b05c3ed8ffed90a44d9172b6 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Sat, 28 Jan 2012 13:36:50 +0100 Subject: [PATCH 11/13] [Form] Fixed docblock and unneeded use statement --- src/Symfony/Component/Form/Util/FormUtil.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Form/Util/FormUtil.php b/src/Symfony/Component/Form/Util/FormUtil.php index 9a4eec8b50..d4f6b087f8 100644 --- a/src/Symfony/Component/Form/Util/FormUtil.php +++ b/src/Symfony/Component/Form/Util/FormUtil.php @@ -11,8 +11,9 @@ namespace Symfony\Component\Form\Util; -use Symfony\Component\Form\Exception\UnexpectedTypeException; - +/** + * @author Bernhard Schussek + */ abstract class FormUtil { /** From 600cec746a793765ae1f5ce8589ba602d6b34ae5 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Sat, 28 Jan 2012 13:37:13 +0100 Subject: [PATCH 12/13] [Form] Added missing entries to CHANGELOG and UPGRADE --- CHANGELOG-2.1.md | 8 ++++++++ UPGRADE-2.1.md | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/CHANGELOG-2.1.md b/CHANGELOG-2.1.md index 5dbe07a044..9b7b631444 100644 --- a/CHANGELOG-2.1.md +++ b/CHANGELOG-2.1.md @@ -165,6 +165,14 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c * allowed setting different options for RepeatedType fields (like the label) * added support for empty form name at root level, this enables rendering forms without form name prefix in field names + + * [BC BREAK] made form naming more restrictive. Form and field names must + start with a letter, digit or underscore and only contain letters, digits, + underscores, hyphens and colons + + * [BC BREAK] changed default name of the prototype in the "collection" type + from "$$name$$" to "__name__". No dollars are appended/prepended to custom + names anymore. * [BC BREAK] greatly improved `ChoiceListInterface` and all of its implementations. `EntityChoiceList` was adapted, the methods `getEntities()`, diff --git a/UPGRADE-2.1.md b/UPGRADE-2.1.md index 7439f0eb10..9b79eb32ae 100644 --- a/UPGRADE-2.1.md +++ b/UPGRADE-2.1.md @@ -122,3 +122,22 @@ UPGRADE FROM 2.0 to 2.1 {{ choice.label }} {% endfor %} + +* In the template of the collection type, the default name of the prototype + field has changed from "$$name$$" to "__name__" + + For custom names, no dollars are prepended/appended anymore. You are advised + to prepend and append double underscores wherever you have configured the + prototype name manually. + + Before: + + $builder->add('tags', 'collection', array('prototype' => 'proto')); + + // results in the name "$$proto$$" in the template + + After: + + $builder->add('tags', 'collection', array('prototype' => '__proto__')); + + // results in the name "__proto__" in the template From 8dc78bd0c9cdfef411f174776a56e72bed8bc049 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Sat, 28 Jan 2012 13:37:24 +0100 Subject: [PATCH 13/13] [Form] Fixed YODA issues --- .../Form/Extension/Core/ChoiceList/ChoiceList.php | 14 +++++++++++--- .../Extension/Core/ChoiceList/ObjectChoiceList.php | 2 +- .../Core/DataMapper/PropertyPathMapper.php | 4 ++-- .../DataTransformer/ChoiceToValueTransformer.php | 6 +++--- .../DataTransformer/DateTimeToArrayTransformer.php | 2 +- .../NumberToLocalizedStringTransformer.php | 2 +- .../Core/EventListener/FixRadioInputListener.php | 2 +- .../Form/Extension/Core/Type/DateTimeType.php | 10 +++++----- .../Form/Extension/Core/Type/DateType.php | 12 ++++++------ .../Form/Extension/Core/Type/TimeType.php | 14 +++++++------- .../Validator/Validator/DelegatingValidator.php | 2 +- src/Symfony/Component/Form/Form.php | 2 +- src/Symfony/Component/Form/Util/PropertyPath.php | 2 +- 13 files changed, 41 insertions(+), 33 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php index 374a65543a..311e0f7361 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -20,7 +20,7 @@ use Symfony\Component\Form\Extension\Core\View\ChoiceView; /** * Base class for choice list implementations. * - * @author Bernhard Schussek + * @author Bernhard Schussek */ class ChoiceList implements ChoiceListInterface { @@ -530,7 +530,11 @@ class ChoiceList implements ChoiceListInterface */ protected function fixValues(array $values) { - return array_map(array($this, 'fixValue'), $values); + foreach ($values as $i => $value) { + $values[$i] = $this->fixValue($value); + } + + return $values; } /** @@ -560,7 +564,11 @@ class ChoiceList implements ChoiceListInterface */ protected function fixIndices(array $indices) { - return array_map(array($this, 'fixIndex'), $indices); + foreach ($indices as $i => $index) { + $indices[$i] = $this->fixIndex($index); + } + + return $indices; } /** diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php index f5ea145437..3533326be1 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php @@ -105,7 +105,7 @@ class ObjectChoiceList extends ChoiceList throw new UnexpectedTypeException($choices, 'array or \Traversable'); } - if ($this->groupPath !== null) { + if (null !== $this->groupPath) { $groupedChoices = array(); foreach ($choices as $i => $choice) { diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php index 6652c3843e..e9c3c126e4 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php @@ -59,7 +59,7 @@ class PropertyPathMapper implements DataMapperInterface public function mapDataToForm($data, FormInterface $form) { if (!empty($data)) { - if ($form->getAttribute('property_path') !== null) { + if (null !== $form->getAttribute('property_path')) { $form->setData($form->getAttribute('property_path')->getValue($data)); } } @@ -77,7 +77,7 @@ class PropertyPathMapper implements DataMapperInterface public function mapFormToData(FormInterface $form, &$data) { - if ($form->getAttribute('property_path') !== null && $form->isSynchronized()) { + if (null !== $form->getAttribute('property_path') && $form->isSynchronized()) { $propertyPath = $form->getAttribute('property_path'); // If the data is identical to the value in $data, we are diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php index 13d90a9151..dfab6ec454 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php @@ -46,18 +46,18 @@ class ChoiceToValueTransformer implements DataTransformerInterface // These are now valid ChoiceList values, so we can return null // right away - if ($value === '' || $value === null) { + if ('' === $value || null === $value) { return null; } $choices = $this->choiceList->getChoicesForValues(array($value)); - if (count($choices) !== 1) { + if (1 !== count($choices)) { throw new TransformationFailedException('The choice "' . $value . '" does not exist or is not unique'); } $choice = current($choices); - return $choice === '' ? null : $choice; + return '' === $choice ? null : $choice; } } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php index e9f5fb2f2a..fb0a3bf2dd 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php @@ -124,7 +124,7 @@ class DateTimeToArrayTransformer extends BaseDateTimeTransformer throw new UnexpectedTypeException($value, 'array'); } - if (implode('', $value) === '') { + if ('' === implode('', $value)) { return null; } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php index 56f4c68bcf..699dec00ea 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php @@ -122,7 +122,7 @@ class NumberToLocalizedStringTransformer implements DataTransformerInterface { $formatter = new \NumberFormatter(\Locale::getDefault(), \NumberFormatter::DECIMAL); - if ($this->precision !== null) { + if (null !== $this->precision) { $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->precision); $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode); } diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php index e094a77a38..1378b2e2f7 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php @@ -41,7 +41,7 @@ class FixRadioInputListener implements EventSubscriberInterface $value = $event->getData(); $index = current($this->choiceList->getIndicesForValues(array($value))); - $event->setData($index !== false ? array($index => $value) : array()); + $event->setData(false !== $index ? array($index => $value) : array()); } static public function getSubscribedEvents() diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php index 35fe0ffa29..1c63d21641 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php @@ -45,7 +45,7 @@ class DateTimeType extends AbstractType throw new FormException(sprintf('Options "date_widget" and "time_widget" need to be identical. Used: "date_widget" = "%s" and "time_widget" = "%s".', $options['date_widget'] ?: 'choice', $options['time_widget'] ?: 'choice')); } - if ($options['widget'] === 'single_text') { + if ('single_text' === $options['widget']) { $builder->appendClientTransformer(new DateTimeToStringTransformer($options['data_timezone'], $options['user_timezone'], $format)); } else { // Only pass a subset of the options to children @@ -105,15 +105,15 @@ class DateTimeType extends AbstractType ; } - if ($options['input'] === 'string') { + if ('string' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToStringTransformer($options['data_timezone'], $options['data_timezone'], $format) )); - } elseif ($options['input'] === 'timestamp') { + } elseif ('timestamp' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToTimestampTransformer($options['data_timezone'], $options['data_timezone']) )); - } elseif ($options['input'] === 'array') { + } elseif ('array' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToArrayTransformer($options['data_timezone'], $options['data_timezone'], $parts) )); @@ -200,7 +200,7 @@ class DateTimeType extends AbstractType */ public function getParent(array $options) { - return $options['widget'] === 'single_text' ? 'field' : 'form'; + return 'single_text' === $options['widget'] ? 'field' : 'form'; } /** diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php index 9e878d6535..8c2af29e7f 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php @@ -63,12 +63,12 @@ class DateType extends AbstractType $pattern ); - if ($options['widget'] === 'single_text') { + if ('single_text' === $options['widget']) { $builder->appendClientTransformer(new DateTimeToLocalizedStringTransformer($options['data_timezone'], $options['user_timezone'], $format, \IntlDateFormatter::NONE, \IntlDateFormatter::GREGORIAN, $pattern)); } else { $yearOptions = $monthOptions = $dayOptions = array(); - if ($options['widget'] === 'choice') { + if ('choice' === $options['widget']) { if (is_array($options['empty_value'])) { $options['empty_value'] = array_merge(array('year' => null, 'month' => null, 'day' => null), $options['empty_value']); } else { @@ -123,15 +123,15 @@ class DateType extends AbstractType ; } - if ($options['input'] === 'string') { + if ('string' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToStringTransformer($options['data_timezone'], $options['data_timezone'], 'Y-m-d') )); - } elseif ($options['input'] === 'timestamp') { + } elseif ('timestamp' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToTimestampTransformer($options['data_timezone'], $options['data_timezone']) )); - } elseif ($options['input'] === 'array') { + } elseif ('array' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToArrayTransformer($options['data_timezone'], $options['data_timezone'], array('year', 'month', 'day')) )); @@ -212,7 +212,7 @@ class DateType extends AbstractType */ public function getParent(array $options) { - return $options['widget'] === 'single_text' ? 'field' : 'form'; + return 'single_text' === $options['widget'] ? 'field' : 'form'; } /** diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php index 1eeaa40ec5..7bfe50dc6b 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php @@ -35,12 +35,12 @@ class TimeType extends AbstractType $parts[] = 'second'; } - if ($options['widget'] === 'single_text') { + if ('single_text' === $options['widget']) { $builder->appendClientTransformer(new DateTimeToStringTransformer($options['data_timezone'], $options['user_timezone'], $format)); } else { $hourOptions = $minuteOptions = $secondOptions = array(); - if ($options['widget'] === 'choice') { + if ('choice' === $options['widget']) { if (is_array($options['empty_value'])) { $options['empty_value'] = array_merge(array('hour' => null, 'minute' => null, 'second' => null), $options['empty_value']); } else { @@ -103,18 +103,18 @@ class TimeType extends AbstractType $builder->add('second', $options['widget'], $secondOptions); } - $builder->appendClientTransformer(new DateTimeToArrayTransformer($options['data_timezone'], $options['user_timezone'], $parts, $options['widget'] === 'text')); + $builder->appendClientTransformer(new DateTimeToArrayTransformer($options['data_timezone'], $options['user_timezone'], $parts, 'text' === $options['widget'])); } - if ($options['input'] === 'string') { + if ('string' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToStringTransformer($options['data_timezone'], $options['data_timezone'], $format) )); - } elseif ($options['input'] === 'timestamp') { + } elseif ('timestamp' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToTimestampTransformer($options['data_timezone'], $options['data_timezone']) )); - } elseif ($options['input'] === 'array') { + } elseif ('array' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToArrayTransformer($options['data_timezone'], $options['data_timezone'], $parts) )); @@ -184,7 +184,7 @@ class TimeType extends AbstractType */ public function getParent(array $options) { - return $options['widget'] === 'single_text' ? 'field' : 'form'; + return 'single_text' === $options['widget'] ? 'field' : 'form'; } /** diff --git a/src/Symfony/Component/Form/Extension/Validator/Validator/DelegatingValidator.php b/src/Symfony/Component/Form/Extension/Validator/Validator/DelegatingValidator.php index 2b7c099403..dce76724c4 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Validator/DelegatingValidator.php +++ b/src/Symfony/Component/Form/Extension/Validator/Validator/DelegatingValidator.php @@ -229,7 +229,7 @@ class DelegatingValidator implements FormValidatorInterface $nestedNamePath = $namePath.'.'.$child->getName(); - if (strpos($path, '[') === 0) { + if (0 === strpos($path, '[')) { $nestedDataPaths = array($dataPath.$path); } else { $nestedDataPaths = array($dataPath.'.'.$path); diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 5d5d8011ce..d82f2f08e3 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -1098,6 +1098,6 @@ class Form implements \IteratorAggregate, FormInterface */ static public function isValidName($name) { - return $name === '' || preg_match('/^[a-zA-Z0-9_][a-zA-Z0-9_\-:]*$/D', $name); + return '' === $name || preg_match('/^[a-zA-Z0-9_][a-zA-Z0-9_\-:]*$/D', $name); } } diff --git a/src/Symfony/Component/Form/Util/PropertyPath.php b/src/Symfony/Component/Form/Util/PropertyPath.php index cc4626405a..96fa57f202 100644 --- a/src/Symfony/Component/Form/Util/PropertyPath.php +++ b/src/Symfony/Component/Form/Util/PropertyPath.php @@ -67,7 +67,7 @@ class PropertyPath implements \IteratorAggregate $pattern = '/^(([^\.\[]+)|\[([^\]]+)\])(.*)/'; while (preg_match($pattern, $remaining, $matches)) { - if ($matches[2] !== '') { + if ('' !== $matches[2]) { $this->elements[] = $matches[2]; $this->isIndex[] = false; } else {