diff --git a/CHANGELOG-2.1.md b/CHANGELOG-2.1.md index b54e3498e6..f51c9827ea 100644 --- a/CHANGELOG-2.1.md +++ b/CHANGELOG-2.1.md @@ -166,6 +166,45 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c * allowed setting different options for RepeatedType fields (like the label) * added support for empty form name at root level, this enables rendering forms without form name prefix in field names + + * [BC BREAK] made form naming more restrictive. Form and field names must + start with a letter, digit or underscore and only contain letters, digits, + underscores, hyphens and colons + + * [BC BREAK] changed default name of the prototype in the "collection" type + from "$$name$$" to "__name__". No dollars are appended/prepended to custom + names anymore. + + * [BC BREAK] greatly improved `ChoiceListInterface` and all of its + implementations. `EntityChoiceList` was adapted, the methods `getEntities()`, + `getEntitiesByKeys()`, `getIdentifier()` and `getIdentifierValues()` were + removed/made private. Instead of the first two you can use `getChoices()` + and `getChoicesByValues()`, for the latter two no replacement exists. + `ArrayChoiceList` was replaced by `SimpleChoiceList`. + `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 diff --git a/UPGRADE-2.1.md b/UPGRADE-2.1.md index 985463f06c..9b79eb32ae 100644 --- a/UPGRADE-2.1.md +++ b/UPGRADE-2.1.md @@ -76,4 +76,68 @@ 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. + +* 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`. + +* In the template of the choice type, the structure of the "choices" variable + has changed + + "choices" now contains ChoiceView objects with two getters `getValue()` + and `getLabel()` to access the choice data. The indices of the array + store an index whose generation is controlled by the "index_generation" + option of the choice field. + + Before: + + {% for choice, label in choices %} + + {% endfor %} + + After: + + {% for choice in choices %} + + {% endfor %} + +* In the template of the collection type, the default name of the prototype + field has changed from "$$name$$" to "__name__" + + For custom names, no dollars are prepended/appended anymore. You are advised + to prepend and append double underscores wherever you have configured the + prototype name manually. + + Before: + + $builder->add('tags', 'collection', array('prototype' => 'proto')); + + // results in the name "$$proto$$" in the template + + After: + + $builder->add('tags', 'collection', array('prototype' => '__proto__')); + + // results in the name "__proto__" in the template diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index f81d58bd4f..ee8019a0fd 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,289 @@ 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 A property path pointing to the property used + * to group the choices. Only allowed if + * the choices are given as flat array. */ - public function __construct(ObjectManager $manager, $class, $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 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 choice views of the preferred choices as nested array with + * the choice groups as top-level keys. + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getPreferredViews() + { + if (!$this->loaded) { + $this->load(); + } + + return parent::getPreferredViews(); + } + + /** + * Returns the choice views of the choices that are not preferred as nested + * array with the choice groups as top-level keys. + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getRemainingViews() + { + if (!$this->loaded) { + $this->load(); + } + + return parent::getRemainingViews(); + } + + /** + * 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(str_replace('argument $labelPath', 'option "property"', $e->getMessage()), 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 +358,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..ea459a6487 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php @@ -19,9 +19,21 @@ 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 + * @return array The entities. */ function getEntities(); + + /** + * Returns an array of entities matching the given identifiers. + * + * @param string $identifier The identifier field of the object. This method + * is not applicable for fields with multiple + * identifiers. + * @param array $values The values of the identifiers. + * + * @return array The entities. + */ + 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..2d92b03784 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,20 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface { return $this->queryBuilder->getQuery()->execute(); } + + /** + * {@inheritDoc} + */ + public function getEntitiesByIds($identifier, array $values) + { + $qb = clone ($this->queryBuilder); + $alias = current($qb->getRootAliases()); + $parameter = 'ORMQueryBuilderLoader_getEntitiesByIds_'.$identifier; + $where = $qb->expr()->in($alias.'.'.$identifier, ':'.$parameter); + + return $qb->andWhere($where) + ->getQuery() + ->setParameter($parameter, $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..2fa93356e4 --- /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 $collection A collection of entities + * + * @return mixed An array of entities + */ + 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 entities + * + * @return Collection A collection of entities + */ + 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/Extension/FormExtension.php b/src/Symfony/Bridge/Twig/Extension/FormExtension.php index 0452a0f130..1662ef8022 100644 --- a/src/Symfony/Bridge/Twig/Extension/FormExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/FormExtension.php @@ -15,6 +15,7 @@ use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser; use Symfony\Component\Form\FormView; use Symfony\Component\Form\Exception\FormException; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Util\FormUtil; /** @@ -95,9 +96,9 @@ class FormExtension extends \Twig_Extension return FormUtil::isChoiceGroup($label); } - public function isChoiceSelected(FormView $view, $choice) + public function isChoiceSelected(FormView $view, ChoiceView $choice) { - return FormUtil::isChoiceSelected($choice, $view->get('value')); + return FormUtil::isChoiceSelected($choice->getValue(), $view->get('value')); } /** diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index 3ba5fc5f4b..f07d31f17b 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -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_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..66c664fb1d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php @@ -1,11 +1,11 @@ - $label): ?> - isChoiceGroup($label)): ?> - - $nestedLabel): ?> - + $choice): ?> + isChoiceGroup($choice)): ?> + + + - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php index 910fa36b22..2004b4e67e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php @@ -16,6 +16,7 @@ use Symfony\Component\Templating\EngineInterface; use Symfony\Component\Form\FormView; use Symfony\Component\Form\Exception\FormException; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Util\FormUtil; /** @@ -63,9 +64,9 @@ class FormHelper extends Helper return FormUtil::isChoiceGroup($label); } - public function isChoiceSelected(FormView $view, $choice) + public function isChoiceSelected(FormView $view, ChoiceView $choice) { - return FormUtil::isChoiceSelected($choice, $view->get('value')); + return FormUtil::isChoiceSelected($choice->getValue(), $view->get('value')); } /** diff --git a/src/Symfony/Component/Form/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..311e0f7361 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -0,0 +1,602 @@ + + * + * 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\Form; +use Symfony\Component\Form\Util\FormUtil; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Exception\InvalidConfigurationException; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; + +/** + * Base class for choice list implementations. + * + * @author Bernhard Schussek + */ +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 preferred view objects as hierarchy containing also the choice groups + * with the indices of the matching choices as bottom-level keys. + * + * @var array + */ + private $preferredViews = array(); + + /** + * The non-preferred view objects as hierarchy containing also the choice + * groups with the indices of the matching choices as bottom-level keys. + * + * @var array + */ + private $remainingViews = 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 = self::GENERATE, $indexStrategy = self::GENERATE) + { + $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) + { + $this->choices = array(); + $this->values = array(); + $this->preferredViews = array(); + $this->remainingViews = array(); + + $this->addChoices( + $this->preferredViews, + $this->remainingViews, + $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 values for the choices + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getValues() + { + return $this->values; + } + + /** + * Returns the choice views of the preferred choices as nested array with + * the choice groups as top-level keys. + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getPreferredViews() + { + return $this->preferredViews; + } + + /** + * Returns the choice views of the choices that are not preferred as nested + * array with the choice groups as top-level keys. + * + * @return array + * + * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + public function getRemainingViews() + { + return $this->remainingViews; + } + + /** + * 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 (self::COPY_CHOICE === $this->valueStrategy) { + 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 (0 === count($values)) { + 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 (self::COPY_CHOICE === $this->valueStrategy) { + 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 (0 === count($choices)) { + 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 === $givenChoice) { + $indices[] = $i; + unset($choices[$j]); + + if (0 === count($choices)) { + 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 (0 === count($values)) { + break 2; + } + } + } + } + + return $indices; + } + + /** + * Recursively adds the given choices to the list. + * + * @param array $bucketForPreferred The bucket where to store the preferred + * view objects. + * @param array $bucketForRemaining The bucket where to store the + * non-preferred view objects. + * @param array $choices The list of choices. + * @param array $labels The labels corresponding to the choices. + * @param array $preferredChoices The preferred choices. + * + * @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) + { + if (!is_array($choices) && !$choices instanceof \Traversable) { + throw new UnexpectedTypeException($choices, 'array or \Traversable'); + } + + // Add choices to the nested buckets + foreach ($choices as $group => $choice) { + if (is_array($choice)) { + 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 + * view objects. + * @param array $bucketForRemaining The bucket where to store the + * non-preferred view objects. + * @param array $choices The list of choices in the group. + * @param array $labels The labels corresponding to the choices in the group. + * @param array $preferredChoices The preferred choices. + */ + 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 + * view objects. + * @param array $bucketForRemaining The bucket where to store the + * non-preferred view objects. + * @param mixed $choice The choice to add. + * @param string $label The label for the choice. + * @param array $preferredChoices The preferred choices. + */ + protected function addChoice(&$bucketForPreferred, &$bucketForRemaining, $choice, $label, array $preferredChoices) + { + $index = $this->createIndex($choice); + + if ('' === $index || null === $index || !Form::isValidName((string)$index)) { + throw new InvalidConfigurationException('The choice list index "' . $index . '" is invalid. Please set the choice field option "index_generation" to ChoiceList::GENERATE.'); + } + + $value = $this->createValue($choice); + + if (!is_scalar($value)) { + throw new InvalidConfigurationException('The choice list value of type "' . gettype($value) . '" should be a scalar. Please set the choice field option "value_generation" to ChoiceList::GENERATE.'); + } + + // Always store values as strings to facilitate comparisons + $value = $this->fixValue($value); + $view = new ChoiceView($value, $label); + + $this->choices[$index] = $this->fixChoice($choice); + $this->values[$index] = $value; + + if ($this->isPreferred($choice, $preferredChoices)) { + $bucketForPreferred[$index] = $view; + } else { + $bucketForRemaining[$index] = $view; + } + } + + /** + * 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 (self::COPY_CHOICE === $this->indexStrategy) { + 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 (self::COPY_CHOICE === $this->valueStrategy) { + 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) + { + foreach ($values as $i => $value) { + $values[$i] = $this->fixValue($value); + } + + return $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) + { + foreach ($indices as $i => $index) { + $indices[$i] = $this->fixIndex($index); + } + + return $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..122b50e7e5 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php @@ -11,12 +11,130 @@ 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 values for the choices + * + * @return array The values with the corresponding choice indices as keys. + */ + function getValues(); + + /** + * Returns the choice views of the preferred choices as nested array with + * the choice groups as top-level keys. + * + * Example: + * + * + * array( + * 'Group 1' => array( + * 10 => ChoiceView object, + * 20 => ChoiceView object, + * ), + * 'Group 2' => array( + * 30 => ChoiceView object, + * ), + * ) + * + * + * @return array A nested array containing the views with the corresponding + * choice indices as keys on the lowest levels and the choice + * group names in the keys of the higher levels. + */ + function getPreferredViews(); + + /** + * Returns the choice views of the choices that are not preferred as nested + * array with the choice groups as top-level keys. + * + * Example: + * + * + * array( + * 'Group 1' => array( + * 10 => ChoiceView object, + * 20 => ChoiceView object, + * ), + * 'Group 2' => array( + * 30 => ChoiceView object, + * ), + * ) + * + * + * @return array A nested array containing the views with the corresponding + * choice indices as keys on the lowest levels and the choice + * group names in the keys of the higher levels. + * + * @see getPreferredValues + */ + function getRemainingViews(); + + /** + * 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/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..3533326be1 --- /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 (null !== $this->groupPath) { + $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 (null === $group) { + $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('A "__toString()" method was not found on the objects of type "' . get_class($choice) . '" passed to the choice field. To read a custom getter instead, set the argument $labelPath to the desired property path.'); + } + } + } +} diff --git a/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..f59827e7d8 --- /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 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/DataMapper/PropertyPathMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php index 6652c3843e..e9c3c126e4 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php @@ -59,7 +59,7 @@ class PropertyPathMapper implements DataMapperInterface public function mapDataToForm($data, FormInterface $form) { if (!empty($data)) { - if ($form->getAttribute('property_path') !== null) { + if (null !== $form->getAttribute('property_path')) { $form->setData($form->getAttribute('property_path')->getValue($data)); } } @@ -77,7 +77,7 @@ class PropertyPathMapper implements DataMapperInterface public function mapFormToData(FormInterface $form, &$data) { - if ($form->getAttribute('property_path') !== null && $form->isSynchronized()) { + if (null !== $form->getAttribute('property_path') && $form->isSynchronized()) { $propertyPath = $form->getAttribute('property_path'); // If the data is identical to the value in $data, we are diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/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..dfab6ec454 --- /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 || null === $value) { + return null; + } + + $choices = $this->choiceList->getChoicesForValues(array($value)); + + if (1 !== count($choices)) { + throw new TransformationFailedException('The choice "' . $value . '" does not exist or is not unique'); + } + + $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/DateTimeToArrayTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php index e9f5fb2f2a..fb0a3bf2dd 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php @@ -124,7 +124,7 @@ class DateTimeToArrayTransformer extends BaseDateTimeTransformer throw new UnexpectedTypeException($value, 'array'); } - if (implode('', $value) === '') { + if ('' === implode('', $value)) { return null; } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php index 0a290961b7..4dd98789ed 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php @@ -126,7 +126,7 @@ class NumberToLocalizedStringTransformer implements DataTransformerInterface { $formatter = new \NumberFormatter(\Locale::getDefault(), \NumberFormatter::DECIMAL); - if ($this->precision !== null) { + if (null !== $this->precision) { $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->precision); $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode); } diff --git a/src/Symfony/Component/Form/Extension/Core/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..1378b2e2f7 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(false !== $index ? 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..358c4317b6 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,44 +38,22 @@ 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(); - - // 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) { - if ($options['multiple']) { - $builder->add((string) $choice, 'checkbox', array( - 'value' => $choice, - 'label' => $value, - // 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, - 'translation_domain' => $options['translation_domain'], - )); - } - } + $this->addSubFields($builder, $options['choice_list']->getPreferredViews(), $options); + $this->addSubFields($builder, $options['choice_list']->getRemainingViews(), $options); } // empty value @@ -101,18 +82,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 +104,13 @@ 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->getPreferredViews()) + ->set('choices', $choiceList->getRemainingViews()) ->set('separator', '-------------------') ->set('empty_value', $form->getAttribute('empty_value')) ; @@ -157,6 +137,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, @@ -178,4 +160,36 @@ class ChoiceType extends AbstractType { return 'choice'; } + + /** + * Adds the sub fields for an expanded choice field. + * + * @param FormBuilder $builder The form builder. + * @param array $choiceViews The choice view objects. + * @param array $options The build options. + */ + private function addSubFields(FormBuilder $builder, array $choiceViews, array $options) + { + foreach ($choiceViews as $i => $choiceView) { + if (is_array($choiceView)) { + // Flatten groups + $this->addSubFields($builder, $choiceView, $options); + } elseif ($options['multiple']) { + $builder->add((string) $i, 'checkbox', array( + 'value' => $choiceView->getValue(), + 'label' => $choiceView->getLabel(), + // The user can check 0 or more checkboxes. If required + // is true, he is required to check all of them. + 'required' => false, + 'translation_domain' => $options['translation_domain'], + )); + } else { + $builder->add((string) $i, 'radio', array( + 'value' => $choiceView->getValue(), + 'label' => $choiceView->getLabel(), + 'translation_domain' => $options['translation_domain'], + )); + } + } + } } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php b/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php index 66d49c747c..20ac8345f1 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php @@ -25,7 +25,7 @@ class CollectionType extends AbstractType public function buildForm(FormBuilder $builder, array $options) { if ($options['allow_add'] && $options['prototype']) { - $prototype = $builder->create('$$' . $options['prototype_name'] . '$$', $options['type'], $options['options']); + $prototype = $builder->create($options['prototype_name'], $options['type'], $options['options']); $builder->setAttribute('prototype', $prototype->getForm()); } @@ -78,7 +78,7 @@ class CollectionType extends AbstractType 'allow_add' => false, 'allow_delete' => false, 'prototype' => true, - 'prototype_name' => 'name', + 'prototype_name' => '__name__', 'type' => 'text', 'options' => array(), ); diff --git a/src/Symfony/Component/Form/Extension/Core/Type/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/DateTimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php index 35fe0ffa29..1c63d21641 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php @@ -45,7 +45,7 @@ class DateTimeType extends AbstractType throw new FormException(sprintf('Options "date_widget" and "time_widget" need to be identical. Used: "date_widget" = "%s" and "time_widget" = "%s".', $options['date_widget'] ?: 'choice', $options['time_widget'] ?: 'choice')); } - if ($options['widget'] === 'single_text') { + if ('single_text' === $options['widget']) { $builder->appendClientTransformer(new DateTimeToStringTransformer($options['data_timezone'], $options['user_timezone'], $format)); } else { // Only pass a subset of the options to children @@ -105,15 +105,15 @@ class DateTimeType extends AbstractType ; } - if ($options['input'] === 'string') { + if ('string' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToStringTransformer($options['data_timezone'], $options['data_timezone'], $format) )); - } elseif ($options['input'] === 'timestamp') { + } elseif ('timestamp' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToTimestampTransformer($options['data_timezone'], $options['data_timezone']) )); - } elseif ($options['input'] === 'array') { + } elseif ('array' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToArrayTransformer($options['data_timezone'], $options['data_timezone'], $parts) )); @@ -200,7 +200,7 @@ class DateTimeType extends AbstractType */ public function getParent(array $options) { - return $options['widget'] === 'single_text' ? 'field' : 'form'; + return 'single_text' === $options['widget'] ? 'field' : 'form'; } /** diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php index ba72257768..8c2af29e7f 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; @@ -62,35 +63,47 @@ class DateType extends AbstractType $pattern ); - if ($options['widget'] === 'single_text') { + if ('single_text' === $options['widget']) { $builder->appendClientTransformer(new DateTimeToLocalizedStringTransformer($options['data_timezone'], $options['user_timezone'], $format, \IntlDateFormatter::NONE, \IntlDateFormatter::GREGORIAN, $pattern)); } else { $yearOptions = $monthOptions = $dayOptions = array(); - if ($options['widget'] === 'choice') { + if ('choice' === $options['widget']) { if (is_array($options['empty_value'])) { $options['empty_value'] = array_merge(array('year' => null, 'month' => null, 'day' => null), $options['empty_value']); } else { $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'], ); @@ -110,15 +123,15 @@ class DateType extends AbstractType ; } - if ($options['input'] === 'string') { + if ('string' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToStringTransformer($options['data_timezone'], $options['data_timezone'], 'Y-m-d') )); - } elseif ($options['input'] === 'timestamp') { + } elseif ('timestamp' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToTimestampTransformer($options['data_timezone'], $options['data_timezone']) )); - } elseif ($options['input'] === 'array') { + } elseif ('array' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToArrayTransformer($options['data_timezone'], $options['data_timezone'], array('year', 'month', 'day')) )); @@ -199,7 +212,7 @@ class DateType extends AbstractType */ public function getParent(array $options) { - return $options['widget'] === 'single_text' ? 'field' : 'form'; + return 'single_text' === $options['widget'] ? 'field' : 'form'; } /** @@ -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/FieldType.php b/src/Symfony/Component/Form/Extension/Core/Type/FieldType.php index 05aaf230f1..4de3fe16f2 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FieldType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FieldType.php @@ -88,6 +88,11 @@ class FieldType extends AbstractType } else { $id = $name; $fullName = $name; + + // Strip leading underscores and digits. These are allowed in + // form names, but not in HTML4 ID attributes. + // http://www.w3.org/TR/html401/struct/global.html#adef-id + $id = ltrim($id, '_0123456789'); } $types = array(); diff --git a/src/Symfony/Component/Form/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..7bfe50dc6b 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; @@ -35,37 +35,52 @@ class TimeType extends AbstractType $parts[] = 'second'; } - if ($options['widget'] === 'single_text') { + if ('single_text' === $options['widget']) { $builder->appendClientTransformer(new DateTimeToStringTransformer($options['data_timezone'], $options['user_timezone'], $format)); } else { $hourOptions = $minuteOptions = $secondOptions = array(); - if ($options['widget'] === 'choice') { + if ('choice' === $options['widget']) { if (is_array($options['empty_value'])) { $options['empty_value'] = array_merge(array('hour' => null, 'minute' => null, 'second' => null), $options['empty_value']); } else { $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'], ); } @@ -88,18 +103,18 @@ class TimeType extends AbstractType $builder->add('second', $options['widget'], $secondOptions); } - $builder->appendClientTransformer(new DateTimeToArrayTransformer($options['data_timezone'], $options['user_timezone'], $parts, $options['widget'] === 'text')); + $builder->appendClientTransformer(new DateTimeToArrayTransformer($options['data_timezone'], $options['user_timezone'], $parts, 'text' === $options['widget'])); } - if ($options['input'] === 'string') { + if ('string' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToStringTransformer($options['data_timezone'], $options['data_timezone'], $format) )); - } elseif ($options['input'] === 'timestamp') { + } elseif ('timestamp' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToTimestampTransformer($options['data_timezone'], $options['data_timezone']) )); - } elseif ($options['input'] === 'array') { + } elseif ('array' === $options['input']) { $builder->appendNormTransformer(new ReversedTransformer( new DateTimeToArrayTransformer($options['data_timezone'], $options['data_timezone'], $parts) )); @@ -169,7 +184,7 @@ class TimeType extends AbstractType */ public function getParent(array $options) { - return $options['widget'] === 'single_text' ? 'field' : 'form'; + return 'single_text' === $options['widget'] ? 'field' : 'form'; } /** diff --git a/src/Symfony/Component/Form/Extension/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/Extension/Core/View/ChoiceView.php b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php new file mode 100644 index 0000000000..4e04247900 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\View; + +/** + * Represents a choice in templates. + * + * @author Bernhard Schussek + */ +class ChoiceView +{ + /** + * The view representation of the choice. + * + * @var string + */ + private $value; + + /** + * The label displayed to humans. + * + * @var string + */ + private $label; + + /** + * Creates a new ChoiceView. + * + * @param string $value The view representation of the choice. + * @param string $label The label displayed to humans. + */ + public function __construct($value, $label) + { + $this->value = $value; + $this->label = $label; + } + + /** + * Returns the choice value. + * + * @return string The view representation of the choice. + */ + public function getValue() + { + return $this->value; + } + + /** + * Returns the choice label. + * + * @return string The label displayed to humans. + */ + public function getLabel() + { + return $this->label; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Form/Extension/Validator/Validator/DelegatingValidator.php b/src/Symfony/Component/Form/Extension/Validator/Validator/DelegatingValidator.php index 2b7c099403..dce76724c4 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Validator/DelegatingValidator.php +++ b/src/Symfony/Component/Form/Extension/Validator/Validator/DelegatingValidator.php @@ -229,7 +229,7 @@ class DelegatingValidator implements FormValidatorInterface $nestedNamePath = $namePath.'.'.$child->getName(); - if (strpos($path, '[') === 0) { + if (0 === strpos($path, '[')) { $nestedDataPaths = array($dataPath.$path); } else { $nestedDataPaths = array($dataPath.'.'.$path); diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index f477211d67..1748ac4fb1 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -193,6 +193,10 @@ class Form implements \IteratorAggregate, FormInterface $required = false, $readOnly = false, $errorBubbling = false, $emptyData = null, array $attributes = array()) { + $name = (string) $name; + + self::validateName($name); + foreach ($clientTransformers as $transformer) { if (!$transformer instanceof DataTransformerInterface) { throw new UnexpectedTypeException($transformer, 'Symfony\Component\Form\DataTransformerInterface'); @@ -211,7 +215,7 @@ class Form implements \IteratorAggregate, FormInterface } } - $this->name = (string) $name; + $this->name = $name; $this->dispatcher = $dispatcher; $this->types = $types; $this->clientTransformers = $clientTransformers; @@ -1056,4 +1060,45 @@ class Form implements \IteratorAggregate, FormInterface return $value; } + + /** + * Validates whether the given variable is a valid form name. + * + * @param string $name The tested form name. + * + * @throws UnexpectedTypeException If the name is not a string. + * @throws \InvalidArgumentException If the name contains invalid characters. + */ + static public function validateName($name) + { + if (!is_string($name)) { + throw new UnexpectedTypeException($name, 'string'); + } + + if (!self::isValidName($name)) { + throw new \InvalidArgumentException(sprintf( + 'The name "%s" contains illegal characters. Names should start with a letter, digit or underscore and only contains letters, digits, numbers, underscores ("_"), hyphens ("-") and colons (":").', + $name + )); + } + } + + /** + * Returns whether the given variable contains a valid form name. + * + * A name is accepted if it + * + * * is empty + * * starts with a letter, digit or underscore + * * contains only letters, digits, numbers, underscores ("_"), + * hyphens ("-") and colons (":") + * + * @param string $name The tested form name. + * + * @return Boolean Whether the name is valid. + */ + static public function isValidName($name) + { + return '' === $name || preg_match('/^[a-zA-Z0-9_][a-zA-Z0-9_\-:]*$/D', $name); + } } diff --git a/src/Symfony/Component/Form/FormBuilder.php b/src/Symfony/Component/Form/FormBuilder.php index b3fcfb41fd..9aaf474141 100644 --- a/src/Symfony/Component/Form/FormBuilder.php +++ b/src/Symfony/Component/Form/FormBuilder.php @@ -123,6 +123,10 @@ class FormBuilder */ public function __construct($name, FormFactoryInterface $factory, EventDispatcherInterface $dispatcher, $dataClass = null) { + $name = (string) $name; + + Form::validateName($name); + $this->name = $name; $this->factory = $factory; $this->dispatcher = $dispatcher; diff --git a/src/Symfony/Component/Form/Util/FormUtil.php b/src/Symfony/Component/Form/Util/FormUtil.php index bc521efff2..d4f6b087f8 100644 --- a/src/Symfony/Component/Form/Util/FormUtil.php +++ b/src/Symfony/Component/Form/Util/FormUtil.php @@ -11,22 +11,11 @@ namespace Symfony\Component\Form\Util; +/** + * @author Bernhard Schussek + */ 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 +38,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/src/Symfony/Component/Form/Util/PropertyPath.php b/src/Symfony/Component/Form/Util/PropertyPath.php index cc4626405a..96fa57f202 100644 --- a/src/Symfony/Component/Form/Util/PropertyPath.php +++ b/src/Symfony/Component/Form/Util/PropertyPath.php @@ -67,7 +67,7 @@ class PropertyPath implements \IteratorAggregate $pattern = '/^(([^\.\[]+)|\[([^\]]+)\])(.*)/'; while (preg_match($pattern, $remaining, $matches)) { - if ($matches[2] !== '') { + if ('' !== $matches[2]) { $this->elements[] = $matches[2]; $this->isIndex[] = false; } else { diff --git a/tests/Symfony/Tests/Bridge/Doctrine/Form/ChoiceList/EntityChoiceListTest.php b/tests/Symfony/Tests/Bridge/Doctrine/Form/ChoiceList/EntityChoiceListTest.php index 74feb80d4b..2dc3e698ad 100644 --- a/tests/Symfony/Tests/Bridge/Doctrine/Form/ChoiceList/EntityChoiceListTest.php +++ b/tests/Symfony/Tests/Bridge/Doctrine/Form/ChoiceList/EntityChoiceListTest.php @@ -19,6 +19,7 @@ use Symfony\Tests\Bridge\Doctrine\DoctrineOrmTestCase; use Symfony\Tests\Bridge\Doctrine\Fixtures\ItemGroupEntity; use Symfony\Tests\Bridge\Doctrine\Fixtures\SingleIdentEntity; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class EntityChoiceListTest extends DoctrineOrmTestCase { @@ -89,7 +90,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 +133,11 @@ class EntityChoiceListTest extends DoctrineOrmTestCase ) ); - $this->assertSame(array( - 'group1' => array(1 => 'Foo'), - 'group2' => array(2 => 'Bar') - ), $choiceList->getChoices()); + $this->assertSame(array(1 => $entity1, 2 => $entity2), $choiceList->getChoices()); + $this->assertEquals(array( + 'group1' => array(1 => new ChoiceView('1', 'Foo')), + 'group2' => array(2 => new ChoiceView('2', 'Bar')) + ), $choiceList->getRemainingViews()); } public function testGroupBySupportsString() @@ -164,11 +166,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 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), + 'Group2' => array(3 => new ChoiceView('3', 'Baz')), + 4 => new ChoiceView('4', 'Boo!') + ), $choiceList->getRemainingViews()); } public function testGroupByInvalidPropertyPathReturnsFlatChoices() @@ -188,13 +191,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..8c4f65abb1 100644 --- a/tests/Symfony/Tests/Bridge/Doctrine/Form/Type/EntityTypeTest.php +++ b/tests/Symfony/Tests/Bridge/Doctrine/Form/Type/EntityTypeTest.php @@ -29,6 +29,7 @@ use Symfony\Tests\Bridge\Doctrine\Fixtures\CompositeStringIdentEntity; use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; use Doctrine\ORM\Tools\SchemaTool; use Doctrine\Common\Collections\ArrayCollection; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class EntityTypeTest extends TypeTestCase { @@ -109,7 +110,7 @@ class EntityTypeTest extends TypeTestCase 'property' => 'name' )); - $this->assertEquals(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choices')); + $this->assertEquals(array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), $field->createView()->get('choices')); } public function testSetDataToUninitializedEntityWithNonRequiredToString() @@ -125,7 +126,7 @@ class EntityTypeTest extends TypeTestCase 'required' => false, )); - $this->assertEquals(array("1" => 'Foo', "2" => 'Bar'), $field->createView()->get('choices')); + $this->assertEquals(array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), $field->createView()->get('choices')); } public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder() @@ -144,7 +145,7 @@ class EntityTypeTest extends TypeTestCase 'query_builder' => $qb )); - $this->assertEquals(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choices')); + $this->assertEquals(array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), $field->createView()->get('choices')); } /** @@ -185,7 +186,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 +200,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 +214,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 +228,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 +242,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 +255,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 +276,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 +299,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 +325,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 +356,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 +382,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 +413,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 +434,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 +489,10 @@ class EntityTypeTest extends TypeTestCase $field->bind('2'); - $this->assertEquals(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choices')); + $this->assertEquals(array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), $field->createView()->get('choices')); $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,11 +514,11 @@ class EntityTypeTest extends TypeTestCase $field->bind('2'); - $this->assertEquals(2, $field->getClientData()); + $this->assertSame('2', $field->getClientData()); $this->assertEquals(array( - 'Group1' => array(1 => 'Foo', '2' => 'Bar'), - 'Group2' => array(3 => 'Baz'), - '4' => 'Boo!' + 'Group1' => array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), + 'Group2' => array(3 => new ChoiceView('3', 'Baz')), + '4' => new ChoiceView('4', 'Boo!') ), $field->createView()->get('choices')); } @@ -652,8 +653,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 +675,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..3693afc013 100644 --- a/tests/Symfony/Tests/Component/Form/AbstractLayoutTest.php +++ b/tests/Symfony/Tests/Component/Form/AbstractLayoutTest.php @@ -115,9 +115,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testEnctype() { - $form = $this->factory->createNamedBuilder('form', 'na&me', null, array( - 'property_path' => 'name', - )) + $form = $this->factory->createNamedBuilder('form', 'name') ->add('file', 'file') ->getForm(); @@ -126,9 +124,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testNoEnctype() { - $form = $this->factory->createNamedBuilder('form', 'na&me', null, array( - 'property_path' => 'name', - )) + $form = $this->factory->createNamedBuilder('form', 'name') ->add('text', 'text') ->getForm(); @@ -137,26 +133,22 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testLabel() { - $form = $this->factory->createNamed('text', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('text', 'name'); $view = $form->createView(); $this->renderWidget($view, array('label' => 'foo')); $html = $this->renderLabel($view); $this->assertMatchesXpath($html, '/label - [@for="na&me"] - [.="[trans]Na&me[/trans]"] + [@for="name"] + [.="[trans]Name[/trans]"] ' ); } public function testLabelOnForm() { - $form = $this->factory->createNamed('date', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('date', 'name'); $view = $form->createView(); $this->renderWidget($view, array('label' => 'foo')); $html = $this->renderLabel($view); @@ -164,22 +156,21 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertMatchesXpath($html, '/label [@class=" required"] - [.="[trans]Na&me[/trans]"] + [.="[trans]Name[/trans]"] ' ); } public function testLabelWithCustomTextPassedAsOption() { - $form = $this->factory->createNamed('text', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('text', 'name', null, array( 'label' => 'Custom label', )); $html = $this->renderLabel($form->createView()); $this->assertMatchesXpath($html, '/label - [@for="na&me"] + [@for="name"] [.="[trans]Custom label[/trans]"] ' ); @@ -187,14 +178,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testLabelWithCustomTextPassedDirectly() { - $form = $this->factory->createNamed('text', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('text', 'name'); $html = $this->renderLabel($form->createView(), 'Custom label'); $this->assertMatchesXpath($html, '/label - [@for="na&me"] + [@for="name"] [.="[trans]Custom label[/trans]"] ' ); @@ -202,15 +191,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testLabelWithCustomTextPassedAsOptionAndDirectly() { - $form = $this->factory->createNamed('text', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('text', 'name', null, array( 'label' => 'Custom label', )); $html = $this->renderLabel($form->createView(), 'Overridden label'); $this->assertMatchesXpath($html, '/label - [@for="na&me"] + [@for="name"] [.="[trans]Overridden label[/trans]"] ' ); @@ -218,9 +206,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testLabelWithCustomOptionsPassedDirectly() { - $form = $this->factory->createNamed('text', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('text', 'name'); $html = $this->renderLabel($form->createView(), null, array( 'attr' => array( 'class' => 'my&class' @@ -229,7 +215,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertMatchesXpath($html, '/label - [@for="na&me"] + [@for="name"] [@class="my&class required"] ' ); @@ -237,9 +223,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testLabelWithCustomTextAndCustomOptionsPassedDirectly() { - $form = $this->factory->createNamed('text', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('text', 'name'); $html = $this->renderLabel($form->createView(), 'Custom label', array( 'attr' => array( 'class' => 'my&class' @@ -248,7 +232,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertMatchesXpath($html, '/label - [@for="na&me"] + [@for="name"] [@class="my&class required"] [.="[trans]Custom label[/trans]"] ' @@ -257,9 +241,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testErrors() { - $form = $this->factory->createNamed('text', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('text', 'name'); $form->addError(new FormError('Error 1')); $form->addError(new FormError('Error 2')); $view = $form->createView(); @@ -295,56 +277,49 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testCheckedCheckbox() { - $form = $this->factory->createNamed('checkbox', 'na&me', true, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('checkbox', 'name', true); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="checkbox"] - [@name="na&me"] + [@name="name"] [@checked="checked"] [@value="1"] ' ); } - public function testCheckedCheckboxWithValue() + public function testUncheckedCheckbox() { - $form = $this->factory->createNamed('checkbox', 'na&me', true, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('checkbox', 'name', false); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/input + [@type="checkbox"] + [@name="name"] + [not(@checked)] +' + ); + } + + public function testCheckboxWithValue() + { + $form = $this->factory->createNamed('checkbox', 'name', false, array( 'value' => 'foo&bar', )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="checkbox"] - [@name="na&me"] - [@checked="checked"] + [@name="name"] [@value="foo&bar"] ' ); } - public function testUncheckedCheckbox() - { - $form = $this->factory->createNamed('checkbox', 'na&me', false, array( - 'property_path' => 'name', - )); - - $this->assertWidgetMatchesXpath($form->createView(), array(), -'/input - [@type="checkbox"] - [@name="na&me"] - [not(@checked)] -' - ); - } - public function testSingleChoice() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'multiple' => false, 'expanded' => false, @@ -352,11 +327,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [@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] ' @@ -365,8 +340,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceWithPreferred() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'preferred_choices' => array('&b'), 'multiple' => false, @@ -375,12 +349,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array('separator' => '-- sep --'), '/select - [@name="na&me"] + [@name="name"] [@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] ' @@ -390,8 +364,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceWithPreferredAndNoSeparator() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'preferred_choices' => array('&b'), 'multiple' => false, @@ -400,11 +373,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array('separator' => null), '/select - [@name="na&me"] + [@name="name"] [@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] ' @@ -413,8 +386,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceWithPreferredAndBlankSeparator() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'preferred_choices' => array('&b'), 'multiple' => false, @@ -423,12 +395,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array('separator' => ''), '/select - [@name="na&me"] + [@name="name"] [@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] ' @@ -437,8 +409,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testChoiceWithOnlyPreferred() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'preferred_choices' => array('&a', '&b'), 'multiple' => false, @@ -454,8 +425,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceNonRequired() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'required' => false, 'multiple' => false, @@ -464,12 +434,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [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] ' @@ -478,8 +448,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceNonRequiredNoneSelected() { - $form = $this->factory->createNamed('choice', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', null, array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'required' => false, 'multiple' => false, @@ -488,12 +457,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [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] ' @@ -502,8 +471,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceWithNonRequiredEmptyValue() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'multiple' => false, 'expanded' => false, @@ -513,12 +481,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [not(@required)] [ ./option[@value=""][not(@selected)][.="[trans]Select&Anything&Not&Me[/trans]"] - /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] ' @@ -527,8 +495,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceRequiredWithEmptyValue() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'required' => true, 'multiple' => false, @@ -538,12 +505,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [@required="required"] [ ./option[@value=""][.="[trans]Test&Me[/trans]"] - /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] ' @@ -552,8 +519,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceRequiredWithEmptyValueViaView() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'required' => true, 'multiple' => false, @@ -562,12 +528,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array('empty_value' => ''), '/select - [@name="na&me"] + [@name="name"] [@required="required"] [ ./option[@value=""][.="[trans][/trans]"] - /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] ' @@ -576,8 +542,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceGrouped() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array( 'Group&1' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'Group&2' => array('&c' => 'Choice&C'), @@ -588,16 +553,16 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [./optgroup[@label="[trans]Group&1[/trans]"] [ - ./option[@value="&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] @@ -607,8 +572,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testMultipleChoice() { - $form = $this->factory->createNamed('choice', 'na&me', array('&a'), array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', array('&a'), array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'multiple' => true, 'expanded' => false, @@ -616,11 +580,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me[]"] + [@name="name[]"] [@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] ' @@ -629,8 +593,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testMultipleChoiceSkipEmptyValue() { - $form = $this->factory->createNamed('choice', 'na&me', array('&a'), array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', array('&a'), array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'multiple' => true, 'expanded' => false, @@ -639,11 +602,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me[]"] + [@name="name[]"] [@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] ' @@ -652,8 +615,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testMultipleChoiceNonRequired() { - $form = $this->factory->createNamed('choice', 'na&me', array('&a'), array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', array('&a'), array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'required' => false, 'multiple' => true, @@ -662,11 +624,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me[]"] + [@name="name[]"] [@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] ' @@ -675,8 +637,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceExpanded() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'multiple' => false, 'expanded' => true, @@ -685,10 +646,10 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/div [ - ./input[@type="radio"][@name="na&me"][@id="na&me_&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="name"][@id="name_0"][@value="0"][@checked] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="1"][not(@checked)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] ] [count(./input)=2] ' @@ -697,8 +658,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceExpandedSkipEmptyValue() { - $form = $this->factory->createNamed('choice', 'na&me', '&a', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', '&a', array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), 'multiple' => false, 'expanded' => true, @@ -708,10 +668,10 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/div [ - ./input[@type="radio"][@name="na&me"][@id="na&me_&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="name"][@id="name_0"][@checked] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][not(@checked)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] ] [count(./input)=2] ' @@ -720,8 +680,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSingleChoiceExpandedWithBooleanValue() { - $form = $this->factory->createNamed('choice', 'na&me', true, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', true, array( 'choices' => array('1' => 'Choice&A', '0' => 'Choice&B'), 'multiple' => false, 'expanded' => true, @@ -730,10 +689,10 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/div [ - ./input[@type="radio"][@name="na&me"][@id="na&me_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="name"][@id="name_0"][@checked] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][not(@checked)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] ] [count(./input)=2] ' @@ -742,8 +701,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testMultipleChoiceExpanded() { - $form = $this->factory->createNamed('choice', 'na&me', array('&a', '&c'), array( - 'property_path' => 'name', + $form = $this->factory->createNamed('choice', 'name', array('&a', '&c'), array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B', '&c' => 'Choice&C'), 'multiple' => true, 'expanded' => true, @@ -753,12 +711,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="name[0]"][@id="name_0"][@checked][not(@required)] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="checkbox"][@name="name[1]"][@id="name_1"][not(@checked)][not(@required)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] + /following-sibling::input[@type="checkbox"][@name="name[2]"][@id="name_2"][@checked][not(@required)] + /following-sibling::label[@for="name_2"][.="[trans]Choice&C[/trans]"] ] [count(./input)=3] ' @@ -767,13 +725,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testCountry() { - $form = $this->factory->createNamed('country', 'na&me', 'AT', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('country', 'name', 'AT'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [./option[@value="AT"][@selected="selected"][.="[trans]Austria[/trans]"]] [count(./option)>200] ' @@ -782,15 +738,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testCountryWithEmptyValue() { - $form = $this->factory->createNamed('country', 'na&me', 'AT', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('country', 'name', 'AT', array( 'empty_value' => 'Select&Country', 'required' => false, )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [./option[@value=""][not(@selected)][.="[trans]Select&Country[/trans]"]] [./option[@value="AT"][@selected="selected"][.="[trans]Austria[/trans]"]] [count(./option)>201] @@ -804,9 +759,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase ->method('generateCsrfToken') ->will($this->returnValue('foo&bar')); - $form = $this->factory->createNamed('csrf', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('csrf', 'name'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input @@ -818,8 +771,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateTime() { - $form = $this->factory->createNamed('datetime', 'na&me', '2011-02-03 04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('datetime', 'name', '2011-02-03 04:05:06', array( 'input' => 'string', 'with_seconds' => false, )); @@ -828,26 +780,26 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./div - [@id="na&me_date"] + [@id="name_date"] [ ./select - [@id="na&me_date_month"] + [@id="name_date_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select - [@id="na&me_date_day"] + [@id="name_date_day"] [./option[@value="3"][@selected="selected"]] /following-sibling::select - [@id="na&me_date_year"] + [@id="name_date_year"] [./option[@value="2011"][@selected="selected"]] ] /following-sibling::div - [@id="na&me_time"] + [@id="name_time"] [ ./select - [@id="na&me_time_hour"] + [@id="name_time_hour"] [./option[@value="4"][@selected="selected"]] /following-sibling::select - [@id="na&me_time_minute"] + [@id="name_time_minute"] [./option[@value="5"][@selected="selected"]] ] ] @@ -858,8 +810,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateTimeWithEmptyValueGlobal() { - $form = $this->factory->createNamed('datetime', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('datetime', 'name', null, array( 'input' => 'string', 'empty_value' => 'Change&Me', 'required' => false, @@ -869,26 +820,26 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./div - [@id="na&me_date"] + [@id="name_date"] [ ./select - [@id="na&me_date_month"] + [@id="name_date_month"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] /following-sibling::select - [@id="na&me_date_day"] + [@id="name_date_day"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] /following-sibling::select - [@id="na&me_date_year"] + [@id="name_date_year"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] ] /following-sibling::div - [@id="na&me_time"] + [@id="name_time"] [ ./select - [@id="na&me_time_hour"] + [@id="name_time_hour"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] /following-sibling::select - [@id="na&me_time_minute"] + [@id="name_time_minute"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] ] ] @@ -899,8 +850,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateTimeWithEmptyValueOnTime() { - $form = $this->factory->createNamed('datetime', 'na&me', '2011-02-03', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('datetime', 'name', '2011-02-03', array( 'input' => 'string', 'empty_value' => array('hour' => 'Change&Me', 'minute' => 'Change&Me'), 'required' => false, @@ -910,26 +860,26 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./div - [@id="na&me_date"] + [@id="name_date"] [ ./select - [@id="na&me_date_month"] + [@id="name_date_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select - [@id="na&me_date_day"] + [@id="name_date_day"] [./option[@value="3"][@selected="selected"]] /following-sibling::select - [@id="na&me_date_year"] + [@id="name_date_year"] [./option[@value="2011"][@selected="selected"]] ] /following-sibling::div - [@id="na&me_time"] + [@id="name_time"] [ ./select - [@id="na&me_time_hour"] + [@id="name_time_hour"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] /following-sibling::select - [@id="na&me_time_minute"] + [@id="name_time_minute"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] ] ] @@ -940,8 +890,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateTimeWithSeconds() { - $form = $this->factory->createNamed('datetime', 'na&me', '2011-02-03 04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('datetime', 'name', '2011-02-03 04:05:06', array( 'input' => 'string', 'with_seconds' => true, )); @@ -950,29 +899,29 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./div - [@id="na&me_date"] + [@id="name_date"] [ ./select - [@id="na&me_date_month"] + [@id="name_date_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select - [@id="na&me_date_day"] + [@id="name_date_day"] [./option[@value="3"][@selected="selected"]] /following-sibling::select - [@id="na&me_date_year"] + [@id="name_date_year"] [./option[@value="2011"][@selected="selected"]] ] /following-sibling::div - [@id="na&me_time"] + [@id="name_time"] [ ./select - [@id="na&me_time_hour"] + [@id="name_time_hour"] [./option[@value="4"][@selected="selected"]] /following-sibling::select - [@id="na&me_time_minute"] + [@id="name_time_minute"] [./option[@value="5"][@selected="selected"]] /following-sibling::select - [@id="na&me_time_second"] + [@id="name_time_second"] [./option[@value="6"][@selected="selected"]] ] ] @@ -983,8 +932,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateTimeSingleText() { - $form = $this->factory->createNamed('datetime', 'na&me', '2011-02-03 04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('datetime', 'name', '2011-02-03 04:05:06', array( 'input' => 'string', 'date_widget' => 'single_text', 'time_widget' => 'single_text', @@ -995,13 +943,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [ ./input [@type="text"] - [@id="na&me_date"] - [@name="na&me[date]"] + [@id="name_date"] + [@name="name[date]"] [@value="Feb 3, 2011"] /following-sibling::input [@type="text"] - [@id="na&me_time"] - [@name="na&me[time]"] + [@id="name_time"] + [@name="name[time]"] [@value="04:05:00"] ] ' @@ -1011,7 +959,6 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateTimeWithWidgetSingleText() { $form = $this->factory->createNamed('datetime', 'name', '2011-02-03 04:05:06', array( - 'property_path' => 'name', 'input' => 'string', 'widget' => 'single_text', )); @@ -1027,8 +974,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateTimeWithWidgetSingleTextIgnoreDateAndTimeWidgets() { - $form = $this->factory->createNamed('datetime', 'na&me', '2011-02-03 04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('datetime', 'name', '2011-02-03 04:05:06', array( 'input' => 'string', 'date_widget' => 'choice', 'time_widget' => 'choice', @@ -1038,7 +984,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="2011-02-03 04:05:00"] ' ); @@ -1046,8 +992,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateChoice() { - $form = $this->factory->createNamed('date', 'na&me', '2011-02-03', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('date', 'name', '2011-02-03', array( 'input' => 'string', 'widget' => 'choice', )); @@ -1056,13 +1001,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_month"] + [@id="name_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select - [@id="na&me_day"] + [@id="name_day"] [./option[@value="3"][@selected="selected"]] /following-sibling::select - [@id="na&me_year"] + [@id="name_year"] [./option[@value="2011"][@selected="selected"]] ] [count(./select)=3] @@ -1072,8 +1017,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateChoiceWithEmptyValueGlobal() { - $form = $this->factory->createNamed('date', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('date', 'name', null, array( 'input' => 'string', 'widget' => 'choice', 'empty_value' => 'Change&Me', @@ -1084,13 +1028,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_month"] + [@id="name_month"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] /following-sibling::select - [@id="na&me_day"] + [@id="name_day"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] /following-sibling::select - [@id="na&me_year"] + [@id="name_year"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] ] [count(./select)=3] @@ -1100,8 +1044,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateChoiceWithEmptyValueOnYear() { - $form = $this->factory->createNamed('date', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('date', 'name', null, array( 'input' => 'string', 'widget' => 'choice', 'required' => false, @@ -1112,13 +1055,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_month"] + [@id="name_month"] [./option[@value="1"]] /following-sibling::select - [@id="na&me_day"] + [@id="name_day"] [./option[@value="1"]] /following-sibling::select - [@id="na&me_year"] + [@id="name_year"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] ] [count(./select)=3] @@ -1128,8 +1071,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateText() { - $form = $this->factory->createNamed('date', 'na&me', '2011-02-03', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('date', 'name', '2011-02-03', array( 'input' => 'string', 'widget' => 'text', )); @@ -1138,15 +1080,15 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./input - [@id="na&me_month"] + [@id="name_month"] [@type="text"] [@value="2"] /following-sibling::input - [@id="na&me_day"] + [@id="name_day"] [@type="text"] [@value="3"] /following-sibling::input - [@id="na&me_year"] + [@id="name_year"] [@type="text"] [@value="2011"] ] @@ -1157,8 +1099,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testDateSingleText() { - $form = $this->factory->createNamed('date', 'na&me', '2011-02-03', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('date', 'name', '2011-02-03', array( 'input' => 'string', 'widget' => 'single_text', )); @@ -1166,7 +1107,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="Feb 3, 2011"] ' ); @@ -1185,8 +1126,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testBirthDay() { - $form = $this->factory->createNamed('birthday', 'na&me', '2000-02-03', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('birthday', 'name', '2000-02-03', array( 'input' => 'string', )); @@ -1194,13 +1134,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_month"] + [@id="name_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select - [@id="na&me_day"] + [@id="name_day"] [./option[@value="3"][@selected="selected"]] /following-sibling::select - [@id="na&me_year"] + [@id="name_year"] [./option[@value="2000"][@selected="selected"]] ] [count(./select)=3] @@ -1210,8 +1150,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testBirthDayWithEmptyValue() { - $form = $this->factory->createNamed('birthday', 'na&me', '1950-01-01', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('birthday', 'name', '1950-01-01', array( 'input' => 'string', 'empty_value' => '', 'required' => false, @@ -1221,15 +1160,15 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_month"] + [@id="name_month"] [./option[@value=""][.="[trans][/trans]"]] [./option[@value="1"][@selected="selected"]] /following-sibling::select - [@id="na&me_day"] + [@id="name_day"] [./option[@value=""][.="[trans][/trans]"]] [./option[@value="1"][@selected="selected"]] /following-sibling::select - [@id="na&me_year"] + [@id="name_year"] [./option[@value=""][.="[trans][/trans]"]] [./option[@value="1950"][@selected="selected"]] ] @@ -1240,14 +1179,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testEmail() { - $form = $this->factory->createNamed('email', 'na&me', 'foo&bar', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('email', 'name', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="email"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] [not(@maxlength)] ' @@ -1256,15 +1193,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testEmailWithMaxLength() { - $form = $this->factory->createNamed('email', 'na&me', 'foo&bar', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('email', 'name', 'foo&bar', array( 'max_length' => 123, )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="email"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] [@maxlength="123"] ' @@ -1273,9 +1209,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testFile() { - $form = $this->factory->createNamed('file', 'na&me', null, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('file', 'name'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input @@ -1286,14 +1220,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testHidden() { - $form = $this->factory->createNamed('hidden', 'na&me', 'foo&bar', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('hidden', 'name', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="hidden"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] ' ); @@ -1301,14 +1233,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testInteger() { - $form = $this->factory->createNamed('integer', 'na&me', 123, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('integer', 'name', 123); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="number"] - [@name="na&me"] + [@name="name"] [@value="123"] ' ); @@ -1316,13 +1246,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testLanguage() { - $form = $this->factory->createNamed('language', 'na&me', 'de', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('language', 'name', 'de'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [./option[@value="de"][@selected="selected"][.="[trans]German[/trans]"]] [count(./option)>200] ' @@ -1331,13 +1259,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testLocale() { - $form = $this->factory->createNamed('locale', 'na&me', 'de_AT', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('locale', 'name', 'de_AT'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [./option[@value="de_AT"][@selected="selected"][.="[trans]German (Austria)[/trans]"]] [count(./option)>200] ' @@ -1346,15 +1272,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testMoney() { - $form = $this->factory->createNamed('money', 'na&me', 1234.56, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('money', 'name', 1234.56, array( 'currency' => 'EUR', )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="1234.56"] [contains(.., "€")] ' @@ -1363,14 +1288,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testNumber() { - $form = $this->factory->createNamed('number', 'na&me', 1234.56, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('number', 'name', 1234.56); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="1234.56"] ' ); @@ -1378,22 +1301,19 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testPassword() { - $form = $this->factory->createNamed('password', 'na&me', 'foo&bar', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('password', 'name', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="password"] - [@name="na&me"] + [@name="name"] ' ); } public function testPasswordBoundNotAlwaysEmpty() { - $form = $this->factory->createNamed('password', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('password', 'name', null, array( 'always_empty' => false, )); $form->bind('foo&bar'); @@ -1401,7 +1321,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="password"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] ' ); @@ -1409,15 +1329,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testPasswordWithMaxLength() { - $form = $this->factory->createNamed('password', 'na&me', 'foo&bar', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('password', 'name', 'foo&bar', array( 'max_length' => 123, )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="password"] - [@name="na&me"] + [@name="name"] [@maxlength="123"] ' ); @@ -1425,14 +1344,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testPercent() { - $form = $this->factory->createNamed('percent', 'na&me', 0.1, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('percent', 'name', 0.1); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="10"] [contains(.., "%")] ' @@ -1441,62 +1358,55 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testCheckedRadio() { - $form = $this->factory->createNamed('radio', 'na&me', true, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('radio', 'name', true); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="radio"] - [@name="na&me"] + [@name="name"] [@checked="checked"] - [@value=""] -' - ); - } - - 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"] ' ); } public function testUncheckedRadio() { - $form = $this->factory->createNamed('radio', 'na&me', false, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('radio', 'name', false); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/input + [@type="radio"] + [@name="name"] + [not(@checked)] +' + ); + } + + public function testRadioWithValue() + { + $form = $this->factory->createNamed('radio', 'name', false, array( + 'value' => 'foo&bar', )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="radio"] - [@name="na&me"] - [not(@checked)] + [@name="name"] + [@value="foo&bar"] ' ); } public function testTextarea() { - $form = $this->factory->createNamed('textarea', 'na&me', 'foo&bar', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('textarea', 'name', 'foo&bar', array( 'pattern' => 'foo', )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/textarea - [@name="na&me"] + [@name="name"] [not(@pattern)] [.="foo&bar"] ' @@ -1505,14 +1415,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testText() { - $form = $this->factory->createNamed('text', 'na&me', 'foo&bar', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('text', 'name', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] [not(@maxlength)] ' @@ -1521,15 +1429,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTextWithMaxLength() { - $form = $this->factory->createNamed('text', 'na&me', 'foo&bar', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('text', 'name', 'foo&bar', array( 'max_length' => 123, )); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] [@maxlength="123"] ' @@ -1538,14 +1445,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testSearch() { - $form = $this->factory->createNamed('search', 'na&me', 'foo&bar', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('search', 'name', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="search"] - [@name="na&me"] + [@name="name"] [@value="foo&bar"] [not(@maxlength)] ' @@ -1554,8 +1459,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTime() { - $form = $this->factory->createNamed('time', 'na&me', '04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('time', 'name', '04:05:06', array( 'input' => 'string', 'with_seconds' => false, )); @@ -1564,11 +1468,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_hour"] + [@id="name_hour"] [@size="1"] [./option[@value="4"][@selected="selected"]] /following-sibling::select - [@id="na&me_minute"] + [@id="name_minute"] [@size="1"] [./option[@value="5"][@selected="selected"]] ] @@ -1579,8 +1483,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTimeWithSeconds() { - $form = $this->factory->createNamed('time', 'na&me', '04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('time', 'name', '04:05:06', array( 'input' => 'string', 'with_seconds' => true, )); @@ -1589,17 +1492,17 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_hour"] + [@id="name_hour"] [@size="1"] [./option[@value="4"][@selected="selected"]] [count(./option)>23] /following-sibling::select - [@id="na&me_minute"] + [@id="name_minute"] [@size="1"] [./option[@value="5"][@selected="selected"]] [count(./option)>59] /following-sibling::select - [@id="na&me_second"] + [@id="name_second"] [@size="1"] [./option[@value="6"][@selected="selected"]] [count(./option)>59] @@ -1611,8 +1514,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTimeText() { - $form = $this->factory->createNamed('time', 'na&me', '04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('time', 'name', '04:05:06', array( 'input' => 'string', 'widget' => 'text', )); @@ -1622,15 +1524,15 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [ ./input [@type="text"] - [@id="na&me_hour"] - [@name="na&me[hour]"] + [@id="name_hour"] + [@name="name[hour]"] [@value="04"] [@size="1"] [@required="required"] /following-sibling::input [@type="text"] - [@id="na&me_minute"] - [@name="na&me[minute]"] + [@id="name_minute"] + [@name="name[minute]"] [@value="05"] [@size="1"] [@required="required"] @@ -1642,8 +1544,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTimeSingleText() { - $form = $this->factory->createNamed('time', 'na&me', '04:05:06', array( - 'property_path' => 'name', + $form = $this->factory->createNamed('time', 'name', '04:05:06', array( 'input' => 'string', 'widget' => 'single_text', )); @@ -1651,7 +1552,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="text"] - [@name="na&me"] + [@name="name"] [@value="04:05:00"] ' ); @@ -1659,8 +1560,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTimeWithEmptyValueGlobal() { - $form = $this->factory->createNamed('time', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('time', 'name', null, array( 'input' => 'string', 'empty_value' => 'Change&Me', 'required' => false, @@ -1670,11 +1570,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_hour"] + [@id="name_hour"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] [count(./option)>24] /following-sibling::select - [@id="na&me_minute"] + [@id="name_minute"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] [count(./option)>60] ] @@ -1685,8 +1585,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTimeWithEmptyValueOnYear() { - $form = $this->factory->createNamed('time', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('time', 'name', null, array( 'input' => 'string', 'required' => false, 'empty_value' => array('hour' => 'Change&Me'), @@ -1696,11 +1595,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select - [@id="na&me_hour"] + [@id="name_hour"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] [count(./option)>24] /following-sibling::select - [@id="na&me_minute"] + [@id="name_minute"] [./option[@value="1"]] [count(./option)>59] ] @@ -1722,13 +1621,11 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTimezone() { - $form = $this->factory->createNamed('timezone', 'na&me', 'Europe/Vienna', array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('timezone', 'name', 'Europe/Vienna'); $this->assertWidgetMatchesXpath($form->createView(), array(), '/select - [@name="na&me"] + [@name="name"] [@required="required"] [./optgroup [@label="[trans]Europe[/trans]"] @@ -1742,8 +1639,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testTimezoneWithEmptyValue() { - $form = $this->factory->createNamed('timezone', 'na&me', null, array( - 'property_path' => 'name', + $form = $this->factory->createNamed('timezone', 'name', null, array( 'empty_value' => 'Select&Timezone', 'required' => false, )); @@ -1760,14 +1656,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testUrl() { $url = 'http://www.google.com?foo1=bar1&foo2=bar2'; - $form = $this->factory->createNamed('url', 'na&me', $url, array( - 'property_path' => 'name', - )); + $form = $this->factory->createNamed('url', 'name', $url); $this->assertWidgetMatchesXpath($form->createView(), array(), '/input [@type="url"] - [@name="na&me"] + [@name="name"] [@value="http://www.google.com?foo1=bar1&foo2=bar2"] ' ); @@ -1775,7 +1669,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testCollectionPrototype() { - $form = $this->factory->createNamedBuilder('form', 'na&me', array('items' => array('one', 'two', 'three'))) + $form = $this->factory->createNamedBuilder('form', 'name', array('items' => array('one', 'two', 'three'))) ->add('items', 'collection', array('allow_add' => true)) ->getForm() ->createView(); @@ -1783,9 +1677,9 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $html = $this->renderWidget($form); $this->assertMatchesXpath($html, - '//div[@id="na&me_items"][@data-prototype] + '//div[@id="name_items"][@data-prototype] | - //table[@id="na&me_items"][@data-prototype] + //table[@id="name_items"][@data-prototype] ' ); @@ -1793,9 +1687,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase public function testEmptyRootFormName() { - $form = $this->factory->createNamedBuilder('form', '', '', array( - 'property_path' => 'name', - )) + $form = $this->factory->createNamedBuilder('form', '', '') ->add('child', 'text') ->getForm(); diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/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/ChoiceListTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ChoiceListTest.php new file mode 100644 index 0000000000..7774b876c1 --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ChoiceListTest.php @@ -0,0 +1,166 @@ + + * + * 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\View\ChoiceView; + +class ChoiceListTest 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 ChoiceList( + 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 ChoiceList( + 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('0', '1', '2', '3'), $this->list->getValues()); + $this->assertEquals(array(1 => new ChoiceView('1', 'B')), $this->list->getPreferredViews()); + $this->assertEquals(array(0 => new ChoiceView('0', 'A'), 2 => new ChoiceView('2', 'C'), 3 => new ChoiceView('3', 'D')), $this->list->getRemainingViews()); + } + + public function testInitNestedArray() + { + $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); + $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues()); + $this->assertEquals(array( + 'Group 1' => array(1 => new ChoiceView('1', 'B')), + 'Group 2' => array(2 => new ChoiceView('2', 'C')) + ), $this->list->getPreferredViews()); + $this->assertEquals(array( + 'Group 1' => array(0 => new ChoiceView('0', 'A')), + 'Group 2' => array(3 => new ChoiceView('3', 'D')) + ), $this->list->getRemainingViews()); + } + + /** + * @expectedException Symfony\Component\Form\Exception\InvalidConfigurationException + */ + public function testInitIndexCopyChoiceWithInvalidIndex() + { + new ChoiceList( + array('a.'), + array('A'), + array(), + ChoiceList::GENERATE, + ChoiceList::COPY_CHOICE + ); + } + + /** + * @expectedException Symfony\Component\Form\Exception\InvalidConfigurationException + */ + public function testInitValueCopyChoiceWithInvalidValue() + { + new ChoiceList( + array($this->obj1), + array('A'), + array(), + ChoiceList::COPY_CHOICE, + ChoiceList::GENERATE + ); + } + + public function testGetIndicesForChoices() + { + $choices = array($this->obj2, $this->obj3); + $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..5635cab57c --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/ObjectChoiceListTest.php @@ -0,0 +1,231 @@ + + * + * 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; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; + +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('0', '1', '2', '3'), $this->list->getValues()); + $this->assertEquals(array(1 => new ChoiceView('1', 'B')), $this->list->getPreferredViews()); + $this->assertEquals(array(0 => new ChoiceView('0', 'A'), 2 => new ChoiceView('2', 'C'), 3 => new ChoiceView('3', 'D')), $this->list->getRemainingViews()); + } + + public function testInitNestedArray() + { + $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); + $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues()); + $this->assertEquals(array( + 'Group 1' => array(1 => new ChoiceView('1', 'B')), + 'Group 2' => array(2 => new ChoiceView('2', 'C')) + ), $this->list->getPreferredViews()); + $this->assertEquals(array( + 'Group 1' => array(0 => new ChoiceView('0', 'A')), + 'Group 2' => array(3 => new ChoiceView('3', 'D')) + ), $this->list->getRemainingViews()); + } + + public function testInitArrayWithGroupPath() + { + $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('0', '1', '2', '3', '4', '5'), $this->list->getValues()); + $this->assertEquals(array( + 'Group 1' => array(1 => new ChoiceView('1', 'B')), + 'Group 2' => array(2 => new ChoiceView('2', 'C')) + ), $this->list->getPreferredViews()); + $this->assertEquals(array( + 'Group 1' => array(0 => new ChoiceView('0', 'A')), + 'Group 2' => array(3 => new ChoiceView('3', 'D')), + 4 => new ChoiceView('4', 'E'), + 5 => new ChoiceView('5', 'F'), + ), $this->list->getRemainingViews()); + } + + /** + * @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('10', '20', '30', '40'), $this->list->getValues()); + $this->assertEquals(array(1 => new ChoiceView('20', 'B'), 2 => new ChoiceView('30', 'C')), $this->list->getPreferredViews()); + $this->assertEquals(array(0 => new ChoiceView('10', 'A'), 3 => new ChoiceView('40', 'D')), $this->list->getRemainingViews()); + } + + public function testInitArrayWithIndexPath() + { + $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 => '0', 20 => '1', 30 => '2', 40 => '3'), $this->list->getValues()); + $this->assertEquals(array(20 => new ChoiceView('1', 'B'), 30 => new ChoiceView('2', 'C')), $this->list->getPreferredViews()); + $this->assertEquals(array(10 => new ChoiceView('0', 'A'), 40 => new ChoiceView('3', 'D')), $this->list->getRemainingViews()); + } + + public function testInitArrayUsesToString() + { + $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('0', '1', '2', '3'), $this->list->getValues()); + $this->assertEquals(array(0 => new ChoiceView('0', 'A'), 1 => new ChoiceView('1', 'B'), 2 => new ChoiceView('2', 'C'), 3 => new ChoiceView('3', 'D')), $this->list->getRemainingViews()); + } + + /** + * @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'); + + new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4) + ); + } +} 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..cb6afcd815 --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/SimpleChoiceListTest.php @@ -0,0 +1,210 @@ + + * + * 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; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; + +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 => '0', 1 => '1', 2 => '2'), $this->list->getValues()); + $this->assertEquals(array(1 => new ChoiceView('1', 'B')), $this->list->getPreferredViews()); + $this->assertEquals(array(0 => new ChoiceView('0', 'A'), 2 => new ChoiceView('2', 'C')), $this->list->getRemainingViews()); + } + + public function testInitArrayValueCopyChoice() + { + $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->getValues()); + $this->assertEquals(array(1 => new ChoiceView('b', 'B')), $this->list->getPreferredViews()); + $this->assertEquals(array(0 => new ChoiceView('a', 'A'), 2 => new ChoiceView('c', 'C')), $this->list->getRemainingViews()); + } + + public function testInitArrayIndexCopyChoice() + { + $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' => '0', 'b' => '1', 'c' => '2'), $this->list->getValues()); + $this->assertEquals(array('b' => new ChoiceView('1', 'B')), $this->list->getPreferredViews()); + $this->assertEquals(array('a' => new ChoiceView('0', 'A'), 'c' => new ChoiceView('2', 'C')), $this->list->getRemainingViews()); + } + + public function testInitNestedArray() + { + $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd'), $this->list->getChoices()); + $this->assertSame(array(0 => '0', 1 => '1', 2 => '2', 3 => '3'), $this->list->getValues()); + $this->assertEquals(array( + 'Group 1' => array(1 => new ChoiceView('1', 'B')), + 'Group 2' => array(2 => new ChoiceView('2', 'C')) + ), $this->list->getPreferredViews()); + $this->assertEquals(array( + 'Group 1' => array(0 => new ChoiceView('0', 'A')), + 'Group 2' => array(3 => new ChoiceView('3', 'D')) + ), $this->list->getRemainingViews()); + } + + public function testGetIndicesForChoices() + { + $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..cf26d5ea29 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,9 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; +use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Exception\UnexpectedTypeException; class ChoiceTypeTest extends TypeTestCase @@ -31,6 +34,8 @@ class ChoiceTypeTest extends TypeTestCase 4 => 'Roman', ); + private $objectChoices; + private $stringButNumericChoices = array( '0' => 'Bernhard', '1' => 'Fabien', @@ -51,8 +56,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 +97,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 +125,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 +185,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 +222,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 +258,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 +287,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 +348,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 +371,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 +392,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 +446,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 +599,12 @@ class ChoiceTypeTest extends TypeTestCase )); $view = $form->createView(); - $this->assertSame($choices, $view->get('choices')); + $this->assertEquals(array( + new ChoiceView('0', 'A'), + new ChoiceView('1', 'B'), + new ChoiceView('2', 'C'), + new ChoiceView('3', 'D'), + ), $view->get('choices')); } public function testPassPreferredChoicesToView() @@ -449,8 +616,41 @@ 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->assertEquals(array( + 0 => new ChoiceView('0', 'A'), + 2 => new ChoiceView('2', 'C'), + ), $view->get('choices')); + $this->assertEquals(array( + 1 => new ChoiceView('1', 'B'), + 3 => new ChoiceView('3', 'D'), + ), $view->get('preferred_choices')); + } + + public function testPassHierarchicalChoicesToView() + { + $form = $this->factory->create('choice', null, array( + 'choices' => $this->groupedChoices, + 'preferred_choices' => array('b', 'd'), + )); + $view = $form->createView(); + + $this->assertEquals(array( + 'Symfony' => array( + 0 => new ChoiceView('0', 'Bernhard'), + 2 => new ChoiceView('2', 'Kris'), + ), + 'Doctrine' => array( + 4 => new ChoiceView('4', 'Roman'), + ), + ), $view->get('choices')); + $this->assertEquals(array( + 'Symfony' => array( + 1 => new ChoiceView('1', 'Fabien'), + ), + 'Doctrine' => array( + 3 => new ChoiceView('3', 'Jon'), + ), + ), $view->get('preferred_choices')); } public function testAdjustFullNameForMultipleNonExpanded() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CollectionTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CollectionTypeTest.php index 8253ba04fd..615a1d4fdc 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CollectionTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CollectionTypeTest.php @@ -125,7 +125,7 @@ class CollectionFormTest extends TypeTestCase 'prototype' => false, )); - $this->assertFalse($form->has('$$name$$')); + $this->assertFalse($form->has('__name__')); } public function testPrototypeMultipartPropagation() @@ -150,7 +150,7 @@ class CollectionFormTest extends TypeTestCase )); $data = $form->getData(); - $this->assertFalse(isset($data['$$name$$'])); + $this->assertFalse(isset($data['__name__'])); } public function testGetDataDoesNotContainsPrototypeNameAfterDataAreSet() @@ -163,7 +163,7 @@ class CollectionFormTest extends TypeTestCase $form->setData(array('foobar.png')); $data = $form->getData(); - $this->assertFalse(isset($data['$$name$$'])); + $this->assertFalse(isset($data['__name__'])); } public function testPrototypeNameOption() @@ -174,15 +174,15 @@ class CollectionFormTest extends TypeTestCase 'allow_add' => true, )); - $this->assertSame('$$name$$', $form->getAttribute('prototype')->getName(), '$$name$$ is the default'); + $this->assertSame('__name__', $form->getAttribute('prototype')->getName(), '__name__ is the default'); $form = $this->factory->create('collection', null, array( 'type' => 'field', 'prototype' => true, 'allow_add' => true, - 'prototype_name' => 'test', + 'prototype_name' => '__test__', )); - $this->assertSame('$$test$$', $form->getAttribute('prototype')->getName()); + $this->assertSame('__test__', $form->getAttribute('prototype')->getName()); } } diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CountryTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CountryTypeTest.php index ff79433e0e..5d819af566 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CountryTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CountryTypeTest.php @@ -11,6 +11,7 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class CountryTypeTest extends LocalizedTestCase { @@ -22,16 +23,11 @@ class CountryTypeTest extends LocalizedTestCase $view = $form->createView(); $choices = $view->get('choices'); - $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->assertEquals(new ChoiceView('DE', 'Deutschland'), $choices['DE']); + $this->assertEquals(new ChoiceView('GB', 'Vereinigtes Königreich'), $choices['GB']); + $this->assertEquals(new ChoiceView('US', 'Vereinigte Staaten'), $choices['US']); + $this->assertEquals(new ChoiceView('FR', 'Frankreich'), $choices['FR']); + $this->assertEquals(new ChoiceView('MY', 'Malaysia'), $choices['MY']); } public function testUnknownCountryIsNotIncluded() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/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..1be7affc70 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/DateTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/DateTypeTest.php @@ -11,8 +11,9 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; -require_once __DIR__ . '/LocalizedTestCase.php'; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; +require_once __DIR__ . '/LocalizedTestCase.php'; class DateTypeTest extends LocalizedTestCase { @@ -325,7 +326,10 @@ class DateTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(2010 => '2010', 2011 => '2011'), $view->getChild('year')->get('choices')); + $this->assertEquals(array( + 2010 => new ChoiceView('2010', '2010'), + 2011 => new ChoiceView('2011', '2011'), + ), $view->getChild('year')->get('choices')); } public function testMonthsOption() @@ -336,7 +340,70 @@ class DateTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('month')->get('choices')); + $this->assertEquals(array( + 6 => new ChoiceView('6', '06'), + 7 => new ChoiceView('7', '07'), + ), $view->getChild('month')->get('choices')); + } + + public function testMonthsOptionNumericIfFormatContainsNoMonth() + { + $form = $this->factory->create('date', null, array( + 'months' => array(6, 7), + 'format' => 'yy', + )); + + $view = $form->createView(); + + $this->assertEquals(array( + 6 => new ChoiceView('6', '06'), + 7 => new ChoiceView('7', '07'), + ), $view->getChild('month')->get('choices')); + } + + public function testMonthsOptionShortFormat() + { + $form = $this->factory->create('date', null, array( + 'months' => array(1, 4), + 'format' => 'dd.MMM.yy', + )); + + $view = $form->createView(); + + $this->assertEquals(array( + 1 => new ChoiceView('1', 'Jän'), + 4 => new ChoiceView('4', 'Apr') + ), $view->getChild('month')->get('choices')); + } + + public function testMonthsOptionLongFormat() + { + $form = $this->factory->create('date', null, array( + 'months' => array(1, 4), + 'format' => 'dd.MMMM.yy', + )); + + $view = $form->createView(); + + $this->assertEquals(array( + 1 => new ChoiceView('1', 'Jänner'), + 4 => new ChoiceView('4', 'April'), + ), $view->getChild('month')->get('choices')); + } + + public function testMonthsOptionLongFormatWithDifferentTimezone() + { + $form = $this->factory->create('date', null, array( + 'months' => array(1, 4), + 'format' => 'dd.MMMM.yy', + )); + + $view = $form->createView(); + + $this->assertEquals(array( + 1 => new ChoiceView('1', 'Jänner'), + 4 => new ChoiceView('4', 'April'), + ), $view->getChild('month')->get('choices')); } public function testIsDayWithinRangeReturnsTrueIfWithin() @@ -347,7 +414,10 @@ class DateTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('day')->get('choices')); + $this->assertEquals(array( + 6 => new ChoiceView('6', '06'), + 7 => new ChoiceView('7', '07'), + ), $view->getChild('day')->get('choices')); } public function testIsPartiallyFilledReturnsFalseIfSingleText() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/FieldTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/FieldTypeTest.php index a308215e15..690167679e 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/FieldTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/FieldTypeTest.php @@ -117,6 +117,16 @@ class FieldTypeTest extends TypeTestCase $this->assertEquals('name', $view->get('full_name')); } + public function testStripLeadingUnderscoresAndDigitsFromId() + { + $form = $this->factory->createNamed('field', '_09name'); + $view = $form->createView(); + + $this->assertEquals('name', $view->get('id')); + $this->assertEquals('_09name', $view->get('name')); + $this->assertEquals('_09name', $view->get('full_name')); + } + public function testPassIdAndNameToViewWithParent() { $parent = $this->factory->createNamed('field', 'parent'); diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LanguageTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LanguageTypeTest.php index 096b568edd..fc80a0e06c 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LanguageTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LanguageTypeTest.php @@ -11,6 +11,7 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class LanguageTypeTest extends LocalizedTestCase { @@ -21,17 +22,13 @@ 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(new ChoiceView('en', 'Englisch'), $choices, '', false, false); + $this->assertContains(new ChoiceView('en_GB', 'Britisches Englisch'), $choices, '', false, false); + $this->assertContains(new ChoiceView('en_US', 'Amerikanisches Englisch'), $choices, '', false, false); + $this->assertContains(new ChoiceView('fr', 'Französisch'), $choices, '', false, false); + $this->assertContains(new ChoiceView('my', 'Birmanisch'), $choices, '', false, false); } public function testMultipleLanguagesIsNotIncluded() @@ -40,6 +37,6 @@ class LanguageTypeTest extends LocalizedTestCase $view = $form->createView(); $choices = $view->get('choices'); - $this->assertArrayNotHasKey('mul', $choices); + $this->assertNotContains(new ChoiceView('mul', 'Mehrsprachig'), $choices, '', false, false); } } diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LocaleTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LocaleTypeTest.php index c813c6c3cc..8f4bd97869 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LocaleTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/LocaleTypeTest.php @@ -11,6 +11,7 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class LocaleTypeTest extends LocalizedTestCase { @@ -22,11 +23,8 @@ class LocaleTypeTest extends LocalizedTestCase $view = $form->createView(); $choices = $view->get('choices'); - $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(new ChoiceView('en', 'Englisch'), $choices, '', false, false); + $this->assertContains(new ChoiceView('en_GB', 'Englisch (Vereinigtes Königreich)'), $choices, '', false, false); + $this->assertContains(new ChoiceView('zh_Hant_MO', 'Chinesisch (traditionell, Sonderverwaltungszone Macao)'), $choices, '', false, false); } } diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/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..34676ddf19 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimeTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimeTypeTest.php @@ -11,6 +11,8 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; + require_once __DIR__ . '/LocalizedTestCase.php'; @@ -243,7 +245,10 @@ class TimeTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('hour')->get('choices')); + $this->assertEquals(array( + 6 => new ChoiceView('6', '06'), + 7 => new ChoiceView('7', '07'), + ), $view->getChild('hour')->get('choices')); } public function testIsMinuteWithinRange_returnsTrueIfWithin() @@ -254,7 +259,10 @@ class TimeTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('minute')->get('choices')); + $this->assertEquals(array( + 6 => new ChoiceView('6', '06'), + 7 => new ChoiceView('7', '07'), + ), $view->getChild('minute')->get('choices')); } public function testIsSecondWithinRange_returnsTrueIfWithin() @@ -266,7 +274,10 @@ class TimeTypeTest extends LocalizedTestCase $view = $form->createView(); - $this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('second')->get('choices')); + $this->assertEquals(array( + 6 => new ChoiceView('6', '06'), + 7 => new ChoiceView('7', '07'), + ), $view->getChild('second')->get('choices')); } public function testIsPartiallyFilled_returnsFalseIfCompletelyEmpty() diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimezoneTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimezoneTypeTest.php index 94e32f9a87..f8f5126c32 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimezoneTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/TimezoneTypeTest.php @@ -11,6 +11,7 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; class TimezoneTypeTest extends TypeTestCase { @@ -19,13 +20,12 @@ 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(new ChoiceView('Africa/Kinshasa', 'Kinshasa'), $choices['Africa'], '', false, false); $this->assertArrayHasKey('America', $choices); - $this->assertArrayHasKey('America/New_York', $choices['America']); - $this->assertEquals('New York', $choices['America']['America/New_York']); + $this->assertContains(new ChoiceView('America/New_York', 'New York'), $choices['America'], '', false, false); } } diff --git a/tests/Symfony/Tests/Component/Form/FormBuilderTest.php b/tests/Symfony/Tests/Component/Form/FormBuilderTest.php index 87a94348cd..505022d32a 100644 --- a/tests/Symfony/Tests/Component/Form/FormBuilderTest.php +++ b/tests/Symfony/Tests/Component/Form/FormBuilderTest.php @@ -36,6 +36,37 @@ class FormBuilderTest extends \PHPUnit_Framework_TestCase $this->builder = null; } + public function getHtml4Ids() + { + // The full list is tested in FormTest, since both Form and FormBuilder + // use the same implementation internally + return array( + array('#', false), + array('a ', false), + array("a\t", false), + array("a\n", false), + array('a.', false), + ); + } + + /** + * @dataProvider getHtml4Ids + */ + public function testConstructAcceptsOnlyNamesValidAsIdsInHtml4($name, $accepted) + { + try { + new FormBuilder($name, $this->factory, $this->dispatcher); + if (!$accepted) { + $this->fail(sprintf('The value "%s" should not be accepted', $name)); + } + } catch (\InvalidArgumentException $e) { + // if the value was not accepted, but should be, rethrow exception + if ($accepted) { + throw $e; + } + } + } + /** * Changing the name is not allowed, otherwise the name and property path * are not synchronized anymore diff --git a/tests/Symfony/Tests/Component/Form/FormTest.php b/tests/Symfony/Tests/Component/Form/FormTest.php index 9975e0bed8..15ac2e8fff 100644 --- a/tests/Symfony/Tests/Component/Form/FormTest.php +++ b/tests/Symfony/Tests/Component/Form/FormTest.php @@ -60,6 +60,62 @@ class FormTest extends \PHPUnit_Framework_TestCase new Form('name', $this->dispatcher, array(), array(), array(), null, $validators); } + public function getHtml4Ids() + { + return array( + array('a0', true), + array('a9', true), + array('z0', true), + array('A0', true), + array('A9', true), + array('Z0', true), + array('#', false), + array('a#', false), + array('a$', false), + array('a%', false), + array('a ', false), + array("a\t", false), + array("a\n", false), + array('a-', true), + array('a_', true), + array('a:', true), + // Periods are allowed by the HTML4 spec, but disallowed by us + // because they break the generated property paths + array('a.', false), + // Contrary to the HTML4 spec, we allow names starting with a + // number, otherwise naming fields by collection indices is not + // possible. + // For root forms, leading digits will be stripped from the + // "id" attribute to produce valid HTML4. + array('0', true), + array('9', true), + // Contrary to the HTML4 spec, we allow names starting with an + // underscore, since this is already a widely used practice in + // Symfony2. + // For root forms, leading underscores will be stripped from the + // "id" attribute to produce valid HTML4. + array('_', true), + ); + } + + /** + * @dataProvider getHtml4Ids + */ + public function testConstructAcceptsOnlyNamesValidAsIdsInHtml4($name, $accepted) + { + try { + new Form($name, $this->dispatcher); + if (!$accepted) { + $this->fail(sprintf('The value "%s" should not be accepted', $name)); + } + } catch (\InvalidArgumentException $e) { + // if the value was not accepted, but should be, rethrow exception + if ($accepted) { + throw $e; + } + } + } + public function testDataIsInitializedEmpty() { $norm = new FixedDataTransformer(array( 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'),