From 03efce1b568379eac21d880e427090e43035f505 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 26 Sep 2014 23:28:34 +0200 Subject: [PATCH 1/9] [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 | 201 ++-- .../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 | 8 +- .../EventListener/FixRadioInputListener.php | 6 +- .../Form/Extension/Core/Type/ChoiceType.php | 286 ++++-- .../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, 5716 insertions(+), 450 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 57e57e6e25..3566a33d7d 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 0000000000..d1f7971e63 --- /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 872e77affe..9cfdd1fe48 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\DBAL\Connection; 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 570cc8f189..ed8e0a7934 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php @@ -14,21 +14,38 @@ namespace Symfony\Bridge\Doctrine\Form; 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 ccc9bfc485..6c90a2eeb0 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 @@ abstract class DoctrineType extends AbstractType 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']); - }; - - $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); - } - - $choiceHashes = $hashes; - } - - $preferredChoiceHashes = $options['preferred_choices']; - - if (is_array($preferredChoiceHashes)) { - array_walk_recursive($preferredChoiceHashes, function (&$value) { - $value = spl_object_hash($value); - }); - } - - // 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( + $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['property'], + $options['query_builder'], $options['loader'], - $options['choices'], - $options['preferred_choices'], - $options['group_by'], - $propertyAccessor - ); + )); + + 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 + ); + } + + return $choiceLoaders[$hash]; + } + }; + + $choiceLabel = function (Options $options) { + // BC with the "property" option + if ($options['property']) { + return $options['property']; } - return $choiceListCache[$hash]; + // 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; + }; + } + + // 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 @@ abstract class DoctrineType extends AbstractType 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 25afbed492..9f1591f308 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 @@ class EntityTypeTest extends TypeTestCase '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 @@ class EntityTypeTest extends TypeTestCase '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 @@ class EntityTypeTest extends TypeTestCase '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 @@ class EntityTypeTest extends TypeTestCase $field->submit(null); $this->assertNull($field->getData()); - $this->assertSame(array(), $field->getViewData()); + $this->assertNull($field->getViewData()); } public function testSubmitSingleNonExpandedNull() @@ -510,7 +512,7 @@ class EntityTypeTest extends TypeTestCase $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 @@ class EntityTypeTest extends TypeTestCase $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 @@ class EntityTypeTest extends TypeTestCase '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 @@ class EntityTypeTest extends TypeTestCase '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 @@ class EntityTypeTest extends TypeTestCase $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 9c7339f702..38270f9f5b 100644 --- a/src/Symfony/Bridge/Twig/Extension/FormExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/FormExtension.php @@ -13,7 +13,7 @@ namespace Symfony\Bridge\Twig\Extension; 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 b39dbf1c80..df0e571602 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 a7a9311d51..81402efffb 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 0000000000..0dfc0f9945 --- /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 0000000000..d79747e048 --- /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 0000000000..62f3158646 --- /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 0000000000..fb43ac8759 --- /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 0000000000..60239423f3 --- /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 0000000000..dd191eea39 --- /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 0000000000..bf91d85eea --- /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 0000000000..91e6bfe408 --- /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 0000000000..9171fe3f16 --- /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 0000000000..8e59620369 --- /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 0000000000..9641f4b1d9 --- /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 0000000000..ded2a55b30 --- /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 9d2a1c42a4..2f7b287b63 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 @@ use Symfony\Component\Form\Extension\Core\View\ChoiceView; * * $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 8f09179a2a..22354e09d8 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 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; * 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 @@ interface ChoiceListInterface */ 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 ee136f7978..24232bc1d6 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 @@ use Symfony\Component\Form\Exception\InvalidArgumentException; * 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 a20d194555..606de43af3 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 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; * * * @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 8d4ddd1242..50a3eb5f4a 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 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; * * * @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 a0153a57eb..231994258e 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 @@ use Symfony\Component\PropertyAccess\PropertyAccess; */ 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 0000000000..d87196475f --- /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 2208f26d1e..736752a41e 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\PropertyAccess; 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 0000000000..aecdb2fad0 --- /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 a91ed55c31..a0b5039317 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 087faf4d3b..1c83782621 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 @@ namespace Symfony\Component\Form\Extension\Core\DataTransformer; 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 @@ class ChoiceToValueTransformer implements DataTransformerInterface 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 f1f13fda28..c38c363329 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 0ee0b0fefd..0a1f2f0288 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 @@ namespace Symfony\Component\Form\Extension\Core\DataTransformer; 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 b201802fbc..297987f799 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\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\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\FormEvent; +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 c5f871756b..d5067b6e33 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 5b52b4ad96..7e80a00bde 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 @@ class ChoiceType extends AbstractType */ 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 @@ class ChoiceType extends AbstractType */ 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']))); - - if (!isset($choiceListCache[$hash])) { - $choiceListCache[$hash] = new SimpleChoiceList($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; } - return $choiceListCache[$hash]; + // BC when choices are in the keys, not in the values + if (!$options['choices_as_values']) { + return $choiceListFactory->createListFromFlippedChoices($choices, $options['choice_value']); + } + + return $choiceListFactory->createListFromChoices($choices, $options['choice_value']); }; $emptyData = function (Options $options) { @@ -219,9 +306,16 @@ class ChoiceType extends AbstractType $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 @@ class ChoiceType extends AbstractType $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 @@ class ChoiceType extends AbstractType 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 @@ class ChoiceType extends AbstractType */ 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'; - } - - $builder->add($i, $choiceType, $choiceOpts); + continue; } + + 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 97cdd214c2..65d7af2464 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 @@ namespace Symfony\Component\Form\Extension\Core\View; * * @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 6375542b28..3bf84d71c8 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -516,6 +516,28 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg ); } + 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 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg ); } + 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 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg ); } + 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 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg ); } + 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 0000000000..0805238f7f --- /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 0000000000..34b22fe041 --- /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 0000000000..74cf2afb4a --- /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 0000000000..031cced280 --- /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 0000000000..42f745e29b --- /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 0000000000..8697a9cac5 --- /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 0000000000..2993721c82 --- /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 17972cbc0a..6a0b6db2ec 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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase )); } + /** + * @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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase } 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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase } 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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase } 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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase )); } - // 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 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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 7c2cebb542..3b684f133e 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 @@ class CountryTypeTest extends TestCase $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 702262f580..802c715b0c 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 @@ class CurrencyTypeTest extends TestCase $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 d8b3312b1f..a658b90465 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 @@ class DateTypeTest extends TestCase $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 @@ class DateTypeTest extends TestCase $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 @@ class DateTypeTest extends TestCase $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 @@ class DateTypeTest extends TestCase $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 @@ class DateTypeTest extends TestCase $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 e234811887..9445c74fd6 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 @@ class LanguageTypeTest extends TestCase $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 @@ class LanguageTypeTest extends TestCase $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 6c1951a4e9..0b729a3b31 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 @@ class LocaleTypeTest extends TestCase $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 dfa8fbc5a1..c3754695b1 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 @@ class TimeTypeTest extends TestCase $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 @@ class TimeTypeTest extends TestCase $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 @@ class TimeTypeTest extends TestCase $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 81df20cbb9..7839954740 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 @@ class TimezoneTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $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); } } From 3846b3750ad60e02e55f4dc7b91d06366a1695f8 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 25 Mar 2015 11:41:36 +0100 Subject: [PATCH 2/9] [DoctrineBridge] Fixed: don't cache choice lists if query builders are constructed dynamically --- ...iceLoader.php => DoctrineChoiceLoader.php} | 101 +++++++++--------- .../Form/ChoiceList/EntityChoiceList.php | 2 +- .../Doctrine/Form/Type/DoctrineType.php | 63 ++++++----- .../Bridge/Doctrine/Form/Type/EntityType.php | 65 ++--------- 4 files changed, 97 insertions(+), 134 deletions(-) rename src/Symfony/Bridge/Doctrine/Form/ChoiceList/{EntityChoiceLoader.php => DoctrineChoiceLoader.php} (71%) diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php similarity index 71% rename from src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php rename to src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php index d1f7971e63..c00c258ca5 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -23,7 +23,7 @@ use Symfony\Component\Form\Exception\RuntimeException; * * @author Bernhard Schussek */ -class EntityChoiceLoader implements ChoiceLoaderInterface +class DoctrineChoiceLoader implements ChoiceLoaderInterface { /** * @var ChoiceListFactoryInterface @@ -48,7 +48,7 @@ class EntityChoiceLoader implements ChoiceLoaderInterface /** * @var null|EntityLoaderInterface */ - private $entityLoader; + private $objectLoader; /** * The identifier field, unless the identifier is composite @@ -70,20 +70,20 @@ class EntityChoiceLoader implements ChoiceLoaderInterface private $choiceList; /** - * Returns the value of the identifier field of an entity. + * Returns the value of the identifier field of an object. * - * Doctrine must know about this entity, that is, the entity must already + * Doctrine must know about this object, that is, the object 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 + * This method assumes that the object 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 + * @param object $object The object 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 + * @throws RuntimeException If the object 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. @@ -106,22 +106,23 @@ class EntityChoiceLoader implements ChoiceLoaderInterface * Creates a new choice loader. * * Optionally, an implementation of {@link EntityLoaderInterface} can be - * passed which optimizes the entity loading for one of the Doctrine + * passed which optimizes the object 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 + * @param string $class The class name of the + * loaded objects + * @param null|EntityLoaderInterface $objectLoader The objects loader */ - public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, EntityLoaderInterface $entityLoader = null) + public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, EntityLoaderInterface $objectLoader = null) { $this->factory = $factory; $this->manager = $manager; $this->classMetadata = $manager->getClassMetadata($class); $this->class = $this->classMetadata->getName(); - $this->entityLoader = $entityLoader; + $this->objectLoader = $objectLoader; $identifier = $this->classMetadata->getIdentifierFieldNames(); @@ -140,51 +141,51 @@ class EntityChoiceLoader implements ChoiceLoaderInterface return $this->choiceList; } - $entities = $this->entityLoader - ? $this->entityLoader->getEntities() + $objects = $this->objectLoader + ? $this->objectLoader->getEntities() : $this->manager->getRepository($this->class)->findAll(); // If the class has a multi-column identifier, we cannot index the - // entities by their IDs + // objects by their IDs if ($this->compositeId) { - $this->choiceList = $this->factory->createListFromChoices($entities, $value); + $this->choiceList = $this->factory->createListFromChoices($objects, $value); return $this->choiceList; } - // Index the entities by ID - $entitiesById = array(); + // Index the objects by ID + $objectsById = array(); - foreach ($entities as $entity) { - $id = self::getIdValue($this->manager, $this->classMetadata, $entity); - $entitiesById[$id] = $entity; + foreach ($objects as $object) { + $id = self::getIdValue($this->manager, $this->classMetadata, $object); + $objectsById[$id] = $object; } - $this->choiceList = $this->factory->createListFromChoices($entitiesById, $value); + $this->choiceList = $this->factory->createListFromChoices($objectsById, $value); return $this->choiceList; } /** - * Loads the values corresponding to the given entities. + * Loads the values corresponding to the given objects. * * The values are returned with the same keys and in the same order as the - * corresponding entities in the given array. + * corresponding objects 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 + * The callable receives the object 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 array $objects An array of objects. Non-existing objects 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) + public function loadValuesForChoices(array $objects, $value = null) { // Performance optimization - if (empty($entities)) { + if (empty($objects)) { return array(); } @@ -195,41 +196,41 @@ class EntityChoiceLoader implements ChoiceLoaderInterface 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) { + // Maintain order and indices of the given objects + foreach ($objects as $i => $object) { + if ($object instanceof $this->class) { // Make sure to convert to the right format - $values[$i] = (string) self::getIdValue($this->manager, $this->classMetadata, $entity); + $values[$i] = (string) self::getIdValue($this->manager, $this->classMetadata, $object); } } return $values; } - return $this->loadChoiceList($value)->getValuesForChoices($entities); + return $this->loadChoiceList($value)->getValuesForChoices($objects); } /** - * Loads the entities corresponding to the given values. + * Loads the objects corresponding to the given values. * - * The entities are returned with the same keys and in the same order as the + * The objects 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 + * The callable receives the object 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 + * @return array An array of objects */ 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 + // object 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 @@ -237,29 +238,29 @@ class EntityChoiceLoader implements ChoiceLoaderInterface return array(); } - // Optimize performance in case we have an entity loader and + // Optimize performance in case we have an object loader and // a single-field identifier - if (!$this->choiceList && !$this->compositeId && $this->entityLoader) { - $unorderedEntities = $this->entityLoader->getEntitiesByIds($this->idField, $values); - $entitiesById = array(); - $entities = array(); + if (!$this->choiceList && !$this->compositeId && $this->objectLoader) { + $unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idField, $values); + $objectsById = array(); + $objects = 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 ($unorderedObjects as $object) { + $id = self::getIdValue($this->manager, $this->classMetadata, $object); + $objectsById[$id] = $object; } foreach ($values as $i => $id) { - if (isset($entitiesById[$id])) { - $entities[$i] = $entitiesById[$id]; + if (isset($objectsById[$id])) { + $objects[$i] = $objectsById[$id]; } } - return $entities; + return $objects; } return $this->loadChoiceList($value)->getChoicesForValues($values); diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index 3566a33d7d..f3d4ff48f6 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -24,7 +24,7 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; * @author Bernhard Schussek * * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. - * Use {@link EntityChoiceLoader} instead. + * Use {@link DoctrineChoiceLoader} instead. */ class EntityChoiceList extends ObjectChoiceList { diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 6c90a2eeb0..76e78cb1f0 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -13,7 +13,7 @@ namespace Symfony\Bridge\Doctrine\Form\Type; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\Common\Persistence\ObjectManager; -use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceLoader; +use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; @@ -41,7 +41,7 @@ abstract class DoctrineType extends AbstractType private $choiceListFactory; /** - * @var EntityChoiceLoader[] + * @var DoctrineChoiceLoader[] */ private $choiceLoaders = array(); @@ -71,32 +71,43 @@ abstract class DoctrineType extends AbstractType $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'], - )); - - 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, + // Don't cache if the query builder is constructed dynamically + if ($options['query_builder'] instanceof \Closure) { + $hash = null; + } else { + $hash = CachingFactoryDecorator::generateHash(array( $options['em'], $options['class'], - $loader - ); + $options['query_builder'], + $options['loader'], + )); + + if (isset($choiceLoaders[$hash])) { + return $choiceLoaders[$hash]; + } } - return $choiceLoaders[$hash]; + if ($options['loader']) { + $entityLoader = $options['loader']; + } elseif (null !== $options['query_builder']) { + $entityLoader = $type->getLoader($options['em'], $options['query_builder'], $options['class']); + } else { + $queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e'); + $entityLoader = $type->getLoader($options['em'], $queryBuilder, $options['class']); + } + + $choiceLoader = new DoctrineChoiceLoader( + $choiceListFactory, + $options['em'], + $options['class'], + $entityLoader + ); + + if (null !== $hash) { + $choiceLoaders[$hash] = $choiceLoader; + } + + return $choiceLoader; } }; @@ -131,7 +142,7 @@ abstract class DoctrineType extends AbstractType }; // The choices are always indexed by ID (see "choices" normalizer - // and EntityChoiceLoader), unless the ID is composite. Then they + // and DoctrineChoiceLoader), 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) { @@ -181,7 +192,7 @@ abstract class DoctrineType extends AbstractType $entitiesById = array(); foreach ($entities as $entity) { - $id = EntityChoiceLoader::getIdValue($om, $classMetadata, $entity); + $id = DoctrineChoiceLoader::getIdValue($om, $classMetadata, $entity); $entitiesById[$id] = $entity; } diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php index e9b89302db..675c289c76 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -17,71 +17,22 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; class EntityType extends DoctrineType { - /** - * @var ORMQueryBuilderLoader[] - */ - private $loaderCache = array(); - /** * Return the default loader object. * - * @param ObjectManager $manager - * @param mixed $queryBuilder - * @param string $class + * @param ObjectManager $manager + * @param QueryBuilder|\Closure $queryBuilder + * @param string $class * * @return ORMQueryBuilderLoader */ public function getLoader(ObjectManager $manager, $queryBuilder, $class) { - if (!$queryBuilder instanceof QueryBuilder) { - return new ORMQueryBuilderLoader( - $queryBuilder, - $manager, - $class - ); - } - - $queryBuilderHash = $this->getQueryBuilderHash($queryBuilder); - $loaderHash = $this->getLoaderHash($manager, $queryBuilderHash, $class); - - if (!isset($this->loaderCache[$loaderHash])) { - $this->loaderCache[$loaderHash] = new ORMQueryBuilderLoader( - $queryBuilder, - $manager, - $class - ); - } - - return $this->loaderCache[$loaderHash]; - } - - /** - * @param QueryBuilder $queryBuilder - * - * @return string - */ - private function getQueryBuilderHash(QueryBuilder $queryBuilder) - { - return hash('sha256', json_encode(array( - 'sql' => $queryBuilder->getQuery()->getSQL(), - 'parameters' => $queryBuilder->getParameters(), - ))); - } - - /** - * @param ObjectManager $manager - * @param string $queryBuilderHash - * @param string $class - * - * @return string - */ - private function getLoaderHash(ObjectManager $manager, $queryBuilderHash, $class) - { - return hash('sha256', json_encode(array( - 'manager' => spl_object_hash($manager), - 'queryBuilder' => $queryBuilderHash, - 'class' => $class, - ))); + return new ORMQueryBuilderLoader( + $queryBuilder, + $manager, + $class + ); } public function getName() From e6739bf05e07ebd5d0ba22f76bb7247af69d62de Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 25 Mar 2015 14:31:10 +0100 Subject: [PATCH 3/9] [DoctrineBridge] DoctrineType now respects the "query_builder" option when caching the choice loader --- .../Form/ChoiceList/ORMQueryBuilderLoader.php | 2 + .../Doctrine/Form/Type/DoctrineType.php | 56 ++++++++++++------- .../Bridge/Doctrine/Form/Type/EntityType.php | 6 +- .../Tests/Form/Type/EntityTypeTest.php | 31 +++++----- 4 files changed, 57 insertions(+), 38 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index 9cfdd1fe48..9d34601c9f 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -51,6 +51,8 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure'); } + // This block is not executed anymore since Symfony 2.7. The query + // builder closure is already invoked in DoctrineType if ($queryBuilder instanceof \Closure) { if (!$manager instanceof EntityManager) { throw new UnexpectedTypeException($manager, 'Doctrine\ORM\EntityManager'); diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 76e78cb1f0..ed8ded5bad 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Form\Type; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; @@ -23,6 +24,7 @@ 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\Exception\UnexpectedTypeException; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -71,20 +73,24 @@ abstract class DoctrineType extends AbstractType $choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) { // Unless the choices are given explicitly, load them on demand if (null === $options['choices']) { - // Don't cache if the query builder is constructed dynamically - if ($options['query_builder'] instanceof \Closure) { - $hash = null; - } else { - $hash = CachingFactoryDecorator::generateHash(array( - $options['em'], - $options['class'], - $options['query_builder'], - $options['loader'], - )); + // We consider two query builders with an equal SQL string and + // equal parameters to be equal + $qbParts = $options['query_builder'] + ? array( + $options['query_builder']->getQuery()->getSQL(), + $options['query_builder']->getParameters()->toArray(), + ) + : null; - if (isset($choiceLoaders[$hash])) { - return $choiceLoaders[$hash]; - } + $hash = CachingFactoryDecorator::generateHash(array( + $options['em'], + $options['class'], + $qbParts, + $options['loader'], + )); + + if (isset($choiceLoaders[$hash])) { + return $choiceLoaders[$hash]; } if ($options['loader']) { @@ -96,18 +102,14 @@ abstract class DoctrineType extends AbstractType $entityLoader = $type->getLoader($options['em'], $queryBuilder, $options['class']); } - $choiceLoader = new DoctrineChoiceLoader( + $choiceLoaders[$hash] = new DoctrineChoiceLoader( $choiceListFactory, $options['em'], $options['class'], $entityLoader ); - if (null !== $hash) { - $choiceLoaders[$hash] = $choiceLoader; - } - - return $choiceLoader; + return $choiceLoaders[$hash]; } }; @@ -199,6 +201,20 @@ abstract class DoctrineType extends AbstractType return $entitiesById; }; + // Invoke the query builder closure so that we can cache choice lists + // for equal query builders + $queryBuilderNormalizer = function (Options $options, $queryBuilder) { + if (is_callable($queryBuilder)) { + $queryBuilder = call_user_func($queryBuilder, $options['em']->getRepository($options['class'])); + + if (!$queryBuilder instanceof QueryBuilder) { + throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder'); + } + } + + return $queryBuilder; + }; + $resolver->setDefaults(array( 'em' => null, 'property' => null, // deprecated, use "choice_label" @@ -216,9 +232,11 @@ abstract class DoctrineType extends AbstractType $resolver->setNormalizer('em', $emNormalizer); $resolver->setNormalizer('choices', $choicesNormalizer); + $resolver->setNormalizer('query_builder', $queryBuilderNormalizer); $resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager')); $resolver->setAllowedTypes('loader', array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface')); + $resolver->setAllowedTypes('query_builder', array('null', 'callable', 'Doctrine\ORM\QueryBuilder')); } /** diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php index 675c289c76..236b9290c7 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -28,11 +28,7 @@ class EntityType extends DoctrineType */ public function getLoader(ObjectManager $manager, $queryBuilder, $class) { - return new ORMQueryBuilderLoader( - $queryBuilder, - $manager, - $class - ); + return new ORMQueryBuilderLoader($queryBuilder, $manager, $class); } public function getName() diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index 9f1591f308..7d80819f6b 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -14,6 +14,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\Type; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Tools\SchemaTool; use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser; @@ -28,6 +29,7 @@ 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\OptionsResolver\Options; use Symfony\Component\PropertyAccess\PropertyAccess; class EntityTypeTest extends TypeTestCase @@ -172,7 +174,7 @@ class EntityTypeTest extends TypeTestCase } /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */ public function testConfigureQueryBuilderWithNonQueryBuilderAndNonClosure() { @@ -786,8 +788,7 @@ class EntityTypeTest extends TypeTestCase $this->persist(array($entity1, $entity2, $entity3)); - $repository = $this->em->getRepository(self::SINGLE_IDENT_CLASS); - $qb = $repository->createQueryBuilder('e')->where('e.id IN (1, 2)'); + $repo = $this->em->getRepository(self::SINGLE_IDENT_CLASS); $entityType = new EntityType( $this->emRegistry, @@ -806,19 +807,23 @@ class EntityTypeTest extends TypeTestCase $formBuilder->add('property1', 'entity', array( 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'query_builder' => $qb, + 'query_builder' => $repo->createQueryBuilder('e')->where('e.id IN (1, 2)'), )); $formBuilder->add('property2', 'entity', array( 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'query_builder' => $qb, + 'query_builder' => function (EntityRepository $repo) { + return $repo->createQueryBuilder('e')->where('e.id IN (1, 2)'); + }, )); $formBuilder->add('property3', 'entity', array( 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'query_builder' => $qb, + 'query_builder' => function (EntityRepository $repo) { + return $repo->createQueryBuilder('e')->where('e.id IN (1, 2)'); + }, )); $form = $formBuilder->getForm(); @@ -829,15 +834,13 @@ class EntityTypeTest extends TypeTestCase 'property3' => 2, )); - $reflectionClass = new \ReflectionObject($entityType); - $reflectionProperty = $reflectionClass->getProperty('loaderCache'); - $reflectionProperty->setAccessible(true); + $choiceList1 = $form->get('property1')->getConfig()->getOption('choice_list'); + $choiceList2 = $form->get('property2')->getConfig()->getOption('choice_list'); + $choiceList3 = $form->get('property3')->getConfig()->getOption('choice_list'); - $loaders = $reflectionProperty->getValue($entityType); - - $reflectionProperty->setAccessible(false); - - $this->assertCount(1, $loaders); + $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\ChoiceListInterface', $choiceList1); + $this->assertSame($choiceList1, $choiceList2); + $this->assertSame($choiceList1, $choiceList3); } public function testCacheChoiceLists() From a289deb97358cf7295bbd1410d954f2d66c5346e Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 25 Mar 2015 15:45:45 +0100 Subject: [PATCH 4/9] [Form] Fixed new ArrayChoiceList to compare choices by their values, if enabled --- .../Form/ChoiceList/ArrayChoiceList.php | 60 +++++++++---- .../Form/ChoiceList/ArrayKeyChoiceList.php | 84 +++++++------------ .../Factory/DefaultChoiceListFactory.php | 48 ++--------- .../Tests/ChoiceList/ArrayChoiceListTest.php | 45 +++++++++- .../ChoiceList/ArrayKeyChoiceListTest.php | 57 +++++++------ .../Factory/DefaultChoiceListFactoryTest.php | 3 +- 6 files changed, 155 insertions(+), 142 deletions(-) diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php index 0dfc0f9945..a3987cc02c 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\ChoiceList; -use Symfony\Component\Form\Exception\InvalidArgumentException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; /** * A list of choices with arbitrary data types. @@ -39,33 +39,46 @@ class ArrayChoiceList implements ChoiceListInterface */ protected $values = array(); + /** + * The callback for creating the value for a choice. + * + * @var callable + */ + protected $valueCallback; + /** * 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 + * @param array $choices The selectable choices + * @param callable $value The callable for creating the value for a + * choice. If `null` is passed, incrementing + * integers are used as values + * @param bool $compareByValue Whether to use the value callback to + * compare choices. If `null`, choices are + * compared by identity */ - public function __construct(array $choices, array $values) + public function __construct(array $choices, $value = null, $compareByValue = false) { - $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) - )); + if (null !== $value && !is_callable($value)) { + throw new UnexpectedTypeException($value, 'null or callable'); } $this->choices = $choices; - $this->values = array_map('strval', $values); + $this->values = array(); + $this->valueCallback = $compareByValue ? $value : null; + + if (null === $value) { + $i = 0; + foreach ($this->choices as $key => $choice) { + $this->values[$key] = (string) $i++; + } + } else { + foreach ($choices as $key => $choice) { + $this->values[$key] = (string) call_user_func($value, $choice, $key); + } + } } /** @@ -116,6 +129,17 @@ class ArrayChoiceList implements ChoiceListInterface { $values = array(); + // Use the value callback to compare choices by their values, if present + if ($this->valueCallback) { + $givenValues = array(); + foreach ($choices as $key => $choice) { + $givenValues[$key] = (string) call_user_func($this->valueCallback, $choice, $key); + } + + return array_intersect($givenValues, $this->values); + } + + // Otherwise compare choices by identity foreach ($choices as $i => $givenChoice) { foreach ($this->choices as $j => $choice) { if ($choice !== $givenChoice) { diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php index d79747e048..918c278f06 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php @@ -43,21 +43,14 @@ use Symfony\Component\Form\Exception\InvalidArgumentException; * @deprecated Added for backwards compatibility in Symfony 2.7, to be removed * in Symfony 3.0. */ -class ArrayKeyChoiceList implements ChoiceListInterface +class ArrayKeyChoiceList extends ArrayChoiceList { /** - * The selectable choices. + * Whether the choices are used as values. * - * @var array + * @var bool */ - private $choices = array(); - - /** - * The values of the choices. - * - * @var string[] - */ - private $values = array(); + private $useChoicesAsValues = false; /** * Casts the given choice to an array key. @@ -100,51 +93,26 @@ class ArrayKeyChoiceList implements ChoiceListInterface * values. * * @param array $choices The selectable choices - * @param string[] $values Optional. The string values of the choices + * @param callable $value The callable for creating the value for a + * choice. If `null` is passed, the choices are + * cast to strings and used as values * * @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()) + public function __construct(array $choices, $value = null) { - if (empty($values)) { - // The cast to strings happens later - $values = $choices; - } else { - $choiceKeys = array_keys($choices); - $valueKeys = array_keys($values); + $choices = array_map(array(__CLASS__, 'toArrayKey'), $choices); - 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) - ) - ); - } + if (null === $value) { + $value = function ($choice) { + return (string) $choice; + }; + $this->useChoicesAsValues = true; } - $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; + parent::__construct($choices, $value); } /** @@ -152,11 +120,15 @@ class ArrayKeyChoiceList implements ChoiceListInterface */ public function getChoicesForValues(array $values) { - $values = array_map('strval', $values); + if ($this->useChoicesAsValues) { + $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)); + // If 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)); + } + + return parent::getChoicesForValues($values); } /** @@ -166,8 +138,12 @@ class ArrayKeyChoiceList implements ChoiceListInterface { $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)); + if ($this->useChoicesAsValues) { + // If the choices are identical to the values, we can just return + // them to improve performance a little bit + return array_map('strval', array_intersect($choices, $this->choices)); + } + + return parent::getValuesForChoices($choices); } } diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index dd191eea39..f0ea07cdf6 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -89,10 +89,6 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface 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); } @@ -102,29 +98,7 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface // 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); + return new ArrayChoiceList($flatChoices, $value); } /** @@ -139,10 +113,6 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface 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); } @@ -157,20 +127,12 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface // 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); + $value = function ($choice) { + return (string) $choice; + }; } - // 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); + return new ArrayKeyChoiceList($flatChoices, $value); } /** diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php index 34b22fe041..0dffd08374 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php @@ -29,7 +29,9 @@ class ArrayChoiceListTest extends AbstractChoiceListTest protected function createChoiceList() { - return new ArrayChoiceList($this->getChoices(), $this->getValues()); + $i = 0; + + return new ArrayChoiceList($this->getChoices()); } protected function getChoices() @@ -49,4 +51,45 @@ class ArrayChoiceListTest extends AbstractChoiceListTest { new ArrayChoiceList(array(0 => 'a', 1 => 'b'), array(1 => 'a', 2 => 'b')); } + + public function testCreateChoiceListWithValueCallback() + { + $callback = function ($choice, $key) { + return $key.':'.$choice; + }; + + $choiceList = new ArrayChoiceList(array(2 => 'foo', 7 => 'bar', 10 => 'baz'), $callback); + + $this->assertSame(array(2 => '2:foo', 7 => '7:bar', 10 => '10:baz'), $choiceList->getValues()); + $this->assertSame(array(1 => 'foo', 2 => 'baz'), $choiceList->getChoicesForValues(array(1 => '2:foo', 2 => '10:baz'))); + $this->assertSame(array(1 => '2:foo', 2 => '10:baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz'))); + } + + public function testCompareChoicesByIdentityByDefault() + { + $callback = function ($choice) { + return $choice->value; + }; + + $obj1 = (object) array('value' => 'value1'); + $obj2 = (object) array('value' => 'value2'); + + $choiceList = new ArrayChoiceList(array($obj1, $obj2), $callback); + $this->assertSame(array(2 => 'value2'), $choiceList->getValuesForChoices(array(2 => $obj2))); + $this->assertSame(array(), $choiceList->getValuesForChoices(array(2 => (object) array('value' => 'value2')))); + } + + public function testCompareChoicesWithValueCallbackIfCompareByValue() + { + $callback = function ($choice) { + return $choice->value; + }; + + $obj1 = (object) array('value' => 'value1'); + $obj2 = (object) array('value' => 'value2'); + + $choiceList = new ArrayChoiceList(array($obj1, $obj2), $callback, true); + $this->assertSame(array(2 => 'value2'), $choiceList->getValuesForChoices(array(2 => $obj2))); + $this->assertSame(array(2 => 'value2'), $choiceList->getValuesForChoices(array(2 => (object) array('value' => 'value2')))); + } } diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php index 74cf2afb4a..78263502d6 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php @@ -29,7 +29,7 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest protected function createChoiceList() { - return new ArrayKeyChoiceList($this->getChoices(), $this->getValues()); + return new ArrayKeyChoiceList($this->getChoices()); } protected function getChoices() @@ -42,14 +42,6 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest 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)); @@ -102,7 +94,7 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest */ public function testConvertChoicesIfNecessary(array $choices, array $converted) { - $list = new ArrayKeyChoiceList($choices, range(0, count($choices) - 1)); + $list = new ArrayKeyChoiceList($choices); $this->assertSame($converted, $list->getChoices()); } @@ -134,7 +126,7 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest */ public function testFailIfInvalidChoices(array $choices) { - new ArrayKeyChoiceList($choices, range(0, count($choices) - 1)); + new ArrayKeyChoiceList($choices); } /** @@ -157,31 +149,48 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest /** * @dataProvider provideConvertibleValues */ - public function testConvertValuesToStrings(array $values, array $converted) + public function testConvertValuesToStrings($value, $converted) { - $list = new ArrayKeyChoiceList(range(0, count($values) - 1), $values); + $callback = function () use ($value) { + return $value; + }; - $this->assertSame($converted, $list->getValues()); + $list = new ArrayKeyChoiceList(array('choice'), $callback); + + $this->assertSame(array($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')), + array(0, '0'), + array(1, '1'), + array('0', '0'), + array('1', '1'), + array('1.23', '1.23'), + array('foobar', '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')), + array(null, ''), + array(1.23, '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('')), + array(true, '1'), + array(false, ''), ); } + + public function testCreateChoiceListWithValueCallback() + { + $callback = function ($choice, $key) { + return $key.':'.$choice; + }; + + $choiceList = new ArrayKeyChoiceList(array(2 => 'foo', 7 => 'bar', 10 => 'baz'), $callback); + + $this->assertSame(array(2 => '2:foo', 7 => '7:bar', 10 => '10:baz'), $choiceList->getValues()); + $this->assertSame(array(1 => 'foo', 2 => 'baz'), $choiceList->getChoicesForValues(array(1 => '2:foo', 2 => '10:baz'))); + $this->assertSame(array(1 => '2:foo', 2 => '10:baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz'))); + } } diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php index 42f745e29b..b144699892 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -83,8 +83,7 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $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') + array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4) ); $this->factory = new DefaultChoiceListFactory(); } From 26eba769b5a1f9a13df71675f80f0269d89b1c2b Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 26 Mar 2015 10:52:07 +0100 Subject: [PATCH 5/9] [Form] Fixed regression: Choices are compared by their values if a value callback is given --- .../Form/ChoiceList/DoctrineChoiceLoader.php | 97 ++--------- .../Doctrine/Form/ChoiceList/IdReader.php | 125 ++++++++++++++ .../Doctrine/Form/Type/DoctrineType.php | 130 +++++++++----- .../Tests/Form/Type/EntityTypeTest.php | 1 - .../Form/ChoiceList/ArrayChoiceList.php | 22 ++- .../Factory/DefaultChoiceListFactory.php | 28 +-- .../Factory/PropertyAccessDecorator.php | 10 +- .../Form/ChoiceList/LazyChoiceList.php | 10 +- .../Tests/ChoiceList/ArrayChoiceListTest.php | 24 +-- .../ChoiceList/ArrayKeyChoiceListTest.php | 10 +- .../Factory/DefaultChoiceListFactoryTest.php | 163 +++++++++--------- 11 files changed, 361 insertions(+), 259 deletions(-) create mode 100644 src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php index c00c258ca5..4b10b45855 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -11,12 +11,10 @@ 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. @@ -41,67 +39,20 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface private $class; /** - * @var ClassMetadata + * @var IdReader */ - private $classMetadata; + private $idReader; /** * @var null|EntityLoaderInterface */ private $objectLoader; - /** - * 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 object. - * - * Doctrine must know about this object, that is, the object must already - * be persisted or added to the identity map before. Otherwise an - * exception is thrown. - * - * This method assumes that the object has a single-column identifier and - * will return a single value instead of an array. - * - * @param object $object The object for which to get the identifier - * - * @return int|string The identifier value - * - * @throws RuntimeException If the object 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. * @@ -114,22 +65,17 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface * @param ObjectManager $manager The object manager * @param string $class The class name of the * loaded objects + * @param IdReader $idReader The reader for the object + * IDs. * @param null|EntityLoaderInterface $objectLoader The objects loader */ - public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, EntityLoaderInterface $objectLoader = null) + public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, IdReader $idReader, EntityLoaderInterface $objectLoader = null) { $this->factory = $factory; $this->manager = $manager; - $this->classMetadata = $manager->getClassMetadata($class); - $this->class = $this->classMetadata->getName(); + $this->class = $manager->getClassMetadata($class)->getName(); + $this->idReader = $idReader; $this->objectLoader = $objectLoader; - - $identifier = $this->classMetadata->getIdentifierFieldNames(); - - if (1 === count($identifier)) { - $this->idField = $identifier[0]; - $this->compositeId = false; - } } /** @@ -145,23 +91,7 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface ? $this->objectLoader->getEntities() : $this->manager->getRepository($this->class)->findAll(); - // If the class has a multi-column identifier, we cannot index the - // objects by their IDs - if ($this->compositeId) { - $this->choiceList = $this->factory->createListFromChoices($objects, $value); - - return $this->choiceList; - } - - // Index the objects by ID - $objectsById = array(); - - foreach ($objects as $object) { - $id = self::getIdValue($this->manager, $this->classMetadata, $object); - $objectsById[$id] = $object; - } - - $this->choiceList = $this->factory->createListFromChoices($objectsById, $value); + $this->choiceList = $this->factory->createListFromChoices($objects, $value); return $this->choiceList; } @@ -193,14 +123,14 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface // know that the IDs are used as values // Attention: This optimization does not check choices for existence - if (!$this->choiceList && !$this->compositeId) { + if (!$this->choiceList && $this->idReader->isSingleId()) { $values = array(); // Maintain order and indices of the given objects foreach ($objects as $i => $object) { if ($object instanceof $this->class) { // Make sure to convert to the right format - $values[$i] = (string) self::getIdValue($this->manager, $this->classMetadata, $object); + $values[$i] = (string) $this->idReader->getIdValue($object); } } @@ -240,8 +170,8 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface // Optimize performance in case we have an object loader and // a single-field identifier - if (!$this->choiceList && !$this->compositeId && $this->objectLoader) { - $unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idField, $values); + if (!$this->choiceList && $this->objectLoader && $this->idReader->isSingleId()) { + $unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idReader->getIdField(), $values); $objectsById = array(); $objects = array(); @@ -250,8 +180,7 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface // "INDEX BY" clause to the Doctrine query in the loader, // but I'm not sure whether that's doable in a generic fashion. foreach ($unorderedObjects as $object) { - $id = self::getIdValue($this->manager, $this->classMetadata, $object); - $objectsById[$id] = $object; + $objectsById[$this->idReader->getIdValue($object)] = $object; } foreach ($values as $i => $id) { diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php new file mode 100644 index 0000000000..f6164725fd --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php @@ -0,0 +1,125 @@ + + * + * 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\Exception\RuntimeException; + +/** + * A utility for reading object IDs. + * + * @since 1.0 + * @author Bernhard Schussek + * + * @internal This class is meant for internal use only. + */ +class IdReader +{ + /** + * @var ObjectManager + */ + private $om; + + /** + * @var ClassMetadata + */ + private $classMetadata; + + /** + * @var bool + */ + private $singleId; + + /** + * @var bool + */ + private $intId; + + /** + * @var string + */ + private $idField; + + public function __construct(ObjectManager $om, ClassMetadata $classMetadata) + { + $ids = $classMetadata->getIdentifierFieldNames(); + $idType = $classMetadata->getTypeOfField(current($ids)); + + $this->om = $om; + $this->classMetadata = $classMetadata; + $this->singleId = 1 === count($ids); + $this->intId = $this->singleId && 1 === count($ids) && in_array($idType, array('integer', 'smallint', 'bigint')); + $this->idField = current($ids); + } + + /** + * Returns whether the class has a single-column ID. + * + * @return bool Returns `true` if the class has a single-column ID and + * `false` otherwise. + */ + public function isSingleId() + { + return $this->singleId; + } + + /** + * Returns whether the class has a single-column integer ID. + * + * @return bool Returns `true` if the class has a single-column integer ID + * and `false` otherwise. + */ + public function isIntId() + { + return $this->intId; + } + + /** + * Returns the ID value for an object. + * + * This method assumes that the object has a single-column ID. + * + * @param object $object The object. + * + * @return mixed The ID value. + */ + public function getIdValue($object) + { + if (!$object) { + return null; + } + + if (!$this->om->contains($object)) { + throw new RuntimeException( + 'Entities passed to the choice field must be managed. Maybe '. + 'persist them in the entity manager?' + ); + } + + $this->om->initializeObject($object); + + return current($this->classMetadata->getIdentifierValues($object)); + } + + /** + * Returns the name of the ID field. + * + * This method assumes that the object has a single-column ID. + * + * @return string The name of the ID field. + */ + public function getIdField() + { + return $this->idField; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index ed8ded5bad..6020d95e92 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -16,6 +16,7 @@ use Doctrine\Common\Persistence\ObjectManager; use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; +use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader; use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; use Symfony\Component\Form\AbstractType; @@ -42,11 +43,55 @@ abstract class DoctrineType extends AbstractType */ private $choiceListFactory; + /** + * @var IdReader[] + */ + private $idReaders = array(); + /** * @var DoctrineChoiceLoader[] */ private $choiceLoaders = array(); + /** + * Creates the label for a choice. + * + * For backwards compatibility, objects are cast to strings by default. + * + * @param object $choice The object. + * + * @return string The string representation of the object. + * + * @internal This method is public to be usable as callback. It should not + * be used in user code. + */ + public static function createChoiceLabel($choice) + { + return (string) $choice; + } + + /** + * Creates the field name for a choice. + * + * This method is used to generate field names if the underlying object has + * a single-column integer ID. In that case, the value of the field is + * the ID of the object. That ID is also used as field name. + * + * @param object $choice The object. + * @param int|string $key The choice key. + * @param string $value The choice value. Corresponds to the object's + * ID here. + * + * @return string The field name. + * + * @internal This method is public to be usable as callback. It should not + * be used in user code. + */ + public static function createChoiceName($choice, $key, $value) + { + return (string) $value; + } + public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) { $this->registry = $registry; @@ -67,9 +112,30 @@ abstract class DoctrineType extends AbstractType { $registry = $this->registry; $choiceListFactory = $this->choiceListFactory; + $idReaders = &$this->idReaders; $choiceLoaders = &$this->choiceLoaders; $type = $this; + $idReader = function (Options $options) use (&$idReaders) { + $hash = CachingFactoryDecorator::generateHash(array( + $options['em'], + $options['class'], + )); + + // The ID reader is a utility that is needed to read the object IDs + // when generating the field values. The callback generating the + // field values has no access to the object manager or the class + // of the field, so we store that information in the reader. + // The reader is cached so that two choice lists for the same class + // (and hence with the same reader) can successfully be cached. + if (!isset($idReaders[$hash])) { + $classMetadata = $options['em']->getClassMetadata($options['class']); + $idReaders[$hash] = new IdReader($options['em'], $classMetadata); + } + + return $idReaders[$hash]; + }; + $choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) { // Unless the choices are given explicitly, load them on demand if (null === $options['choices']) { @@ -106,6 +172,7 @@ abstract class DoctrineType extends AbstractType $choiceListFactory, $options['em'], $options['class'], + $options['id_reader'], $entityLoader ); @@ -120,24 +187,18 @@ abstract class DoctrineType extends AbstractType } // BC: use __toString() by default - return function ($entity) { - return (string) $entity; - }; + return array(__CLASS__, 'createChoiceLabel'); }; $choiceName = function (Options $options) { - /** @var ObjectManager $om */ - $om = $options['em']; - $classMetadata = $om->getClassMetadata($options['class']); - $ids = $classMetadata->getIdentifierFieldNames(); - $idType = $classMetadata->getTypeOfField(current($ids)); + /** @var IdReader $idReader */ + $idReader = $options['id_reader']; - // 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; - }; + // If the object has a single-column, numeric ID, use that ID as + // field name. We can only use numeric IDs as names, as we cannot + // guarantee that a non-numeric ID contains a valid form name + if ($idReader->isIntId()) { + return array(__CLASS__, 'createChoiceName'); } // Otherwise, an incrementing integer is used as name automatically @@ -147,8 +208,16 @@ abstract class DoctrineType extends AbstractType // and DoctrineChoiceLoader), 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; + $choiceValue = function (Options $options) { + /** @var IdReader $idReader */ + $idReader = $options['id_reader']; + + // If the entity has a single-column ID, use that ID as value + if ($idReader->isSingleId()) { + return array($idReader, 'getIdValue'); + } + + // Otherwise, an incrementing integer is used as value automatically }; $emNormalizer = function (Options $options, $em) use ($registry) { @@ -174,33 +243,6 @@ abstract class DoctrineType extends AbstractType 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 = DoctrineChoiceLoader::getIdValue($om, $classMetadata, $entity); - $entitiesById[$id] = $entity; - } - - return $entitiesById; - }; - // Invoke the query builder closure so that we can cache choice lists // for equal query builders $queryBuilderNormalizer = function (Options $options, $queryBuilder) { @@ -226,12 +268,12 @@ abstract class DoctrineType extends AbstractType 'choice_label' => $choiceLabel, 'choice_name' => $choiceName, 'choice_value' => $choiceValue, + 'id_reader' => $idReader, )); $resolver->setRequired(array('class')); $resolver->setNormalizer('em', $emNormalizer); - $resolver->setNormalizer('choices', $choicesNormalizer); $resolver->setNormalizer('query_builder', $queryBuilderNormalizer); $resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager')); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index 7d80819f6b..95e11aff6f 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -29,7 +29,6 @@ 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\OptionsResolver\Options; use Symfony\Component\PropertyAccess\PropertyAccess; class EntityTypeTest extends TypeTestCase diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php index a3987cc02c..515cd15a83 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php @@ -51,15 +51,12 @@ class ArrayChoiceList implements ChoiceListInterface * * The given choice array must have the same array keys as the value array. * - * @param array $choices The selectable choices - * @param callable $value The callable for creating the value for a - * choice. If `null` is passed, incrementing - * integers are used as values - * @param bool $compareByValue Whether to use the value callback to - * compare choices. If `null`, choices are - * compared by identity + * @param array $choices The selectable choices + * @param callable $value The callable for creating the value for a + * choice. If `null` is passed, incrementing + * integers are used as values */ - public function __construct(array $choices, $value = null, $compareByValue = false) + public function __construct(array $choices, $value = null) { if (null !== $value && !is_callable($value)) { throw new UnexpectedTypeException($value, 'null or callable'); @@ -67,7 +64,7 @@ class ArrayChoiceList implements ChoiceListInterface $this->choices = $choices; $this->values = array(); - $this->valueCallback = $compareByValue ? $value : null; + $this->valueCallback = $value; if (null === $value) { $i = 0; @@ -76,7 +73,7 @@ class ArrayChoiceList implements ChoiceListInterface } } else { foreach ($choices as $key => $choice) { - $this->values[$key] = (string) call_user_func($value, $choice, $key); + $this->values[$key] = (string) call_user_func($value, $choice); } } } @@ -132,8 +129,9 @@ class ArrayChoiceList implements ChoiceListInterface // Use the value callback to compare choices by their values, if present if ($this->valueCallback) { $givenValues = array(); - foreach ($choices as $key => $choice) { - $givenValues[$key] = (string) call_user_func($this->valueCallback, $choice, $key); + + foreach ($choices as $i => $givenChoice) { + $givenValues[$i] = (string) call_user_func($this->valueCallback, $givenChoice); } return array_intersect($givenValues, $this->values); diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index f0ea07cdf6..d974bf7d4f 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -191,10 +191,7 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface // The names are generated from an incrementing integer by default if (null === $index) { - $i = 0; - $index = function () use (&$i) { - return $i++; - }; + $index = 0; } // If $groupBy is not given, no grouping is done @@ -267,27 +264,30 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface return new ChoiceListView($otherViews, $preferredViews); } - private static function addChoiceView($choice, $key, $label, $values, $index, $attr, $isPreferred, &$preferredViews, &$otherViews) + private static function addChoiceView($choice, $key, $label, $values, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) { + $value = $values[$key]; + $nextIndex = is_int($index) ? $index++ : call_user_func($index, $choice, $key, $value); + $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], + null === $label ? (string) $key : (string) call_user_func($label, $choice, $key, $value), + $value, $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()) + is_callable($attr) ? call_user_func($attr, $choice, $key, $value) : (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; + if ($isPreferred && call_user_func($isPreferred, $choice, $key, $value)) { + $preferredViews[$nextIndex] = $view; } else { - $otherViews[call_user_func($index, $choice, $key)] = $view; + $otherViews[$nextIndex] = $view; } } - private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $values, $index, $attr, $isPreferred, &$preferredViews, &$otherViews) + 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 @@ -333,9 +333,9 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface } } - private static function addChoiceViewGroupedBy($groupBy, $choice, $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); + $groupLabel = call_user_func($groupBy, $choice, $key, $values[$key]); if (null === $groupLabel) { // If the callable returns null, don't group the choice diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php index bf91d85eea..131690a6ff 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -91,7 +91,15 @@ class PropertyAccessDecorator implements ChoiceListFactoryInterface if ($value instanceof PropertyPath) { $accessor = $this->propertyAccessor; $value = function ($choice) use ($accessor, $value) { - return $accessor->getValue($choice, $value); + // The callable may be invoked with a non-object/array value + // when such values are passed to + // ChoiceListInterface::getValuesForChoices(). Handle this case + // so that the call to getValue() doesn't break. + if (is_object($choice) || is_array($choice)) { + return $accessor->getValue($choice, $value); + } + + return null; }; } diff --git a/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php index 91e6bfe408..3dea398c6d 100644 --- a/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php @@ -43,6 +43,13 @@ class LazyChoiceList implements ChoiceListInterface */ private $value; + /** + * Whether to use the value callback to compare choices. + * + * @var bool + */ + private $compareByValue; + /** * @var ChoiceListInterface */ @@ -59,10 +66,11 @@ class LazyChoiceList implements ChoiceListInterface * @param null|callable $value The callable generating the choice * values */ - public function __construct(ChoiceLoaderInterface $loader, $value = null) + public function __construct(ChoiceLoaderInterface $loader, $value = null, $compareByValue = false) { $this->loader = $loader; $this->value = $value; + $this->compareByValue = $compareByValue; } /** diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php index 0dffd08374..129a093b89 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php @@ -54,15 +54,15 @@ class ArrayChoiceListTest extends AbstractChoiceListTest public function testCreateChoiceListWithValueCallback() { - $callback = function ($choice, $key) { - return $key.':'.$choice; + $callback = function ($choice) { + return ':'.$choice; }; $choiceList = new ArrayChoiceList(array(2 => 'foo', 7 => 'bar', 10 => 'baz'), $callback); - $this->assertSame(array(2 => '2:foo', 7 => '7:bar', 10 => '10:baz'), $choiceList->getValues()); - $this->assertSame(array(1 => 'foo', 2 => 'baz'), $choiceList->getChoicesForValues(array(1 => '2:foo', 2 => '10:baz'))); - $this->assertSame(array(1 => '2:foo', 2 => '10:baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz'))); + $this->assertSame(array(2 => ':foo', 7 => ':bar', 10 => ':baz'), $choiceList->getValues()); + $this->assertSame(array(1 => 'foo', 2 => 'baz'), $choiceList->getChoicesForValues(array(1 => ':foo', 2 => ':baz'))); + $this->assertSame(array(1 => ':foo', 2 => ':baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz'))); } public function testCompareChoicesByIdentityByDefault() @@ -76,20 +76,6 @@ class ArrayChoiceListTest extends AbstractChoiceListTest $choiceList = new ArrayChoiceList(array($obj1, $obj2), $callback); $this->assertSame(array(2 => 'value2'), $choiceList->getValuesForChoices(array(2 => $obj2))); - $this->assertSame(array(), $choiceList->getValuesForChoices(array(2 => (object) array('value' => 'value2')))); - } - - public function testCompareChoicesWithValueCallbackIfCompareByValue() - { - $callback = function ($choice) { - return $choice->value; - }; - - $obj1 = (object) array('value' => 'value1'); - $obj2 = (object) array('value' => 'value2'); - - $choiceList = new ArrayChoiceList(array($obj1, $obj2), $callback, true); - $this->assertSame(array(2 => 'value2'), $choiceList->getValuesForChoices(array(2 => $obj2))); $this->assertSame(array(2 => 'value2'), $choiceList->getValuesForChoices(array(2 => (object) array('value' => 'value2')))); } } diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php index 78263502d6..5024a60db7 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php @@ -183,14 +183,14 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest public function testCreateChoiceListWithValueCallback() { - $callback = function ($choice, $key) { - return $key.':'.$choice; + $callback = function ($choice) { + return ':'.$choice; }; $choiceList = new ArrayKeyChoiceList(array(2 => 'foo', 7 => 'bar', 10 => 'baz'), $callback); - $this->assertSame(array(2 => '2:foo', 7 => '7:bar', 10 => '10:baz'), $choiceList->getValues()); - $this->assertSame(array(1 => 'foo', 2 => 'baz'), $choiceList->getChoicesForValues(array(1 => '2:foo', 2 => '10:baz'))); - $this->assertSame(array(1 => '2:foo', 2 => '10:baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz'))); + $this->assertSame(array(2 => ':foo', 7 => ':bar', 10 => ':baz'), $choiceList->getValues()); + $this->assertSame(array(1 => 'foo', 2 => 'baz'), $choiceList->getChoicesForValues(array(1 => ':foo', 2 => ':baz'))); + $this->assertSame(array(1 => ':foo', 2 => ':baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz'))); } } diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php index b144699892..360d46729f 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -150,23 +150,6 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $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( @@ -217,26 +200,6 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $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 */ @@ -306,23 +269,6 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $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( @@ -380,26 +326,6 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $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'); @@ -537,12 +463,9 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase public function testCreateViewFlatPreferredChoicesClosureReceivesKey() { - $obj2 = $this->obj2; - $obj3 = $this->obj3; - $view = $this->factory->createView( $this->list, - function ($object, $key) use ($obj2, $obj3) { + function ($object, $key) { return 'B' === $key || 'C' === $key; } ); @@ -550,6 +473,18 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertFlatView($view); } + public function testCreateViewFlatPreferredChoicesClosureReceivesValue() + { + $view = $this->factory->createView( + $this->list, + function ($object, $key, $value) { + return '1' === $value || '2' === $value; + } + ); + + $this->assertFlatView($view); + } + public function testCreateViewFlatLabelAsCallable() { $view = $this->factory->createView( @@ -587,6 +522,24 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertFlatView($view); } + public function testCreateViewFlatLabelClosureReceivesValue() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + function ($object, $key, $value) { + switch ($value) { + case '0': return 'A'; + case '1': return 'B'; + case '2': return 'C'; + case '3': return 'D'; + } + } + ); + + $this->assertFlatView($view); + } + public function testCreateViewFlatIndexAsCallable() { $view = $this->factory->createView( @@ -632,6 +585,25 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertFlatViewWithCustomIndices($view); } + public function testCreateViewFlatIndexClosureReceivesValue() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + function ($object, $key, $value) { + switch ($value) { + case '0': return 'w'; + case '1': return 'x'; + case '2': return 'y'; + case '3': return 'z'; + } + } + ); + + $this->assertFlatViewWithCustomIndices($view); + } + public function testCreateViewFlatGroupByAsArray() { $view = $this->factory->createView( @@ -724,6 +696,21 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertGroupedView($view); } + public function testCreateViewFlatGroupByClosureReceivesValue() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + function ($object, $key, $value) { + return '0' === $value || '1' === $value ? 'Group 1' : 'Group 2'; + } + ); + + $this->assertGroupedView($view); + } + public function testCreateViewFlatAttrAsArray() { $view = $this->factory->createView( @@ -805,6 +792,26 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertFlatViewWithAttr($view); } + public function testCreateViewFlatAttrClosureReceivesValue() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + null, // group + function ($object, $key, $value) { + switch ($value) { + case '1': return array('attr1' => 'value1'); + case '2': return array('attr2' => 'value2'); + default: return array(); + } + } + ); + + $this->assertFlatViewWithAttr($view); + } + public function testCreateViewForLegacyChoiceList() { $preferred = array(new ChoiceView('Preferred', 'x', 'x')); From d6179c830be7f2245ad56b6a800a33275a802689 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 26 Mar 2015 10:59:50 +0100 Subject: [PATCH 6/9] [Form] Fixed PR comments --- .../Form/ChoiceList/DoctrineChoiceLoader.php | 38 ++------- .../Form/ChoiceList/EntityChoiceList.php | 2 + .../Form/ChoiceList/ORMQueryBuilderLoader.php | 18 ++-- .../Doctrine/Form/Type/DoctrineType.php | 65 ++++++++++----- .../Bridge/Doctrine/Form/Type/EntityType.php | 6 +- .../Form/ChoiceList/ArrayChoiceList.php | 8 +- .../Form/ChoiceList/ArrayKeyChoiceList.php | 4 +- .../Factory/CachingFactoryDecorator.php | 8 -- .../Factory/DefaultChoiceListFactory.php | 34 +------- .../Factory/PropertyAccessDecorator.php | 16 ++-- .../Form/ChoiceList/LazyChoiceList.php | 2 +- .../Extension/Core/ChoiceList/ChoiceList.php | 4 +- .../Core/ChoiceList/ChoiceListInterface.php | 7 +- .../Core/ChoiceList/LazyChoiceList.php | 7 +- .../Core/ChoiceList/ObjectChoiceList.php | 4 +- .../Core/ChoiceList/SimpleChoiceList.php | 2 +- .../Core/DataMapper/RadioListMapper.php | 2 - .../ChoiceToBooleanArrayTransformer.php | 4 +- .../ChoicesToBooleanArrayTransformer.php | 4 +- .../FixCheckboxInputListener.php | 6 ++ .../EventListener/FixRadioInputListener.php | 6 ++ .../Form/Extension/Core/Type/ChoiceType.php | 24 ++---- .../Form/Extension/Core/View/ChoiceView.php | 9 +- .../Factory/CachingFactoryDecoratorTest.php | 16 ---- .../Factory/DefaultChoiceListFactoryTest.php | 82 ------------------- 25 files changed, 134 insertions(+), 244 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php index 4b10b45855..5456c0eedb 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -97,25 +97,12 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface } /** - * Loads the values corresponding to the given objects. - * - * The values are returned with the same keys and in the same order as the - * corresponding objects in the given array. - * - * Optionally, a callable can be passed for generating the choice values. - * The callable receives the object as first and the array key as the second - * argument. - * - * @param array $objects An array of objects. Non-existing objects in - * this array are ignored - * @param null|callable $value The callable generating the choice values - * - * @return string[] An array of choice values + * {@inheritdoc} */ - public function loadValuesForChoices(array $objects, $value = null) + public function loadValuesForChoices(array $choices, $value = null) { // Performance optimization - if (empty($objects)) { + if (empty($choices)) { return array(); } @@ -127,7 +114,7 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface $values = array(); // Maintain order and indices of the given objects - foreach ($objects as $i => $object) { + foreach ($choices as $i => $object) { if ($object instanceof $this->class) { // Make sure to convert to the right format $values[$i] = (string) $this->idReader->getIdValue($object); @@ -137,24 +124,11 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface return $values; } - return $this->loadChoiceList($value)->getValuesForChoices($objects); + return $this->loadChoiceList($value)->getValuesForChoices($choices); } /** - * Loads the objects corresponding to the given values. - * - * The objects 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 object 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 objects + * {@inheritdoc} */ public function loadChoicesForValues(array $values, $value = null) { diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index f3d4ff48f6..1d4232306d 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -129,6 +129,8 @@ class EntityChoiceList extends ObjectChoiceList } parent::__construct($entities, $labelPath, $preferredEntities, $groupPath, null, $propertyAccessor); + + trigger_error('The '.__CLASS__.' class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader instead.', E_USER_DEPRECATED); } /** diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index 9d34601c9f..92f00cb243 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -37,9 +37,14 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface /** * Construct an ORM Query Builder Loader. * - * @param QueryBuilder|\Closure $queryBuilder - * @param EntityManager $manager - * @param string $class + * @param QueryBuilder|\Closure $queryBuilder The query builder or a closure + * for creating the query builder. + * Passing a closure is + * deprecated and will not be + * supported anymore as of + * Symfony 3.0. + * @param EntityManager $manager Deprecated. + * @param string $class Deprecated. * * @throws UnexpectedTypeException */ @@ -51,13 +56,16 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure'); } - // This block is not executed anymore since Symfony 2.7. The query - // builder closure is already invoked in DoctrineType if ($queryBuilder instanceof \Closure) { + trigger_error('Passing a QueryBuilder closure to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); + if (!$manager instanceof EntityManager) { throw new UnexpectedTypeException($manager, 'Doctrine\ORM\EntityManager'); } + trigger_error('Passing an EntityManager to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); + trigger_error('Passing a class to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); + $queryBuilder = $queryBuilder($manager->getRepository($class)); if (!$queryBuilder instanceof QueryBuilder) { diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 6020d95e92..b8d03c0eb1 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -116,27 +116,10 @@ abstract class DoctrineType extends AbstractType $choiceLoaders = &$this->choiceLoaders; $type = $this; - $idReader = function (Options $options) use (&$idReaders) { - $hash = CachingFactoryDecorator::generateHash(array( - $options['em'], - $options['class'], - )); - - // The ID reader is a utility that is needed to read the object IDs - // when generating the field values. The callback generating the - // field values has no access to the object manager or the class - // of the field, so we store that information in the reader. - // The reader is cached so that two choice lists for the same class - // (and hence with the same reader) can successfully be cached. - if (!isset($idReaders[$hash])) { - $classMetadata = $options['em']->getClassMetadata($options['class']); - $idReaders[$hash] = new IdReader($options['em'], $classMetadata); - } - - return $idReaders[$hash]; - }; - $choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) { + // This closure and the "query_builder" options should be pushed to + // EntityType in Symfony 3.0 as they are specific to the ORM + // Unless the choices are given explicitly, load them on demand if (null === $options['choices']) { // We consider two query builders with an equal SQL string and @@ -243,6 +226,13 @@ abstract class DoctrineType extends AbstractType return $em; }; + // deprecation note + $propertyNormalizer = function (Options $options, $propertyName) { + trigger_error('The "property" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_label" instead.', E_USER_DEPRECATED); + + return $propertyName; + }; + // Invoke the query builder closure so that we can cache choice lists // for equal query builders $queryBuilderNormalizer = function (Options $options, $queryBuilder) { @@ -257,6 +247,35 @@ abstract class DoctrineType extends AbstractType return $queryBuilder; }; + // deprecation note + $loaderNormalizer = function (Options $options, $loader) { + trigger_error('The "loader" option is deprecated since version 2.7 and will be removed in 3.0. Override getLoader() instead.', E_USER_DEPRECATED); + + return $loader; + }; + + // Set the "id_reader" option via the normalizer. This option is not + // supposed to be set by the user. + $idReaderNormalizer = function (Options $options) use (&$idReaders) { + $hash = CachingFactoryDecorator::generateHash(array( + $options['em'], + $options['class'], + )); + + // The ID reader is a utility that is needed to read the object IDs + // when generating the field values. The callback generating the + // field values has no access to the object manager or the class + // of the field, so we store that information in the reader. + // The reader is cached so that two choice lists for the same class + // (and hence with the same reader) can successfully be cached. + if (!isset($idReaders[$hash])) { + $classMetadata = $options['em']->getClassMetadata($options['class']); + $idReaders[$hash] = new IdReader($options['em'], $classMetadata); + } + + return $idReaders[$hash]; + }; + $resolver->setDefaults(array( 'em' => null, 'property' => null, // deprecated, use "choice_label" @@ -268,17 +287,19 @@ abstract class DoctrineType extends AbstractType 'choice_label' => $choiceLabel, 'choice_name' => $choiceName, 'choice_value' => $choiceValue, - 'id_reader' => $idReader, + 'id_reader' => null, // internal )); $resolver->setRequired(array('class')); $resolver->setNormalizer('em', $emNormalizer); + $resolver->setNormalizer('property', $propertyNormalizer); $resolver->setNormalizer('query_builder', $queryBuilderNormalizer); + $resolver->setNormalizer('loader', $loaderNormalizer); + $resolver->setNormalizer('id_reader', $idReaderNormalizer); $resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager')); $resolver->setAllowedTypes('loader', array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface')); - $resolver->setAllowedTypes('query_builder', array('null', 'callable', 'Doctrine\ORM\QueryBuilder')); } /** diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php index 236b9290c7..87b3ee42cb 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -20,9 +20,9 @@ class EntityType extends DoctrineType /** * Return the default loader object. * - * @param ObjectManager $manager - * @param QueryBuilder|\Closure $queryBuilder - * @param string $class + * @param ObjectManager $manager + * @param QueryBuilder $queryBuilder + * @param string $class * * @return ORMQueryBuilderLoader */ diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php index 515cd15a83..f55154b085 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php @@ -51,10 +51,10 @@ class ArrayChoiceList implements ChoiceListInterface * * The given choice array must have the same array keys as the value array. * - * @param array $choices The selectable choices - * @param callable $value The callable for creating the value for a - * choice. If `null` is passed, incrementing - * integers are used as values + * @param array $choices The selectable choices + * @param callable|null $value The callable for creating the value for a + * choice. If `null` is passed, incrementing + * integers are used as values */ public function __construct(array $choices, $value = null) { diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php index 918c278f06..7973072f32 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php @@ -41,7 +41,7 @@ use Symfony\Component\Form\Exception\InvalidArgumentException; * @author Bernhard Schussek * * @deprecated Added for backwards compatibility in Symfony 2.7, to be removed - * in Symfony 3.0. + * in Symfony 3.0. Use {@link ArrayChoiceList} instead. */ class ArrayKeyChoiceList extends ArrayChoiceList { @@ -113,6 +113,8 @@ class ArrayKeyChoiceList extends ArrayChoiceList } parent::__construct($choices, $value); + + trigger_error('The '.__CLASS__.' class was added for backwards compatibility in version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\ArrayChoiceList instead.', E_USER_DEPRECATED); } /** diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php index fb43ac8759..3a2702a335 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php @@ -91,10 +91,6 @@ class CachingFactoryDecorator implements ChoiceListFactoryInterface */ 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); } @@ -124,10 +120,6 @@ class CachingFactoryDecorator implements ChoiceListFactoryInterface */ 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); } diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index d974bf7d4f..31527a9f34 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -85,10 +85,6 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface */ 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); } @@ -109,10 +105,6 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface */ 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); } @@ -140,10 +132,6 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface */ 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); } @@ -152,26 +140,6 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface */ 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) { @@ -247,7 +215,7 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface ); } - // Remove any empty group views that may have been created by + // Remove any empty group view that may have been created by // addChoiceViewGroupedBy() foreach ($preferredViews as $key => $view) { if ($view instanceof ChoiceGroupView && 0 === count($view->choices)) { diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php index 131690a6ff..f6fd823784 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -153,16 +153,12 @@ class PropertyAccessDecorator implements ChoiceListFactoryInterface /** * {@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 + * @param ChoiceListInterface $list The choice list + * @param null|array|callable|string|PropertyPath $preferredChoices The preferred choices + * @param null|callable|string|PropertyPath $label The callable or path generating the choice labels + * @param null|callable|string|PropertyPath $index The callable or path generating the view indices + * @param null|array|\Traversable|callable|string|PropertyPath $groupBy The callable or path generating the group names + * @param null|array|callable|string|PropertyPath $attr The callable or path generating the HTML attributes * * @return ChoiceListView The choice list view */ diff --git a/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php index 3dea398c6d..092e2c4644 100644 --- a/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php @@ -51,7 +51,7 @@ class LazyChoiceList implements ChoiceListInterface private $compareByValue; /** - * @var ChoiceListInterface + * @var ChoiceListInterface|null */ private $loadedList; diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php index 2f7b287b63..817e03ec72 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -35,7 +35,7 @@ use Symfony\Component\Form\Extension\Core\View\ChoiceView; * @author Bernhard Schussek * * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. - * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayChoiceList} instead. + * Use {@link \Symfony\Component\Form\ChoiceList\ArrayChoiceList} instead. */ class ChoiceList implements ChoiceListInterface { @@ -92,6 +92,8 @@ class ChoiceList implements ChoiceListInterface } $this->initialize($choices, $labels, $preferredChoices); + + trigger_error('The '.__CLASS__.' class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\ArrayChoiceList instead.', E_USER_DEPRECATED); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php index 22354e09d8..aef70aef87 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface as BaseChoiceListInterface; + /** * Contains choices that can be selected in a form field. * @@ -27,10 +29,9 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; * @author Bernhard Schussek * * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. - * Use {@link \Symfony\Component\Form\ArrayChoiceList\ChoiceListInterface} - * instead. + * Use {@link BaseChoiceListInterface} instead. */ -interface ChoiceListInterface extends \Symfony\Component\Form\ChoiceList\ChoiceListInterface +interface ChoiceListInterface extends BaseChoiceListInterface { /** * Returns the choice views of the preferred choices as nested array with diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php index 24232bc1d6..f3a7cc028a 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php @@ -23,7 +23,7 @@ use Symfony\Component\Form\Exception\InvalidArgumentException; * @author Bernhard Schussek * * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. - * Use {@link \Symfony\Component\Form\ArrayChoiceList\LazyChoiceList} + * Use {@link \Symfony\Component\Form\ChoiceList\LazyChoiceList} * instead. */ abstract class LazyChoiceList implements ChoiceListInterface @@ -35,6 +35,11 @@ abstract class LazyChoiceList implements ChoiceListInterface */ private $choiceList; + public function __construct() + { + trigger_error('The '.__CLASS__.' class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\LazyChoiceList instead.', E_USER_DEPRECATED); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php index 606de43af3..c356ce466b 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php @@ -34,7 +34,7 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; * @author Bernhard Schussek * * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. - * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayChoiceList} + * Use {@link \Symfony\Component\Form\ChoiceList\ArrayChoiceList} * instead. */ class ObjectChoiceList extends ChoiceList @@ -97,6 +97,8 @@ class ObjectChoiceList extends ChoiceList $this->valuePath = null !== $valuePath ? new PropertyPath($valuePath) : null; parent::__construct($choices, array(), $preferredChoices); + + trigger_error('The '.__CLASS__.' class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\ArrayChoiceList instead.', E_USER_DEPRECATED); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php index 50a3eb5f4a..6dd8fb091e 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php @@ -30,7 +30,7 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; * @author Bernhard Schussek * * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. - * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayKeyChoiceList} + * Use {@link \Symfony\Component\Form\ChoiceList\ArrayChoiceList} * instead. */ class SimpleChoiceList extends ChoiceList diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php index aecdb2fad0..19db183a28 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php @@ -58,8 +58,6 @@ class RadioListMapper implements DataMapperInterface foreach ($radios as $radio) { if ($radio->getData()) { if ('placeholder' === $radio->getName()) { - $choice = null; - return; } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php index a0b5039317..108c1ca6a3 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php @@ -19,7 +19,7 @@ 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} + * Use {@link \Symfony\Component\Form\ChoiceList\LazyChoiceList} * instead. */ class ChoiceToBooleanArrayTransformer implements DataTransformerInterface @@ -38,6 +38,8 @@ class ChoiceToBooleanArrayTransformer implements DataTransformerInterface { $this->choiceList = $choiceList; $this->placeholderPresent = $placeholderPresent; + + trigger_error('The class '.__CLASS__.' is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\LazyChoiceList instead.', E_USER_DEPRECATED); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php index c38c363329..a632bc03c7 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php @@ -19,7 +19,7 @@ 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} + * Use {@link \Symfony\Component\Form\ChoiceList\LazyChoiceList} * instead. */ class ChoicesToBooleanArrayTransformer implements DataTransformerInterface @@ -29,6 +29,8 @@ class ChoicesToBooleanArrayTransformer implements DataTransformerInterface public function __construct(ChoiceListInterface $choiceList) { $this->choiceList = $choiceList; + + trigger_error('The class '.__CLASS__.' is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\LazyChoiceList instead.', E_USER_DEPRECATED); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php index 297987f799..85b08c7b32 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php @@ -22,6 +22,10 @@ use Symfony\Component\Form\FormEvents; * indexed array. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper} + * instead. */ class FixCheckboxInputListener implements EventSubscriberInterface { @@ -35,6 +39,8 @@ class FixCheckboxInputListener implements EventSubscriberInterface public function __construct(ChoiceListInterface $choiceList) { $this->choiceList = $choiceList; + + trigger_error('The class '.__CLASS__.' is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper instead.', E_USER_DEPRECATED); } public function preSubmit(FormEvent $event) diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php index d5067b6e33..8641ea725d 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php @@ -21,6 +21,10 @@ use Symfony\Component\Form\FormEvents; * to an array. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper} + * instead. */ class FixRadioInputListener implements EventSubscriberInterface { @@ -38,6 +42,8 @@ class FixRadioInputListener implements EventSubscriberInterface { $this->choiceList = $choiceList; $this->placeholderPresent = $placeholderPresent; + + trigger_error('The class '.__CLASS__.' is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper instead.', E_USER_DEPRECATED); } public function preSubmit(FormEvent $event) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 7e80a00bde..a950be8d89 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -233,15 +233,8 @@ class ChoiceType extends AbstractType { $choiceListFactory = $this->choiceListFactory; - $choiceList = function (Options $options) use ($choiceListFactory) { + $choiceList = function (Options $options, $choiceList) 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'] @@ -251,13 +244,6 @@ class ChoiceType extends AbstractType // Harden against NULL values (like in EntityType and ModelType) $choices = null !== $options['choices'] ? $options['choices'] : array(); - // 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; - } - // BC when choices are in the keys, not in the values if (!$options['choices_as_values']) { return $choiceListFactory->createListFromFlippedChoices($choices, $options['choice_value']); @@ -283,6 +269,13 @@ class ChoiceType extends AbstractType return $options['empty_value']; }; + // deprecation note + $choiceListNormalizer = function (Options $options, $choiceList) { + trigger_error('The "choice_list" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_loader" instead.', E_USER_DEPRECATED); + + return $choiceList; + }; + $placeholderNormalizer = function (Options $options, $placeholder) { if ($options['multiple']) { // never use an empty value for this case @@ -327,6 +320,7 @@ class ChoiceType extends AbstractType 'data_class' => null, )); + $resolver->setNormalizer('choice_list', $choiceListNormalizer); $resolver->setNormalizer('empty_value', $placeholderNormalizer); $resolver->setNormalizer('placeholder', $placeholderNormalizer); diff --git a/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php index 65d7af2464..0cbeecab9f 100644 --- a/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php +++ b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php @@ -11,12 +11,17 @@ namespace Symfony\Component\Form\Extension\Core\View; +use Symfony\Component\Form\ChoiceList\View\ChoiceView as BaseChoiceView; + /** * Represents a choice in templates. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link BaseChoiceView} instead. */ -class ChoiceView extends \Symfony\Component\Form\ChoiceList\View\ChoiceView +class ChoiceView extends BaseChoiceView { /** * Creates a new ChoiceView. @@ -28,5 +33,7 @@ class ChoiceView extends \Symfony\Component\Form\ChoiceList\View\ChoiceView public function __construct($data, $value, $label) { parent::__construct($label, $value, $data); + + trigger_error('The '.__CLASS__.' class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\View\ChoiceView instead.', E_USER_DEPRECATED); } } diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php index 031cced280..716468276a 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php @@ -34,14 +34,6 @@ class CachingFactoryDecoratorTest extends \PHPUnit_Framework_TestCase $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(); @@ -163,14 +155,6 @@ class CachingFactoryDecoratorTest extends \PHPUnit_Framework_TestCase $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(); diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php index 360d46729f..a2b817ed8d 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -88,22 +88,6 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $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()); @@ -200,22 +184,6 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $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()); @@ -345,56 +313,6 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $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); From 1d89922782922fe3bcacb09f91e8c3c992d29d3f Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 31 Mar 2015 12:25:01 +0200 Subject: [PATCH 7/9] [Form] Fixed tests using legacy functionality --- .../Doctrine/Form/Type/DoctrineType.php | 9 ++- .../GenericEntityChoiceListTest.php | 28 ++++---- .../LoadedEntityChoiceListCompositeIdTest.php | 1 + .../LoadedEntityChoiceListSingleIntIdTest.php | 1 + ...adedEntityChoiceListSingleStringIdTest.php | 1 + ...nloadedEntityChoiceListCompositeIdTest.php | 3 +- ...iceListCompositeIdWithQueryBuilderTest.php | 1 + ...nloadedEntityChoiceListSingleIntIdTest.php | 9 +-- ...iceListSingleIntIdWithQueryBuilderTest.php | 1 + ...adedEntityChoiceListSingleStringIdTest.php | 3 +- ...ListSingleStringIdWithQueryBuilderTest.php | 1 + .../Tests/Form/Type/EntityTypeTest.php | 72 ++++++++++++------- .../Form/ChoiceList/ArrayKeyChoiceList.php | 5 -- .../ChoiceToValueTransformer.php | 12 +--- .../Form/Extension/Core/Type/ChoiceType.php | 47 ++++++------ .../ChoiceList/AbstractChoiceListTest.php | 56 ++++----------- .../Core/ChoiceList/ChoiceListTest.php | 13 ++-- .../Core/ChoiceList/LazyChoiceListTest.php | 20 ++++-- .../Core/ChoiceList/ObjectChoiceListTest.php | 25 ++++--- .../Core/ChoiceList/SimpleChoiceListTest.php | 9 ++- .../SimpleNumericChoiceListTest.php | 13 ++-- .../ChoiceToValueTransformerTest.php | 16 ++--- .../ChoicesToValuesTransformerTest.php | 9 ++- .../FixRadioInputListenerTest.php | 15 ++-- .../Extension/Core/Type/ChoiceTypeTest.php | 28 ++++++-- 25 files changed, 210 insertions(+), 188 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index b8d03c0eb1..a478574190 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -228,7 +228,9 @@ abstract class DoctrineType extends AbstractType // deprecation note $propertyNormalizer = function (Options $options, $propertyName) { - trigger_error('The "property" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_label" instead.', E_USER_DEPRECATED); + if ($propertyName) { + trigger_error('The "property" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_label" instead.', E_USER_DEPRECATED); + } return $propertyName; }; @@ -249,7 +251,9 @@ abstract class DoctrineType extends AbstractType // deprecation note $loaderNormalizer = function (Options $options, $loader) { - trigger_error('The "loader" option is deprecated since version 2.7 and will be removed in 3.0. Override getLoader() instead.', E_USER_DEPRECATED); + if ($loader) { + trigger_error('The "loader" option is deprecated since version 2.7 and will be removed in 3.0. Override getLoader() instead.', E_USER_DEPRECATED); + } return $loader; }; @@ -300,6 +304,7 @@ abstract class DoctrineType extends AbstractType $resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager')); $resolver->setAllowedTypes('loader', array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface')); + $resolver->setAllowedTypes('query_builder', array('null', 'callable', 'Doctrine\ORM\QueryBuilder')); } /** diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/GenericEntityChoiceListTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/GenericEntityChoiceListTest.php index 9b60c87661..3226d69d1a 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/GenericEntityChoiceListTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/GenericEntityChoiceListTest.php @@ -19,6 +19,9 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList; use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Doctrine\ORM\Tools\SchemaTool; +/** + * @group legacy + */ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase { const SINGLE_INT_ID_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity'; @@ -36,6 +39,8 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase protected function setUp() { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + $this->em = DoctrineTestHelper::createTestEntityManager(); $schemaTool = new SchemaTool($this->em); @@ -70,7 +75,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase * @expectedException \Symfony\Component\Form\Exception\StringCastException * @expectedMessage Entity "Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity" passed to the choice field must have a "__toString()" method defined (or you can also override the "property" option). */ - public function testEntitiesMustHaveAToStringMethod() + public function testLegacyEntitiesMustHaveAToStringMethod() { $entity1 = new SingleIntIdNoToStringEntity(1, 'Foo'); $entity2 = new SingleIntIdNoToStringEntity(2, 'Bar'); @@ -96,7 +101,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase /** * @expectedException \Symfony\Component\Form\Exception\RuntimeException */ - public function testChoicesMustBeManaged() + public function testLegacyChoicesMustBeManaged() { $entity1 = new SingleIntIdEntity(1, 'Foo'); $entity2 = new SingleIntIdEntity(2, 'Bar'); @@ -118,7 +123,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase $choiceList->getChoices(); } - public function testInitExplicitChoices() + public function testLegacyInitExplicitChoices() { $entity1 = new SingleIntIdEntity(1, 'Foo'); $entity2 = new SingleIntIdEntity(2, 'Bar'); @@ -141,7 +146,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array(1 => $entity1, 2 => $entity2), $choiceList->getChoices()); } - public function testInitEmptyChoices() + public function testLegacyInitEmptyChoices() { $entity1 = new SingleIntIdEntity(1, 'Foo'); $entity2 = new SingleIntIdEntity(2, 'Bar'); @@ -161,7 +166,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array(), $choiceList->getChoices()); } - public function testInitNestedChoices() + public function testLegacyInitNestedChoices() { $entity1 = new SingleIntIdEntity(1, 'Foo'); $entity2 = new SingleIntIdEntity(2, 'Bar'); @@ -189,7 +194,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase ), $choiceList->getRemainingViews()); } - public function testGroupByPropertyPath() + public function testLegacyGroupByPropertyPath() { $item1 = new GroupableEntity(1, 'Foo', 'Group1'); $item2 = new GroupableEntity(2, 'Bar', 'Group1'); @@ -224,7 +229,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase ), $choiceList->getRemainingViews()); } - public function testGroupByInvalidPropertyPathReturnsFlatChoices() + public function testLegacyGroupByInvalidPropertyPathReturnsFlatChoices() { $item1 = new GroupableEntity(1, 'Foo', 'Group1'); $item2 = new GroupableEntity(2, 'Bar', 'Group1'); @@ -251,7 +256,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase ), $choiceList->getChoices()); } - public function testInitShorthandEntityName() + public function testLegacyInitShorthandEntityName() { $item1 = new SingleIntIdEntity(1, 'Foo'); $item2 = new SingleIntIdEntity(2, 'Bar'); @@ -267,13 +272,8 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array(1, 2), $choiceList->getValuesForChoices(array($item1, $item2))); } - /** - * @group legacy - */ - public function testLegacyInitShorthandEntityName() + public function testLegacyInitShorthandEntityName2() { - $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); - $item1 = new SingleIntIdEntity(1, 'Foo'); $item2 = new SingleIntIdEntity(2, 'Bar'); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListCompositeIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListCompositeIdTest.php index 90cbf1d7c8..a2ee7cdc8a 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListCompositeIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListCompositeIdTest.php @@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; /** * @author Bernhard Schussek + * @group legacy */ class LoadedEntityChoiceListCompositeIdTest extends AbstractEntityChoiceListCompositeIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleIntIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleIntIdTest.php index 52d04c3879..f655784004 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleIntIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleIntIdTest.php @@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; /** * @author Bernhard Schussek + * @group legacy */ class LoadedEntityChoiceListSingleIntIdTest extends AbstractEntityChoiceListSingleIntIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleStringIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleStringIdTest.php index 690d4b3d23..629b399ac3 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleStringIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleStringIdTest.php @@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; /** * @author Bernhard Schussek + * @group legacy */ class LoadedEntityChoiceListSingleStringIdTest extends AbstractEntityChoiceListSingleStringIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdTest.php index 5740a2ff94..15436a8627 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdTest.php @@ -13,10 +13,11 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListCompositeIdTest extends AbstractEntityChoiceListCompositeIdTest { - public function testGetIndicesForValuesIgnoresNonExistingValues() + public function testLegacyGetIndicesForValuesIgnoresNonExistingValues() { $this->markTestSkipped('Non-existing values are not detected for unloaded choice lists.'); } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest.php index 9c72ccccd9..422295feb1 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest.php @@ -16,6 +16,7 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest extends UnloadedEntityChoiceListCompositeIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdTest.php index dd53bf4226..2fa11f0d0b 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdTest.php @@ -13,17 +13,10 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListSingleIntIdTest extends AbstractEntityChoiceListSingleIntIdTest { - public function testGetIndicesForValuesIgnoresNonExistingValues() - { - $this->markTestSkipped('Non-existing values are not detected for unloaded choice lists.'); - } - - /** - * @group legacy - */ public function testLegacyGetIndicesForValuesIgnoresNonExistingValues() { $this->markTestSkipped('Non-existing values are not detected for unloaded choice lists.'); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest.php index fa5bb80ae7..c093782ff0 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest.php @@ -16,6 +16,7 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest extends UnloadedEntityChoiceListSingleIntIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdTest.php index 5b25b49a71..6600e49e89 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdTest.php @@ -13,10 +13,11 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListSingleStringIdTest extends AbstractEntityChoiceListSingleStringIdTest { - public function testGetIndicesForValuesIgnoresNonExistingValues() + public function testLegacyGetIndicesForValuesIgnoresNonExistingValues() { $this->markTestSkipped('Non-existing values are not detected for unloaded choice lists.'); } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest.php index 9fba5b9295..23329e80df 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest.php @@ -16,6 +16,7 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest extends UnloadedEntityChoiceListSingleStringIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index 95e11aff6f..27d1d88e43 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -131,7 +131,7 @@ class EntityTypeTest extends TypeTestCase 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => 'name', )); $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); @@ -165,7 +165,7 @@ class EntityTypeTest extends TypeTestCase 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => 'name', 'query_builder' => $qb, )); @@ -294,7 +294,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -316,7 +316,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); // the collection key is used here @@ -340,7 +340,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit(array('1', '3')); @@ -365,7 +365,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $existing = new ArrayCollection(array(0 => $entity2)); @@ -396,7 +396,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); // because of the composite key collection keys are used @@ -422,7 +422,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $existing = new ArrayCollection(array(0 => $entity2)); @@ -452,7 +452,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => true, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -478,7 +478,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => true, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit(array('1', '3')); @@ -508,7 +508,7 @@ class EntityTypeTest extends TypeTestCase 'class' => self::SINGLE_IDENT_CLASS, // not all persisted entities should be displayed 'choices' => array($entity1, $entity2), - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -532,7 +532,7 @@ class EntityTypeTest extends TypeTestCase 'em' => 'default', 'class' => self::ITEM_GROUP_CLASS, 'choices' => array($item1, $item2, $item3, $item4), - 'property' => 'name', + 'choice_label' => 'name', 'group_by' => 'groupName', )); @@ -563,7 +563,7 @@ class EntityTypeTest extends TypeTestCase 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'preferred_choices' => array($entity3, $entity2), - 'property' => 'name', + 'choice_label' => 'name', )); $this->assertEquals(array(3 => new ChoiceView('Baz', '3', $entity3), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['preferred_choices']); @@ -583,7 +583,7 @@ class EntityTypeTest extends TypeTestCase 'class' => self::SINGLE_IDENT_CLASS, 'choices' => array($entity2, $entity3), 'preferred_choices' => array($entity3), - 'property' => 'name', + 'choice_label' => 'name', )); $this->assertEquals(array(3 => new ChoiceView('Baz', '3', $entity3)), $field->createView()->vars['preferred_choices']); @@ -602,7 +602,7 @@ class EntityTypeTest extends TypeTestCase 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'choices' => array($entity1, $entity2), - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('3'); @@ -623,7 +623,7 @@ class EntityTypeTest extends TypeTestCase 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, 'choices' => array($entity1, $entity2), - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -647,7 +647,7 @@ class EntityTypeTest extends TypeTestCase 'class' => self::SINGLE_IDENT_CLASS, 'query_builder' => $repository->createQueryBuilder('e') ->where('e.id IN (1, 2)'), - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('3'); @@ -671,7 +671,7 @@ class EntityTypeTest extends TypeTestCase return $repository->createQueryBuilder('e') ->where('e.id IN (1, 2)'); }, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('3'); @@ -695,7 +695,7 @@ class EntityTypeTest extends TypeTestCase return $repository->createQueryBuilder('e') ->where('e.id1 IN (10, 50)'); }, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -715,7 +715,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_STRING_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('foo'); @@ -736,7 +736,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::COMPOSITE_STRING_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); // the collection key is used here @@ -760,7 +760,7 @@ class EntityTypeTest extends TypeTestCase $this->factory->createNamed('name', 'entity', null, array( 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => 'name', )); } @@ -775,7 +775,7 @@ class EntityTypeTest extends TypeTestCase $this->factory->createNamed('name', 'entity', null, array( 'em' => $this->em, 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); } @@ -852,20 +852,42 @@ class EntityTypeTest extends TypeTestCase 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => 'name', )); $field2 = $this->factory->createNamed('name', 'entity', null, array( 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => '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')); } + /** + * @group legacy + */ + public function testLegacyPropertyOption() + { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + + $entity1 = new SingleIntIdEntity(1, 'Foo'); + $entity2 = new SingleIntIdEntity(2, 'Bar'); + + $this->persist(array($entity1, $entity2)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'required' => false, + 'property' => 'name', + )); + + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); + } + protected function createRegistryMock($name, $em) { $registry = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php index 7973072f32..30709108e8 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php @@ -39,9 +39,6 @@ use Symfony\Component\Form\Exception\InvalidArgumentException; * ``` * * @author Bernhard Schussek - * - * @deprecated Added for backwards compatibility in Symfony 2.7, to be removed - * in Symfony 3.0. Use {@link ArrayChoiceList} instead. */ class ArrayKeyChoiceList extends ArrayChoiceList { @@ -113,8 +110,6 @@ class ArrayKeyChoiceList extends ArrayChoiceList } parent::__construct($choices, $value); - - trigger_error('The '.__CLASS__.' class was added for backwards compatibility in version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\ArrayChoiceList instead.', E_USER_DEPRECATED); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php index 1c83782621..2b4d026db7 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php @@ -43,20 +43,12 @@ class ChoiceToValueTransformer implements DataTransformerInterface throw new TransformationFailedException('Expected a scalar.'); } - // These are now valid ArrayChoiceList values, so we can return null - // right away - if ('' === $value || null === $value) { - return; - } - - $choices = $this->choiceList->getChoicesForValues(array($value)); + $choices = $this->choiceList->getChoicesForValues(array((string) $value)); if (1 !== count($choices)) { throw new TransformationFailedException(sprintf('The choice "%s" does not exist or is not unique', $value)); } - $choice = current($choices); - - return '' === $choice ? null : $choice; + return current($choices); } } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index a950be8d89..a597e1d4f4 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -233,25 +233,6 @@ class ChoiceType extends AbstractType { $choiceListFactory = $this->choiceListFactory; - $choiceList = function (Options $options, $choiceList) use ($choiceListFactory) { - if (null !== $options['choice_loader']) { - return $choiceListFactory->createListFromLoader( - $options['choice_loader'], - $options['choice_value'] - ); - } - - // Harden against NULL values (like in EntityType and ModelType) - $choices = null !== $options['choices'] ? $options['choices'] : array(); - - // BC when choices are in the keys, not in the values - if (!$options['choices_as_values']) { - return $choiceListFactory->createListFromFlippedChoices($choices, $options['choice_value']); - } - - return $choiceListFactory->createListFromChoices($choices, $options['choice_value']); - }; - $emptyData = function (Options $options) { if ($options['multiple'] || $options['expanded']) { return array(); @@ -269,11 +250,29 @@ class ChoiceType extends AbstractType return $options['empty_value']; }; - // deprecation note - $choiceListNormalizer = function (Options $options, $choiceList) { - trigger_error('The "choice_list" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_loader" instead.', E_USER_DEPRECATED); + $choiceListNormalizer = function (Options $options, $choiceList) use ($choiceListFactory) { + if ($choiceList) { + trigger_error('The "choice_list" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_loader" instead.', E_USER_DEPRECATED); - return $choiceList; + return $choiceList; + } + + if (null !== $options['choice_loader']) { + return $choiceListFactory->createListFromLoader( + $options['choice_loader'], + $options['choice_value'] + ); + } + + // Harden against NULL values (like in EntityType and ModelType) + $choices = null !== $options['choices'] ? $options['choices'] : array(); + + // BC when choices are in the keys, not in the values + if (!$options['choices_as_values']) { + return $choiceListFactory->createListFromFlippedChoices($choices, $options['choice_value']); + } + + return $choiceListFactory->createListFromChoices($choices, $options['choice_value']); }; $placeholderNormalizer = function (Options $options, $placeholder) { @@ -299,7 +298,7 @@ class ChoiceType extends AbstractType $resolver->setDefaults(array( 'multiple' => false, 'expanded' => false, - 'choice_list' => $choiceList, // deprecated + 'choice_list' => null, // deprecated 'choices' => array(), 'choices_as_values' => false, 'choice_loader' => null, diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/AbstractChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/AbstractChoiceListTest.php index 68ef4dca4f..710c30c6c5 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/AbstractChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/AbstractChoiceListTest.php @@ -123,6 +123,8 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase protected function setUp() { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + parent::setUp(); $this->list = $this->createChoiceList(); @@ -151,19 +153,16 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase } } - public function testGetChoices() + public function testLegacyGetChoices() { $this->assertSame($this->choices, $this->list->getChoices()); } - public function testGetValues() + public function testLegacyGetValues() { $this->assertSame($this->values, $this->list->getValues()); } - /** - * @group legacy - */ public function testLegacyGetIndicesForChoices() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -172,9 +171,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array($this->index1, $this->index2), $this->list->getIndicesForChoices($choices)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForChoicesPreservesKeys() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -183,9 +179,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array(5 => $this->index1, 8 => $this->index2), $this->list->getIndicesForChoices($choices)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForChoicesPreservesOrder() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -194,9 +187,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array($this->index2, $this->index1), $this->list->getIndicesForChoices($choices)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForChoicesIgnoresNonExistingChoices() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -205,9 +195,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array($this->index1, $this->index2), $this->list->getIndicesForChoices($choices)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForChoicesEmpty() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -215,9 +202,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array(), $this->list->getIndicesForChoices(array())); } - /** - * @group legacy - */ public function testLegacyGetIndicesForValues() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -227,9 +211,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array($this->index1, $this->index2), $this->list->getIndicesForValues($values)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForValuesPreservesKeys() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -239,9 +220,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array(5 => $this->index1, 8 => $this->index2), $this->list->getIndicesForValues($values)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForValuesPreservesOrder() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -250,9 +228,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array($this->index2, $this->index1), $this->list->getIndicesForValues($values)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForValuesIgnoresNonExistingValues() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -261,9 +236,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array($this->index1, $this->index2), $this->list->getIndicesForValues($values)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForValuesEmpty() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -271,61 +243,61 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array(), $this->list->getIndicesForValues(array())); } - public function testGetChoicesForValues() + public function testLegacyGetChoicesForValues() { $values = array($this->value1, $this->value2); $this->assertSame(array($this->choice1, $this->choice2), $this->list->getChoicesForValues($values)); } - public function testGetChoicesForValuesPreservesKeys() + public function testLegacyGetChoicesForValuesPreservesKeys() { $values = array(5 => $this->value1, 8 => $this->value2); $this->assertSame(array(5 => $this->choice1, 8 => $this->choice2), $this->list->getChoicesForValues($values)); } - public function testGetChoicesForValuesPreservesOrder() + public function testLegacyGetChoicesForValuesPreservesOrder() { $values = array($this->value2, $this->value1); $this->assertSame(array($this->choice2, $this->choice1), $this->list->getChoicesForValues($values)); } - public function testGetChoicesForValuesIgnoresNonExistingValues() + public function testLegacyGetChoicesForValuesIgnoresNonExistingValues() { $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() + public function testLegacyGetChoicesForValuesEmpty() { $this->assertSame(array(), $this->list->getChoicesForValues(array())); } - public function testGetValuesForChoices() + public function testLegacyGetValuesForChoices() { $choices = array($this->choice1, $this->choice2); $this->assertSame(array($this->value1, $this->value2), $this->list->getValuesForChoices($choices)); } - public function testGetValuesForChoicesPreservesKeys() + public function testLegacyGetValuesForChoicesPreservesKeys() { $choices = array(5 => $this->choice1, 8 => $this->choice2); $this->assertSame(array(5 => $this->value1, 8 => $this->value2), $this->list->getValuesForChoices($choices)); } - public function testGetValuesForChoicesPreservesOrder() + public function testLegacyGetValuesForChoicesPreservesOrder() { $choices = array($this->choice2, $this->choice1); $this->assertSame(array($this->value2, $this->value1), $this->list->getValuesForChoices($choices)); } - public function testGetValuesForChoicesIgnoresNonExistingChoices() + public function testLegacyGetValuesForChoicesIgnoresNonExistingChoices() { $choices = array($this->choice1, $this->choice2, 'foobar'); $this->assertSame(array($this->value1, $this->value2), $this->list->getValuesForChoices($choices)); } - public function testGetValuesForChoicesEmpty() + public function testLegacyGetValuesForChoicesEmpty() { $this->assertSame(array(), $this->list->getValuesForChoices(array())); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ChoiceListTest.php index 538bbc1b3d..25b4fdd45b 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ChoiceListTest.php @@ -14,6 +14,9 @@ namespace Symfony\Component\Form\Tests\Extension\Core\ChoiceList; use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; use Symfony\Component\Form\Extension\Core\View\ChoiceView; +/** + * @group legacy + */ class ChoiceListTest extends AbstractChoiceListTest { private $obj1; @@ -34,7 +37,7 @@ class ChoiceListTest extends AbstractChoiceListTest parent::setUp(); } - public function testInitArray() + public function testLegacyInitArray() { $this->list = new ChoiceList( array($this->obj1, $this->obj2, $this->obj3, $this->obj4), @@ -53,7 +56,7 @@ class ChoiceListTest extends AbstractChoiceListTest * choices parameter. A choice itself that is an object implementing \Traversable * is not treated as hierarchical structure, but as-is. */ - public function testInitNestedTraversable() + public function testLegacyInitNestedTraversable() { $traversableChoice = new \ArrayIterator(array($this->obj3, $this->obj4)); @@ -80,7 +83,7 @@ class ChoiceListTest extends AbstractChoiceListTest ), $this->list->getRemainingViews()); } - public function testInitNestedArray() + public function testLegacyInitNestedArray() { $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues()); @@ -97,7 +100,7 @@ class ChoiceListTest extends AbstractChoiceListTest /** * @expectedException \InvalidArgumentException */ - public function testInitWithInsufficientLabels() + public function testLegacyInitWithInsufficientLabels() { $this->list = new ChoiceList( array($this->obj1, $this->obj2), @@ -105,7 +108,7 @@ class ChoiceListTest extends AbstractChoiceListTest ); } - public function testInitWithLabelsContainingNull() + public function testLegacyInitWithLabelsContainingNull() { $this->list = new ChoiceList( array($this->obj1, $this->obj2), diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/LazyChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/LazyChoiceListTest.php index 0e5e2e6527..15018b2830 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/LazyChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/LazyChoiceListTest.php @@ -15,8 +15,14 @@ use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList; use Symfony\Component\Form\Extension\Core\View\ChoiceView; +/** + * @group legacy + */ class LazyChoiceListTest extends \PHPUnit_Framework_TestCase { + /** + * @var LazyChoiceListTest_Impl + */ private $list; protected function setUp() @@ -37,22 +43,22 @@ class LazyChoiceListTest extends \PHPUnit_Framework_TestCase $this->list = null; } - public function testGetChoices() + public function testLegacyGetChoices() { $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getChoices()); } - public function testGetValues() + public function testLegacyGetValues() { $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getValues()); } - public function testGetPreferredViews() + public function testLegacyGetPreferredViews() { $this->assertEquals(array(1 => new ChoiceView('b', 'b', 'B')), $this->list->getPreferredViews()); } - public function testGetRemainingViews() + public function testLegacyGetRemainingViews() { $this->assertEquals(array(0 => new ChoiceView('a', 'a', 'A'), 2 => new ChoiceView('c', 'c', 'C')), $this->list->getRemainingViews()); } @@ -79,13 +85,13 @@ class LazyChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array(1, 2), $this->list->getIndicesForValues($values)); } - public function testGetChoicesForValues() + public function testLegacyGetChoicesForValues() { $values = array('b', 'c'); $this->assertSame(array('b', 'c'), $this->list->getChoicesForValues($values)); } - public function testGetValuesForChoices() + public function testLegacyGetValuesForChoices() { $choices = array('b', 'c'); $this->assertSame(array('b', 'c'), $this->list->getValuesForChoices($choices)); @@ -94,7 +100,7 @@ class LazyChoiceListTest extends \PHPUnit_Framework_TestCase /** * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException */ - public function testLoadChoiceListShouldReturnChoiceList() + public function testLegacyLoadChoiceListShouldReturnChoiceList() { $list = new LazyChoiceListTest_InvalidImpl(); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php index 2bb06349ae..63dc8a9ea1 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php @@ -29,6 +29,9 @@ class ObjectChoiceListTest_EntityWithToString } } +/** + * @group legacy + */ class ObjectChoiceListTest extends AbstractChoiceListTest { private $obj1; @@ -49,7 +52,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest parent::setUp(); } - public function testInitArray() + public function testLegacyInitArray() { $this->list = new ObjectChoiceList( array($this->obj1, $this->obj2, $this->obj3, $this->obj4), @@ -63,7 +66,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest $this->assertEquals(array(0 => new ChoiceView($this->obj1, '0', 'A'), 2 => new ChoiceView($this->obj3, '2', 'C'), 3 => new ChoiceView($this->obj4, '3', 'D')), $this->list->getRemainingViews()); } - public function testInitNestedArray() + public function testLegacyInitNestedArray() { $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues()); @@ -77,7 +80,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest ), $this->list->getRemainingViews()); } - public function testInitArrayWithGroupPath() + public function testLegacyInitArrayWithGroupPath() { $this->obj1 = (object) array('name' => 'A', 'category' => 'Group 1'); $this->obj2 = (object) array('name' => 'B', 'category' => 'Group 1'); @@ -115,7 +118,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest /** * @expectedException \InvalidArgumentException */ - public function testInitArrayWithGroupPathThrowsExceptionIfNestedArray() + public function testLegacyInitArrayWithGroupPathThrowsExceptionIfNestedArray() { $this->obj1 = (object) array('name' => 'A', 'category' => 'Group 1'); $this->obj2 = (object) array('name' => 'B', 'category' => 'Group 1'); @@ -133,7 +136,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest ); } - public function testInitArrayWithValuePath() + public function testLegacyInitArrayWithValuePath() { $this->obj1 = (object) array('name' => 'A', 'id' => 10); $this->obj2 = (object) array('name' => 'B', 'id' => 20); @@ -154,7 +157,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest $this->assertEquals(array(0 => new ChoiceView($this->obj1, '10', 'A'), 3 => new ChoiceView($this->obj4, '40', 'D')), $this->list->getRemainingViews()); } - public function testInitArrayUsesToString() + public function testLegacyInitArrayUsesToString() { $this->obj1 = new ObjectChoiceListTest_EntityWithToString('A'); $this->obj2 = new ObjectChoiceListTest_EntityWithToString('B'); @@ -173,7 +176,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest /** * @expectedException \Symfony\Component\Form\Exception\StringCastException */ - public function testInitArrayThrowsExceptionIfToStringNotFound() + public function testLegacyInitArrayThrowsExceptionIfToStringNotFound() { $this->obj1 = new ObjectChoiceListTest_EntityWithToString('A'); $this->obj2 = new ObjectChoiceListTest_EntityWithToString('B'); @@ -262,7 +265,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest $this->assertSame(array($this->index1, $this->index2), $this->list->getIndicesForChoices($choices)); } - public function testGetValuesForChoicesWithValuePath() + public function testLegacyGetValuesForChoicesWithValuePath() { $this->list = new ObjectChoiceList( array($this->obj1, $this->obj2, $this->obj3, $this->obj4), @@ -276,7 +279,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest $this->assertSame(array('A', 'B'), $this->list->getValuesForChoices($choices)); } - public function testGetValuesForChoicesWithValuePathPreservesKeys() + public function testLegacyGetValuesForChoicesWithValuePathPreservesKeys() { $this->list = new ObjectChoiceList( array($this->obj1, $this->obj2, $this->obj3, $this->obj4), @@ -290,7 +293,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest $this->assertSame(array(5 => 'A', 8 => 'B'), $this->list->getValuesForChoices($choices)); } - public function testGetValuesForChoicesWithValuePathPreservesOrder() + public function testLegacyGetValuesForChoicesWithValuePathPreservesOrder() { $this->list = new ObjectChoiceList( array($this->obj1, $this->obj2, $this->obj3, $this->obj4), @@ -304,7 +307,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest $this->assertSame(array('B', 'A'), $this->list->getValuesForChoices($choices)); } - public function testGetValuesForChoicesWithValuePathIgnoresNonExistingChoices() + public function testLegacyGetValuesForChoicesWithValuePathIgnoresNonExistingChoices() { $this->list = new ObjectChoiceList( array($this->obj1, $this->obj2, $this->obj3, $this->obj4), diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleChoiceListTest.php index 3a5804ef28..ddf714f793 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleChoiceListTest.php @@ -14,9 +14,12 @@ namespace Symfony\Component\Form\Tests\Extension\Core\ChoiceList; use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; use Symfony\Component\Form\Extension\Core\View\ChoiceView; +/** + * @group legacy + */ class SimpleChoiceListTest extends AbstractChoiceListTest { - public function testInitArray() + public function testLegacyInitArray() { $choices = array('a' => 'A', 'b' => 'B', 'c' => 'C'); $this->list = new SimpleChoiceList($choices, array('b')); @@ -27,7 +30,7 @@ class SimpleChoiceListTest extends AbstractChoiceListTest $this->assertEquals(array(0 => new ChoiceView('a', 'a', 'A'), 2 => new ChoiceView('c', 'c', 'C')), $this->list->getRemainingViews()); } - public function testInitNestedArray() + public function testLegacyInitNestedArray() { $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd'), $this->list->getChoices()); $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd'), $this->list->getValues()); @@ -44,7 +47,7 @@ class SimpleChoiceListTest extends AbstractChoiceListTest /** * @dataProvider dirtyValuesProvider */ - public function testGetValuesForChoicesDealsWithDirtyValues($choice, $value) + public function testLegacyGetValuesForChoicesDealsWithDirtyValues($choice, $value) { $choices = array( '0' => 'Zero', diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleNumericChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleNumericChoiceListTest.php index b351790c45..0fd5fb92d9 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleNumericChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleNumericChoiceListTest.php @@ -13,11 +13,11 @@ namespace Symfony\Component\Form\Tests\Extension\Core\ChoiceList; use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; +/** + * @group legacy + */ class SimpleNumericChoiceListTest extends AbstractChoiceListTest { - /** - * @group legacy - */ public function testLegacyGetIndicesForChoicesDealsWithNumericChoices() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -27,9 +27,6 @@ class SimpleNumericChoiceListTest extends AbstractChoiceListTest $this->assertSame(array(0, 1), $this->list->getIndicesForChoices($choices)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForValuesDealsWithNumericValues() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -39,14 +36,14 @@ class SimpleNumericChoiceListTest extends AbstractChoiceListTest $this->assertSame(array(0, 1), $this->list->getIndicesForValues($values)); } - public function testGetChoicesForValuesDealsWithNumericValues() + public function testLegacyGetChoicesForValuesDealsWithNumericValues() { // Pass values as strings although they are integers $values = array('0', '1'); $this->assertSame(array(0, 1), $this->list->getChoicesForValues($values)); } - public function testGetValuesForChoicesDealsWithNumericValues() + public function testLegacyGetValuesForChoicesDealsWithNumericValues() { // Pass values as strings although they are integers $values = array('0', '1'); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php index bbae0621ce..c58d072f47 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer; -use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; +use Symfony\Component\Form\ChoiceList\ArrayChoiceList; use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer; class ChoiceToValueTransformerTest extends \PHPUnit_Framework_TestCase @@ -20,7 +20,8 @@ class ChoiceToValueTransformerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $list = new SimpleChoiceList(array('' => 'A', 0 => 'B', 1 => 'C')); + $list = new ArrayChoiceList(array('', 0, 'X')); + $this->transformer = new ChoiceToValueTransformer($list); } @@ -33,9 +34,8 @@ class ChoiceToValueTransformerTest extends \PHPUnit_Framework_TestCase { return array( // more extensive test set can be found in FormUtilTest - array(0, '0'), - array(false, '0'), - array('', ''), + array('', '0'), + array(0, '1'), ); } @@ -52,9 +52,9 @@ class ChoiceToValueTransformerTest extends \PHPUnit_Framework_TestCase return array( // values are expected to be valid choice keys already and stay // the same - array('0', 0), - array('', null), - array(null, null), + array('0', ''), + array('1', 0), + array('2', 'X'), ); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php index 87f5018b04..a7dc40aca2 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer; -use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; +use Symfony\Component\Form\ChoiceList\ArrayChoiceList; use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer; class ChoicesToValuesTransformerTest extends \PHPUnit_Framework_TestCase @@ -20,7 +20,7 @@ class ChoicesToValuesTransformerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $list = new SimpleChoiceList(array(0 => 'A', 1 => 'B', 2 => 'C')); + $list = new ArrayChoiceList(array('A', 'B', 'C')); $this->transformer = new ChoicesToValuesTransformer($list); } @@ -31,8 +31,7 @@ class ChoicesToValuesTransformerTest extends \PHPUnit_Framework_TestCase public function testTransform() { - // Value strategy in SimpleChoiceList is to copy and convert to string - $in = array(0, 1, 2); + $in = array('A', 'B', 'C'); $out = array('0', '1', '2'); $this->assertSame($out, $this->transformer->transform($in)); @@ -55,7 +54,7 @@ class ChoicesToValuesTransformerTest extends \PHPUnit_Framework_TestCase { // values are expected to be valid choices and stay the same $in = array('0', '1', '2'); - $out = array(0, 1, 2); + $out = array('A', 'B', 'C'); $this->assertSame($out, $this->transformer->reverseTransform($in)); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php index 426293395c..cae43b6e79 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php @@ -15,12 +15,17 @@ use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener; use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; +/** + * @group legacy + */ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase { private $choiceList; protected function setUp() { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + parent::setUp(); $this->choiceList = new SimpleChoiceList(array('' => 'Empty', 0 => 'A', 1 => 'B')); @@ -33,7 +38,7 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $listener = null; } - public function testFixRadio() + public function testLegacyFixRadio() { $data = '1'; $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); @@ -46,7 +51,7 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array(2 => '1'), $event->getData()); } - public function testFixZero() + public function testLegacyFixZero() { $data = '0'; $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); @@ -59,7 +64,7 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array(1 => '0'), $event->getData()); } - public function testFixEmptyString() + public function testLegacyFixEmptyString() { $data = ''; $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); @@ -72,7 +77,7 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array(0 => ''), $event->getData()); } - public function testConvertEmptyStringToPlaceholderIfNotFound() + public function testLegacyConvertEmptyStringToPlaceholderIfNotFound() { $list = new SimpleChoiceList(array(0 => 'A', 1 => 'B')); @@ -86,7 +91,7 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array('placeholder' => ''), $event->getData()); } - public function testDontConvertEmptyStringToPlaceholderIfNoPlaceholderUsed() + public function testLegacyDontConvertEmptyStringToPlaceholderIfNoPlaceholderUsed() { $list = new SimpleChoiceList(array(0 => 'A', 1 => '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 6a0b6db2ec..d34d5b2184 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -368,8 +368,13 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertEquals('2', $form->getViewData()); } - public function testSubmitSingleNonExpandedObjectChoicesBc() + /** + * @group legacy + */ + public function testLegacySubmitSingleNonExpandedObjectChoices() { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + $form = $this->factory->create('choice', null, array( 'multiple' => false, 'expanded' => false, @@ -483,8 +488,13 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertEquals(array('2', '3'), $form->getViewData()); } - public function testSubmitMultipleNonExpandedObjectChoicesBc() + /** + * @group legacy + */ + public function testLegacySubmitMultipleNonExpandedObjectChoices() { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + $form = $this->factory->create('choice', null, array( 'multiple' => true, 'expanded' => false, @@ -959,8 +969,13 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertNull($form[4]->getViewData()); } - public function testSubmitSingleExpandedObjectChoicesBc() + /** + * @group legacy + */ + public function testLegacySubmitSingleExpandedObjectChoices() { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + $form = $this->factory->create('choice', null, array( 'multiple' => false, 'expanded' => true, @@ -1182,8 +1197,13 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertNull($form[4]->getViewData()); } - public function testSubmitMultipleExpandedObjectChoicesBc() + /** + * @group legacy + */ + public function testLegacySubmitMultipleExpandedObjectChoices() { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + $form = $this->factory->create('choice', null, array( 'multiple' => true, 'expanded' => true, From 7e0960d7168e845dc5d0ad30296e27397fce44c1 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 31 Mar 2015 14:56:46 +0200 Subject: [PATCH 8/9] [Form] Fixed failing layout tests --- .../Extension/FormExtensionDivLayoutTest.php | 4 +- .../Tests/AbstractBootstrap3LayoutTest.php | 130 ++++++++++++++++++ 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php index 0c25ad44cd..334abd7092 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php @@ -17,8 +17,8 @@ use Symfony\Bridge\Twig\Form\TwigRendererEngine; use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\FormView; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Tests\AbstractDivLayoutTest; class FormExtensionDivLayoutTest extends AbstractDivLayoutTest @@ -132,7 +132,7 @@ class FormExtensionDivLayoutTest extends AbstractDivLayoutTest */ public function testIsChoiceSelected($expected, $choice, $value) { - $choice = new ChoiceView($choice, $choice, $choice.' label'); + $choice = new ChoiceView($choice.' label', $choice, $choice); $this->assertSame($expected, $this->extension->isSelectedChoice($choice, $value)); } diff --git a/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php index b5354e0bd2..6cc7edd7a3 100644 --- a/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php @@ -231,6 +231,29 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest ); } + 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('attr' => array('class' => 'my&class')), +'/select + [@name="name"] + [@class="my&class form-control"] + [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( @@ -496,6 +519,31 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest ); } + 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('attr' => array('class' => 'my&class')), +'/select + [@name="name[]"] + [@class="my&class form-control"] + [@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( @@ -577,6 +625,42 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest ); } + 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 + [ + ./div + [@class="radio"] + [ + ./label + [.="[trans]Choice&A[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + ] + ] + /following-sibling::div + [@class="radio"] + [ + ./label + [.="[trans]Choice&B[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)][@class="foo&bar"] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"][@class="form-control"] + ] +' + ); + } + public function testSingleChoiceExpandedWithPlaceholder() { $form = $this->factory->createNamed('name', 'choice', '&a', array( @@ -702,6 +786,52 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest ); } + 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 + [ + ./div + [@class="checkbox"] + [ + ./label + [.="[trans]Choice&A[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)] + ] + ] + /following-sibling::div + [@class="checkbox"] + [ + ./label + [.="[trans]Choice&B[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_1"][not(@checked)][not(@required)][@class="foo&bar"] + ] + ] + /following-sibling::div + [@class="checkbox"] + [ + ./label + [.="[trans]Choice&C[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"][@class="form-control"] + ] +' + ); + } + public function testCountry() { $form = $this->factory->createNamed('name', 'country', 'AT'); From 94d18e961cc72008adf97c0557856da63760688d Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 31 Mar 2015 14:58:03 +0200 Subject: [PATCH 9/9] [Form] Fixed CS --- src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php | 2 +- .../Form/ChoiceList/Factory/CachingFactoryDecorator.php | 1 - .../Form/ChoiceList/Factory/DefaultChoiceListFactory.php | 1 - .../Form/ChoiceList/Factory/PropertyAccessDecorator.php | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php index f6164725fd..7b48005408 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php @@ -96,7 +96,7 @@ class IdReader public function getIdValue($object) { if (!$object) { - return null; + return; } if (!$this->om->contains($object)) { diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php index 3a2702a335..f9848c2d0e 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php @@ -14,7 +14,6 @@ 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. diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index 31527a9f34..907829be0e 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -19,7 +19,6 @@ 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; /** diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php index f6fd823784..0f4bcaa14e 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -99,7 +99,7 @@ class PropertyAccessDecorator implements ChoiceListFactoryInterface return $accessor->getValue($choice, $value); } - return null; + return; }; }