From 87b16e7015c02e794c33dbfc05812cf070b4ac68 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 17 Jan 2012 00:46:26 +0100 Subject: [PATCH] [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'),