From 03efce1b568379eac21d880e427090e43035f505 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 26 Sep 2014 23:28:34 +0200 Subject: [PATCH] [Form] Refactored choice lists to support dynamic label, value, index and attribute generation --- .../Form/ChoiceList/EntityChoiceList.php | 7 +- .../Form/ChoiceList/EntityChoiceLoader.php | 267 +++++ .../Form/ChoiceList/ORMQueryBuilderLoader.php | 5 +- .../Doctrine/Form/DoctrineOrmExtension.php | 21 +- .../Doctrine/Form/Type/DoctrineType.php | 193 ++-- .../Tests/Form/Type/EntityTypeTest.php | 77 +- .../Bridge/Twig/Extension/FormExtension.php | 2 +- .../views/Form/form_div_layout.html.twig | 16 +- .../views/Form/choice_widget_options.html.php | 8 +- .../Form/ChoiceList/ArrayChoiceList.php | 136 +++ .../Form/ChoiceList/ArrayKeyChoiceList.php | 173 ++++ .../Form/ChoiceList/ChoiceListInterface.php | 76 ++ .../Factory/CachingFactoryDecorator.php | 189 ++++ .../Factory/ChoiceListFactoryInterface.php | 124 +++ .../Factory/DefaultChoiceListFactory.php | 414 ++++++++ .../Factory/PropertyAccessDecorator.php | 226 ++++ .../Form/ChoiceList/LazyChoiceList.php | 115 +++ .../Loader/ChoiceLoaderInterface.php | 76 ++ .../Form/ChoiceList/View/ChoiceGroupView.php | 55 + .../Form/ChoiceList/View/ChoiceListView.php | 51 + .../Form/ChoiceList/View/ChoiceView.php | 64 ++ .../Extension/Core/ChoiceList/ChoiceList.php | 5 +- .../Core/ChoiceList/ChoiceListInterface.php | 51 +- .../Core/ChoiceList/LazyChoiceList.php | 4 + .../Core/ChoiceList/ObjectChoiceList.php | 4 + .../Core/ChoiceList/SimpleChoiceList.php | 4 + .../Form/Extension/Core/CoreExtension.php | 25 +- .../Core/DataMapper/CheckboxListMapper.php | 93 ++ .../Core/DataMapper/PropertyPathMapper.php | 4 +- .../Core/DataMapper/RadioListMapper.php | 73 ++ .../ChoiceToBooleanArrayTransformer.php | 6 +- .../ChoiceToValueTransformer.php | 4 +- .../ChoicesToBooleanArrayTransformer.php | 6 +- .../ChoicesToValuesTransformer.php | 2 +- .../FixCheckboxInputListener.php | 6 +- .../EventListener/FixRadioInputListener.php | 6 +- .../Form/Extension/Core/Type/ChoiceType.php | 282 +++-- .../Form/Extension/Core/View/ChoiceView.php | 27 +- .../Form/Tests/AbstractLayoutTest.php | 95 ++ .../ChoiceList/AbstractChoiceListTest.php | 173 ++++ .../Tests/ChoiceList/ArrayChoiceListTest.php | 52 + .../ChoiceList/ArrayKeyChoiceListTest.php | 187 ++++ .../Factory/CachingFactoryDecoratorTest.php | 668 ++++++++++++ .../Factory/DefaultChoiceListFactoryTest.php | 970 ++++++++++++++++++ .../Factory/PropertyAccessDecoratorTest.php | 338 ++++++ .../Tests/ChoiceList/LazyChoiceListTest.php | 141 +++ .../Extension/Core/Type/ChoiceTypeTest.php | 547 +++++++--- .../Extension/Core/Type/CountryTypeTest.php | 12 +- .../Extension/Core/Type/CurrencyTypeTest.php | 8 +- .../Extension/Core/Type/DateTypeTest.php | 22 +- .../Extension/Core/Type/LanguageTypeTest.php | 14 +- .../Extension/Core/Type/LocaleTypeTest.php | 8 +- .../Extension/Core/Type/TimeTypeTest.php | 14 +- .../Extension/Core/Type/TimezoneTypeTest.php | 6 +- 54 files changed, 5709 insertions(+), 443 deletions(-) create mode 100644 src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php create mode 100644 src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php create mode 100644 src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php create mode 100644 src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php create mode 100644 src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.php create mode 100644 src/Symfony/Component/Form/ChoiceList/View/ChoiceGroupView.php create mode 100644 src/Symfony/Component/Form/ChoiceList/View/ChoiceListView.php create mode 100644 src/Symfony/Component/Form/ChoiceList/View/ChoiceView.php create mode 100644 src/Symfony/Component/Form/Extension/Core/DataMapper/CheckboxListMapper.php create mode 100644 src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/Factory/PropertyAccessDecoratorTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index 57e57e6e25db..3566a33d7d58 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -11,17 +11,20 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList; +use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\Exception\RuntimeException; use Symfony\Component\Form\Exception\StringCastException; use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; -use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; /** * A choice list presenting a list of Doctrine entities as choices. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link EntityChoiceLoader} instead. */ class EntityChoiceList extends ObjectChoiceList { diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php new file mode 100644 index 000000000000..d1f7971e63cb --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php @@ -0,0 +1,267 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Form\ChoiceList; + +use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\Common\Persistence\ObjectManager; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\Exception\RuntimeException; + +/** + * Loads choices using a Doctrine object manager. + * + * @author Bernhard Schussek + */ +class EntityChoiceLoader implements ChoiceLoaderInterface +{ + /** + * @var ChoiceListFactoryInterface + */ + private $factory; + + /** + * @var ObjectManager + */ + private $manager; + + /** + * @var string + */ + private $class; + + /** + * @var ClassMetadata + */ + private $classMetadata; + + /** + * @var null|EntityLoaderInterface + */ + private $entityLoader; + + /** + * The identifier field, unless the identifier is composite + * + * @var null|string + */ + private $idField = null; + + /** + * Whether to use the identifier for value generation + * + * @var bool + */ + private $compositeId = true; + + /** + * @var ChoiceListInterface + */ + private $choiceList; + + /** + * Returns the value of the identifier field of an entity. + * + * Doctrine must know about this entity, that is, the entity must already + * be persisted or added to the identity map before. Otherwise an + * exception is thrown. + * + * This method assumes that the entity has a single-column identifier and + * will return a single value instead of an array. + * + * @param object $object The entity for which to get the identifier + * + * @return int|string The identifier value + * + * @throws RuntimeException If the entity does not exist in Doctrine's identity map + * + * @internal Should not be accessed by user-land code. This method is public + * only to be usable as callback. + */ + public static function getIdValue(ObjectManager $om, ClassMetadata $classMetadata, $object) + { + if (!$om->contains($object)) { + throw new RuntimeException( + 'Entities passed to the choice field must be managed. Maybe '. + 'persist them in the entity manager?' + ); + } + + $om->initializeObject($object); + + return current($classMetadata->getIdentifierValues($object)); + } + + /** + * Creates a new choice loader. + * + * Optionally, an implementation of {@link EntityLoaderInterface} can be + * passed which optimizes the entity loading for one of the Doctrine + * mapper implementations. + * + * @param ChoiceListFactoryInterface $factory The factory for creating + * the loaded choice list + * @param ObjectManager $manager The object manager + * @param string $class The entity class name + * @param null|EntityLoaderInterface $entityLoader The entity loader + */ + public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, EntityLoaderInterface $entityLoader = null) + { + $this->factory = $factory; + $this->manager = $manager; + $this->classMetadata = $manager->getClassMetadata($class); + $this->class = $this->classMetadata->getName(); + $this->entityLoader = $entityLoader; + + $identifier = $this->classMetadata->getIdentifierFieldNames(); + + if (1 === count($identifier)) { + $this->idField = $identifier[0]; + $this->compositeId = false; + } + } + + /** + * {@inheritdoc} + */ + public function loadChoiceList($value = null) + { + if ($this->choiceList) { + return $this->choiceList; + } + + $entities = $this->entityLoader + ? $this->entityLoader->getEntities() + : $this->manager->getRepository($this->class)->findAll(); + + // If the class has a multi-column identifier, we cannot index the + // entities by their IDs + if ($this->compositeId) { + $this->choiceList = $this->factory->createListFromChoices($entities, $value); + + return $this->choiceList; + } + + // Index the entities by ID + $entitiesById = array(); + + foreach ($entities as $entity) { + $id = self::getIdValue($this->manager, $this->classMetadata, $entity); + $entitiesById[$id] = $entity; + } + + $this->choiceList = $this->factory->createListFromChoices($entitiesById, $value); + + return $this->choiceList; + } + + /** + * Loads the values corresponding to the given entities. + * + * The values are returned with the same keys and in the same order as the + * corresponding entities in the given array. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the entity as first and the array key as the second + * argument. + * + * @param array $entities An array of entities. Non-existing entities + * in this array are ignored + * @param null|callable $value The callable generating the choice values + * + * @return string[] An array of choice values + */ + public function loadValuesForChoices(array $entities, $value = null) + { + // Performance optimization + if (empty($entities)) { + return array(); + } + + // 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 (!$this->choiceList && !$this->compositeId) { + $values = array(); + + // Maintain order and indices of the given entities + foreach ($entities as $i => $entity) { + if ($entity instanceof $this->class) { + // Make sure to convert to the right format + $values[$i] = (string) self::getIdValue($this->manager, $this->classMetadata, $entity); + } + } + + return $values; + } + + return $this->loadChoiceList($value)->getValuesForChoices($entities); + } + + /** + * Loads the entities corresponding to the given values. + * + * The entities are returned with the same keys and in the same order as the + * corresponding values in the given array. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the entity as first and the array key as the second + * argument. + * + * @param string[] $values An array of choice values. Non-existing + * values in this array are ignored + * @param null|callable $value The callable generating the choice values + * + * @return array An array of entities + */ + public function loadChoicesForValues(array $values, $value = null) + { + // Performance optimization + // Also prevents the generation of "WHERE id IN ()" queries through the + // entity loader. At least with MySQL and on the development machine + // this was tested on, no exception was thrown for such invalid + // statements, consequently no test fails when this code is removed. + // https://github.com/symfony/symfony/pull/8981#issuecomment-24230557 + if (empty($values)) { + return array(); + } + + // Optimize performance in case we have an entity loader and + // a single-field identifier + if (!$this->choiceList && !$this->compositeId && $this->entityLoader) { + $unorderedEntities = $this->entityLoader->getEntitiesByIds($this->idField, $values); + $entitiesById = array(); + $entities = array(); + + // Maintain order and indices from the given $values + // An alternative approach to the following loop is to add the + // "INDEX BY" clause to the Doctrine query in the loader, + // but I'm not sure whether that's doable in a generic fashion. + foreach ($unorderedEntities as $entity) { + $id = self::getIdValue($this->manager, $this->classMetadata, $entity); + $entitiesById[$id] = $entity; + } + + foreach ($values as $i => $id) { + if (isset($entitiesById[$id])) { + $entities[$i] = $entitiesById[$id]; + } + } + + return $entities; + } + + return $this->loadChoiceList($value)->getChoicesForValues($values); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index 872e77affe0b..9cfdd1fe4855 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -17,7 +17,10 @@ use Doctrine\ORM\EntityManager; /** - * Getting Entities through the ORM QueryBuilder. + * Loads entities using a {@link QueryBuilder} instance. + * + * @author Benjamin Eberlei + * @author Bernhard Schussek */ class ORMQueryBuilderLoader implements EntityLoaderInterface { diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php index 570cc8f189df..ed8e0a793444 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php @@ -14,21 +14,38 @@ use Doctrine\Common\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; class DoctrineOrmExtension extends AbstractExtension { protected $registry; - public function __construct(ManagerRegistry $registry) + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + /** + * @var ChoiceListFactoryInterface + */ + private $choiceListFactory; + + public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) { $this->registry = $registry; + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor)); } protected function loadTypes() { return array( - new EntityType($this->registry, PropertyAccess::createPropertyAccessor()), + new EntityType($this->registry, $this->propertyAccessor, $this->choiceListFactory), ); } diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index ccc9bfc485e7..6c90a2eeb098 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -12,17 +12,20 @@ namespace Symfony\Bridge\Doctrine\Form\Type; use Doctrine\Common\Persistence\ManagerRegistry; -use Symfony\Component\Form\Exception\RuntimeException; use Doctrine\Common\Persistence\ObjectManager; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList; +use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; -use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; +use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\Exception\RuntimeException; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; abstract class DoctrineType extends AbstractType @@ -33,19 +36,19 @@ abstract class DoctrineType extends AbstractType protected $registry; /** - * @var array + * @var ChoiceListFactoryInterface */ - private $choiceListCache = array(); + private $choiceListFactory; /** - * @var PropertyAccessorInterface + * @var EntityChoiceLoader[] */ - private $propertyAccessor; + private $choiceLoaders = array(); - public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null) + public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) { $this->registry = $registry; - $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->choiceListFactory = $choiceListFactory ?: new PropertyAccessDecorator(new DefaultChoiceListFactory(), $propertyAccessor); } public function buildForm(FormBuilderInterface $builder, array $options) @@ -60,86 +63,79 @@ public function buildForm(FormBuilderInterface $builder, array $options) public function configureOptions(OptionsResolver $resolver) { - $choiceListCache = &$this->choiceListCache; $registry = $this->registry; - $propertyAccessor = $this->propertyAccessor; + $choiceListFactory = $this->choiceListFactory; + $choiceLoaders = &$this->choiceLoaders; $type = $this; - $loader = function (Options $options) use ($type) { - $queryBuilder = (null !== $options['query_builder']) - ? $options['query_builder'] - : $options['em']->getRepository($options['class'])->createQueryBuilder('e'); - - return $type->getLoader($options['em'], $queryBuilder, $options['class']); - }; + $choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) { + // Unless the choices are given explicitly, load them on demand + if (null === $options['choices']) { + $hash = CachingFactoryDecorator::generateHash(array( + $options['em'], + $options['class'], + $options['query_builder'], + $options['loader'], + )); - $choiceList = function (Options $options) use (&$choiceListCache, $propertyAccessor) { - // Support for closures - $propertyHash = is_object($options['property']) - ? spl_object_hash($options['property']) - : $options['property']; - - $choiceHashes = $options['choices']; - - // Support for recursive arrays - if (is_array($choiceHashes)) { - // A second parameter ($key) is passed, so we cannot use - // spl_object_hash() directly (which strictly requires - // one parameter) - array_walk_recursive($choiceHashes, function (&$value) { - $value = spl_object_hash($value); - }); - } elseif ($choiceHashes instanceof \Traversable) { - $hashes = array(); - foreach ($choiceHashes as $value) { - $hashes[] = spl_object_hash($value); + if (!isset($choiceLoaders[$hash])) { + if ($options['loader']) { + $loader = $options['loader']; + } elseif (null !== $options['query_builder']) { + $loader = $type->getLoader($options['em'], $options['query_builder'], $options['class']); + } else { + $queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e'); + $loader = $type->getLoader($options['em'], $queryBuilder, $options['class']); + } + + $choiceLoaders[$hash] = new EntityChoiceLoader( + $choiceListFactory, + $options['em'], + $options['class'], + $loader + ); } - $choiceHashes = $hashes; + return $choiceLoaders[$hash]; } + }; - $preferredChoiceHashes = $options['preferred_choices']; - - if (is_array($preferredChoiceHashes)) { - array_walk_recursive($preferredChoiceHashes, function (&$value) { - $value = spl_object_hash($value); - }); + $choiceLabel = function (Options $options) { + // BC with the "property" option + if ($options['property']) { + return $options['property']; } - // Support for custom loaders (with query builders) - $loaderHash = is_object($options['loader']) - ? spl_object_hash($options['loader']) - : $options['loader']; - - // Support for closures - $groupByHash = is_object($options['group_by']) - ? spl_object_hash($options['group_by']) - : $options['group_by']; - - $hash = hash('sha256', json_encode(array( - spl_object_hash($options['em']), - $options['class'], - $propertyHash, - $loaderHash, - $choiceHashes, - $preferredChoiceHashes, - $groupByHash, - ))); - - if (!isset($choiceListCache[$hash])) { - $choiceListCache[$hash] = new EntityChoiceList( - $options['em'], - $options['class'], - $options['property'], - $options['loader'], - $options['choices'], - $options['preferred_choices'], - $options['group_by'], - $propertyAccessor - ); + // BC: use __toString() by default + return function ($entity) { + return (string) $entity; + }; + }; + + $choiceName = function (Options $options) { + /** @var ObjectManager $om */ + $om = $options['em']; + $classMetadata = $om->getClassMetadata($options['class']); + $ids = $classMetadata->getIdentifierFieldNames(); + $idType = $classMetadata->getTypeOfField(current($ids)); + + // If the entity has a single-column, numeric ID, use that ID as + // field name + if (1 === count($ids) && in_array($idType, array('integer', 'smallint', 'bigint'))) { + return function ($entity, $id) { + return $id; + }; } - return $choiceListCache[$hash]; + // Otherwise, an incrementing integer is used as name automatically + }; + + // The choices are always indexed by ID (see "choices" normalizer + // and EntityChoiceLoader), unless the ID is composite. Then they + // are indexed by an incrementing integer. + // Use the ID/incrementing integer as choice value. + $choiceValue = function ($entity, $key) { + return $key; }; $emNormalizer = function (Options $options, $em) use ($registry) { @@ -165,19 +161,50 @@ public function configureOptions(OptionsResolver $resolver) return $em; }; + $choicesNormalizer = function (Options $options, $entities) { + if (null === $entities || 0 === count($entities)) { + return $entities; + } + + // Make sure that the entities are indexed by their ID + /** @var ObjectManager $om */ + $om = $options['em']; + $classMetadata = $om->getClassMetadata($options['class']); + $ids = $classMetadata->getIdentifierFieldNames(); + + // We cannot use composite IDs as indices. In that case, keep the + // given indices + if (count($ids) > 1) { + return $entities; + } + + $entitiesById = array(); + + foreach ($entities as $entity) { + $id = EntityChoiceLoader::getIdValue($om, $classMetadata, $entity); + $entitiesById[$id] = $entity; + } + + return $entitiesById; + }; + $resolver->setDefaults(array( 'em' => null, - 'property' => null, + 'property' => null, // deprecated, use "choice_label" 'query_builder' => null, - 'loader' => $loader, + 'loader' => null, // deprecated, use "choice_loader" 'choices' => null, - 'choice_list' => $choiceList, - 'group_by' => null, + 'choices_as_values' => true, + 'choice_loader' => $choiceLoader, + 'choice_label' => $choiceLabel, + 'choice_name' => $choiceName, + 'choice_value' => $choiceValue, )); $resolver->setRequired(array('class')); $resolver->setNormalizer('em', $emNormalizer); + $resolver->setNormalizer('choices', $choicesNormalizer); $resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager')); $resolver->setAllowedTypes('loader', array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface')); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index 25afbed49215..9f1591f30825 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -11,21 +11,23 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\Type; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Tools\SchemaTool; +use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\Form\Forms; -use Symfony\Component\Form\Test\TypeTestCase; +use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity; +use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\GroupableEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity; -use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity; -use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity; -use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; -use Doctrine\ORM\Tools\SchemaTool; -use Doctrine\Common\Collections\ArrayCollection; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Forms; +use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\PropertyAccess\PropertyAccess; class EntityTypeTest extends TypeTestCase @@ -37,12 +39,12 @@ class EntityTypeTest extends TypeTestCase const COMPOSITE_STRING_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity'; /** - * @var \Doctrine\ORM\EntityManager + * @var EntityManager */ private $em; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit_Framework_MockObject_MockObject|ManagerRegistry */ private $emRegistry; @@ -131,7 +133,7 @@ public function testSetDataToUninitializedEntityWithNonRequired() 'property' => 'name', )); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); } public function testSetDataToUninitializedEntityWithNonRequiredToString() @@ -147,7 +149,7 @@ public function testSetDataToUninitializedEntityWithNonRequiredToString() 'required' => false, )); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); } public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder() @@ -166,7 +168,7 @@ public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder() 'query_builder' => $qb, )); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); } /** @@ -249,7 +251,7 @@ public function testSubmitSingleExpandedNull() $field->submit(null); $this->assertNull($field->getData()); - $this->assertSame(array(), $field->getViewData()); + $this->assertNull($field->getViewData()); } public function testSubmitSingleNonExpandedNull() @@ -510,7 +512,7 @@ public function testOverrideChoices() $field->submit('2'); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); $this->assertTrue($field->isSynchronized()); $this->assertSame($entity2, $field->getData()); $this->assertSame('2', $field->getViewData()); @@ -537,9 +539,14 @@ public function testGroupByChoices() $this->assertSame('2', $field->getViewData()); $this->assertEquals(array( - 'Group1' => array(1 => new ChoiceView($item1, '1', 'Foo'), 2 => new ChoiceView($item2, '2', 'Bar')), - 'Group2' => array(3 => new ChoiceView($item3, '3', 'Baz')), - '4' => new ChoiceView($item4, '4', 'Boo!'), + 'Group1' => new ChoiceGroupView('Group1', array( + 1 => new ChoiceView('Foo', '1', $item1), + 2 => new ChoiceView('Bar', '2', $item2), + )), + 'Group2' => new ChoiceGroupView('Group2', array( + 3 => new ChoiceView('Baz', '3', $item3), + )), + 4 => new ChoiceView('Boo!', '4', $item4), ), $field->createView()->vars['choices']); } @@ -558,8 +565,8 @@ public function testPreferredChoices() 'property' => 'name', )); - $this->assertEquals(array(3 => new ChoiceView($entity3, '3', 'Baz'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['preferred_choices']); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo')), $field->createView()->vars['choices']); + $this->assertEquals(array(3 => new ChoiceView('Baz', '3', $entity3), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['preferred_choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1)), $field->createView()->vars['choices']); } public function testOverrideChoicesWithPreferredChoices() @@ -578,8 +585,8 @@ public function testOverrideChoicesWithPreferredChoices() 'property' => 'name', )); - $this->assertEquals(array(3 => new ChoiceView($entity3, '3', 'Baz')), $field->createView()->vars['preferred_choices']); - $this->assertEquals(array(2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(3 => new ChoiceView('Baz', '3', $entity3)), $field->createView()->vars['preferred_choices']); + $this->assertEquals(array(2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); } public function testDisallowChoicesThatAreNotIncludedChoicesSingleIdentifier() @@ -833,6 +840,30 @@ public function testLoaderCaching() $this->assertCount(1, $loaders); } + public function testCacheChoiceLists() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + + $this->persist(array($entity1)); + + $field1 = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'required' => false, + 'property' => 'name', + )); + + $field2 = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'required' => false, + 'property' => 'name', + )); + + $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\ChoiceListInterface', $field1->getConfig()->getOption('choice_list')); + $this->assertSame($field1->getConfig()->getOption('choice_list'), $field2->getConfig()->getOption('choice_list')); + } + protected function createRegistryMock($name, $em) { $registry = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); diff --git a/src/Symfony/Bridge/Twig/Extension/FormExtension.php b/src/Symfony/Bridge/Twig/Extension/FormExtension.php index 9c7339f70295..38270f9f5ba4 100644 --- a/src/Symfony/Bridge/Twig/Extension/FormExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/FormExtension.php @@ -13,7 +13,7 @@ use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser; use Symfony\Bridge\Twig\Form\TwigRendererInterface; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; /** * FormExtension extends Twig with form capabilities. 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 b39dbf1c80e5..df0e571602ec 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 @@ -79,7 +79,8 @@ {{- block('choice_widget_options') -}} {%- else -%} - + {% set attr = choice.attr %} + {%- endif -%} {% endfor %} {%- endblock choice_widget_options -%} @@ -355,3 +356,16 @@ {%- endif -%} {%- endfor -%} {%- endblock button_attributes -%} + +{% block attributes -%} + {%- for attrname, attrvalue in attr -%} + {{- " " -}} + {%- if attrname in ['placeholder', 'title'] -%} + {{- attrname }}="{{ attrvalue|trans({}, translation_domain) }}" + {%- elseif attrvalue is sameas(true) -%} + {{- attrname }}="{{ attrname }}" + {%- elseif attrvalue is not sameas(false) -%} + {{- attrname }}="{{ attrvalue }}" + {%- endif -%} + {%- endfor -%} +{%- endblock attributes -%} diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php index a7a9311d5132..81402efffb10 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php @@ -1,11 +1,13 @@ - + $choice): ?> - + block($form, 'choice_widget_options', array('choices' => $choice)) ?> - + diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php new file mode 100644 index 000000000000..0dfc0f9945a0 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList; + +use Symfony\Component\Form\Exception\InvalidArgumentException; + +/** + * A list of choices with arbitrary data types. + * + * The user of this class is responsible for assigning string values to the + * choices. Both the choices and their values are passed to the constructor. + * Each choice must have a corresponding value (with the same array key) in + * the value array. + * + * @author Bernhard Schussek + */ +class ArrayChoiceList implements ChoiceListInterface +{ + /** + * The choices in the list. + * + * @var array + */ + protected $choices = array(); + + /** + * The values of the choices. + * + * @var string[] + */ + protected $values = array(); + + /** + * Creates a list with the given choices and values. + * + * The given choice array must have the same array keys as the value array. + * + * @param array $choices The selectable choices + * @param string[] $values The string values of the choices + * + * @throws InvalidArgumentException If the keys of the choices don't match + * the keys of the values + */ + public function __construct(array $choices, array $values) + { + $choiceKeys = array_keys($choices); + $valueKeys = array_keys($values); + + if ($choiceKeys !== $valueKeys) { + throw new InvalidArgumentException(sprintf( + 'The keys of the choices and the values must match. The choice '. + 'keys are: "%s". The value keys are: "%s".', + implode('", "', $choiceKeys), + implode('", "', $valueKeys) + )); + } + + $this->choices = $choices; + $this->values = array_map('strval', $values); + } + + /** + * {@inheritdoc} + */ + public function getChoices() + { + return $this->choices; + } + + /** + * {@inheritdoc} + */ + public function getValues() + { + return $this->values; + } + + /** + * {@inheritdoc} + */ + public function getChoicesForValues(array $values) + { + $choices = array(); + + foreach ($values as $i => $givenValue) { + foreach ($this->values as $j => $value) { + if ($value !== (string) $givenValue) { + continue; + } + + $choices[$i] = $this->choices[$j]; + unset($values[$i]); + + if (0 === count($values)) { + break 2; + } + } + } + + return $choices; + } + + /** + * {@inheritdoc} + */ + public function getValuesForChoices(array $choices) + { + $values = array(); + + foreach ($choices as $i => $givenChoice) { + foreach ($this->choices as $j => $choice) { + if ($choice !== $givenChoice) { + continue; + } + + $values[$i] = $this->values[$j]; + unset($choices[$i]); + + if (0 === count($choices)) { + break 2; + } + } + } + + return $values; + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php new file mode 100644 index 000000000000..d79747e0485b --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList; + +use Symfony\Component\Form\Exception\InvalidArgumentException; + +/** + * A list of choices that can be stored in the keys of a PHP array. + * + * PHP arrays accept only strings and integers as array keys. Other scalar types + * are cast to integers and strings according to the description of + * {@link toArrayKey()}. This implementation applies the same casting rules for + * the choices passed to the constructor and to {@link getValuesForChoices()}. + * + * By default, the choices are cast to strings and used as values. Optionally, + * you may pass custom values. The keys of the value array must match the keys + * of the choice array. + * + * Example: + * + * ```php + * $choices = array('' => 'Don\'t know', 0 => 'No', 1 => 'Yes'); + * $choiceList = new ArrayKeyChoiceList(array_keys($choices)); + * + * $values = $choiceList->getValues() + * // => array('', '0', '1') + * + * $selectedValues = $choiceList->getValuesForChoices(array(true)); + * // => array('1') + * ``` + * + * @author Bernhard Schussek + * + * @deprecated Added for backwards compatibility in Symfony 2.7, to be removed + * in Symfony 3.0. + */ +class ArrayKeyChoiceList implements ChoiceListInterface +{ + /** + * The selectable choices. + * + * @var array + */ + private $choices = array(); + + /** + * The values of the choices. + * + * @var string[] + */ + private $values = array(); + + /** + * Casts the given choice to an array key. + * + * PHP arrays accept only strings and integers as array keys. Integer + * strings such as "42" are automatically cast to integers. The boolean + * values "true" and "false" are cast to the integers 1 and 0. Every other + * scalar value is cast to a string. + * + * @param mixed $choice The choice + * + * @return int|string The choice as PHP array key + * + * @throws InvalidArgumentException If the choice is not scalar + */ + public static function toArrayKey($choice) + { + if (!is_scalar($choice) && null !== $choice) { + throw new InvalidArgumentException(sprintf( + 'The value of type "%s" cannot be converted to a valid array key.', + gettype($choice) + )); + } + + if (is_bool($choice) || (string) (int) $choice === (string) $choice) { + return (int) $choice; + } + + return (string) $choice; + } + + /** + * Creates a list with the given choices and values. + * + * The given choice array must have the same array keys as the value array. + * Each choice must be castable to an integer/string according to the + * casting rules described in {@link toArrayKey()}. + * + * If no values are given, the choices are cast to strings and used as + * values. + * + * @param array $choices The selectable choices + * @param string[] $values Optional. The string values of the choices + * + * @throws InvalidArgumentException If the keys of the choices don't match + * the keys of the values or if any of the + * choices is not scalar + */ + public function __construct(array $choices, array $values = array()) + { + if (empty($values)) { + // The cast to strings happens later + $values = $choices; + } else { + $choiceKeys = array_keys($choices); + $valueKeys = array_keys($values); + + if ($choiceKeys !== $valueKeys) { + throw new InvalidArgumentException( + sprintf( + 'The keys of the choices and the values must match. The choice '. + 'keys are: "%s". The value keys are: "%s".', + implode('", "', $choiceKeys), + implode('", "', $valueKeys) + ) + ); + } + } + + $this->choices = array_map(array(__CLASS__, 'toArrayKey'), $choices); + $this->values = array_map('strval', $values); + } + + /** + * {@inheritdoc} + */ + public function getChoices() + { + return $this->choices; + } + + /** + * {@inheritdoc} + */ + public function getValues() + { + return $this->values; + } + + /** + * {@inheritdoc} + */ + public function getChoicesForValues(array $values) + { + $values = array_map('strval', $values); + + // The values are identical to the choices, so we can just return them + // to improve performance a little bit + return array_map(array(__CLASS__, 'toArrayKey'), array_intersect($values, $this->values)); + } + + /** + * {@inheritdoc} + */ + public function getValuesForChoices(array $choices) + { + $choices = array_map(array(__CLASS__, 'toArrayKey'), $choices); + + // The choices are identical to the values, so we can just return them + // to improve performance a little bit + return array_map('strval', array_intersect($choices, $this->choices)); + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php b/src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php new file mode 100644 index 000000000000..62f315864646 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList; + +/** + * A list of choices that can be selected in a choice field. + * + * A choice list assigns string values to each of a list of choices. These + * string values are displayed in the "value" attributes in HTML and submitted + * back to the server. + * + * The acceptable data types for the choices depend on the implementation. + * Values must always be strings and (within the list) free of duplicates. + * + * The choices returned by {@link getChoices()} and the values returned by + * {@link getValues()} must have the same array indices. + * + * @author Bernhard Schussek + */ +interface ChoiceListInterface +{ + /** + * Returns all selectable choices. + * + * The keys of the choices correspond to the keys of the values returned by + * {@link getValues()}. + * + * @return array The selectable choices + */ + public function getChoices(); + + /** + * Returns the values for the choices. + * + * The keys of the values correspond to the keys of the choices returned by + * {@link getChoices()}. + * + * @return string[] The choice values + */ + public function getValues(); + + /** + * Returns the choices corresponding to the given values. + * + * The choices are returned with the same keys and in the same order as the + * corresponding values in the given array. + * + * @param string[] $values An array of choice values. Non-existing values in + * this array are ignored + * + * @return array An array of choices + */ + public function getChoicesForValues(array $values); + + /** + * Returns the values corresponding to the given choices. + * + * The values are returned with the same keys and in the same order as the + * corresponding choices in the given array. + * + * @param array $choices An array of choices. Non-existing choices in this + * array are ignored + * + * @return string[] An array of choice values + */ + public function getValuesForChoices(array $choices); +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php new file mode 100644 index 000000000000..fb43ac87594c --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * Caches the choice lists created by the decorated factory. + * + * @author Bernhard Schussek + */ +class CachingFactoryDecorator implements ChoiceListFactoryInterface +{ + /** + * @var ChoiceListFactoryInterface + */ + private $decoratedFactory; + + /** + * @var ChoiceListInterface[] + */ + private $lists = array(); + + /** + * @var ChoiceListView[] + */ + private $views = array(); + + /** + * Generates a SHA-256 hash for the given value. + * + * Optionally, a namespace string can be passed. Calling this method will + * the same values, but different namespaces, will return different hashes. + * + * @param mixed $value The value to hash + * @param string $namespace Optional. The namespace + * + * @return string The SHA-256 hash + * + * @internal Should not be used by user-land code. + */ + public static function generateHash($value, $namespace = '') + { + if (is_object($value)) { + $value = spl_object_hash($value); + } elseif (is_array($value)) { + array_walk_recursive($value, function (&$v) { + if (is_object($v)) { + $v = spl_object_hash($v); + } + }); + } + + return hash('sha256', $namespace.':'.json_encode($value)); + } + + /** + * Decorates the given factory. + * + * @param ChoiceListFactoryInterface $decoratedFactory The decorated factory + */ + public function __construct(ChoiceListFactoryInterface $decoratedFactory) + { + $this->decoratedFactory = $decoratedFactory; + } + + /** + * Returns the decorated factory. + * + * @return ChoiceListFactoryInterface The decorated factory + */ + public function getDecoratedFactory() + { + return $this->decoratedFactory; + } + + /** + * {@inheritdoc} + */ + public function createListFromChoices($choices, $value = null) + { + if (!is_array($choices) && !$choices instanceof \Traversable) { + throw new UnexpectedTypeException($choices, 'array or \Traversable'); + } + + if ($choices instanceof \Traversable) { + $choices = iterator_to_array($choices); + } + + // The value is not validated on purpose. The decorated factory may + // decide which values to accept and which not. + + // We ignore the choice groups for caching. If two choice lists are + // requested with the same choices, but a different grouping, the same + // choice list is returned. + DefaultChoiceListFactory::flatten($choices, $flatChoices); + + $hash = self::generateHash(array($flatChoices, $value), 'fromChoices'); + + if (!isset($this->lists[$hash])) { + $this->lists[$hash] = $this->decoratedFactory->createListFromChoices($choices, $value); + } + + return $this->lists[$hash]; + } + + /** + * {@inheritdoc} + * + * @deprecated Added for backwards compatibility in Symfony 2.7, to be + * removed in Symfony 3.0. + */ + public function createListFromFlippedChoices($choices, $value = null) + { + if (!is_array($choices) && !$choices instanceof \Traversable) { + throw new UnexpectedTypeException($choices, 'array or \Traversable'); + } + + if ($choices instanceof \Traversable) { + $choices = iterator_to_array($choices); + } + + // The value is not validated on purpose. The decorated factory may + // decide which values to accept and which not. + + // We ignore the choice groups for caching. If two choice lists are + // requested with the same choices, but a different grouping, the same + // choice list is returned. + DefaultChoiceListFactory::flattenFlipped($choices, $flatChoices); + + $hash = self::generateHash(array($flatChoices, $value), 'fromFlippedChoices'); + + if (!isset($this->lists[$hash])) { + $this->lists[$hash] = $this->decoratedFactory->createListFromFlippedChoices($choices, $value); + } + + return $this->lists[$hash]; + } + + /** + * {@inheritdoc} + */ + public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null) + { + $hash = self::generateHash(array($loader, $value), 'fromLoader'); + + if (!isset($this->lists[$hash])) { + $this->lists[$hash] = $this->decoratedFactory->createListFromLoader($loader, $value); + } + + return $this->lists[$hash]; + } + + /** + * {@inheritdoc} + */ + public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null) + { + // The input is not validated on purpose. This way, the decorated + // factory may decide which input to accept and which not. + + $hash = self::generateHash(array($list, $preferredChoices, $label, $index, $groupBy, $attr)); + + if (!isset($this->views[$hash])) { + $this->views[$hash] = $this->decoratedFactory->createView( + $list, + $preferredChoices, + $label, + $index, + $groupBy, + $attr + ); + } + + return $this->views[$hash]; + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php b/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php new file mode 100644 index 000000000000..60239423f335 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; + +/** + * Creates {@link ChoiceListInterface} instances. + * + * @author Bernhard Schussek + */ +interface ChoiceListFactoryInterface +{ + /** + * Creates a choice list for the given choices. + * + * The choices should be passed in the values of the choices array. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param array|\Traversable $choices The choices + * @param null|callable $value The callable generating the choice + * values + * + * @return ChoiceListInterface The choice list + */ + public function createListFromChoices($choices, $value = null); + + /** + * Creates a choice list for the given choices. + * + * The choices should be passed in the keys of the choices array. Since the + * choices array will be flipped, the entries of the array must be strings + * or integers. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param array|\Traversable $choices The choices + * @param null|callable $value The callable generating the choice + * values + * + * @return ChoiceListInterface The choice list + * + * @deprecated Added for backwards compatibility in Symfony 2.7, to be + * removed in Symfony 3.0. + */ + public function createListFromFlippedChoices($choices, $value = null); + + /** + * Creates a choice list that is loaded with the given loader. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param ChoiceLoaderInterface $loader The choice loader + * @param null|callable $value The callable generating the choice + * values + * + * @return ChoiceListInterface The choice list + */ + public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null); + + /** + * Creates a view for the given choice list. + * + * Callables may be passed for all optional arguments. The callables receive + * the choice as first and the array key as the second argument. + * + * * The callable for the label and the name should return the generated + * label/choice name. + * * The callable for the preferred choices should return true or false, + * depending on whether the choice should be preferred or not. + * * The callable for the grouping should return the group name or null if + * a choice should not be grouped. + * * The callable for the attributes should return an array of HTML + * attributes that will be inserted in the tag of the choice. + * + * If no callable is passed, the labels will be generated from the choice + * keys. The view indices will be generated using an incrementing integer + * by default. + * + * The preferred choices can also be passed as array. Each choice that is + * contained in that array will be marked as preferred. + * + * The groups can be passed as a multi-dimensional array. In that case, a + * group will be created for each array entry containing a nested array. + * For all other entries, the choice for the corresponding key will be + * inserted at that position. + * + * The attributes can be passed as multi-dimensional array. The keys should + * match the keys of the choices. The values should be arrays of HTML + * attributes that should be added to the respective choice. + * + * @param ChoiceListInterface $list The choice list + * @param null|array|callable $preferredChoices The preferred choices + * @param null|callable $label The callable generating + * the choice labels + * @param null|callable $index The callable generating + * the view indices + * @param null|array|\Traversable|callable $groupBy The callable generating + * the group names + * @param null|array|callable $attr The callable generating + * the HTML attributes + * + * @return ChoiceListView The choice list view + */ + public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null); +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php new file mode 100644 index 000000000000..dd191eea39a1 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -0,0 +1,414 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\ArrayKeyChoiceList; +use Symfony\Component\Form\ChoiceList\ArrayChoiceList; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\LazyChoiceList; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface as LegacyChoiceListInterface; + +/** + * Default implementation of {@link ChoiceListFactoryInterface}. + * + * @author Bernhard Schussek + */ +class DefaultChoiceListFactory implements ChoiceListFactoryInterface +{ + /** + * Flattens an array into the given output variable. + * + * @param array $array The array to flatten + * @param array $output The flattened output + * + * @internal Should not be used by user-land code + */ + public static function flatten(array $array, &$output) + { + if (null === $output) { + $output = array(); + } + + foreach ($array as $key => $value) { + if (is_array($value)) { + self::flatten($value, $output); + continue; + } + + $output[$key] = $value; + } + } + + /** + * Flattens and flips an array into the given output variable. + * + * During the flattening, the keys and values of the input array are + * flipped. + * + * @param array $array The array to flatten + * @param array $output The flattened output + * + * @internal Should not be used by user-land code + */ + public static function flattenFlipped(array $array, &$output) + { + if (null === $output) { + $output = array(); + } + + foreach ($array as $key => $value) { + if (is_array($value)) { + self::flattenFlipped($value, $output); + continue; + } + + $output[$value] = $key; + } + } + + /** + * {@inheritdoc} + */ + public function createListFromChoices($choices, $value = null) + { + if (!is_array($choices) && !$choices instanceof \Traversable) { + throw new UnexpectedTypeException($choices, 'array or \Traversable'); + } + + if (null !== $value && !is_callable($value)) { + throw new UnexpectedTypeException($value, 'null or callable'); + } + + if ($choices instanceof \Traversable) { + $choices = iterator_to_array($choices); + } + + // If the choices are given as recursive array (i.e. with explicit + // choice groups), flatten the array. The grouping information is needed + // in the view only. + self::flatten($choices, $flatChoices); + + // If no values are given, use incrementing integers as values + // We can not use the choices themselves, because we don't know whether + // choices can be converted to (duplicate-free) strings + if (null === $value) { + $values = $flatChoices; + $i = 0; + + foreach ($values as $key => $value) { + $values[$key] = (string) $i++; + } + + return new ArrayChoiceList($flatChoices, $values); + } + + // Can't use array_map(), because array_map() doesn't pass the key + // Can't use array_walk(), which ignores the return value of the + // closure + $values = array(); + foreach ($flatChoices as $key => $choice) { + $values[$key] = call_user_func($value, $choice, $key); + } + + return new ArrayChoiceList($flatChoices, $values); + } + + /** + * {@inheritdoc} + * + * @deprecated Added for backwards compatibility in Symfony 2.7, to be + * removed in Symfony 3.0. + */ + public function createListFromFlippedChoices($choices, $value = null) + { + if (!is_array($choices) && !$choices instanceof \Traversable) { + throw new UnexpectedTypeException($choices, 'array or \Traversable'); + } + + if (null !== $value && !is_callable($value)) { + throw new UnexpectedTypeException($value, 'null or callable'); + } + + if ($choices instanceof \Traversable) { + $choices = iterator_to_array($choices); + } + + // If the choices are given as recursive array (i.e. with explicit + // choice groups), flatten the array. The grouping information is needed + // in the view only. + self::flattenFlipped($choices, $flatChoices); + + // If no values are given, use the choices as values + // Since the choices are stored in the collection keys, i.e. they are + // strings or integers, we are guaranteed to be able to convert them + // to strings + if (null === $value) { + $values = array_map('strval', $flatChoices); + + return new ArrayKeyChoiceList($flatChoices, $values); + } + + // Can't use array_map(), because array_map() doesn't pass the key + // Can't use array_walk(), which ignores the return value of the + // closure + $values = array(); + foreach ($flatChoices as $key => $choice) { + $values[$key] = call_user_func($value, $choice, $key); + } + + return new ArrayKeyChoiceList($flatChoices, $values); + } + + /** + * {@inheritdoc} + */ + public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null) + { + if (null !== $value && !is_callable($value)) { + throw new UnexpectedTypeException($value, 'null or callable'); + } + + return new LazyChoiceList($loader, $value); + } + + /** + * {@inheritdoc} + */ + public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null) + { + if (null !== $preferredChoices && !is_array($preferredChoices) && !is_callable($preferredChoices)) { + throw new UnexpectedTypeException($preferredChoices, 'null, array or callable'); + } + + if (null !== $label && !is_callable($label)) { + throw new UnexpectedTypeException($label, 'null or callable'); + } + + if (null !== $index && !is_callable($index)) { + throw new UnexpectedTypeException($index, 'null or callable'); + } + + if (null !== $groupBy && !is_array($groupBy) && !$groupBy instanceof \Traversable && !is_callable($groupBy)) { + throw new UnexpectedTypeException($groupBy, 'null, array, \Traversable or callable'); + } + + if (null !== $attr && !is_array($attr) && !is_callable($attr)) { + throw new UnexpectedTypeException($attr, 'null, array or callable'); + } + + // Backwards compatibility + if ($list instanceof LegacyChoiceListInterface && null === $preferredChoices + && null === $label && null === $index && null === $groupBy && null === $attr) { + return new ChoiceListView($list->getRemainingViews(), $list->getPreferredViews()); + } + + $preferredViews = array(); + $otherViews = array(); + $choices = $list->getChoices(); + $values = $list->getValues(); + + if (!is_callable($preferredChoices) && !empty($preferredChoices)) { + $preferredChoices = function ($choice) use ($preferredChoices) { + return false !== array_search($choice, $preferredChoices, true); + }; + } + + // The names are generated from an incrementing integer by default + if (null === $index) { + $i = 0; + $index = function () use (&$i) { + return $i++; + }; + } + + // If $groupBy is not given, no grouping is done + if (empty($groupBy)) { + foreach ($choices as $key => $choice) { + self::addChoiceView( + $choice, + $key, + $label, + $values, + $index, + $attr, + $preferredChoices, + $preferredViews, + $otherViews + ); + } + + return new ChoiceListView($otherViews, $preferredViews); + } + + // If $groupBy is a callable, choices are added to the group with the + // name returned by the callable. If the callable returns null, the + // choice is not added to any group + if (is_callable($groupBy)) { + foreach ($choices as $key => $choice) { + self::addChoiceViewGroupedBy( + $groupBy, + $choice, + $key, + $label, + $values, + $index, + $attr, + $preferredChoices, + $preferredViews, + $otherViews + ); + } + } else { + // If $groupBy is passed as array, use that array as template for + // constructing the groups + self::addChoiceViewsGroupedBy( + $groupBy, + $label, + $choices, + $values, + $index, + $attr, + $preferredChoices, + $preferredViews, + $otherViews + ); + } + + // Remove any empty group views that may have been created by + // addChoiceViewGroupedBy() + foreach ($preferredViews as $key => $view) { + if ($view instanceof ChoiceGroupView && 0 === count($view->choices)) { + unset($preferredViews[$key]); + } + } + + foreach ($otherViews as $key => $view) { + if ($view instanceof ChoiceGroupView && 0 === count($view->choices)) { + unset($otherViews[$key]); + } + } + + return new ChoiceListView($otherViews, $preferredViews); + } + + private static function addChoiceView($choice, $key, $label, $values, $index, $attr, $isPreferred, &$preferredViews, &$otherViews) + { + $view = new ChoiceView( + // If the labels are null, use the choice key by default + null === $label ? (string) $key : (string) call_user_func($label, $choice, $key), + $values[$key], + $choice, + // The attributes may be a callable or a mapping from choice indices + // to nested arrays + is_callable($attr) ? call_user_func($attr, $choice, $key) : (isset($attr[$key]) ? $attr[$key] : array()) + ); + + // $isPreferred may be null if no choices are preferred + if ($isPreferred && call_user_func($isPreferred, $choice, $key)) { + $preferredViews[call_user_func($index, $choice, $key)] = $view; + } else { + $otherViews[call_user_func($index, $choice, $key)] = $view; + } + } + + private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $values, $index, $attr, $isPreferred, &$preferredViews, &$otherViews) + { + foreach ($groupBy as $key => $content) { + // Add the contents of groups to new ChoiceGroupView instances + if (is_array($content)) { + $preferredViewsForGroup = array(); + $otherViewsForGroup = array(); + + self::addChoiceViewsGroupedBy( + $content, + $label, + $choices, + $values, + $index, + $attr, + $isPreferred, + $preferredViewsForGroup, + $otherViewsForGroup + ); + + if (count($preferredViewsForGroup) > 0) { + $preferredViews[$key] = new ChoiceGroupView($key, $preferredViewsForGroup); + } + + if (count($otherViewsForGroup) > 0) { + $otherViews[$key] = new ChoiceGroupView($key, $otherViewsForGroup); + } + + continue; + } + + // Add ungrouped items directly + self::addChoiceView( + $choices[$key], + $key, + $label, + $values, + $index, + $attr, + $isPreferred, + $preferredViews, + $otherViews + ); + } + } + + private static function addChoiceViewGroupedBy($groupBy, $choice, $key, $label, $values, $index, $attr, $isPreferred, &$preferredViews, &$otherViews) + { + $groupLabel = call_user_func($groupBy, $choice, $key); + + if (null === $groupLabel) { + // If the callable returns null, don't group the choice + self::addChoiceView( + $choice, + $key, + $label, + $values, + $index, + $attr, + $isPreferred, + $preferredViews, + $otherViews + ); + + return; + } + + // Initialize the group views if necessary. Unnnecessarily built group + // views will be cleaned up at the end of createView() + if (!isset($preferredViews[$groupLabel])) { + $preferredViews[$groupLabel] = new ChoiceGroupView($groupLabel); + $otherViews[$groupLabel] = new ChoiceGroupView($groupLabel); + } + + self::addChoiceView( + $choice, + $key, + $label, + $values, + $index, + $attr, + $isPreferred, + $preferredViews[$groupLabel]->choices, + $otherViews[$groupLabel]->choices + ); + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php new file mode 100644 index 000000000000..bf91d85eea64 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -0,0 +1,226 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyAccess\PropertyPath; + +/** + * Adds property path support to a choice list factory. + * + * Pass the decorated factory to the constructor: + * + * ```php + * $decorator = new PropertyAccessDecorator($factory); + * ``` + * + * You can now pass property paths for generating choice values, labels, view + * indices, HTML attributes and for determining the preferred choices and the + * choice groups: + * + * ```php + * // extract values from the $value property + * $list = $createListFromChoices($objects, 'value'); + * ``` + * + * @author Bernhard Schussek + */ +class PropertyAccessDecorator implements ChoiceListFactoryInterface +{ + /** + * @var ChoiceListFactoryInterface + */ + private $decoratedFactory; + + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + /** + * Decorates the given factory. + * + * @param ChoiceListFactoryInterface $decoratedFactory The decorated factory + * @param null|PropertyAccessorInterface $propertyAccessor The used property accessor + */ + public function __construct(ChoiceListFactoryInterface $decoratedFactory, PropertyAccessorInterface $propertyAccessor = null) + { + $this->decoratedFactory = $decoratedFactory; + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + } + + /** + * Returns the decorated factory. + * + * @return ChoiceListFactoryInterface The decorated factory + */ + public function getDecoratedFactory() + { + return $this->decoratedFactory; + } + + /** + * {@inheritdoc} + * + * @param array|\Traversable $choices The choices + * @param null|callable|string|PropertyPath $value The callable or path for + * generating the choice values + * + * @return ChoiceListInterface The choice list + */ + public function createListFromChoices($choices, $value = null) + { + if (is_string($value)) { + $value = new PropertyPath($value); + } + + if ($value instanceof PropertyPath) { + $accessor = $this->propertyAccessor; + $value = function ($choice) use ($accessor, $value) { + return $accessor->getValue($choice, $value); + }; + } + + return $this->decoratedFactory->createListFromChoices($choices, $value); + } + + /** + * {@inheritdoc} + * + * @param array|\Traversable $choices The choices + * @param null|callable|string|PropertyPath $value The callable or path for + * generating the choice values + * + * @return ChoiceListInterface The choice list + * + * @deprecated Added for backwards compatibility in Symfony 2.7, to be + * removed in Symfony 3.0. + */ + public function createListFromFlippedChoices($choices, $value = null) + { + // Property paths are not supported here, because array keys can never + // be objects + return $this->decoratedFactory->createListFromFlippedChoices($choices, $value); + } + + /** + * {@inheritdoc} + * + * @param ChoiceLoaderInterface $loader The choice loader + * @param null|callable|string|PropertyPath $value The callable or path for + * generating the choice values + * + * @return ChoiceListInterface The choice list + */ + public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null) + { + if (is_string($value)) { + $value = new PropertyPath($value); + } + + if ($value instanceof PropertyPath) { + $accessor = $this->propertyAccessor; + $value = function ($choice) use ($accessor, $value) { + return $accessor->getValue($choice, $value); + }; + } + + return $this->decoratedFactory->createListFromLoader($loader, $value); + } + + /** + * {@inheritdoc} + * + * @param ChoiceListInterface $list The choice list + * @param null|array|callable|PropertyPath $preferredChoices The preferred choices + * @param null|callable|PropertyPath $label The callable or path + * generating the choice labels + * @param null|callable|PropertyPath $index The callable or path + * generating the view indices + * @param null|array|\Traversable|callable|PropertyPath $groupBy The callable or path + * generating the group names + * @param null|array|callable|PropertyPath $attr The callable or path + * generating the HTML attributes + * + * @return ChoiceListView The choice list view + */ + public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null) + { + $accessor = $this->propertyAccessor; + + if (is_string($label)) { + $label = new PropertyPath($label); + } + + if ($label instanceof PropertyPath) { + $label = function ($choice) use ($accessor, $label) { + return $accessor->getValue($choice, $label); + }; + } + + if (is_string($preferredChoices)) { + $preferredChoices = new PropertyPath($preferredChoices); + } + + if ($preferredChoices instanceof PropertyPath) { + $preferredChoices = function ($choice) use ($accessor, $preferredChoices) { + try { + return $accessor->getValue($choice, $preferredChoices); + } catch (UnexpectedTypeException $e) { + // Assume not preferred if not readable + return false; + } + }; + } + + if (is_string($index)) { + $index = new PropertyPath($index); + } + + if ($index instanceof PropertyPath) { + $index = function ($choice) use ($accessor, $index) { + return $accessor->getValue($choice, $index); + }; + } + + if (is_string($groupBy)) { + $groupBy = new PropertyPath($groupBy); + } + + if ($groupBy instanceof PropertyPath) { + $groupBy = function ($choice) use ($accessor, $groupBy) { + try { + return $accessor->getValue($choice, $groupBy); + } catch (UnexpectedTypeException $e) { + // Don't group if path is not readable + } + }; + } + + if (is_string($attr)) { + $attr = new PropertyPath($attr); + } + + if ($attr instanceof PropertyPath) { + $attr = function ($choice) use ($accessor, $attr) { + return $accessor->getValue($choice, $attr); + }; + } + + return $this->decoratedFactory->createView($list, $preferredChoices, $label, $index, $groupBy, $attr); + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php new file mode 100644 index 000000000000..91e6bfe4088d --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList; + +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; + +/** + * A choice list that loads its choices lazily. + * + * The choices are fetched using a {@link ChoiceLoaderInterface} instance. + * If only {@link getChoicesForValues()} or {@link getValuesForChoices()} is + * called, the choice list is only loaded partially for improved performance. + * + * Once {@link getChoices()} or {@link getValues()} is called, the list is + * loaded fully. + * + * @author Bernhard Schussek + */ +class LazyChoiceList implements ChoiceListInterface +{ + /** + * The choice loader. + * + * @var ChoiceLoaderInterface + */ + private $loader; + + /** + * The callable creating string values for each choice. + * + * If null, choices are simply cast to strings. + * + * @var null|callable + */ + private $value; + + /** + * @var ChoiceListInterface + */ + private $loadedList; + + /** + * Creates a lazily-loaded list using the given loader. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param ChoiceLoaderInterface $loader The choice loader + * @param null|callable $value The callable generating the choice + * values + */ + public function __construct(ChoiceLoaderInterface $loader, $value = null) + { + $this->loader = $loader; + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function getChoices() + { + if (!$this->loadedList) { + $this->loadedList = $this->loader->loadChoiceList($this->value); + } + + return $this->loadedList->getChoices(); + } + + /** + * {@inheritdoc} + */ + public function getValues() + { + if (!$this->loadedList) { + $this->loadedList = $this->loader->loadChoiceList($this->value); + } + + return $this->loadedList->getValues(); + } + + /** + * {@inheritdoc} + */ + public function getChoicesForValues(array $values) + { + if (!$this->loadedList) { + return $this->loader->loadChoicesForValues($values, $this->value); + } + + return $this->loadedList->getChoicesForValues($values); + } + + /** + * {@inheritdoc} + */ + public function getValuesForChoices(array $choices) + { + if (!$this->loadedList) { + return $this->loader->loadValuesForChoices($choices, $this->value); + } + + return $this->loadedList->getValuesForChoices($choices); + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.php b/src/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.php new file mode 100644 index 000000000000..9171fe3f1653 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Loader; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; + +/** + * Loads a choice list. + * + * The methods {@link loadChoicesForValues()} and {@link loadValuesForChoices()} + * can be used to load the list only partially in cases where a fully-loaded + * list is not necessary. + * + * @author Bernhard Schussek + */ +interface ChoiceLoaderInterface +{ + /** + * Loads a list of choices. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param null|callable $value The callable which generates the values + * from choices + * + * @return ChoiceListInterface The loaded choice list + */ + public function loadChoiceList($value = null); + + /** + * Loads the choices corresponding to the given values. + * + * The choices are returned with the same keys and in the same order as the + * corresponding values in the given array. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param string[] $values An array of choice values. Non-existing + * values in this array are ignored + * @param null|callable $value The callable generating the choice values + * + * @return array An array of choices + */ + public function loadChoicesForValues(array $values, $value = null); + + /** + * Loads the values corresponding to the given choices. + * + * The values are returned with the same keys and in the same order as the + * corresponding choices in the given array. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param array $choices An array of choices. Non-existing choices in + * this array are ignored + * @param null|callable $value The callable generating the choice values + * + * @return string[] An array of choice values + */ + public function loadValuesForChoices(array $choices, $value = null); +} diff --git a/src/Symfony/Component/Form/ChoiceList/View/ChoiceGroupView.php b/src/Symfony/Component/Form/ChoiceList/View/ChoiceGroupView.php new file mode 100644 index 000000000000..8e5962036969 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/View/ChoiceGroupView.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\View; + +/** + * Represents a group of choices in templates. + * + * @author Bernhard Schussek + */ +class ChoiceGroupView implements \IteratorAggregate +{ + /** + * The label of the group + * + * @var string + */ + public $label; + + /** + * The choice views in the group + * + * @var ChoiceGroupView[]|ChoiceView[] + */ + public $choices; + + /** + * Creates a new choice group view. + * + * @param string $label The label of the group. + * @param ChoiceGroupView[]|ChoiceView[] $choices The choice views in the + * group. + */ + public function __construct($label, array $choices = array()) + { + $this->label = $label; + $this->choices = $choices; + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + return new \ArrayIterator($this->choices); + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/View/ChoiceListView.php b/src/Symfony/Component/Form/ChoiceList/View/ChoiceListView.php new file mode 100644 index 000000000000..9641f4b1d943 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/View/ChoiceListView.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\View; + +/** + * Represents a choice list in templates. + * + * A choice list contains choices and optionally preferred choices which are + * displayed in the very beginning of the list. Both choices and preferred + * choices may be grouped in {@link ChoiceGroupView} instances. + * + * @author Bernhard Schussek + */ +class ChoiceListView +{ + /** + * The choices. + * + * @var ChoiceGroupView[]|ChoiceView[] + */ + public $choices; + + /** + * The preferred choices. + * + * @var ChoiceGroupView[]|ChoiceView[] + */ + public $preferredChoices; + + /** + * Creates a new choice list view. + * + * @param ChoiceGroupView[]|ChoiceView[] $choices The choice views. + * @param ChoiceGroupView[]|ChoiceView[] $preferredChoices The preferred + * choice views. + */ + public function __construct(array $choices = array(), array $preferredChoices = array()) + { + $this->choices = $choices; + $this->preferredChoices = $preferredChoices; + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/View/ChoiceView.php b/src/Symfony/Component/Form/ChoiceList/View/ChoiceView.php new file mode 100644 index 000000000000..ded2a55b30ad --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/View/ChoiceView.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\View; + +/** + * Represents a choice in templates. + * + * @author Bernhard Schussek + */ +class ChoiceView +{ + /** + * The label displayed to humans. + * + * @var string + */ + public $label; + + /** + * The view representation of the choice. + * + * @var string + */ + public $value; + + /** + * The original choice value. + * + * @var mixed + */ + public $data; + + /** + * Additional attributes for the HTML tag. + * + * @var array + */ + public $attr; + + /** + * Creates a new choice view. + * + * @param string $label The label displayed to humans + * @param string $value The view representation of the choice + * @param mixed $data The original choice + * @param array $attr Additional attributes for the HTML tag + */ + public function __construct($label, $value, $data, array $attr = array()) + { + $this->label = $label; + $this->value = $value; + $this->data = $data; + $this->attr = $attr; + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php index 9d2a1c42a46c..2f7b287b63e8 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -29,10 +29,13 @@ * * $choices = array(true, false); * $labels = array('Agree', 'Disagree'); - * $choiceList = new ChoiceList($choices, $labels); + * $choiceList = new ArrayChoiceList($choices, $labels); * * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayChoiceList} instead. */ class ChoiceList implements ChoiceListInterface { diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php index 8f09179a2a8f..22354e09d852 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php @@ -25,23 +25,13 @@ * in the HTML "value" attribute. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\ChoiceListInterface} + * instead. */ -interface ChoiceListInterface +interface ChoiceListInterface extends \Symfony\Component\Form\ChoiceList\ChoiceListInterface { - /** - * Returns the list of choices. - * - * @return array The choices with their indices as keys - */ - public function getChoices(); - - /** - * Returns the values for the choices. - * - * @return array The values with the corresponding choice indices as keys - */ - public function getValues(); - /** * Returns the choice views of the preferred choices as nested array with * the choice groups as top-level keys. @@ -92,37 +82,6 @@ public function getPreferredViews(); */ public function getRemainingViews(); - /** - * Returns the choices corresponding to the given values. - * - * The choices can have any data type. - * - * The choices must be returned with the same keys and in the same order - * as the corresponding values in the given array. - * - * @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 - */ - public function getChoicesForValues(array $values); - - /** - * Returns the values corresponding to the given choices. - * - * The values must be strings. - * - * The values must be returned with the same keys and in the same order - * as the corresponding choices in the given array. - * - * @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 - */ - public function getValuesForChoices(array $choices); - /** * Returns the indices corresponding to the given choices. * diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php index ee136f79780f..24232bc1d67a 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php @@ -21,6 +21,10 @@ * which should return a ChoiceListInterface instance. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\LazyChoiceList} + * instead. */ abstract class LazyChoiceList implements ChoiceListInterface { diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php index a20d19455577..606de43af3ef 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php @@ -32,6 +32,10 @@ * * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayChoiceList} + * instead. */ class ObjectChoiceList extends ChoiceList { diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php index 8d4ddd12423e..50a3eb5f4a29 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php @@ -28,6 +28,10 @@ * * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayKeyChoiceList} + * instead. */ class SimpleChoiceList extends ChoiceList { diff --git a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php index a0153a57eb70..231994258e8d 100644 --- a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php +++ b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php @@ -12,7 +12,12 @@ namespace Symfony\Component\Form\Extension\Core; use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** * Represents the main form extension, which loads the core functionality. @@ -21,13 +26,29 @@ */ class CoreExtension extends AbstractExtension { + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + /** + * @var ChoiceListFactoryInterface + */ + private $choiceListFactory; + + public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) + { + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor)); + } + protected function loadTypes() { return array( - new Type\FormType(PropertyAccess::createPropertyAccessor()), + new Type\FormType($this->propertyAccessor), new Type\BirthdayType(), new Type\CheckboxType(), - new Type\ChoiceType(), + new Type\ChoiceType($this->choiceListFactory), new Type\CollectionType(), new Type\CountryType(), new Type\DateType(), diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/CheckboxListMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/CheckboxListMapper.php new file mode 100644 index 000000000000..d87196475fec --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/CheckboxListMapper.php @@ -0,0 +1,93 @@ + + * + * 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\DataMapper; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\Exception; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Maps choices to/from checkbox forms. + * + * A {@link ChoiceListInterface} implementation is used to find the + * corresponding string values for the choices. Each checkbox form whose "value" + * option corresponds to any of the selected values is marked as selected. + * + * @author Bernhard Schussek + */ +class CheckboxListMapper implements DataMapperInterface +{ + /** + * @var ChoiceListInterface + */ + private $choiceList; + + public function __construct(ChoiceListInterface $choiceList) + { + $this->choiceList = $choiceList; + } + + /** + * {@inheritdoc} + */ + public function mapDataToForms($choices, $checkboxes) + { + if (null === $choices) { + $choices = array(); + } + + if (!is_array($choices)) { + throw new TransformationFailedException('Expected an array.'); + } + + try { + $valueMap = array_flip($this->choiceList->getValuesForChoices($choices)); + } catch (\Exception $e) { + throw new TransformationFailedException( + 'Can not read the choices from the choice list.', + $e->getCode(), + $e + ); + } + + foreach ($checkboxes as $checkbox) { + $value = $checkbox->getConfig()->getOption('value'); + $checkbox->setData(isset($valueMap[$value]) ? true : false); + } + } + + /** + * {@inheritdoc} + */ + public function mapFormsToData($checkboxes, &$choices) + { + $values = array(); + + foreach ($checkboxes as $checkbox) { + if ($checkbox->getData()) { + // construct an array of choice values + $values[] = $checkbox->getConfig()->getOption('value'); + } + } + + try { + $choices = $this->choiceList->getChoicesForValues($values); + } catch (\Exception $e) { + throw new TransformationFailedException( + 'Can not read the values from the choice list.', + $e->getCode(), + $e + ); + } + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php index 2208f26d1e5d..736752a41e19 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php @@ -17,7 +17,7 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** - * A data mapper using property paths to read/write data. + * Maps arrays/objects to/from forms using property paths. * * @author Bernhard Schussek */ @@ -31,7 +31,7 @@ class PropertyPathMapper implements DataMapperInterface /** * Creates a new property path mapper. * - * @param PropertyAccessorInterface $propertyAccessor + * @param PropertyAccessorInterface $propertyAccessor The property accessor */ public function __construct(PropertyAccessorInterface $propertyAccessor = null) { diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php new file mode 100644 index 000000000000..aecdb2fad0c7 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataMapper; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\DataMapperInterface; + +/** + * Maps choices to/from radio forms. + * + * A {@link ChoiceListInterface} implementation is used to find the + * corresponding string values for the choices. The radio form whose "value" + * option corresponds to the selected value is marked as selected. + * + * @author Bernhard Schussek + */ +class RadioListMapper implements DataMapperInterface +{ + /** + * @var ChoiceListInterface + */ + private $choiceList; + + public function __construct(ChoiceListInterface $choiceList) + { + $this->choiceList = $choiceList; + } + + /** + * {@inheritdoc} + */ + public function mapDataToForms($choice, $radios) + { + $valueMap = array_flip($this->choiceList->getValuesForChoices(array($choice))); + + foreach ($radios as $radio) { + $value = $radio->getConfig()->getOption('value'); + $radio->setData(isset($valueMap[$value]) ? true : false); + } + } + + /** + * {@inheritdoc} + */ + public function mapFormsToData($radios, &$choice) + { + $choice = null; + + foreach ($radios as $radio) { + if ($radio->getData()) { + if ('placeholder' === $radio->getName()) { + $choice = null; + + return; + } + + $value = $radio->getConfig()->getOption('value'); + $choice = current($this->choiceList->getChoicesForValues(array($value))); + + return; + } + } + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php index a91ed55c3125..a0b5039317b5 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php @@ -11,12 +11,16 @@ namespace Symfony\Component\Form\Extension\Core\DataTransformer; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; /** * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\LazyChoiceList} + * instead. */ class ChoiceToBooleanArrayTransformer implements DataTransformerInterface { diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php index 087faf4d3b4e..1c8378262135 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php @@ -13,7 +13,7 @@ use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; /** * @author Bernhard Schussek @@ -43,7 +43,7 @@ public function reverseTransform($value) throw new TransformationFailedException('Expected a scalar.'); } - // These are now valid ChoiceList values, so we can return null + // These are now valid ArrayChoiceList values, so we can return null // right away if ('' === $value || null === $value) { return; diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php index f1f13fda2884..c38c36332901 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php @@ -11,12 +11,16 @@ namespace Symfony\Component\Form\Extension\Core\DataTransformer; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; /** * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\LazyChoiceList} + * instead. */ class ChoicesToBooleanArrayTransformer implements DataTransformerInterface { diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php index 0ee0b0fefd5f..0a1f2f028863 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php @@ -13,7 +13,7 @@ use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\DataTransformerInterface; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; /** * @author Bernhard Schussek diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php index b201802fbcff..297987f79972 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Form\Extension\Core\EventListener; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\Exception\TransformationFailedException; -use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormEvent; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\FormEvents; /** * Takes care of converting the input from a list of checkboxes to a correctly diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php index c5f871756bab..d5067b6e3350 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Form\Extension\Core\EventListener; -use Symfony\Component\Form\FormEvents; -use Symfony\Component\Form\FormEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; /** * Takes care of converting the input from a single radio button diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 5b52b4ad96a7..7e80a00bdec9 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -12,19 +12,25 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper; +use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; -use Symfony\Component\Form\Exception\LogicException; -use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; -use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener; -use Symfony\Component\Form\Extension\Core\EventListener\FixCheckboxInputListener; use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener; 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; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -33,54 +39,111 @@ class ChoiceType extends AbstractType /** * Caches created choice lists. * - * @var array + * @var ChoiceListFactoryInterface */ - private $choiceListCache = array(); + private $choiceListFactory; + + public function __construct(ChoiceListFactoryInterface $choiceListFactory = null) + { + $this->choiceListFactory = $choiceListFactory ?: new PropertyAccessDecorator(new DefaultChoiceListFactory()); + } /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { - if (!$options['choice_list'] && !is_array($options['choices']) && !$options['choices'] instanceof \Traversable) { - throw new LogicException('Either the option "choices" or "choice_list" must be set.'); - } - if ($options['expanded']) { + $builder->setDataMapper($options['multiple'] + ? new CheckboxListMapper($options['choice_list']) + : new RadioListMapper($options['choice_list'])); + // Initialize all choices before doing the index check below. // This helps in cases where index checks are optimized for non // initialized choice lists. For example, when using an SQL driver, // the index check would read in one SQL query and the initialization // requires another SQL query. When the initialization is done first, // one SQL query is sufficient. - $preferredViews = $options['choice_list']->getPreferredViews(); - $remainingViews = $options['choice_list']->getRemainingViews(); + + $choiceListView = $this->createChoiceListView($options['choice_list'], $options); + $builder->setAttribute('choice_list_view', $choiceListView); // Check if the choices already contain the empty value - // Only add the empty value option if this is not the case + // Only add the placeholder option if this is not the case if (null !== $options['placeholder'] && 0 === count($options['choice_list']->getChoicesForValues(array('')))) { - $placeholderView = new ChoiceView(null, '', $options['placeholder']); + $placeholderView = new ChoiceView($options['placeholder'], '', null); - // "placeholder" is a reserved index - $this->addSubForms($builder, array('placeholder' => $placeholderView), $options); + // "placeholder" is a reserved name + $this->addSubForm($builder, 'placeholder', $placeholderView, $options); } - $this->addSubForms($builder, $preferredViews, $options); - $this->addSubForms($builder, $remainingViews, $options); + $this->addSubForms($builder, $choiceListView->preferredChoices, $options); + $this->addSubForms($builder, $choiceListView->choices, $options); - if ($options['multiple']) { - $builder->addViewTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list'])); - $builder->addEventSubscriber(new FixCheckboxInputListener($options['choice_list']), 10); - } else { - $builder->addViewTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list'], $builder->has('placeholder'))); - $builder->addEventSubscriber(new FixRadioInputListener($options['choice_list'], $builder->has('placeholder')), 10); + // Make sure that scalar, submitted values are converted to arrays + // which can be submitted to the checkboxes/radio buttons + $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + $form = $event->getForm(); + $data = $event->getData(); + + // Convert the submitted data to a string, if scalar, before + // casting it to an array + if (!is_array($data)) { + $data = (array) (string) $data; + } + + // A map from submitted values to integers + $valueMap = array_flip($data); + + // Make a copy of the value map to determine whether any unknown + // values were submitted + $unknownValues = $valueMap; + + // Reconstruct the data as mapping from child names to values + $data = array(); + + foreach ($form as $child) { + $value = $child->getConfig()->getOption('value'); + + // Add the value to $data with the child's name as key + if (isset($valueMap[$value])) { + $data[$child->getName()] = $value; + unset($unknownValues[$value]); + continue; + } + } + + // The empty value is always known, independent of whether a + // field exists for it or not + unset($unknownValues['']); + + // Throw exception if unknown values were submitted + if (count($unknownValues) > 0) { + throw new TransformationFailedException(sprintf( + 'The choices "%s" do not exist in the choice list.', + implode('", "', array_keys($unknownValues)) + )); + } + + $event->setData($data); + }); + + if (!$options['multiple']) { + // For radio lists, transform empty arrays to null + // This is kind of a hack necessary because the RadioListMapper + // is not invoked for forms without choices + $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) { + if (array() === $event->getData()) { + $event->setData(null); + } + }); } + } elseif ($options['multiple']) { + // tag without "multiple" option + $builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list'])); } if ($options['multiple'] && $options['by_reference']) { @@ -95,11 +158,16 @@ public function buildForm(FormBuilderInterface $builder, array $options) */ public function buildView(FormView $view, FormInterface $form, array $options) { + /** @var ChoiceListView $choiceListView */ + $choiceListView = $form->getConfig()->hasAttribute('choice_list_view') + ? $form->getConfig()->getAttribute('choice_list_view') + : $this->createChoiceListView($options['choice_list'], $options); + $view->vars = array_replace($view->vars, array( 'multiple' => $options['multiple'], 'expanded' => $options['expanded'], - 'preferred_choices' => $options['choice_list']->getPreferredViews(), - 'choices' => $options['choice_list']->getRemainingViews(), + 'preferred_choices' => $choiceListView->preferredChoices, + 'choices' => $choiceListView->choices, 'separator' => '-------------------', 'placeholder' => null, )); @@ -163,20 +231,39 @@ public function finishView(FormView $view, FormInterface $form, array $options) */ public function configureOptions(OptionsResolver $resolver) { - $choiceListCache = &$this->choiceListCache; + $choiceListFactory = $this->choiceListFactory; + + $choiceList = function (Options $options) use ($choiceListFactory) { + if (null !== $options['choice_loader']) { + // Due to a bug in OptionsResolver, the choices haven't been + // validated yet at this point. Remove the if statement once that + // bug is resolved + if (!$options['choice_loader'] instanceof ChoiceLoaderInterface) { + return; + } + + return $choiceListFactory->createListFromLoader( + $options['choice_loader'], + $options['choice_value'] + ); + } - $choiceList = function (Options $options) use (&$choiceListCache) { // Harden against NULL values (like in EntityType and ModelType) $choices = null !== $options['choices'] ? $options['choices'] : array(); - // Reuse existing choice lists in order to increase performance - $hash = hash('sha256', serialize(array($choices, $options['preferred_choices']))); + // Due to a bug in OptionsResolver, the choices haven't been + // validated yet at this point. Remove the if statement once that + // bug is resolved + if (!is_array($choices) && !$choices instanceof \Traversable) { + return; + } - if (!isset($choiceListCache[$hash])) { - $choiceListCache[$hash] = new SimpleChoiceList($choices, $options['preferred_choices']); + // BC when choices are in the keys, not in the values + if (!$options['choices_as_values']) { + return $choiceListFactory->createListFromFlippedChoices($choices, $options['choice_value']); } - return $choiceListCache[$hash]; + return $choiceListFactory->createListFromChoices($choices, $options['choice_value']); }; $emptyData = function (Options $options) { @@ -219,9 +306,16 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setDefaults(array( 'multiple' => false, 'expanded' => false, - 'choice_list' => $choiceList, + 'choice_list' => $choiceList, // deprecated 'choices' => array(), + 'choices_as_values' => false, + 'choice_loader' => null, + 'choice_label' => null, + 'choice_name' => null, + 'choice_value' => null, + 'choice_attr' => null, 'preferred_choices' => array(), + 'group_by' => null, 'empty_data' => $emptyData, 'empty_value' => $emptyValue, // deprecated 'placeholder' => $placeholder, @@ -236,7 +330,16 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setNormalizer('empty_value', $placeholderNormalizer); $resolver->setNormalizer('placeholder', $placeholderNormalizer); - $resolver->setAllowedTypes('choice_list', array('null', 'Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface')); + $resolver->setAllowedTypes('choice_list', array('null', 'Symfony\Component\Form\ChoiceList\ChoiceListInterface')); + $resolver->setAllowedTypes('choices', array('null', 'array', '\Traversable')); + $resolver->setAllowedTypes('choices_as_values', 'bool'); + $resolver->setAllowedTypes('choice_loader', array('null', 'Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface')); + $resolver->setAllowedTypes('choice_label', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('choice_name', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('choice_value', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('choice_attr', array('null', 'array', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('preferred_choices', array('array', '\Traversable', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('group_by', array('null', 'array', '\Traversable', 'string', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); } /** @@ -247,6 +350,21 @@ public function getName() return 'choice'; } + private static function flipRecursive($choices, &$output = array()) + { + foreach ($choices as $key => $value) { + if (is_array($value)) { + $output[$key] = array(); + self::flipRecursive($value, $output[$key]); + continue; + } + + $output[$value] = $key; + } + + return $output; + } + /** * Adds the sub fields for an expanded choice field. * @@ -256,29 +374,69 @@ public function getName() */ private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options) { - foreach ($choiceViews as $i => $choiceView) { + foreach ($choiceViews as $name => $choiceView) { + // Flatten groups if (is_array($choiceView)) { - // Flatten groups $this->addSubForms($builder, $choiceView, $options); - } else { - $choiceOpts = array( - 'value' => $choiceView->value, - 'label' => $choiceView->label, - 'translation_domain' => $options['translation_domain'], - 'block_name' => 'entry', - ); - - if ($options['multiple']) { - $choiceType = 'checkbox'; - // The user can check 0 or more checkboxes. If required - // is true, he is required to check all of them. - $choiceOpts['required'] = false; - } else { - $choiceType = 'radio'; - } + continue; + } - $builder->add($i, $choiceType, $choiceOpts); + if ($choiceView instanceof ChoiceGroupView) { + $this->addSubForms($builder, $choiceView->choices, $options); + continue; } + + $this->addSubForm($builder, $name, $choiceView, $options); + } + } + + /** + * @param FormBuilderInterface $builder + * @param $name + * @param $choiceView + * @param array $options + * + * @return mixed + */ + private function addSubForm(FormBuilderInterface $builder, $name, ChoiceView $choiceView, array $options) + { + $choiceOpts = array( + 'value' => $choiceView->value, + 'label' => $choiceView->label, + 'attr' => $choiceView->attr, + 'translation_domain' => $options['translation_domain'], + 'block_name' => 'entry', + ); + + if ($options['multiple']) { + $choiceType = 'checkbox'; + // The user can check 0 or more checkboxes. If required + // is true, he is required to check all of them. + $choiceOpts['required'] = false; + } else { + $choiceType = 'radio'; } + + $builder->add($name, $choiceType, $choiceOpts); + } + + private function createChoiceListView(ChoiceListInterface $choiceList, array $options) + { + // If no explicit grouping information is given, use the structural + // information from the "choices" option for creating groups + if (!$options['group_by'] && $options['choices']) { + $options['group_by'] = !$options['choices_as_values'] + ? ChoiceType::flipRecursive($options['choices']) + : $options['choices']; + } + + return $this->choiceListFactory->createView( + $choiceList, + $options['preferred_choices'], + $options['choice_label'], + $options['choice_name'], + $options['group_by'], + $options['choice_attr'] + ); } } diff --git a/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php index 97cdd214c28f..65d7af246478 100644 --- a/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php +++ b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php @@ -16,29 +16,8 @@ * * @author Bernhard Schussek */ -class ChoiceView +class ChoiceView extends \Symfony\Component\Form\ChoiceList\View\ChoiceView { - /** - * The original choice value. - * - * @var mixed - */ - public $data; - - /** - * The view representation of the choice. - * - * @var string - */ - public $value; - - /** - * The label displayed to humans. - * - * @var string - */ - public $label; - /** * Creates a new ChoiceView. * @@ -48,8 +27,6 @@ class ChoiceView */ public function __construct($data, $value, $label) { - $this->data = $data; - $this->value = $value; - $this->label = $label; + parent::__construct($label, $value, $data); } } diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index 6375542b2834..3bf84d71c82c 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -516,6 +516,28 @@ public function testSingleChoice() ); } + public function testSingleChoiceAttributes() + { + $form = $this->factory->createNamed('name', 'choice', '&a', array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => false, + 'expanded' => false, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/select + [@name="name"] + [not(@required)] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + public function testSingleChoiceWithPreferred() { $form = $this->factory->createNamed('name', 'choice', '&a', array( @@ -776,6 +798,30 @@ public function testMultipleChoice() ); } + public function testMultipleChoiceAttributes() + { + $form = $this->factory->createNamed('name', 'choice', array('&a'), array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'required' => true, + 'multiple' => true, + 'expanded' => false, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/select + [@name="name[]"] + [@required="required"] + [@multiple="multiple"] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + public function testMultipleChoiceSkipsPlaceholder() { $form = $this->factory->createNamed('name', 'choice', array('&a'), array( @@ -842,6 +888,29 @@ public function testSingleChoiceExpanded() ); } + public function testSingleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', 'choice', '&a', array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => false, + 'expanded' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][@class="foo&bar"][not(@checked)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] + [count(./input)=3] +' + ); + } + public function testSingleChoiceExpandedWithPlaceholder() { $form = $this->factory->createNamed('name', 'choice', '&a', array( @@ -914,6 +983,32 @@ public function testMultipleChoiceExpanded() ); } + public function testMultipleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', 'choice', array('&a', '&c'), array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B', '&c' => 'Choice&C'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => true, + 'expanded' => true, + 'required' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][@class="foo&bar"][not(@checked)][not(@required)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] + /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)] + /following-sibling::label[@for="name_2"][.="[trans]Choice&C[/trans]"] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] + [count(./input)=4] +' + ); + } + public function testCountry() { $form = $this->factory->createNamed('name', 'country', 'AT'); diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php new file mode 100644 index 000000000000..0805238f7f77 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList; + +/** + * @author Bernhard Schussek + */ +abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + protected $list; + + /** + * @var array + */ + protected $choices; + + /** + * @var array + */ + protected $values; + + /** + * @var mixed + */ + protected $choice1; + + /** + * @var mixed + */ + protected $choice2; + + /** + * @var mixed + */ + protected $choice3; + + /** + * @var mixed + */ + protected $choice4; + + /** + * @var string + */ + protected $value1; + + /** + * @var string + */ + protected $value2; + + /** + * @var string + */ + protected $value3; + + /** + * @var string + */ + protected $value4; + + protected function setUp() + { + parent::setUp(); + + $this->list = $this->createChoiceList(); + + $this->choices = $this->getChoices(); + $this->values = $this->getValues(); + + // allow access to the individual entries without relying on their indices + reset($this->choices); + reset($this->values); + + for ($i = 1; $i <= 4; ++$i) { + $this->{'choice'.$i} = current($this->choices); + $this->{'value'.$i} = current($this->values); + + next($this->choices); + next($this->values); + } + } + + public function testGetChoices() + { + $this->assertSame($this->choices, $this->list->getChoices()); + } + + public function testGetValues() + { + $this->assertSame($this->values, $this->list->getValues()); + } + + public function testGetChoicesForValues() + { + $values = array($this->value1, $this->value2); + $this->assertSame(array($this->choice1, $this->choice2), $this->list->getChoicesForValues($values)); + } + + public function testGetChoicesForValuesPreservesKeys() + { + $values = array(5 => $this->value1, 8 => $this->value2); + $this->assertSame(array(5 => $this->choice1, 8 => $this->choice2), $this->list->getChoicesForValues($values)); + } + + public function testGetChoicesForValuesPreservesOrder() + { + $values = array($this->value2, $this->value1); + $this->assertSame(array($this->choice2, $this->choice1), $this->list->getChoicesForValues($values)); + } + + public function testGetChoicesForValuesIgnoresNonExistingValues() + { + $values = array($this->value1, $this->value2, 'foobar'); + $this->assertSame(array($this->choice1, $this->choice2), $this->list->getChoicesForValues($values)); + } + + // https://github.com/symfony/symfony/issues/3446 + public function testGetChoicesForValuesEmpty() + { + $this->assertSame(array(), $this->list->getChoicesForValues(array())); + } + + public function testGetValuesForChoices() + { + $choices = array($this->choice1, $this->choice2); + $this->assertSame(array($this->value1, $this->value2), $this->list->getValuesForChoices($choices)); + } + + public function testGetValuesForChoicesPreservesKeys() + { + $choices = array(5 => $this->choice1, 8 => $this->choice2); + $this->assertSame(array(5 => $this->value1, 8 => $this->value2), $this->list->getValuesForChoices($choices)); + } + + public function testGetValuesForChoicesPreservesOrder() + { + $choices = array($this->choice2, $this->choice1); + $this->assertSame(array($this->value2, $this->value1), $this->list->getValuesForChoices($choices)); + } + + public function testGetValuesForChoicesIgnoresNonExistingChoices() + { + $choices = array($this->choice1, $this->choice2, 'foobar'); + $this->assertSame(array($this->value1, $this->value2), $this->list->getValuesForChoices($choices)); + } + + public function testGetValuesForChoicesEmpty() + { + $this->assertSame(array(), $this->list->getValuesForChoices(array())); + } + + /** + * @return \Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + abstract protected function createChoiceList(); + + abstract protected function getChoices(); + + abstract protected function getValues(); +} diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php new file mode 100644 index 000000000000..34b22fe04177 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList; + +use Symfony\Component\Form\ChoiceList\ArrayChoiceList; + +/** + * @author Bernhard Schussek + */ +class ArrayChoiceListTest extends AbstractChoiceListTest +{ + private $object; + + protected function setUp() + { + parent::setUp(); + + $this->object = new \stdClass(); + } + + protected function createChoiceList() + { + return new ArrayChoiceList($this->getChoices(), $this->getValues()); + } + + protected function getChoices() + { + return array(0, 1, '1', 'a', false, true, $this->object); + } + + protected function getValues() + { + return array('0', '1', '2', '3', '4', '5', '6'); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException + */ + public function testFailIfKeyMismatch() + { + new ArrayChoiceList(array(0 => 'a', 1 => 'b'), array(1 => 'a', 2 => 'b')); + } +} diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php new file mode 100644 index 000000000000..74cf2afb4a2a --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php @@ -0,0 +1,187 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList; + +use Symfony\Component\Form\ChoiceList\ArrayKeyChoiceList; + +/** + * @author Bernhard Schussek + */ +class ArrayKeyChoiceListTest extends AbstractChoiceListTest +{ + private $object; + + protected function setUp() + { + parent::setUp(); + + $this->object = new \stdClass(); + } + + protected function createChoiceList() + { + return new ArrayKeyChoiceList($this->getChoices(), $this->getValues()); + } + + protected function getChoices() + { + return array(0, 1, 'a', 'b', ''); + } + + protected function getValues() + { + return array('0', '1', 'a', 'b', ''); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException + */ + public function testFailIfKeyMismatch() + { + new ArrayKeyChoiceList(array(0 => 'a', 1 => 'b'), array(1 => 'a', 2 => 'b')); + } + + public function testUseChoicesAsValuesByDefault() + { + $list = new ArrayKeyChoiceList(array(1 => '', 3 => 0, 7 => '1', 10 => 1.23)); + + $this->assertSame(array(1 => '', 3 => '0', 7 => '1', 10 => '1.23'), $list->getValues()); + } + + public function testNoChoices() + { + $list = new ArrayKeyChoiceList(array()); + + $this->assertSame(array(), $list->getValues()); + } + + public function testGetChoicesForValuesConvertsValuesToStrings() + { + $this->assertSame(array(0), $this->list->getChoicesForValues(array(0))); + $this->assertSame(array(0), $this->list->getChoicesForValues(array('0'))); + $this->assertSame(array(1), $this->list->getChoicesForValues(array(1))); + $this->assertSame(array(1), $this->list->getChoicesForValues(array('1'))); + $this->assertSame(array('a'), $this->list->getChoicesForValues(array('a'))); + $this->assertSame(array('b'), $this->list->getChoicesForValues(array('b'))); + $this->assertSame(array(''), $this->list->getChoicesForValues(array(''))); + // "1" === (string) true + $this->assertSame(array(1), $this->list->getChoicesForValues(array(true))); + // "" === (string) false + $this->assertSame(array(''), $this->list->getChoicesForValues(array(false))); + // "" === (string) null + $this->assertSame(array(''), $this->list->getChoicesForValues(array(null))); + $this->assertSame(array(), $this->list->getChoicesForValues(array(1.23))); + } + + public function testGetValuesForChoicesConvertsChoicesToArrayKeys() + { + $this->assertSame(array('0'), $this->list->getValuesForChoices(array(0))); + $this->assertSame(array('0'), $this->list->getValuesForChoices(array('0'))); + $this->assertSame(array('1'), $this->list->getValuesForChoices(array(1))); + $this->assertSame(array('1'), $this->list->getValuesForChoices(array('1'))); + $this->assertSame(array('a'), $this->list->getValuesForChoices(array('a'))); + $this->assertSame(array('b'), $this->list->getValuesForChoices(array('b'))); + // Always cast booleans to 0 and 1, because: + // array(true => 'Yes', false => 'No') === array(1 => 'Yes', 0 => 'No') + // see ChoiceTypeTest::testSetDataSingleNonExpandedAcceptsBoolean + $this->assertSame(array('0'), $this->list->getValuesForChoices(array(false))); + $this->assertSame(array('1'), $this->list->getValuesForChoices(array(true))); + } + + /** + * @dataProvider provideConvertibleChoices + */ + public function testConvertChoicesIfNecessary(array $choices, array $converted) + { + $list = new ArrayKeyChoiceList($choices, range(0, count($choices) - 1)); + + $this->assertSame($converted, $list->getChoices()); + } + + public function provideConvertibleChoices() + { + return array( + array(array(0), array(0)), + array(array(1), array(1)), + array(array('0'), array(0)), + array(array('1'), array(1)), + array(array('1.23'), array('1.23')), + array(array('foobar'), array('foobar')), + // The default value of choice fields is NULL. It should be treated + // like the empty value for this choice list type + array(array(null), array('')), + array(array(1.23), array('1.23')), + // Always cast booleans to 0 and 1, because: + // array(true => 'Yes', false => 'No') === array(1 => 'Yes', 0 => 'No') + // see ChoiceTypeTest::testSetDataSingleNonExpandedAcceptsBoolean + array(array(true), array(1)), + array(array(false), array(0)), + ); + } + + /** + * @dataProvider provideInvalidChoices + * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException + */ + public function testFailIfInvalidChoices(array $choices) + { + new ArrayKeyChoiceList($choices, range(0, count($choices) - 1)); + } + + /** + * @dataProvider provideInvalidChoices + * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException + */ + public function testGetValuesForChoicesFailsIfInvalidChoices(array $choices) + { + $this->list->getValuesForChoices($choices); + } + + public function provideInvalidChoices() + { + return array( + array(array(new \stdClass())), + array(array(array(1, 2))), + ); + } + + /** + * @dataProvider provideConvertibleValues + */ + public function testConvertValuesToStrings(array $values, array $converted) + { + $list = new ArrayKeyChoiceList(range(0, count($values) - 1), $values); + + $this->assertSame($converted, $list->getValues()); + } + + public function provideConvertibleValues() + { + return array( + array(array(0), array('0')), + array(array(1), array('1')), + array(array('0'), array('0')), + array(array('1'), array('1')), + array(array('1.23'), array('1.23')), + array(array('foobar'), array('foobar')), + // The default value of choice fields is NULL. It should be treated + // like the empty value for this choice list type + array(array(null), array('')), + array(array(1.23), array('1.23')), + // Always cast booleans to 0 and 1, because: + // array(true => 'Yes', false => 'No') === array(1 => 'Yes', 0 => 'No') + // see ChoiceTypeTest::testSetDataSingleNonExpandedAcceptsBoolean + array(array(true), array('1')), + array(array(false), array('')), + ); + } +} diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php new file mode 100644 index 000000000000..031cced28028 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php @@ -0,0 +1,668 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; + +/** + * @author Bernhard Schussek + */ +class CachingFactoryDecoratorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $decoratedFactory; + + /** + * @var CachingFactoryDecorator + */ + private $factory; + + protected function setUp() + { + $this->decoratedFactory = $this->getMock('Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface'); + $this->factory = new CachingFactoryDecorator($this->decoratedFactory); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateFromChoicesFailsIfChoicesNotArrayOrTraversable() + { + $this->factory->createListFromChoices('foobar'); + } + + public function testCreateFromChoicesEmpty() + { + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromChoices') + ->with(array()) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromChoices(array())); + $this->assertSame($list, $this->factory->createListFromChoices(array())); + } + + public function testCreateFromChoicesComparesTraversableChoicesAsArray() + { + // The top-most traversable is converted to an array + $choices1 = new \ArrayIterator(array('A' => 'a')); + $choices2 = array('A' => 'a'); + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices2) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromChoices($choices1)); + $this->assertSame($list, $this->factory->createListFromChoices($choices2)); + } + + public function testCreateFromChoicesFlattensChoices() + { + $choices1 = array('key' => array('A' => 'a')); + $choices2 = array('A' => 'a'); + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices1) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromChoices($choices1)); + $this->assertSame($list, $this->factory->createListFromChoices($choices2)); + } + + /** + * @dataProvider provideSameChoices + */ + public function testCreateFromChoicesSameChoices($choice1, $choice2) + { + $choices1 = array($choice1); + $choices2 = array($choice2); + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices1) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromChoices($choices1)); + $this->assertSame($list, $this->factory->createListFromChoices($choices2)); + } + + /** + * @dataProvider provideDistinguishedChoices + */ + public function testCreateFromChoicesDifferentChoices($choice1, $choice2) + { + $choices1 = array($choice1); + $choices2 = array($choice2); + $list1 = new \stdClass(); + $list2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createListFromChoices') + ->with($choices1) + ->will($this->returnValue($list1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createListFromChoices') + ->with($choices2) + ->will($this->returnValue($list2)); + + $this->assertSame($list1, $this->factory->createListFromChoices($choices1)); + $this->assertSame($list2, $this->factory->createListFromChoices($choices2)); + } + + public function testCreateFromChoicesSameValueClosure() + { + $choices = array(1); + $list = new \stdClass(); + $closure = function () {}; + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices, $closure) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromChoices($choices, $closure)); + $this->assertSame($list, $this->factory->createListFromChoices($choices, $closure)); + } + + public function testCreateFromChoicesDifferentValueClosure() + { + $choices = array(1); + $list1 = new \stdClass(); + $list2 = new \stdClass(); + $closure1 = function () {}; + $closure2 = function () {}; + + $this->decoratedFactory->expects($this->at(0)) + ->method('createListFromChoices') + ->with($choices, $closure1) + ->will($this->returnValue($list1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createListFromChoices') + ->with($choices, $closure2) + ->will($this->returnValue($list2)); + + $this->assertSame($list1, $this->factory->createListFromChoices($choices, $closure1)); + $this->assertSame($list2, $this->factory->createListFromChoices($choices, $closure2)); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateFromFlippedChoicesFailsIfChoicesNotArrayOrTraversable() + { + $this->factory->createListFromFlippedChoices('foobar'); + } + + public function testCreateFromFlippedChoicesEmpty() + { + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromFlippedChoices') + ->with(array()) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromFlippedChoices(array())); + $this->assertSame($list, $this->factory->createListFromFlippedChoices(array())); + } + + public function testCreateFromFlippedChoicesComparesTraversableChoicesAsArray() + { + // The top-most traversable is converted to an array + $choices1 = new \ArrayIterator(array('a' => 'A')); + $choices2 = array('a' => 'A'); + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromFlippedChoices') + ->with($choices2) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices1)); + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices2)); + } + + public function testCreateFromFlippedChoicesFlattensChoices() + { + $choices1 = array('key' => array('a' => 'A')); + $choices2 = array('a' => 'A'); + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromFlippedChoices') + ->with($choices1) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices1)); + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices2)); + } + + /** + * @dataProvider provideSameKeyChoices + */ + public function testCreateFromFlippedChoicesSameChoices($choice1, $choice2) + { + $choices1 = array($choice1); + $choices2 = array($choice2); + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromFlippedChoices') + ->with($choices1) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices1)); + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices2)); + } + + /** + * @dataProvider provideDistinguishedKeyChoices + */ + public function testCreateFromFlippedChoicesDifferentChoices($choice1, $choice2) + { + $choices1 = array($choice1); + $choices2 = array($choice2); + $list1 = new \stdClass(); + $list2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createListFromFlippedChoices') + ->with($choices1) + ->will($this->returnValue($list1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createListFromFlippedChoices') + ->with($choices2) + ->will($this->returnValue($list2)); + + $this->assertSame($list1, $this->factory->createListFromFlippedChoices($choices1)); + $this->assertSame($list2, $this->factory->createListFromFlippedChoices($choices2)); + } + + public function testCreateFromFlippedChoicesSameValueClosure() + { + $choices = array(1); + $list = new \stdClass(); + $closure = function () {}; + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromFlippedChoices') + ->with($choices, $closure) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices, $closure)); + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices, $closure)); + } + + public function testCreateFromFlippedChoicesDifferentValueClosure() + { + $choices = array(1); + $list1 = new \stdClass(); + $list2 = new \stdClass(); + $closure1 = function () {}; + $closure2 = function () {}; + + $this->decoratedFactory->expects($this->at(0)) + ->method('createListFromFlippedChoices') + ->with($choices, $closure1) + ->will($this->returnValue($list1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createListFromFlippedChoices') + ->with($choices, $closure2) + ->will($this->returnValue($list2)); + + $this->assertSame($list1, $this->factory->createListFromFlippedChoices($choices, $closure1)); + $this->assertSame($list2, $this->factory->createListFromFlippedChoices($choices, $closure2)); + } + + public function testCreateFromLoaderSameLoader() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromLoader') + ->with($loader) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromLoader($loader)); + $this->assertSame($list, $this->factory->createListFromLoader($loader)); + } + + public function testCreateFromLoaderDifferentLoader() + { + $loader1 = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + $loader2 = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + $list1 = new \stdClass(); + $list2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createListFromLoader') + ->with($loader1) + ->will($this->returnValue($list1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createListFromLoader') + ->with($loader2) + ->will($this->returnValue($list2)); + + $this->assertSame($list1, $this->factory->createListFromLoader($loader1)); + $this->assertSame($list2, $this->factory->createListFromLoader($loader2)); + } + + public function testCreateFromLoaderSameValueClosure() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + $list = new \stdClass(); + $closure = function () {}; + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromLoader') + ->with($loader, $closure) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromLoader($loader, $closure)); + $this->assertSame($list, $this->factory->createListFromLoader($loader, $closure)); + } + + public function testCreateFromLoaderDifferentValueClosure() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + $list1 = new \stdClass(); + $list2 = new \stdClass(); + $closure1 = function () {}; + $closure2 = function () {}; + + $this->decoratedFactory->expects($this->at(0)) + ->method('createListFromLoader') + ->with($loader, $closure1) + ->will($this->returnValue($list1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createListFromLoader') + ->with($loader, $closure2) + ->will($this->returnValue($list2)); + + $this->assertSame($list1, $this->factory->createListFromLoader($loader, $closure1)); + $this->assertSame($list2, $this->factory->createListFromLoader($loader, $closure2)); + } + + public function testCreateViewSamePreferredChoices() + { + $preferred = array('a'); + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, $preferred) + ->will($this->returnValue($view)); + + $this->assertSame($view, $this->factory->createView($list, $preferred)); + $this->assertSame($view, $this->factory->createView($list, $preferred)); + } + + public function testCreateViewDifferentPreferredChoices() + { + $preferred1 = array('a'); + $preferred2 = array('b'); + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view1 = new \stdClass(); + $view2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createView') + ->with($list, $preferred1) + ->will($this->returnValue($view1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createView') + ->with($list, $preferred2) + ->will($this->returnValue($view2)); + + $this->assertSame($view1, $this->factory->createView($list, $preferred1)); + $this->assertSame($view2, $this->factory->createView($list, $preferred2)); + } + + public function testCreateViewSamePreferredChoicesClosure() + { + $preferred = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, $preferred) + ->will($this->returnValue($view)); + + $this->assertSame($view, $this->factory->createView($list, $preferred)); + $this->assertSame($view, $this->factory->createView($list, $preferred)); + } + + public function testCreateViewDifferentPreferredChoicesClosure() + { + $preferred1 = function () {}; + $preferred2 = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view1 = new \stdClass(); + $view2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createView') + ->with($list, $preferred1) + ->will($this->returnValue($view1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createView') + ->with($list, $preferred2) + ->will($this->returnValue($view2)); + + $this->assertSame($view1, $this->factory->createView($list, $preferred1)); + $this->assertSame($view2, $this->factory->createView($list, $preferred2)); + } + + public function testCreateViewSameLabelClosure() + { + $labels = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, $labels) + ->will($this->returnValue($view)); + + $this->assertSame($view, $this->factory->createView($list, null, $labels)); + $this->assertSame($view, $this->factory->createView($list, null, $labels)); + } + + public function testCreateViewDifferentLabelClosure() + { + $labels1 = function () {}; + $labels2 = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view1 = new \stdClass(); + $view2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createView') + ->with($list, null, $labels1) + ->will($this->returnValue($view1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createView') + ->with($list, null, $labels2) + ->will($this->returnValue($view2)); + + $this->assertSame($view1, $this->factory->createView($list, null, $labels1)); + $this->assertSame($view2, $this->factory->createView($list, null, $labels2)); + } + + public function testCreateViewSameIndexClosure() + { + $index = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, $index) + ->will($this->returnValue($view)); + + $this->assertSame($view, $this->factory->createView($list, null, null, $index)); + $this->assertSame($view, $this->factory->createView($list, null, null, $index)); + } + + public function testCreateViewDifferentIndexClosure() + { + $index1 = function () {}; + $index2 = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view1 = new \stdClass(); + $view2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createView') + ->with($list, null, null, $index1) + ->will($this->returnValue($view1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createView') + ->with($list, null, null, $index2) + ->will($this->returnValue($view2)); + + $this->assertSame($view1, $this->factory->createView($list, null, null, $index1)); + $this->assertSame($view2, $this->factory->createView($list, null, null, $index2)); + } + + public function testCreateViewSameGroupByClosure() + { + $groupBy = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, $groupBy) + ->will($this->returnValue($view)); + + $this->assertSame($view, $this->factory->createView($list, null, null, null, $groupBy)); + $this->assertSame($view, $this->factory->createView($list, null, null, null, $groupBy)); + } + + public function testCreateViewDifferentGroupByClosure() + { + $groupBy1 = function () {}; + $groupBy2 = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view1 = new \stdClass(); + $view2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createView') + ->with($list, null, null, null, $groupBy1) + ->will($this->returnValue($view1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createView') + ->with($list, null, null, null, $groupBy2) + ->will($this->returnValue($view2)); + + $this->assertSame($view1, $this->factory->createView($list, null, null, null, $groupBy1)); + $this->assertSame($view2, $this->factory->createView($list, null, null, null, $groupBy2)); + } + + public function testCreateViewSameAttributes() + { + $attr = array('class' => 'foobar'); + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, null, $attr) + ->will($this->returnValue($view)); + + $this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr)); + $this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr)); + } + + public function testCreateViewDifferentAttributes() + { + $attr1 = array('class' => 'foobar1'); + $attr2 = array('class' => 'foobar2'); + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view1 = new \stdClass(); + $view2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createView') + ->with($list, null, null, null, null, $attr1) + ->will($this->returnValue($view1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createView') + ->with($list, null, null, null, null, $attr2) + ->will($this->returnValue($view2)); + + $this->assertSame($view1, $this->factory->createView($list, null, null, null, null, $attr1)); + $this->assertSame($view2, $this->factory->createView($list, null, null, null, null, $attr2)); + } + + public function testCreateViewSameAttributesClosure() + { + $attr = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, null, $attr) + ->will($this->returnValue($view)); + + $this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr)); + $this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr)); + } + + public function testCreateViewDifferentAttributesClosure() + { + $attr1 = function () {}; + $attr2 = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view1 = new \stdClass(); + $view2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createView') + ->with($list, null, null, null, null, $attr1) + ->will($this->returnValue($view1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createView') + ->with($list, null, null, null, null, $attr2) + ->will($this->returnValue($view2)); + + $this->assertSame($view1, $this->factory->createView($list, null, null, null, null, $attr1)); + $this->assertSame($view2, $this->factory->createView($list, null, null, null, null, $attr2)); + } + + public function provideSameChoices() + { + $object = (object) array('foo' => 'bar'); + + return array( + array(0, 0), + array('a', 'a'), + // https://github.com/symfony/symfony/issues/10409 + array(chr(181).'meter', chr(181).'meter'), // UTF-8 + array($object, $object), + ); + } + + public function provideDistinguishedChoices() + { + return array( + array(0, false), + array(0, null), + array(0, '0'), + array(0, ''), + array(1, true), + array(1, '1'), + array(1, 'a'), + array('', false), + array('', null), + array(false, null), + // Same properties, but not identical + array((object) array('foo' => 'bar'), (object) array('foo' => 'bar')), + ); + } + + public function provideSameKeyChoices() + { + // Only test types here that can be used as array keys + return array( + array(0, 0), + array(0, '0'), + array('a', 'a'), + array(chr(181).'meter', chr(181).'meter'), + ); + } + + public function provideDistinguishedKeyChoices() + { + // Only test types here that can be used as array keys + return array( + array(0, ''), + array(1, 'a'), + array('', 'a'), + ); + } +} diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php new file mode 100644 index 000000000000..42f745e29b73 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -0,0 +1,970 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\ArrayChoiceList; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\LazyChoiceList; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; + +class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase +{ + private $obj1; + + private $obj2; + + private $obj3; + + private $obj4; + + private $list; + + /** + * @var DefaultChoiceListFactory + */ + private $factory; + + public function getValue($object) + { + return $object->value; + } + + public function getScalarValue($choice) + { + switch ($choice) { + case 'a': return 'a'; + case 'b': return 'b'; + case 'c': return '1'; + case 'd': return '2'; + } + } + + public function getLabel($object) + { + return $object->label; + } + + public function getFormIndex($object) + { + return $object->index; + } + + public function isPreferred($object) + { + return $this->obj2 === $object || $this->obj3 === $object; + } + + public function getAttr($object) + { + return $object->attr; + } + + public function getGroup($object) + { + return $this->obj1 === $object || $this->obj2 === $object ? 'Group 1' : 'Group 2'; + } + + protected function setUp() + { + $this->obj1 = (object) array('label' => 'A', 'index' => 'w', 'value' => 'a', 'preferred' => false, 'group' => 'Group 1', 'attr' => array()); + $this->obj2 = (object) array('label' => 'B', 'index' => 'x', 'value' => 'b', 'preferred' => true, 'group' => 'Group 1', 'attr' => array('attr1' => 'value1')); + $this->obj3 = (object) array('label' => 'C', 'index' => 'y', 'value' => 1, 'preferred' => true, 'group' => 'Group 2', 'attr' => array('attr2' => 'value2')); + $this->obj4 = (object) array('label' => 'D', 'index' => 'z', 'value' => 2, 'preferred' => false, 'group' => 'Group 2', 'attr' => array()); + $this->list = new ArrayChoiceList( + array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4), + array('A' => '0', 'B' => '1', 'C' => '2', 'D' => '3') + ); + $this->factory = new DefaultChoiceListFactory(); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateFromChoicesFailsIfChoicesNotArrayOrTraversable() + { + $this->factory->createListFromChoices('foobar'); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateFromChoicesFailsIfValuesNotCallableOrString() + { + $this->factory->createListFromChoices(array(), new \stdClass()); + } + + public function testCreateFromChoicesEmpty() + { + $list = $this->factory->createListFromChoices(array()); + + $this->assertSame(array(), $list->getChoices()); + $this->assertSame(array(), $list->getValues()); + } + + public function testCreateFromChoicesFlat() + { + $list = $this->factory->createListFromChoices( + array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4) + ); + + $this->assertObjectListWithGeneratedValues($list); + } + + public function testCreateFromChoicesFlatTraversable() + { + $list = $this->factory->createListFromChoices( + new \ArrayIterator(array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4)) + ); + + $this->assertObjectListWithGeneratedValues($list); + } + + public function testCreateFromChoicesFlatValuesAsCallable() + { + $list = $this->factory->createListFromChoices( + array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4), + array($this, 'getValue') + ); + + $this->assertObjectListWithCustomValues($list); + } + + public function testCreateFromChoicesFlatValuesAsClosure() + { + $list = $this->factory->createListFromChoices( + array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4), + function ($object) { return $object->value; } + ); + + $this->assertObjectListWithCustomValues($list); + } + + public function testCreateFromChoicesFlatValuesClosureReceivesKey() + { + $list = $this->factory->createListFromChoices( + array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4), + function ($object, $key) { + switch ($key) { + case 'A': return 'a'; + case 'B': return 'b'; + case 'C': return '1'; + case 'D': return '2'; + } + } + ); + + $this->assertObjectListWithCustomValues($list); + } + + public function testCreateFromChoicesGrouped() + { + $list = $this->factory->createListFromChoices( + array( + 'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2), + 'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4), + ) + ); + + $this->assertObjectListWithGeneratedValues($list); + } + + public function testCreateFromChoicesGroupedTraversable() + { + $list = $this->factory->createListFromChoices( + new \ArrayIterator(array( + 'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2), + 'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4), + )) + ); + + $this->assertObjectListWithGeneratedValues($list); + } + + public function testCreateFromChoicesGroupedValuesAsCallable() + { + $list = $this->factory->createListFromChoices( + array( + 'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2), + 'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4), + ), + array($this, 'getValue') + ); + + $this->assertObjectListWithCustomValues($list); + } + + public function testCreateFromChoicesGroupedValuesAsClosure() + { + $list = $this->factory->createListFromChoices( + array( + 'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2), + 'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4), + ), + function ($object) { return $object->value; } + ); + + $this->assertObjectListWithCustomValues($list); + } + + public function testCreateFromChoicesGroupedValuesAsClosureReceivesKey() + { + $list = $this->factory->createListFromChoices( + array( + 'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2), + 'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4), + ), + function ($object, $key) { + switch ($key) { + case 'A': return 'a'; + case 'B': return 'b'; + case 'C': return '1'; + case 'D': return '2'; + } + } + ); + + $this->assertObjectListWithCustomValues($list); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateFromFlippedChoicesFailsIfChoicesNotArrayOrTraversable() + { + $this->factory->createListFromFlippedChoices('foobar'); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateFromFlippedChoicesFailsIfValuesNotCallableOrString() + { + $this->factory->createListFromFlippedChoices(array(), new \stdClass()); + } + + public function testCreateFromFlippedChoicesEmpty() + { + $list = $this->factory->createListFromFlippedChoices(array()); + + $this->assertSame(array(), $list->getChoices()); + $this->assertSame(array(), $list->getValues()); + } + + public function testCreateFromFlippedChoicesFlat() + { + $list = $this->factory->createListFromFlippedChoices( + array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D') + ); + + $this->assertScalarListWithGeneratedValues($list); + } + + public function testCreateFromFlippedChoicesFlatTraversable() + { + $list = $this->factory->createListFromFlippedChoices( + new \ArrayIterator(array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D')) + ); + + $this->assertScalarListWithGeneratedValues($list); + } + + public function testCreateFromFlippedChoicesFlatValuesAsCallable() + { + $list = $this->factory->createListFromFlippedChoices( + array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D'), + array($this, 'getScalarValue') + ); + + $this->assertScalarListWithCustomValues($list); + } + + public function testCreateFromFlippedChoicesFlatValuesAsClosure() + { + $list = $this->factory->createListFromFlippedChoices( + array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D'), + function ($choice) { + switch ($choice) { + case 'a': return 'a'; + case 'b': return 'b'; + case 'c': return '1'; + case 'd': return '2'; + } + } + ); + + $this->assertScalarListWithCustomValues($list); + } + + public function testCreateFromFlippedChoicesFlatValuesClosureReceivesKey() + { + $list = $this->factory->createListFromFlippedChoices( + array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D'), + function ($choice, $key) { + switch ($key) { + case 'A': return 'a'; + case 'B': return 'b'; + case 'C': return '1'; + case 'D': return '2'; + } + } + ); + + $this->assertScalarListWithCustomValues($list); + } + + public function testCreateFromFlippedChoicesGrouped() + { + $list = $this->factory->createListFromFlippedChoices( + array( + 'Group 1' => array('a' => 'A', 'b' => 'B'), + 'Group 2' => array('c' => 'C', 'd' => 'D'), + ) + ); + + $this->assertScalarListWithGeneratedValues($list); + } + + public function testCreateFromFlippedChoicesGroupedTraversable() + { + $list = $this->factory->createListFromFlippedChoices( + new \ArrayIterator(array( + 'Group 1' => array('a' => 'A', 'b' => 'B'), + 'Group 2' => array('c' => 'C', 'd' => 'D'), + )) + ); + + $this->assertScalarListWithGeneratedValues($list); + } + + public function testCreateFromFlippedChoicesGroupedValuesAsCallable() + { + $list = $this->factory->createListFromFlippedChoices( + array( + 'Group 1' => array('a' => 'A', 'b' => 'B'), + 'Group 2' => array('c' => 'C', 'd' => 'D'), + ), + array($this, 'getScalarValue') + ); + + $this->assertScalarListWithCustomValues($list); + } + + public function testCreateFromFlippedChoicesGroupedValuesAsClosure() + { + $list = $this->factory->createListFromFlippedChoices( + array( + 'Group 1' => array('a' => 'A', 'b' => 'B'), + 'Group 2' => array('c' => 'C', 'd' => 'D'), + ), + function ($choice) { + switch ($choice) { + case 'a': return 'a'; + case 'b': return 'b'; + case 'c': return '1'; + case 'd': return '2'; + } + } + ); + + $this->assertScalarListWithCustomValues($list); + } + + public function testCreateFromFlippedChoicesGroupedValuesAsClosureReceivesKey() + { + $list = $this->factory->createListFromFlippedChoices( + array( + 'Group 1' => array('a' => 'A', 'b' => 'B'), + 'Group 2' => array('c' => 'C', 'd' => 'D'), + ), + function ($choice, $key) { + switch ($key) { + case 'A': return 'a'; + case 'B': return 'b'; + case 'C': return '1'; + case 'D': return '2'; + } + } + ); + + $this->assertScalarListWithCustomValues($list); + } + + public function testCreateFromLoader() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + + $list = $this->factory->createListFromLoader($loader); + + $this->assertEquals(new LazyChoiceList($loader), $list); + } + + public function testCreateFromLoaderWithValues() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + + $value = function () {}; + $list = $this->factory->createListFromLoader($loader, $value); + + $this->assertEquals(new LazyChoiceList($loader, $value), $list); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateFromLoaderFailsIfValuesNotCallableOrString() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + + $this->factory->createListFromLoader($loader, new \stdClass()); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateViewFailsIfPreferredChoicesInvalid() + { + $this->factory->createView($this->list, new \stdClass()); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateViewFailsIfLabelInvalid() + { + $this->factory->createView($this->list, null, new \stdClass()); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateViewFailsIfIndexInvalid() + { + $this->factory->createView($this->list, null, null, new \stdClass()); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateViewFailsIfGroupByInvalid() + { + $this->factory->createView($this->list, null, null, null, new \stdClass()); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateViewFailsIfAttrInvalid() + { + $this->factory->createView($this->list, null, null, null, null, new \stdClass()); + } + + public function testCreateViewFlat() + { + $view = $this->factory->createView($this->list); + + $this->assertEquals(new ChoiceListView( + array( + 0 => new ChoiceView('A', '0', $this->obj1), + 1 => new ChoiceView('B', '1', $this->obj2), + 2 => new ChoiceView('C', '2', $this->obj3), + 3 => new ChoiceView('D', '3', $this->obj4), + ), array() + ), $view); + } + + public function testCreateViewFlatPreferredChoices() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3) + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatPreferredChoicesEmptyArray() + { + $view = $this->factory->createView( + $this->list, + array() + ); + + $this->assertEquals(new ChoiceListView( + array( + 0 => new ChoiceView('A', '0', $this->obj1), + 1 => new ChoiceView('B', '1', $this->obj2), + 2 => new ChoiceView('C', '2', $this->obj3), + 3 => new ChoiceView('D', '3', $this->obj4), + ), array() + ), $view); + } + + public function testCreateViewFlatPreferredChoicesAsCallable() + { + $view = $this->factory->createView( + $this->list, + array($this, 'isPreferred') + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatPreferredChoicesAsClosure() + { + $obj2 = $this->obj2; + $obj3 = $this->obj3; + + $view = $this->factory->createView( + $this->list, + function ($object) use ($obj2, $obj3) { + return $obj2 === $object || $obj3 === $object; + } + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatPreferredChoicesClosureReceivesKey() + { + $obj2 = $this->obj2; + $obj3 = $this->obj3; + + $view = $this->factory->createView( + $this->list, + function ($object, $key) use ($obj2, $obj3) { + return 'B' === $key || 'C' === $key; + } + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatLabelAsCallable() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + array($this, 'getLabel') + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatLabelAsClosure() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + function ($object) { + return $object->label; + } + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatLabelClosureReceivesKey() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + function ($object, $key) { + return $key; + } + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatIndexAsCallable() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + array($this, 'getFormIndex') + ); + + $this->assertFlatViewWithCustomIndices($view); + } + + public function testCreateViewFlatIndexAsClosure() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + function ($object) { + return $object->index; + } + ); + + $this->assertFlatViewWithCustomIndices($view); + } + + public function testCreateViewFlatIndexClosureReceivesKey() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + function ($object, $key) { + switch ($key) { + case 'A': return 'w'; + case 'B': return 'x'; + case 'C': return 'y'; + case 'D': return 'z'; + } + } + ); + + $this->assertFlatViewWithCustomIndices($view); + } + + public function testCreateViewFlatGroupByAsArray() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + array( + 'Group 1' => array('A' => true, 'B' => true), + 'Group 2' => array('C' => true, 'D' => true), + ) + ); + + $this->assertGroupedView($view); + } + + public function testCreateViewFlatGroupByAsTraversable() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + new \ArrayIterator(array( + 'Group 1' => array('A' => true, 'B' => true), + 'Group 2' => array('C' => true, 'D' => true), + )) + ); + + $this->assertGroupedView($view); + } + + public function testCreateViewFlatGroupByEmpty() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + array() // ignored + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatGroupByAsCallable() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + array($this, 'getGroup') + ); + + $this->assertGroupedView($view); + } + + public function testCreateViewFlatGroupByAsClosure() + { + $obj1 = $this->obj1; + $obj2 = $this->obj2; + + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + function ($object) use ($obj1, $obj2) { + return $obj1 === $object || $obj2 === $object ? 'Group 1' + : 'Group 2'; + } + ); + + $this->assertGroupedView($view); + } + + public function testCreateViewFlatGroupByClosureReceivesKey() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + function ($object, $key) { + return 'A' === $key || 'B' === $key ? 'Group 1' : 'Group 2'; + } + ); + + $this->assertGroupedView($view); + } + + public function testCreateViewFlatAttrAsArray() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + null, // group + array( + 'B' => array('attr1' => 'value1'), + 'C' => array('attr2' => 'value2') + ) + ); + + $this->assertFlatViewWithAttr($view); + } + + public function testCreateViewFlatAttrEmpty() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + null, // group + array() + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatAttrAsCallable() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + null, // group + array($this, 'getAttr') + ); + + $this->assertFlatViewWithAttr($view); + } + + public function testCreateViewFlatAttrAsClosure() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + null, // group + function ($object) { + return $object->attr; + } + ); + + $this->assertFlatViewWithAttr($view); + } + + public function testCreateViewFlatAttrClosureReceivesKey() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + null, // group + function ($object, $key) { + switch ($key) { + case 'B': return array('attr1' => 'value1'); + case 'C': return array('attr2' => 'value2'); + default: return array(); + } + } + ); + + $this->assertFlatViewWithAttr($view); + } + + public function testCreateViewForLegacyChoiceList() + { + $preferred = array(new ChoiceView('Preferred', 'x', 'x')); + $other = array(new ChoiceView('Other', 'y', 'y')); + + $list = $this->getMock('Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface'); + + $list->expects($this->once()) + ->method('getPreferredViews') + ->will($this->returnValue($preferred)); + $list->expects($this->once()) + ->method('getRemainingViews') + ->will($this->returnValue($other)); + + $view = $this->factory->createView($list); + + $this->assertSame($other, $view->choices); + $this->assertSame($preferred, $view->preferredChoices); + } + + private function assertScalarListWithGeneratedValues(ChoiceListInterface $list) + { + $this->assertSame(array( + 'A' => 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + ), $list->getChoices()); + + $this->assertSame(array( + 'A' => 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + ), $list->getValues()); + } + + private function assertObjectListWithGeneratedValues(ChoiceListInterface $list) + { + $this->assertSame(array( + 'A' => $this->obj1, + 'B' => $this->obj2, + 'C' => $this->obj3, + 'D' => $this->obj4, + ), $list->getChoices()); + + $this->assertSame(array( + 'A' => '0', + 'B' => '1', + 'C' => '2', + 'D' => '3', + ), $list->getValues()); + } + + private function assertScalarListWithCustomValues(ChoiceListInterface $list) + { + $this->assertSame(array( + 'A' => 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + ), $list->getChoices()); + + $this->assertSame(array( + 'A' => 'a', + 'B' => 'b', + 'C' => '1', + 'D' => '2', + ), $list->getValues()); + } + + private function assertObjectListWithCustomValues(ChoiceListInterface $list) + { + $this->assertSame(array( + 'A' => $this->obj1, + 'B' => $this->obj2, + 'C' => $this->obj3, + 'D' => $this->obj4, + ), $list->getChoices()); + + $this->assertSame(array( + 'A' => 'a', + 'B' => 'b', + 'C' => '1', + 'D' => '2', + ), $list->getValues()); + } + + private function assertFlatView($view) + { + $this->assertEquals(new ChoiceListView( + array( + 0 => new ChoiceView('A', '0', $this->obj1), + 3 => new ChoiceView('D', '3', $this->obj4), + ), array( + 1 => new ChoiceView('B', '1', $this->obj2), + 2 => new ChoiceView('C', '2', $this->obj3), + ) + ), $view); + } + + private function assertFlatViewWithCustomIndices($view) + { + $this->assertEquals(new ChoiceListView( + array( + 'w' => new ChoiceView('A', '0', $this->obj1), + 'z' => new ChoiceView('D', '3', $this->obj4), + ), array( + 'x' => new ChoiceView('B', '1', $this->obj2), + 'y' => new ChoiceView('C', '2', $this->obj3), + ) + ), $view); + } + + private function assertFlatViewWithAttr($view) + { + $this->assertEquals(new ChoiceListView( + array( + 0 => new ChoiceView('A', '0', $this->obj1), + 3 => new ChoiceView('D', '3', $this->obj4), + ), array( + 1 => new ChoiceView( + 'B', + '1', + $this->obj2, + array('attr1' => 'value1') + ), + 2 => new ChoiceView( + 'C', + '2', + $this->obj3, + array('attr2' => 'value2') + ), + ) + ), $view); + } + + private function assertGroupedView($view) + { + $this->assertEquals(new ChoiceListView( + array( + 'Group 1' => new ChoiceGroupView( + 'Group 1', + array(0 => new ChoiceView('A', '0', $this->obj1)) + ), + 'Group 2' => new ChoiceGroupView( + 'Group 2', + array(3 => new ChoiceView('D', '3', $this->obj4)) + ), + ), array( + 'Group 1' => new ChoiceGroupView( + 'Group 1', + array(1 => new ChoiceView('B', '1', $this->obj2)) + ), + 'Group 2' => new ChoiceGroupView( + 'Group 2', + array(2 => new ChoiceView('C', '2', $this->obj3)) + ), + ) + ), $view); + } +} diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/PropertyAccessDecoratorTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/PropertyAccessDecoratorTest.php new file mode 100644 index 000000000000..8697a9cac55d --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/PropertyAccessDecoratorTest.php @@ -0,0 +1,338 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\PropertyAccess\PropertyPath; + +/** + * @author Bernhard Schussek + */ +class PropertyAccessDecoratorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $decoratedFactory; + + /** + * @var PropertyAccessDecorator + */ + private $factory; + + protected function setUp() + { + $this->decoratedFactory = $this->getMock('Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface'); + $this->factory = new PropertyAccessDecorator($this->decoratedFactory); + } + + public function testCreateFromChoicesPropertyPath() + { + $choices = array((object) array('property' => 'value')); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($choices, $callback) { + return array_map($callback, $choices); + })); + + $this->assertSame(array('value'), $this->factory->createListFromChoices($choices, 'property')); + } + + public function testCreateFromChoicesPropertyPathInstance() + { + $choices = array((object) array('property' => 'value')); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($choices, $callback) { + return array_map($callback, $choices); + })); + + $this->assertSame(array('value'), $this->factory->createListFromChoices($choices, new PropertyPath('property'))); + } + + public function testCreateFromFlippedChoices() + { + // Property paths are not supported here, because array keys can never + // be objects anyway + $choices = array('a' => 'A'); + $value = 'foobar'; + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromFlippedChoices') + ->with($choices, $value) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices, $value)); + } + + public function testCreateFromLoaderPropertyPath() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromLoader') + ->with($loader, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($loader, $callback) { + return $callback((object) array('property' => 'value')); + })); + + $this->assertSame('value', $this->factory->createListFromLoader($loader, 'property')); + } + + public function testCreateFromLoaderPropertyPathInstance() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromLoader') + ->with($loader, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($loader, $callback) { + return $callback((object) array('property' => 'value')); + })); + + $this->assertSame('value', $this->factory->createListFromLoader($loader, new PropertyPath('property'))); + } + + public function testCreateViewPreferredChoicesAsPropertyPath() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred) { + return $preferred((object) array('property' => true)); + })); + + $this->assertTrue($this->factory->createView( + $list, + 'property' + )); + } + + public function testCreateViewPreferredChoicesAsPropertyPathInstance() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred) { + return $preferred((object) array('property' => true)); + })); + + $this->assertTrue($this->factory->createView( + $list, + new PropertyPath('property') + )); + } + + // https://github.com/symfony/symfony/issues/5494 + public function testCreateViewAssumeNullIfPreferredChoicesPropertyPathUnreadable() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred) { + return $preferred((object) array('category' => null)); + })); + + $this->assertFalse($this->factory->createView( + $list, + 'category.preferred' + )); + } + + public function testCreateViewLabelsAsPropertyPath() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label) { + return $label((object) array('property' => 'label')); + })); + + $this->assertSame('label', $this->factory->createView( + $list, + null, // preferred choices + 'property' + )); + } + + public function testCreateViewLabelsAsPropertyPathInstance() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label) { + return $label((object) array('property' => 'label')); + })); + + $this->assertSame('label', $this->factory->createView( + $list, + null, // preferred choices + new PropertyPath('property') + )); + } + + public function testCreateViewIndicesAsPropertyPath() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label, $index) { + return $index((object) array('property' => 'index')); + })); + + $this->assertSame('index', $this->factory->createView( + $list, + null, // preferred choices + null, // label + 'property' + )); + } + + public function testCreateViewIndicesAsPropertyPathInstance() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label, $index) { + return $index((object) array('property' => 'index')); + })); + + $this->assertSame('index', $this->factory->createView( + $list, + null, // preferred choices + null, // label + new PropertyPath('property') + )); + } + + public function testCreateViewGroupsAsPropertyPath() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label, $index, $groupBy) { + return $groupBy((object) array('property' => 'group')); + })); + + $this->assertSame('group', $this->factory->createView( + $list, + null, // preferred choices + null, // label + null, // index + 'property' + )); + } + + public function testCreateViewGroupsAsPropertyPathInstance() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label, $index, $groupBy) { + return $groupBy((object) array('property' => 'group')); + })); + + $this->assertSame('group', $this->factory->createView( + $list, + null, // preferred choices + null, // label + null, // index + new PropertyPath('property') + )); + } + + // https://github.com/symfony/symfony/issues/5494 + public function testCreateViewAssumeNullIfGroupsPropertyPathUnreadable() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label, $index, $groupBy) { + return $groupBy((object) array('group' => null)); + })); + + $this->assertNull($this->factory->createView( + $list, + null, // preferred choices + null, // label + null, // index + 'group.name' + )); + } + + public function testCreateViewAttrAsPropertyPath() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label, $index, $groupBy, $attr) { + return $attr((object) array('property' => 'attr')); + })); + + $this->assertSame('attr', $this->factory->createView( + $list, + null, // preferred choices + null, // label + null, // index + null, // groups + 'property' + )); + } + + public function testCreateViewAttrAsPropertyPathInstance() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label, $index, $groupBy, $attr) { + return $attr((object) array('property' => 'attr')); + })); + + $this->assertSame('attr', $this->factory->createView( + $list, + null, // preferred choices + null, // label + null, // index + null, // groups + new PropertyPath('property') + )); + } +} diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php new file mode 100644 index 000000000000..2993721c8279 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList; + +use Symfony\Component\Form\ChoiceList\LazyChoiceList; + +/** + * @author Bernhard Schussek + */ +class LazyChoiceListTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var LazyChoiceList + */ + private $list; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $innerList; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $loader; + + private $value; + + protected function setUp() + { + $this->innerList = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $this->loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + $this->value = function () {}; + $this->list = new LazyChoiceList($this->loader, $this->value); + } + + public function testGetChoicesLoadsInnerListOnFirstCall() + { + $this->loader->expects($this->once()) + ->method('loadChoiceList') + ->with($this->value) + ->will($this->returnValue($this->innerList)); + + $this->innerList->expects($this->exactly(2)) + ->method('getChoices') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->list->getChoices()); + $this->assertSame('RESULT', $this->list->getChoices()); + } + + public function testGetValuesLoadsInnerListOnFirstCall() + { + $this->loader->expects($this->once()) + ->method('loadChoiceList') + ->with($this->value) + ->will($this->returnValue($this->innerList)); + + $this->innerList->expects($this->exactly(2)) + ->method('getValues') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->list->getValues()); + $this->assertSame('RESULT', $this->list->getValues()); + } + + public function testGetChoicesForValuesForwardsCallIfListNotLoaded() + { + $this->loader->expects($this->exactly(2)) + ->method('loadChoicesForValues') + ->with(array('a', 'b')) + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->list->getChoicesForValues(array('a', 'b'))); + $this->assertSame('RESULT', $this->list->getChoicesForValues(array('a', 'b'))); + } + + public function testGetChoicesForValuesUsesLoadedList() + { + $this->loader->expects($this->once()) + ->method('loadChoiceList') + ->with($this->value) + ->will($this->returnValue($this->innerList)); + + $this->loader->expects($this->never()) + ->method('loadChoicesForValues'); + + $this->innerList->expects($this->exactly(2)) + ->method('getChoicesForValues') + ->with(array('a', 'b')) + ->will($this->returnValue('RESULT')); + + // load choice list + $this->list->getChoices(); + + $this->assertSame('RESULT', $this->list->getChoicesForValues(array('a', 'b'))); + $this->assertSame('RESULT', $this->list->getChoicesForValues(array('a', 'b'))); + } + + public function testGetValuesForChoicesForwardsCallIfListNotLoaded() + { + $this->loader->expects($this->exactly(2)) + ->method('loadValuesForChoices') + ->with(array('a', 'b')) + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->list->getValuesForChoices(array('a', 'b'))); + $this->assertSame('RESULT', $this->list->getValuesForChoices(array('a', 'b'))); + } + + public function testGetValuesForChoicesUsesLoadedList() + { + $this->loader->expects($this->once()) + ->method('loadChoiceList') + ->with($this->value) + ->will($this->returnValue($this->innerList)); + + $this->loader->expects($this->never()) + ->method('loadValuesForChoices'); + + $this->innerList->expects($this->exactly(2)) + ->method('getValuesForChoices') + ->with(array('a', 'b')) + ->will($this->returnValue('RESULT')); + + // load choice list + $this->list->getChoices(); + + $this->assertSame('RESULT', $this->list->getValuesForChoices(array('a', 'b'))); + $this->assertSame('RESULT', $this->list->getValuesForChoices(array('a', 'b'))); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php index 17972cbc0a85..6a0b6db2eceb 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -11,8 +11,9 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase { @@ -66,6 +67,16 @@ protected function tearDown() $this->objectChoices = null; } + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testChoicesOptionExpectsArrayOrTraversable() + { + $this->factory->create('choice', null, array( + 'choices' => new \stdClass(), + )); + } + /** * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */ @@ -76,6 +87,16 @@ public function testChoiceListOptionExpectsChoiceListInterface() )); } + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testChoiceLoaderOptionExpectsChoiceLoaderInterface() + { + $this->factory->create('choice', null, array( + 'choice_loader' => new \stdClass(), + )); + } + public function testChoiceListAndChoicesCanBeEmpty() { $this->factory->create('choice'); @@ -236,7 +257,118 @@ public function testSubmitSingleNonExpandedInvalidChoice() $this->assertFalse($form->isSynchronized()); } + public function testSubmitSingleNonExpandedNull() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $form->submit(null); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleNonExpandedNullNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => array(), + )); + + $form->submit(null); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + public function testSubmitSingleNonExpandedEmpty() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $form->submit(''); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleNonExpandedEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => array(), + )); + + $form->submit(''); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + public function testSubmitSingleNonExpandedFalse() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $form->submit(false); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleNonExpandedFalseNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => array(), + )); + + $form->submit(false); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + public function testSubmitSingleNonExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->objectChoices, + 'choices_as_values' => true, + 'choice_label' => 'name', + 'choice_value' => 'id', + )); + + // "id" value of the second entry + $form->submit('2'); + + $this->assertEquals($this->objectChoices[1], $form->getData()); + $this->assertEquals('2', $form->getViewData()); + } + + public function testSubmitSingleNonExpandedObjectChoicesBc() { $form = $this->factory->create('choice', null, array( 'multiple' => false, @@ -273,6 +405,37 @@ public function testSubmitMultipleNonExpanded() $this->assertEquals(array('a', 'b'), $form->getViewData()); } + public function testSubmitMultipleNonExpandedEmpty() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $form->submit(array()); + + $this->assertSame(array(), $form->getData()); + $this->assertSame(array(), $form->getViewData()); + } + + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitMultipleNonExpandedEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => false, + 'choices' => array(), + )); + + $form->submit(array()); + + $this->assertSame(array(), $form->getData()); + $this->assertSame(array(), $form->getViewData()); + } + public function testSubmitMultipleNonExpandedInvalidScalarChoice() { $form = $this->factory->create('choice', null, array( @@ -304,6 +467,23 @@ public function testSubmitMultipleNonExpandedInvalidArrayChoice() } public function testSubmitMultipleNonExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => false, + 'choices' => $this->objectChoices, + 'choices_as_values' => true, + 'choice_label' => 'name', + 'choice_value' => 'id', + )); + + $form->submit(array('2', '3')); + + $this->assertEquals(array($this->objectChoices[1], $this->objectChoices[2]), $form->getData()); + $this->assertEquals(array('2', '3'), $form->getViewData()); + } + + public function testSubmitMultipleNonExpandedObjectChoicesBc() { $form = $this->factory->create('choice', null, array( 'multiple' => true, @@ -337,13 +517,7 @@ public function testSubmitSingleExpandedRequired() $form->submit('b'); $this->assertSame('b', $form->getData()); - $this->assertSame(array( - 0 => false, - 1 => true, - 2 => false, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertSame('b', $form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -399,14 +573,7 @@ public function testSubmitSingleExpandedNonRequired() $form->submit('b'); $this->assertSame('b', $form->getData()); - $this->assertSame(array( - 0 => false, - 1 => true, - 2 => false, - 3 => false, - 4 => false, - 'placeholder' => false, - ), $form->getViewData()); + $this->assertSame('b', $form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -464,13 +631,7 @@ public function testSubmitSingleExpandedRequiredNull() $form->submit(null); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -486,6 +647,26 @@ public function testSubmitSingleExpandedRequiredNull() $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedRequiredNullNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => true, + 'choices' => array(), + )); + + $form->submit(null); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedRequiredEmpty() { $form = $this->factory->create('choice', null, array( @@ -498,13 +679,7 @@ public function testSubmitSingleExpandedRequiredEmpty() $form->submit(''); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -520,6 +695,26 @@ public function testSubmitSingleExpandedRequiredEmpty() $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedRequiredEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => true, + 'choices' => array(), + )); + + $form->submit(''); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedRequiredFalse() { $form = $this->factory->create('choice', null, array( @@ -532,13 +727,7 @@ public function testSubmitSingleExpandedRequiredFalse() $form->submit(false); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -554,6 +743,26 @@ public function testSubmitSingleExpandedRequiredFalse() $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedRequiredFalseNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => true, + 'choices' => array(), + )); + + $form->submit(false); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedNonRequiredNull() { $form = $this->factory->create('choice', null, array( @@ -566,14 +775,7 @@ public function testSubmitSingleExpandedNonRequiredNull() $form->submit(null); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - 'placeholder' => true, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -591,6 +793,26 @@ public function testSubmitSingleExpandedNonRequiredNull() $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedNonRequiredNullNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => false, + 'choices' => array(), + )); + + $form->submit(null); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedNonRequiredEmpty() { $form = $this->factory->create('choice', null, array( @@ -603,14 +825,7 @@ public function testSubmitSingleExpandedNonRequiredEmpty() $form->submit(''); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - 'placeholder' => true, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -628,6 +843,26 @@ public function testSubmitSingleExpandedNonRequiredEmpty() $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedNonRequiredEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => false, + 'choices' => array(), + )); + + $form->submit(''); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedNonRequiredFalse() { $form = $this->factory->create('choice', null, array( @@ -640,14 +875,7 @@ public function testSubmitSingleExpandedNonRequiredFalse() $form->submit(false); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - 'placeholder' => true, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -665,6 +893,26 @@ public function testSubmitSingleExpandedNonRequiredFalse() $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedNonRequiredFalseNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => false, + 'choices' => array(), + )); + + $form->submit(false); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedWithEmptyChild() { $form = $this->factory->create('choice', null, array( @@ -686,6 +934,32 @@ public function testSubmitSingleExpandedWithEmptyChild() } public function testSubmitSingleExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'choices' => $this->objectChoices, + 'choices_as_values' => true, + 'choice_label' => 'name', + 'choice_value' => 'id', + )); + + $form->submit('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->assertNull($form[0]->getViewData()); + $this->assertSame('2', $form[1]->getViewData()); + $this->assertNull($form[2]->getViewData()); + $this->assertNull($form[3]->getViewData()); + $this->assertNull($form[4]->getViewData()); + } + + public function testSubmitSingleExpandedObjectChoicesBc() { $form = $this->factory->create('choice', null, array( 'multiple' => false, @@ -750,13 +1024,7 @@ public function testSubmitMultipleExpanded() $form->submit(array('a', 'c')); $this->assertSame(array('a', 'c'), $form->getData()); - $this->assertSame(array( - 0 => true, - 1 => false, - 2 => true, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertSame(array('a', 'c'), $form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -849,6 +1117,22 @@ public function testSubmitMultipleExpandedEmpty() $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitMultipleExpandedEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => true, + 'choices' => array(), + )); + + $form->submit(array()); + + $this->assertSame(array(), $form->getData()); + } + public function testSubmitMultipleExpandedWithEmptyChild() { $form = $this->factory->create('choice', null, array( @@ -873,6 +1157,32 @@ public function testSubmitMultipleExpandedWithEmptyChild() } public function testSubmitMultipleExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => true, + 'choices' => $this->objectChoices, + 'choices_as_values' => true, + 'choice_label' => 'name', + 'choice_value' => 'id', + )); + + $form->submit(array('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]->getViewData()); + $this->assertSame('2', $form[1]->getViewData()); + $this->assertNull($form[2]->getViewData()); + $this->assertNull($form[3]->getViewData()); + $this->assertNull($form[4]->getViewData()); + } + + public function testSubmitMultipleExpandedObjectChoicesBc() { $form = $this->factory->create('choice', null, array( 'multiple' => true, @@ -1134,10 +1444,10 @@ public function testPassChoicesToView() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('a', 'a', 'A'), - new ChoiceView('b', 'b', 'B'), - new ChoiceView('c', 'c', 'C'), - new ChoiceView('d', 'd', 'D'), + new ChoiceView('A', 'a', 'a'), + new ChoiceView('B', 'b', 'b'), + new ChoiceView('C', 'c', 'c'), + new ChoiceView('D', 'd', 'd'), ), $view->vars['choices']); } @@ -1151,12 +1461,12 @@ public function testPassPreferredChoicesToView() $view = $form->createView(); $this->assertEquals(array( - 0 => new ChoiceView('a', 'a', 'A'), - 2 => new ChoiceView('c', 'c', 'C'), + 0 => new ChoiceView('A', 'a', 'a'), + 2 => new ChoiceView('C', 'c', 'c'), ), $view->vars['choices']); $this->assertEquals(array( - 1 => new ChoiceView('b', 'b', 'B'), - 3 => new ChoiceView('d', 'd', 'D'), + 1 => new ChoiceView('B', 'b', 'b'), + 3 => new ChoiceView('D', 'd', 'd'), ), $view->vars['preferred_choices']); } @@ -1169,21 +1479,21 @@ public function testPassHierarchicalChoicesToView() $view = $form->createView(); $this->assertEquals(array( - 'Symfony' => array( - 0 => new ChoiceView('a', 'a', 'Bernhard'), - 2 => new ChoiceView('c', 'c', 'Kris'), - ), - 'Doctrine' => array( - 4 => new ChoiceView('e', 'e', 'Roman'), - ), + 'Symfony' => new ChoiceGroupView('Symfony', array( + 0 => new ChoiceView('Bernhard', 'a', 'a'), + 2 => new ChoiceView('Kris', 'c', 'c'), + )), + 'Doctrine' => new ChoiceGroupView('Doctrine', array( + 4 => new ChoiceView('Roman', 'e', 'e'), + )), ), $view->vars['choices']); $this->assertEquals(array( - 'Symfony' => array( - 1 => new ChoiceView('b', 'b', 'Fabien'), - ), - 'Doctrine' => array( - 3 => new ChoiceView('d', 'd', 'Jon'), - ), + 'Symfony' => new ChoiceGroupView('Symfony', array( + 1 => new ChoiceView('Fabien', 'b', 'b'), + )), + 'Doctrine' => new ChoiceGroupView('Doctrine', array( + 3 => new ChoiceView('Jon', 'd', 'd'), + )), ), $view->vars['preferred_choices']); } @@ -1194,15 +1504,18 @@ public function testPassChoiceDataToView() $obj3 = (object) array('value' => 'c', 'label' => 'C'); $obj4 = (object) array('value' => 'd', 'label' => 'D'); $form = $this->factory->create('choice', null, array( - 'choice_list' => new ObjectChoiceList(array($obj1, $obj2, $obj3, $obj4), 'label', array(), null, 'value'), + 'choices' => array($obj1, $obj2, $obj3, $obj4), + 'choices_as_values' => true, + 'choice_label' => 'label', + 'choice_value' => 'value', )); $view = $form->createView(); $this->assertEquals(array( - new ChoiceView($obj1, 'a', 'A'), - new ChoiceView($obj2, 'b', 'B'), - new ChoiceView($obj3, 'c', 'C'), - new ChoiceView($obj4, 'd', 'D'), + new ChoiceView('A', 'a', $obj1), + new ChoiceView('B', 'b', $obj2), + new ChoiceView('C', 'c', $obj3), + new ChoiceView('D', 'd', $obj4), ), $view->vars['choices']); } @@ -1226,47 +1539,6 @@ public function testInitializeWithEmptyChoices() )); } - // https://github.com/symfony/symfony/issues/10409 - public function testReuseNonUtf8ChoiceLists() - { - $form1 = $this->factory->createNamed('name', 'choice', null, array( - 'choices' => array( - 'meter' => 'm', - 'millimeter' => 'mm', - 'micrometer' => chr(181).'meter', - ), - )); - - $form2 = $this->factory->createNamed('name', 'choice', null, array( - 'choices' => array( - 'meter' => 'm', - 'millimeter' => 'mm', - 'micrometer' => chr(181).'meter', - ), - )); - - $form3 = $this->factory->createNamed('name', 'choice', null, array( - 'choices' => array( - 'meter' => 'm', - 'millimeter' => 'mm', - 'micrometer' => null, - ), - )); - - // $form1 and $form2 use the same ChoiceList - $this->assertSame( - $form1->getConfig()->getOption('choice_list'), - $form2->getConfig()->getOption('choice_list') - ); - - // $form3 doesn't, but used to use the same when using json_encode() - // instead of serialize for the hashing algorithm - $this->assertNotSame( - $form1->getConfig()->getOption('choice_list'), - $form3->getConfig()->getOption('choice_list') - ); - } - public function testInitializeWithDefaultObjectChoice() { $obj1 = (object) array('value' => 'a', 'label' => 'A'); @@ -1275,7 +1547,10 @@ public function testInitializeWithDefaultObjectChoice() $obj4 = (object) array('value' => 'd', 'label' => 'D'); $form = $this->factory->create('choice', null, array( - 'choice_list' => new ObjectChoiceList(array($obj1, $obj2, $obj3, $obj4), 'label', array(), null, 'value'), + 'choices' => array($obj1, $obj2, $obj3, $obj4), + 'choices_as_values' => true, + 'choice_label' => 'label', + 'choice_value' => 'value', // Used to break because "data_class" was inferred, which needs to // remain null in every case (because it refers to the view format) 'data' => $obj3, diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/CountryTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/CountryTypeTest.php index 7c2cebb54206..3b684f133edd 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/CountryTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/CountryTypeTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Test\TypeTestCase as TestCase; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Intl\Util\IntlTestHelper; class CountryTypeTest extends TestCase @@ -31,11 +31,11 @@ public function testCountriesAreSelectable() $choices = $view->vars['choices']; // Don't check objects for identity - $this->assertContains(new ChoiceView('DE', 'DE', 'Germany'), $choices, '', false, false); - $this->assertContains(new ChoiceView('GB', 'GB', 'United Kingdom'), $choices, '', false, false); - $this->assertContains(new ChoiceView('US', 'US', 'United States'), $choices, '', false, false); - $this->assertContains(new ChoiceView('FR', 'FR', 'France'), $choices, '', false, false); - $this->assertContains(new ChoiceView('MY', 'MY', 'Malaysia'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Germany', 'DE', 'DE'), $choices, '', false, false); + $this->assertContains(new ChoiceView('United Kingdom', 'GB', 'GB'), $choices, '', false, false); + $this->assertContains(new ChoiceView('United States', 'US', 'US'), $choices, '', false, false); + $this->assertContains(new ChoiceView('France', 'FR', 'FR'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Malaysia', 'MY', 'MY'), $choices, '', false, false); } public function testUnknownCountryIsNotIncluded() diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/CurrencyTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/CurrencyTypeTest.php index 702262f58038..802c715b0c4a 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/CurrencyTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/CurrencyTypeTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Test\TypeTestCase as TestCase; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Intl\Util\IntlTestHelper; class CurrencyTypeTest extends TestCase @@ -30,8 +30,8 @@ public function testCurrenciesAreSelectable() $view = $form->createView(); $choices = $view->vars['choices']; - $this->assertContains(new ChoiceView('EUR', 'EUR', 'Euro'), $choices, '', false, false); - $this->assertContains(new ChoiceView('USD', 'USD', 'US Dollar'), $choices, '', false, false); - $this->assertContains(new ChoiceView('SIT', 'SIT', 'Slovenian Tolar'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Euro', 'EUR', 'EUR'), $choices, '', false, false); + $this->assertContains(new ChoiceView('US Dollar', 'USD', 'USD'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Slovenian Tolar', 'SIT', 'SIT'), $choices, '', false, false); } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php index d8b3312b1f6b..a658b90465f1 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\FormError; use Symfony\Component\Form\Test\TypeTestCase as TestCase; use Symfony\Component\Intl\Util\IntlTestHelper; @@ -490,8 +490,8 @@ public function testMonthsOption() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['month']->vars['choices']); } @@ -505,8 +505,8 @@ public function testMonthsOptionShortFormat() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('1', '1', 'Jän'), - new ChoiceView('4', '4', 'Apr.'), + new ChoiceView('Jän', '1', '1'), + new ChoiceView('Apr.', '4', '4'), ), $view['month']->vars['choices']); } @@ -520,8 +520,8 @@ public function testMonthsOptionLongFormat() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('1', '1', 'Jänner'), - new ChoiceView('4', '4', 'April'), + new ChoiceView('Jänner', '1', '1'), + new ChoiceView('April', '4', '4'), ), $view['month']->vars['choices']); } @@ -535,8 +535,8 @@ public function testMonthsOptionLongFormatWithDifferentTimezone() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('1', '1', 'Jänner'), - new ChoiceView('4', '4', 'April'), + new ChoiceView('Jänner', '1', '1'), + new ChoiceView('April', '4', '4'), ), $view['month']->vars['choices']); } @@ -549,8 +549,8 @@ public function testIsDayWithinRangeReturnsTrueIfWithin() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['day']->vars['choices']); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/LanguageTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/LanguageTypeTest.php index e23481188729..9445c74fd6f3 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/LanguageTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/LanguageTypeTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Test\TypeTestCase as TestCase; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Intl\Util\IntlTestHelper; class LanguageTypeTest extends TestCase @@ -30,11 +30,11 @@ public function testCountriesAreSelectable() $view = $form->createView(); $choices = $view->vars['choices']; - $this->assertContains(new ChoiceView('en', 'en', 'English'), $choices, '', false, false); - $this->assertContains(new ChoiceView('en_GB', 'en_GB', 'British English'), $choices, '', false, false); - $this->assertContains(new ChoiceView('en_US', 'en_US', 'American English'), $choices, '', false, false); - $this->assertContains(new ChoiceView('fr', 'fr', 'French'), $choices, '', false, false); - $this->assertContains(new ChoiceView('my', 'my', 'Burmese'), $choices, '', false, false); + $this->assertContains(new ChoiceView('English', 'en', 'en'), $choices, '', false, false); + $this->assertContains(new ChoiceView('British English', 'en_GB', 'en_GB'), $choices, '', false, false); + $this->assertContains(new ChoiceView('American English', 'en_US', 'en_US'), $choices, '', false, false); + $this->assertContains(new ChoiceView('French', 'fr', 'fr'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Burmese', 'my', 'my'), $choices, '', false, false); } public function testMultipleLanguagesIsNotIncluded() @@ -43,6 +43,6 @@ public function testMultipleLanguagesIsNotIncluded() $view = $form->createView(); $choices = $view->vars['choices']; - $this->assertNotContains(new ChoiceView('mul', 'mul', 'Mehrsprachig'), $choices, '', false, false); + $this->assertNotContains(new ChoiceView('Mehrsprachig', 'mul', 'mul'), $choices, '', false, false); } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/LocaleTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/LocaleTypeTest.php index 6c1951a4e91d..0b729a3b3150 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/LocaleTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/LocaleTypeTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Test\TypeTestCase as TestCase; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Intl\Util\IntlTestHelper; class LocaleTypeTest extends TestCase @@ -30,8 +30,8 @@ public function testLocalesAreSelectable() $view = $form->createView(); $choices = $view->vars['choices']; - $this->assertContains(new ChoiceView('en', 'en', 'English'), $choices, '', false, false); - $this->assertContains(new ChoiceView('en_GB', 'en_GB', 'English (United Kingdom)'), $choices, '', false, false); - $this->assertContains(new ChoiceView('zh_Hant_MO', 'zh_Hant_MO', 'Chinese (Traditional, Macau SAR China)'), $choices, '', false, false); + $this->assertContains(new ChoiceView('English', 'en', 'en'), $choices, '', false, false); + $this->assertContains(new ChoiceView('English (United Kingdom)', 'en_GB', 'en_GB'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Chinese (Traditional, Macau SAR China)', 'zh_Hant_MO', 'zh_Hant_MO'), $choices, '', false, false); } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php index dfa8fbc5a1a5..c3754695b16b 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\FormError; use Symfony\Component\Form\Test\TypeTestCase as TestCase; use Symfony\Component\Intl\Util\IntlTestHelper; @@ -319,8 +319,8 @@ public function testHoursOption() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['hour']->vars['choices']); } @@ -333,8 +333,8 @@ public function testIsMinuteWithinRangeReturnsTrueIfWithin() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['minute']->vars['choices']); } @@ -348,8 +348,8 @@ public function testIsSecondWithinRangeReturnsTrueIfWithin() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['second']->vars['choices']); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php index 81df20cbb902..78399547400f 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; class TimezoneTypeTest extends \Symfony\Component\Form\Test\TypeTestCase { @@ -22,9 +22,9 @@ public function testTimezonesAreSelectable() $choices = $view->vars['choices']; $this->assertArrayHasKey('Africa', $choices); - $this->assertContains(new ChoiceView('Africa/Kinshasa', 'Africa/Kinshasa', 'Kinshasa'), $choices['Africa'], '', false, false); + $this->assertContains(new ChoiceView('Kinshasa', 'Africa/Kinshasa', 'Africa/Kinshasa'), $choices['Africa'], '', false, false); $this->assertArrayHasKey('America', $choices); - $this->assertContains(new ChoiceView('America/New_York', 'America/New_York', 'New York'), $choices['America'], '', false, false); + $this->assertContains(new ChoiceView('New York', 'America/New_York', 'America/New_York'), $choices['America'], '', false, false); } }