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 | 193 ++-- .../Tests/Form/Type/EntityTypeTest.php | 77 +- .../Bridge/Twig/Extension/FormExtension.php | 2 +- .../views/Form/form_div_layout.html.twig | 16 +- .../views/Form/choice_widget_options.html.php | 8 +- .../Form/ChoiceList/ArrayChoiceList.php | 136 +++ .../Form/ChoiceList/ArrayKeyChoiceList.php | 173 ++++ .../Form/ChoiceList/ChoiceListInterface.php | 76 ++ .../Factory/CachingFactoryDecorator.php | 189 ++++ .../Factory/ChoiceListFactoryInterface.php | 124 +++ .../Factory/DefaultChoiceListFactory.php | 414 ++++++++ .../Factory/PropertyAccessDecorator.php | 226 ++++ .../Form/ChoiceList/LazyChoiceList.php | 115 +++ .../Loader/ChoiceLoaderInterface.php | 76 ++ .../Form/ChoiceList/View/ChoiceGroupView.php | 55 + .../Form/ChoiceList/View/ChoiceListView.php | 51 + .../Form/ChoiceList/View/ChoiceView.php | 64 ++ .../Extension/Core/ChoiceList/ChoiceList.php | 5 +- .../Core/ChoiceList/ChoiceListInterface.php | 51 +- .../Core/ChoiceList/LazyChoiceList.php | 4 + .../Core/ChoiceList/ObjectChoiceList.php | 4 + .../Core/ChoiceList/SimpleChoiceList.php | 4 + .../Form/Extension/Core/CoreExtension.php | 25 +- .../Core/DataMapper/CheckboxListMapper.php | 93 ++ .../Core/DataMapper/PropertyPathMapper.php | 4 +- .../Core/DataMapper/RadioListMapper.php | 73 ++ .../ChoiceToBooleanArrayTransformer.php | 6 +- .../ChoiceToValueTransformer.php | 4 +- .../ChoicesToBooleanArrayTransformer.php | 6 +- .../ChoicesToValuesTransformer.php | 2 +- .../FixCheckboxInputListener.php | 6 +- .../EventListener/FixRadioInputListener.php | 6 +- .../Form/Extension/Core/Type/ChoiceType.php | 282 +++-- .../Form/Extension/Core/View/ChoiceView.php | 27 +- .../Form/Tests/AbstractLayoutTest.php | 95 ++ .../ChoiceList/AbstractChoiceListTest.php | 173 ++++ .../Tests/ChoiceList/ArrayChoiceListTest.php | 52 + .../ChoiceList/ArrayKeyChoiceListTest.php | 187 ++++ .../Factory/CachingFactoryDecoratorTest.php | 668 ++++++++++++ .../Factory/DefaultChoiceListFactoryTest.php | 970 ++++++++++++++++++ .../Factory/PropertyAccessDecoratorTest.php | 338 ++++++ .../Tests/ChoiceList/LazyChoiceListTest.php | 141 +++ .../Extension/Core/Type/ChoiceTypeTest.php | 547 +++++++--- .../Extension/Core/Type/CountryTypeTest.php | 12 +- .../Extension/Core/Type/CurrencyTypeTest.php | 8 +- .../Extension/Core/Type/DateTypeTest.php | 22 +- .../Extension/Core/Type/LanguageTypeTest.php | 14 +- .../Extension/Core/Type/LocaleTypeTest.php | 8 +- .../Extension/Core/Type/TimeTypeTest.php | 14 +- .../Extension/Core/Type/TimezoneTypeTest.php | 6 +- 54 files changed, 5709 insertions(+), 443 deletions(-) create mode 100644 src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php create mode 100644 src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php create mode 100644 src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php create mode 100644 src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php create mode 100644 src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.php create mode 100644 src/Symfony/Component/Form/ChoiceList/View/ChoiceGroupView.php create mode 100644 src/Symfony/Component/Form/ChoiceList/View/ChoiceListView.php create mode 100644 src/Symfony/Component/Form/ChoiceList/View/ChoiceView.php create mode 100644 src/Symfony/Component/Form/Extension/Core/DataMapper/CheckboxListMapper.php create mode 100644 src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/Factory/PropertyAccessDecoratorTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index 57e57e6e25db9..3566a33d7d583 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 0000000000000..d1f7971e63cb3 --- /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 872e77affe0b7..9cfdd1fe4855b 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -17,7 +17,10 @@ use Doctrine\ORM\EntityManager; /** - * Getting Entities through the ORM QueryBuilder. + * Loads entities using a {@link QueryBuilder} instance. + * + * @author Benjamin Eberlei + * @author Bernhard Schussek */ class ORMQueryBuilderLoader implements EntityLoaderInterface { diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php index 570cc8f189dff..ed8e0a793444c 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php @@ -14,21 +14,38 @@ use Doctrine\Common\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; class DoctrineOrmExtension extends AbstractExtension { protected $registry; - public function __construct(ManagerRegistry $registry) + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + /** + * @var ChoiceListFactoryInterface + */ + private $choiceListFactory; + + public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) { $this->registry = $registry; + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor)); } protected function loadTypes() { return array( - new EntityType($this->registry, PropertyAccess::createPropertyAccessor()), + new EntityType($this->registry, $this->propertyAccessor, $this->choiceListFactory), ); } diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index ccc9bfc485e72..6c90a2eeb098d 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -12,17 +12,20 @@ namespace Symfony\Bridge\Doctrine\Form\Type; use Doctrine\Common\Persistence\ManagerRegistry; -use Symfony\Component\Form\Exception\RuntimeException; use Doctrine\Common\Persistence\ObjectManager; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList; +use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; -use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; +use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\Exception\RuntimeException; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; abstract class DoctrineType extends AbstractType @@ -33,19 +36,19 @@ abstract class DoctrineType extends AbstractType protected $registry; /** - * @var array + * @var ChoiceListFactoryInterface */ - private $choiceListCache = array(); + private $choiceListFactory; /** - * @var PropertyAccessorInterface + * @var EntityChoiceLoader[] */ - private $propertyAccessor; + private $choiceLoaders = array(); - public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null) + public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) { $this->registry = $registry; - $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->choiceListFactory = $choiceListFactory ?: new PropertyAccessDecorator(new DefaultChoiceListFactory(), $propertyAccessor); } public function buildForm(FormBuilderInterface $builder, array $options) @@ -60,86 +63,79 @@ public function buildForm(FormBuilderInterface $builder, array $options) public function configureOptions(OptionsResolver $resolver) { - $choiceListCache = &$this->choiceListCache; $registry = $this->registry; - $propertyAccessor = $this->propertyAccessor; + $choiceListFactory = $this->choiceListFactory; + $choiceLoaders = &$this->choiceLoaders; $type = $this; - $loader = function (Options $options) use ($type) { - $queryBuilder = (null !== $options['query_builder']) - ? $options['query_builder'] - : $options['em']->getRepository($options['class'])->createQueryBuilder('e'); - - return $type->getLoader($options['em'], $queryBuilder, $options['class']); - }; + $choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) { + // Unless the choices are given explicitly, load them on demand + if (null === $options['choices']) { + $hash = CachingFactoryDecorator::generateHash(array( + $options['em'], + $options['class'], + $options['query_builder'], + $options['loader'], + )); - $choiceList = function (Options $options) use (&$choiceListCache, $propertyAccessor) { - // Support for closures - $propertyHash = is_object($options['property']) - ? spl_object_hash($options['property']) - : $options['property']; - - $choiceHashes = $options['choices']; - - // Support for recursive arrays - if (is_array($choiceHashes)) { - // A second parameter ($key) is passed, so we cannot use - // spl_object_hash() directly (which strictly requires - // one parameter) - array_walk_recursive($choiceHashes, function (&$value) { - $value = spl_object_hash($value); - }); - } elseif ($choiceHashes instanceof \Traversable) { - $hashes = array(); - foreach ($choiceHashes as $value) { - $hashes[] = spl_object_hash($value); + if (!isset($choiceLoaders[$hash])) { + if ($options['loader']) { + $loader = $options['loader']; + } elseif (null !== $options['query_builder']) { + $loader = $type->getLoader($options['em'], $options['query_builder'], $options['class']); + } else { + $queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e'); + $loader = $type->getLoader($options['em'], $queryBuilder, $options['class']); + } + + $choiceLoaders[$hash] = new EntityChoiceLoader( + $choiceListFactory, + $options['em'], + $options['class'], + $loader + ); } - $choiceHashes = $hashes; + return $choiceLoaders[$hash]; } + }; - $preferredChoiceHashes = $options['preferred_choices']; - - if (is_array($preferredChoiceHashes)) { - array_walk_recursive($preferredChoiceHashes, function (&$value) { - $value = spl_object_hash($value); - }); + $choiceLabel = function (Options $options) { + // BC with the "property" option + if ($options['property']) { + return $options['property']; } - // Support for custom loaders (with query builders) - $loaderHash = is_object($options['loader']) - ? spl_object_hash($options['loader']) - : $options['loader']; - - // Support for closures - $groupByHash = is_object($options['group_by']) - ? spl_object_hash($options['group_by']) - : $options['group_by']; - - $hash = hash('sha256', json_encode(array( - spl_object_hash($options['em']), - $options['class'], - $propertyHash, - $loaderHash, - $choiceHashes, - $preferredChoiceHashes, - $groupByHash, - ))); - - if (!isset($choiceListCache[$hash])) { - $choiceListCache[$hash] = new EntityChoiceList( - $options['em'], - $options['class'], - $options['property'], - $options['loader'], - $options['choices'], - $options['preferred_choices'], - $options['group_by'], - $propertyAccessor - ); + // BC: use __toString() by default + return function ($entity) { + return (string) $entity; + }; + }; + + $choiceName = function (Options $options) { + /** @var ObjectManager $om */ + $om = $options['em']; + $classMetadata = $om->getClassMetadata($options['class']); + $ids = $classMetadata->getIdentifierFieldNames(); + $idType = $classMetadata->getTypeOfField(current($ids)); + + // If the entity has a single-column, numeric ID, use that ID as + // field name + if (1 === count($ids) && in_array($idType, array('integer', 'smallint', 'bigint'))) { + return function ($entity, $id) { + return $id; + }; } - return $choiceListCache[$hash]; + // Otherwise, an incrementing integer is used as name automatically + }; + + // The choices are always indexed by ID (see "choices" normalizer + // and EntityChoiceLoader), unless the ID is composite. Then they + // are indexed by an incrementing integer. + // Use the ID/incrementing integer as choice value. + $choiceValue = function ($entity, $key) { + return $key; }; $emNormalizer = function (Options $options, $em) use ($registry) { @@ -165,19 +161,50 @@ public function configureOptions(OptionsResolver $resolver) return $em; }; + $choicesNormalizer = function (Options $options, $entities) { + if (null === $entities || 0 === count($entities)) { + return $entities; + } + + // Make sure that the entities are indexed by their ID + /** @var ObjectManager $om */ + $om = $options['em']; + $classMetadata = $om->getClassMetadata($options['class']); + $ids = $classMetadata->getIdentifierFieldNames(); + + // We cannot use composite IDs as indices. In that case, keep the + // given indices + if (count($ids) > 1) { + return $entities; + } + + $entitiesById = array(); + + foreach ($entities as $entity) { + $id = EntityChoiceLoader::getIdValue($om, $classMetadata, $entity); + $entitiesById[$id] = $entity; + } + + return $entitiesById; + }; + $resolver->setDefaults(array( 'em' => null, - 'property' => null, + 'property' => null, // deprecated, use "choice_label" 'query_builder' => null, - 'loader' => $loader, + 'loader' => null, // deprecated, use "choice_loader" 'choices' => null, - 'choice_list' => $choiceList, - 'group_by' => null, + 'choices_as_values' => true, + 'choice_loader' => $choiceLoader, + 'choice_label' => $choiceLabel, + 'choice_name' => $choiceName, + 'choice_value' => $choiceValue, )); $resolver->setRequired(array('class')); $resolver->setNormalizer('em', $emNormalizer); + $resolver->setNormalizer('choices', $choicesNormalizer); $resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager')); $resolver->setAllowedTypes('loader', array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface')); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index 25afbed492150..9f1591f308253 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -11,21 +11,23 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\Type; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Tools\SchemaTool; +use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\Form\Forms; -use Symfony\Component\Form\Test\TypeTestCase; +use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity; +use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\GroupableEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity; -use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity; -use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity; -use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; -use Doctrine\ORM\Tools\SchemaTool; -use Doctrine\Common\Collections\ArrayCollection; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Forms; +use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\PropertyAccess\PropertyAccess; class EntityTypeTest extends TypeTestCase @@ -37,12 +39,12 @@ class EntityTypeTest extends TypeTestCase const COMPOSITE_STRING_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity'; /** - * @var \Doctrine\ORM\EntityManager + * @var EntityManager */ private $em; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit_Framework_MockObject_MockObject|ManagerRegistry */ private $emRegistry; @@ -131,7 +133,7 @@ public function testSetDataToUninitializedEntityWithNonRequired() 'property' => 'name', )); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); } public function testSetDataToUninitializedEntityWithNonRequiredToString() @@ -147,7 +149,7 @@ public function testSetDataToUninitializedEntityWithNonRequiredToString() 'required' => false, )); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); } public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder() @@ -166,7 +168,7 @@ public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder() 'query_builder' => $qb, )); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); } /** @@ -249,7 +251,7 @@ public function testSubmitSingleExpandedNull() $field->submit(null); $this->assertNull($field->getData()); - $this->assertSame(array(), $field->getViewData()); + $this->assertNull($field->getViewData()); } public function testSubmitSingleNonExpandedNull() @@ -510,7 +512,7 @@ public function testOverrideChoices() $field->submit('2'); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); $this->assertTrue($field->isSynchronized()); $this->assertSame($entity2, $field->getData()); $this->assertSame('2', $field->getViewData()); @@ -537,9 +539,14 @@ public function testGroupByChoices() $this->assertSame('2', $field->getViewData()); $this->assertEquals(array( - 'Group1' => array(1 => new ChoiceView($item1, '1', 'Foo'), 2 => new ChoiceView($item2, '2', 'Bar')), - 'Group2' => array(3 => new ChoiceView($item3, '3', 'Baz')), - '4' => new ChoiceView($item4, '4', 'Boo!'), + 'Group1' => new ChoiceGroupView('Group1', array( + 1 => new ChoiceView('Foo', '1', $item1), + 2 => new ChoiceView('Bar', '2', $item2), + )), + 'Group2' => new ChoiceGroupView('Group2', array( + 3 => new ChoiceView('Baz', '3', $item3), + )), + 4 => new ChoiceView('Boo!', '4', $item4), ), $field->createView()->vars['choices']); } @@ -558,8 +565,8 @@ public function testPreferredChoices() 'property' => 'name', )); - $this->assertEquals(array(3 => new ChoiceView($entity3, '3', 'Baz'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['preferred_choices']); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo')), $field->createView()->vars['choices']); + $this->assertEquals(array(3 => new ChoiceView('Baz', '3', $entity3), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['preferred_choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1)), $field->createView()->vars['choices']); } public function testOverrideChoicesWithPreferredChoices() @@ -578,8 +585,8 @@ public function testOverrideChoicesWithPreferredChoices() 'property' => 'name', )); - $this->assertEquals(array(3 => new ChoiceView($entity3, '3', 'Baz')), $field->createView()->vars['preferred_choices']); - $this->assertEquals(array(2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(3 => new ChoiceView('Baz', '3', $entity3)), $field->createView()->vars['preferred_choices']); + $this->assertEquals(array(2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); } public function testDisallowChoicesThatAreNotIncludedChoicesSingleIdentifier() @@ -833,6 +840,30 @@ public function testLoaderCaching() $this->assertCount(1, $loaders); } + public function testCacheChoiceLists() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + + $this->persist(array($entity1)); + + $field1 = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'required' => false, + 'property' => 'name', + )); + + $field2 = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'required' => false, + 'property' => 'name', + )); + + $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\ChoiceListInterface', $field1->getConfig()->getOption('choice_list')); + $this->assertSame($field1->getConfig()->getOption('choice_list'), $field2->getConfig()->getOption('choice_list')); + } + protected function createRegistryMock($name, $em) { $registry = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); diff --git a/src/Symfony/Bridge/Twig/Extension/FormExtension.php b/src/Symfony/Bridge/Twig/Extension/FormExtension.php index 9c7339f70295f..38270f9f5ba4d 100644 --- a/src/Symfony/Bridge/Twig/Extension/FormExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/FormExtension.php @@ -13,7 +13,7 @@ use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser; use Symfony\Bridge\Twig\Form\TwigRendererInterface; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; /** * FormExtension extends Twig with form capabilities. diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index b39dbf1c80e5e..df0e571602ecd 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 a7a9311d51326..81402efffb102 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 0000000000000..0dfc0f9945a0d --- /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 0000000000000..d79747e0485b8 --- /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 0000000000000..62f3158646466 --- /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 0000000000000..fb43ac87594c1 --- /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 0000000000000..60239423f3359 --- /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 0000000000000..dd191eea39a1e --- /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 0000000000000..bf91d85eea64b --- /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 0000000000000..91e6bfe4088de --- /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 0000000000000..9171fe3f1653c --- /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 0000000000000..8e59620369692 --- /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 0000000000000..9641f4b1d9435 --- /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 0000000000000..ded2a55b30ad8 --- /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 9d2a1c42a46c0..2f7b287b63e8b 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -29,10 +29,13 @@ * * $choices = array(true, false); * $labels = array('Agree', 'Disagree'); - * $choiceList = new ChoiceList($choices, $labels); + * $choiceList = new ArrayChoiceList($choices, $labels); * * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayChoiceList} instead. */ class ChoiceList implements ChoiceListInterface { diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php index 8f09179a2a8fb..22354e09d852e 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php @@ -25,23 +25,13 @@ * in the HTML "value" attribute. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\ChoiceListInterface} + * instead. */ -interface ChoiceListInterface +interface ChoiceListInterface extends \Symfony\Component\Form\ChoiceList\ChoiceListInterface { - /** - * Returns the list of choices. - * - * @return array The choices with their indices as keys - */ - public function getChoices(); - - /** - * Returns the values for the choices. - * - * @return array The values with the corresponding choice indices as keys - */ - public function getValues(); - /** * Returns the choice views of the preferred choices as nested array with * the choice groups as top-level keys. @@ -92,37 +82,6 @@ public function getPreferredViews(); */ public function getRemainingViews(); - /** - * Returns the choices corresponding to the given values. - * - * The choices can have any data type. - * - * The choices must be returned with the same keys and in the same order - * as the corresponding values in the given array. - * - * @param array $values An array of choice values. Not existing values in - * this array are ignored - * - * @return array An array of choices with ascending, 0-based numeric keys - */ - public function getChoicesForValues(array $values); - - /** - * Returns the values corresponding to the given choices. - * - * The values must be strings. - * - * The values must be returned with the same keys and in the same order - * as the corresponding choices in the given array. - * - * @param array $choices An array of choices. Not existing choices in this - * array are ignored - * - * @return array An array of choice values with ascending, 0-based numeric - * keys - */ - public function getValuesForChoices(array $choices); - /** * Returns the indices corresponding to the given choices. * diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php index ee136f79780f5..24232bc1d67af 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php @@ -21,6 +21,10 @@ * which should return a ChoiceListInterface instance. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\LazyChoiceList} + * instead. */ abstract class LazyChoiceList implements ChoiceListInterface { diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php index a20d19455577d..606de43af3ef5 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php @@ -32,6 +32,10 @@ * * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayChoiceList} + * instead. */ class ObjectChoiceList extends ChoiceList { diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php index 8d4ddd12423e6..50a3eb5f4a29f 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php @@ -28,6 +28,10 @@ * * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayKeyChoiceList} + * instead. */ class SimpleChoiceList extends ChoiceList { diff --git a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php index a0153a57eb700..231994258e8d6 100644 --- a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php +++ b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php @@ -12,7 +12,12 @@ namespace Symfony\Component\Form\Extension\Core; use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** * Represents the main form extension, which loads the core functionality. @@ -21,13 +26,29 @@ */ class CoreExtension extends AbstractExtension { + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + /** + * @var ChoiceListFactoryInterface + */ + private $choiceListFactory; + + public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) + { + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor)); + } + protected function loadTypes() { return array( - new Type\FormType(PropertyAccess::createPropertyAccessor()), + new Type\FormType($this->propertyAccessor), new Type\BirthdayType(), new Type\CheckboxType(), - new Type\ChoiceType(), + new Type\ChoiceType($this->choiceListFactory), new Type\CollectionType(), new Type\CountryType(), new Type\DateType(), diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/CheckboxListMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/CheckboxListMapper.php new file mode 100644 index 0000000000000..d87196475fec2 --- /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 2208f26d1e5d4..736752a41e19c 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php @@ -17,7 +17,7 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** - * A data mapper using property paths to read/write data. + * Maps arrays/objects to/from forms using property paths. * * @author Bernhard Schussek */ @@ -31,7 +31,7 @@ class PropertyPathMapper implements DataMapperInterface /** * Creates a new property path mapper. * - * @param PropertyAccessorInterface $propertyAccessor + * @param PropertyAccessorInterface $propertyAccessor The property accessor */ public function __construct(PropertyAccessorInterface $propertyAccessor = null) { diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php new file mode 100644 index 0000000000000..aecdb2fad0c73 --- /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 a91ed55c3125b..a0b5039317b51 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 087faf4d3b4eb..1c8378262135c 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php @@ -13,7 +13,7 @@ use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; /** * @author Bernhard Schussek @@ -43,7 +43,7 @@ public function reverseTransform($value) throw new TransformationFailedException('Expected a scalar.'); } - // These are now valid ChoiceList values, so we can return null + // These are now valid ArrayChoiceList values, so we can return null // right away if ('' === $value || null === $value) { return; diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php index f1f13fda28845..c38c363329012 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 0ee0b0fefd5fe..0a1f2f028863a 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php @@ -13,7 +13,7 @@ use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\DataTransformerInterface; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; /** * @author Bernhard Schussek diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php index b201802fbcfff..297987f799729 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Form\Extension\Core\EventListener; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\Exception\TransformationFailedException; -use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormEvent; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\FormEvents; /** * Takes care of converting the input from a list of checkboxes to a correctly diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php index c5f871756bab4..d5067b6e33500 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 5b52b4ad96a7d..7e80a00bdec99 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -12,19 +12,25 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper; +use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; -use Symfony\Component\Form\Exception\LogicException; -use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; -use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener; -use Symfony\Component\Form\Extension\Core\EventListener\FixCheckboxInputListener; use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener; use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer; -use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToBooleanArrayTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer; -use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToBooleanArrayTransformer; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -33,54 +39,111 @@ class ChoiceType extends AbstractType /** * Caches created choice lists. * - * @var array + * @var ChoiceListFactoryInterface */ - private $choiceListCache = array(); + private $choiceListFactory; + + public function __construct(ChoiceListFactoryInterface $choiceListFactory = null) + { + $this->choiceListFactory = $choiceListFactory ?: new PropertyAccessDecorator(new DefaultChoiceListFactory()); + } /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { - if (!$options['choice_list'] && !is_array($options['choices']) && !$options['choices'] instanceof \Traversable) { - throw new LogicException('Either the option "choices" or "choice_list" must be set.'); - } - if ($options['expanded']) { + $builder->setDataMapper($options['multiple'] + ? new CheckboxListMapper($options['choice_list']) + : new RadioListMapper($options['choice_list'])); + // Initialize all choices before doing the index check below. // This helps in cases where index checks are optimized for non // initialized choice lists. For example, when using an SQL driver, // the index check would read in one SQL query and the initialization // requires another SQL query. When the initialization is done first, // one SQL query is sufficient. - $preferredViews = $options['choice_list']->getPreferredViews(); - $remainingViews = $options['choice_list']->getRemainingViews(); + + $choiceListView = $this->createChoiceListView($options['choice_list'], $options); + $builder->setAttribute('choice_list_view', $choiceListView); // Check if the choices already contain the empty value - // Only add the empty value option if this is not the case + // Only add the placeholder option if this is not the case if (null !== $options['placeholder'] && 0 === count($options['choice_list']->getChoicesForValues(array('')))) { - $placeholderView = new ChoiceView(null, '', $options['placeholder']); + $placeholderView = new ChoiceView($options['placeholder'], '', null); - // "placeholder" is a reserved index - $this->addSubForms($builder, array('placeholder' => $placeholderView), $options); + // "placeholder" is a reserved name + $this->addSubForm($builder, 'placeholder', $placeholderView, $options); } - $this->addSubForms($builder, $preferredViews, $options); - $this->addSubForms($builder, $remainingViews, $options); + $this->addSubForms($builder, $choiceListView->preferredChoices, $options); + $this->addSubForms($builder, $choiceListView->choices, $options); - if ($options['multiple']) { - $builder->addViewTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list'])); - $builder->addEventSubscriber(new FixCheckboxInputListener($options['choice_list']), 10); - } else { - $builder->addViewTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list'], $builder->has('placeholder'))); - $builder->addEventSubscriber(new FixRadioInputListener($options['choice_list'], $builder->has('placeholder')), 10); + // Make sure that scalar, submitted values are converted to arrays + // which can be submitted to the checkboxes/radio buttons + $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + $form = $event->getForm(); + $data = $event->getData(); + + // Convert the submitted data to a string, if scalar, before + // casting it to an array + if (!is_array($data)) { + $data = (array) (string) $data; + } + + // A map from submitted values to integers + $valueMap = array_flip($data); + + // Make a copy of the value map to determine whether any unknown + // values were submitted + $unknownValues = $valueMap; + + // Reconstruct the data as mapping from child names to values + $data = array(); + + foreach ($form as $child) { + $value = $child->getConfig()->getOption('value'); + + // Add the value to $data with the child's name as key + if (isset($valueMap[$value])) { + $data[$child->getName()] = $value; + unset($unknownValues[$value]); + continue; + } + } + + // The empty value is always known, independent of whether a + // field exists for it or not + unset($unknownValues['']); + + // Throw exception if unknown values were submitted + if (count($unknownValues) > 0) { + throw new TransformationFailedException(sprintf( + 'The choices "%s" do not exist in the choice list.', + implode('", "', array_keys($unknownValues)) + )); + } + + $event->setData($data); + }); + + if (!$options['multiple']) { + // For radio lists, transform empty arrays to null + // This is kind of a hack necessary because the RadioListMapper + // is not invoked for forms without choices + $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) { + if (array() === $event->getData()) { + $event->setData(null); + } + }); } + } elseif ($options['multiple']) { + // tag without "multiple" option + $builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list'])); } if ($options['multiple'] && $options['by_reference']) { @@ -95,11 +158,16 @@ public function buildForm(FormBuilderInterface $builder, array $options) */ public function buildView(FormView $view, FormInterface $form, array $options) { + /** @var ChoiceListView $choiceListView */ + $choiceListView = $form->getConfig()->hasAttribute('choice_list_view') + ? $form->getConfig()->getAttribute('choice_list_view') + : $this->createChoiceListView($options['choice_list'], $options); + $view->vars = array_replace($view->vars, array( 'multiple' => $options['multiple'], 'expanded' => $options['expanded'], - 'preferred_choices' => $options['choice_list']->getPreferredViews(), - 'choices' => $options['choice_list']->getRemainingViews(), + 'preferred_choices' => $choiceListView->preferredChoices, + 'choices' => $choiceListView->choices, 'separator' => '-------------------', 'placeholder' => null, )); @@ -163,20 +231,39 @@ public function finishView(FormView $view, FormInterface $form, array $options) */ public function configureOptions(OptionsResolver $resolver) { - $choiceListCache = &$this->choiceListCache; + $choiceListFactory = $this->choiceListFactory; + + $choiceList = function (Options $options) use ($choiceListFactory) { + if (null !== $options['choice_loader']) { + // Due to a bug in OptionsResolver, the choices haven't been + // validated yet at this point. Remove the if statement once that + // bug is resolved + if (!$options['choice_loader'] instanceof ChoiceLoaderInterface) { + return; + } + + return $choiceListFactory->createListFromLoader( + $options['choice_loader'], + $options['choice_value'] + ); + } - $choiceList = function (Options $options) use (&$choiceListCache) { // Harden against NULL values (like in EntityType and ModelType) $choices = null !== $options['choices'] ? $options['choices'] : array(); - // Reuse existing choice lists in order to increase performance - $hash = hash('sha256', serialize(array($choices, $options['preferred_choices']))); + // Due to a bug in OptionsResolver, the choices haven't been + // validated yet at this point. Remove the if statement once that + // bug is resolved + if (!is_array($choices) && !$choices instanceof \Traversable) { + return; + } - if (!isset($choiceListCache[$hash])) { - $choiceListCache[$hash] = new SimpleChoiceList($choices, $options['preferred_choices']); + // BC when choices are in the keys, not in the values + if (!$options['choices_as_values']) { + return $choiceListFactory->createListFromFlippedChoices($choices, $options['choice_value']); } - return $choiceListCache[$hash]; + return $choiceListFactory->createListFromChoices($choices, $options['choice_value']); }; $emptyData = function (Options $options) { @@ -219,9 +306,16 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setDefaults(array( 'multiple' => false, 'expanded' => false, - 'choice_list' => $choiceList, + 'choice_list' => $choiceList, // deprecated 'choices' => array(), + 'choices_as_values' => false, + 'choice_loader' => null, + 'choice_label' => null, + 'choice_name' => null, + 'choice_value' => null, + 'choice_attr' => null, 'preferred_choices' => array(), + 'group_by' => null, 'empty_data' => $emptyData, 'empty_value' => $emptyValue, // deprecated 'placeholder' => $placeholder, @@ -236,7 +330,16 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setNormalizer('empty_value', $placeholderNormalizer); $resolver->setNormalizer('placeholder', $placeholderNormalizer); - $resolver->setAllowedTypes('choice_list', array('null', 'Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface')); + $resolver->setAllowedTypes('choice_list', array('null', 'Symfony\Component\Form\ChoiceList\ChoiceListInterface')); + $resolver->setAllowedTypes('choices', array('null', 'array', '\Traversable')); + $resolver->setAllowedTypes('choices_as_values', 'bool'); + $resolver->setAllowedTypes('choice_loader', array('null', 'Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface')); + $resolver->setAllowedTypes('choice_label', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('choice_name', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('choice_value', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('choice_attr', array('null', 'array', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('preferred_choices', array('array', '\Traversable', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('group_by', array('null', 'array', '\Traversable', 'string', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); } /** @@ -247,6 +350,21 @@ public function getName() return 'choice'; } + private static function flipRecursive($choices, &$output = array()) + { + foreach ($choices as $key => $value) { + if (is_array($value)) { + $output[$key] = array(); + self::flipRecursive($value, $output[$key]); + continue; + } + + $output[$value] = $key; + } + + return $output; + } + /** * Adds the sub fields for an expanded choice field. * @@ -256,29 +374,69 @@ public function getName() */ private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options) { - foreach ($choiceViews as $i => $choiceView) { + foreach ($choiceViews as $name => $choiceView) { + // Flatten groups if (is_array($choiceView)) { - // Flatten groups $this->addSubForms($builder, $choiceView, $options); - } else { - $choiceOpts = array( - 'value' => $choiceView->value, - 'label' => $choiceView->label, - 'translation_domain' => $options['translation_domain'], - 'block_name' => 'entry', - ); - - if ($options['multiple']) { - $choiceType = 'checkbox'; - // The user can check 0 or more checkboxes. If required - // is true, he is required to check all of them. - $choiceOpts['required'] = false; - } else { - $choiceType = 'radio'; - } + continue; + } - $builder->add($i, $choiceType, $choiceOpts); + if ($choiceView instanceof ChoiceGroupView) { + $this->addSubForms($builder, $choiceView->choices, $options); + continue; } + + $this->addSubForm($builder, $name, $choiceView, $options); + } + } + + /** + * @param FormBuilderInterface $builder + * @param $name + * @param $choiceView + * @param array $options + * + * @return mixed + */ + private function addSubForm(FormBuilderInterface $builder, $name, ChoiceView $choiceView, array $options) + { + $choiceOpts = array( + 'value' => $choiceView->value, + 'label' => $choiceView->label, + 'attr' => $choiceView->attr, + 'translation_domain' => $options['translation_domain'], + 'block_name' => 'entry', + ); + + if ($options['multiple']) { + $choiceType = 'checkbox'; + // The user can check 0 or more checkboxes. If required + // is true, he is required to check all of them. + $choiceOpts['required'] = false; + } else { + $choiceType = 'radio'; } + + $builder->add($name, $choiceType, $choiceOpts); + } + + private function createChoiceListView(ChoiceListInterface $choiceList, array $options) + { + // If no explicit grouping information is given, use the structural + // information from the "choices" option for creating groups + if (!$options['group_by'] && $options['choices']) { + $options['group_by'] = !$options['choices_as_values'] + ? ChoiceType::flipRecursive($options['choices']) + : $options['choices']; + } + + return $this->choiceListFactory->createView( + $choiceList, + $options['preferred_choices'], + $options['choice_label'], + $options['choice_name'], + $options['group_by'], + $options['choice_attr'] + ); } } diff --git a/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php index 97cdd214c28f8..65d7af246478d 100644 --- a/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php +++ b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php @@ -16,29 +16,8 @@ * * @author Bernhard Schussek */ -class ChoiceView +class ChoiceView extends \Symfony\Component\Form\ChoiceList\View\ChoiceView { - /** - * The original choice value. - * - * @var mixed - */ - public $data; - - /** - * The view representation of the choice. - * - * @var string - */ - public $value; - - /** - * The label displayed to humans. - * - * @var string - */ - public $label; - /** * Creates a new ChoiceView. * @@ -48,8 +27,6 @@ class ChoiceView */ public function __construct($data, $value, $label) { - $this->data = $data; - $this->value = $value; - $this->label = $label; + parent::__construct($label, $value, $data); } } diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index 6375542b28340..3bf84d71c82c8 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -516,6 +516,28 @@ public function testSingleChoice() ); } + public function testSingleChoiceAttributes() + { + $form = $this->factory->createNamed('name', 'choice', '&a', array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => false, + 'expanded' => false, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/select + [@name="name"] + [not(@required)] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + public function testSingleChoiceWithPreferred() { $form = $this->factory->createNamed('name', 'choice', '&a', array( @@ -776,6 +798,30 @@ public function testMultipleChoice() ); } + public function testMultipleChoiceAttributes() + { + $form = $this->factory->createNamed('name', 'choice', array('&a'), array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'required' => true, + 'multiple' => true, + 'expanded' => false, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/select + [@name="name[]"] + [@required="required"] + [@multiple="multiple"] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + public function testMultipleChoiceSkipsPlaceholder() { $form = $this->factory->createNamed('name', 'choice', array('&a'), array( @@ -842,6 +888,29 @@ public function testSingleChoiceExpanded() ); } + public function testSingleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', 'choice', '&a', array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => false, + 'expanded' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][@class="foo&bar"][not(@checked)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] + [count(./input)=3] +' + ); + } + public function testSingleChoiceExpandedWithPlaceholder() { $form = $this->factory->createNamed('name', 'choice', '&a', array( @@ -914,6 +983,32 @@ public function testMultipleChoiceExpanded() ); } + public function testMultipleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', 'choice', array('&a', '&c'), array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B', '&c' => 'Choice&C'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => true, + 'expanded' => true, + 'required' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][@class="foo&bar"][not(@checked)][not(@required)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] + /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)] + /following-sibling::label[@for="name_2"][.="[trans]Choice&C[/trans]"] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] + [count(./input)=4] +' + ); + } + public function testCountry() { $form = $this->factory->createNamed('name', 'country', 'AT'); diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php new file mode 100644 index 0000000000000..0805238f7f77b --- /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 0000000000000..34b22fe04177b --- /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 0000000000000..74cf2afb4a2af --- /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 0000000000000..031cced280287 --- /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 0000000000000..42f745e29b73e --- /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 0000000000000..8697a9cac55d6 --- /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 0000000000000..2993721c82797 --- /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 17972cbc0a85a..6a0b6db2eceb2 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -11,8 +11,9 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase { @@ -66,6 +67,16 @@ protected function tearDown() $this->objectChoices = null; } + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testChoicesOptionExpectsArrayOrTraversable() + { + $this->factory->create('choice', null, array( + 'choices' => new \stdClass(), + )); + } + /** * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */ @@ -76,6 +87,16 @@ public function testChoiceListOptionExpectsChoiceListInterface() )); } + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testChoiceLoaderOptionExpectsChoiceLoaderInterface() + { + $this->factory->create('choice', null, array( + 'choice_loader' => new \stdClass(), + )); + } + public function testChoiceListAndChoicesCanBeEmpty() { $this->factory->create('choice'); @@ -236,7 +257,118 @@ public function testSubmitSingleNonExpandedInvalidChoice() $this->assertFalse($form->isSynchronized()); } + public function testSubmitSingleNonExpandedNull() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $form->submit(null); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleNonExpandedNullNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => array(), + )); + + $form->submit(null); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + public function testSubmitSingleNonExpandedEmpty() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $form->submit(''); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleNonExpandedEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => array(), + )); + + $form->submit(''); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + public function testSubmitSingleNonExpandedFalse() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $form->submit(false); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleNonExpandedFalseNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => array(), + )); + + $form->submit(false); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + public function testSubmitSingleNonExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->objectChoices, + 'choices_as_values' => true, + 'choice_label' => 'name', + 'choice_value' => 'id', + )); + + // "id" value of the second entry + $form->submit('2'); + + $this->assertEquals($this->objectChoices[1], $form->getData()); + $this->assertEquals('2', $form->getViewData()); + } + + public function testSubmitSingleNonExpandedObjectChoicesBc() { $form = $this->factory->create('choice', null, array( 'multiple' => false, @@ -273,6 +405,37 @@ public function testSubmitMultipleNonExpanded() $this->assertEquals(array('a', 'b'), $form->getViewData()); } + public function testSubmitMultipleNonExpandedEmpty() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $form->submit(array()); + + $this->assertSame(array(), $form->getData()); + $this->assertSame(array(), $form->getViewData()); + } + + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitMultipleNonExpandedEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => false, + 'choices' => array(), + )); + + $form->submit(array()); + + $this->assertSame(array(), $form->getData()); + $this->assertSame(array(), $form->getViewData()); + } + public function testSubmitMultipleNonExpandedInvalidScalarChoice() { $form = $this->factory->create('choice', null, array( @@ -304,6 +467,23 @@ public function testSubmitMultipleNonExpandedInvalidArrayChoice() } public function testSubmitMultipleNonExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => false, + 'choices' => $this->objectChoices, + 'choices_as_values' => true, + 'choice_label' => 'name', + 'choice_value' => 'id', + )); + + $form->submit(array('2', '3')); + + $this->assertEquals(array($this->objectChoices[1], $this->objectChoices[2]), $form->getData()); + $this->assertEquals(array('2', '3'), $form->getViewData()); + } + + public function testSubmitMultipleNonExpandedObjectChoicesBc() { $form = $this->factory->create('choice', null, array( 'multiple' => true, @@ -337,13 +517,7 @@ public function testSubmitSingleExpandedRequired() $form->submit('b'); $this->assertSame('b', $form->getData()); - $this->assertSame(array( - 0 => false, - 1 => true, - 2 => false, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertSame('b', $form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -399,14 +573,7 @@ public function testSubmitSingleExpandedNonRequired() $form->submit('b'); $this->assertSame('b', $form->getData()); - $this->assertSame(array( - 0 => false, - 1 => true, - 2 => false, - 3 => false, - 4 => false, - 'placeholder' => false, - ), $form->getViewData()); + $this->assertSame('b', $form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -464,13 +631,7 @@ public function testSubmitSingleExpandedRequiredNull() $form->submit(null); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -486,6 +647,26 @@ public function testSubmitSingleExpandedRequiredNull() $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedRequiredNullNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => true, + 'choices' => array(), + )); + + $form->submit(null); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedRequiredEmpty() { $form = $this->factory->create('choice', null, array( @@ -498,13 +679,7 @@ public function testSubmitSingleExpandedRequiredEmpty() $form->submit(''); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -520,6 +695,26 @@ public function testSubmitSingleExpandedRequiredEmpty() $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedRequiredEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => true, + 'choices' => array(), + )); + + $form->submit(''); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedRequiredFalse() { $form = $this->factory->create('choice', null, array( @@ -532,13 +727,7 @@ public function testSubmitSingleExpandedRequiredFalse() $form->submit(false); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -554,6 +743,26 @@ public function testSubmitSingleExpandedRequiredFalse() $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedRequiredFalseNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => true, + 'choices' => array(), + )); + + $form->submit(false); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedNonRequiredNull() { $form = $this->factory->create('choice', null, array( @@ -566,14 +775,7 @@ public function testSubmitSingleExpandedNonRequiredNull() $form->submit(null); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - 'placeholder' => true, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -591,6 +793,26 @@ public function testSubmitSingleExpandedNonRequiredNull() $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedNonRequiredNullNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => false, + 'choices' => array(), + )); + + $form->submit(null); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedNonRequiredEmpty() { $form = $this->factory->create('choice', null, array( @@ -603,14 +825,7 @@ public function testSubmitSingleExpandedNonRequiredEmpty() $form->submit(''); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - 'placeholder' => true, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -628,6 +843,26 @@ public function testSubmitSingleExpandedNonRequiredEmpty() $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedNonRequiredEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => false, + 'choices' => array(), + )); + + $form->submit(''); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedNonRequiredFalse() { $form = $this->factory->create('choice', null, array( @@ -640,14 +875,7 @@ public function testSubmitSingleExpandedNonRequiredFalse() $form->submit(false); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - 'placeholder' => true, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -665,6 +893,26 @@ public function testSubmitSingleExpandedNonRequiredFalse() $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedNonRequiredFalseNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => false, + 'choices' => array(), + )); + + $form->submit(false); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedWithEmptyChild() { $form = $this->factory->create('choice', null, array( @@ -686,6 +934,32 @@ public function testSubmitSingleExpandedWithEmptyChild() } public function testSubmitSingleExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'choices' => $this->objectChoices, + 'choices_as_values' => true, + 'choice_label' => 'name', + 'choice_value' => 'id', + )); + + $form->submit('2'); + + $this->assertSame($this->objectChoices[1], $form->getData()); + $this->assertFalse($form[0]->getData()); + $this->assertTrue($form[1]->getData()); + $this->assertFalse($form[2]->getData()); + $this->assertFalse($form[3]->getData()); + $this->assertFalse($form[4]->getData()); + $this->assertNull($form[0]->getViewData()); + $this->assertSame('2', $form[1]->getViewData()); + $this->assertNull($form[2]->getViewData()); + $this->assertNull($form[3]->getViewData()); + $this->assertNull($form[4]->getViewData()); + } + + public function testSubmitSingleExpandedObjectChoicesBc() { $form = $this->factory->create('choice', null, array( 'multiple' => false, @@ -750,13 +1024,7 @@ public function testSubmitMultipleExpanded() $form->submit(array('a', 'c')); $this->assertSame(array('a', 'c'), $form->getData()); - $this->assertSame(array( - 0 => true, - 1 => false, - 2 => true, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertSame(array('a', 'c'), $form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -849,6 +1117,22 @@ public function testSubmitMultipleExpandedEmpty() $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitMultipleExpandedEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => true, + 'choices' => array(), + )); + + $form->submit(array()); + + $this->assertSame(array(), $form->getData()); + } + public function testSubmitMultipleExpandedWithEmptyChild() { $form = $this->factory->create('choice', null, array( @@ -873,6 +1157,32 @@ public function testSubmitMultipleExpandedWithEmptyChild() } public function testSubmitMultipleExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => true, + 'choices' => $this->objectChoices, + 'choices_as_values' => true, + 'choice_label' => 'name', + 'choice_value' => 'id', + )); + + $form->submit(array('1', '2')); + + $this->assertSame(array($this->objectChoices[0], $this->objectChoices[1]), $form->getData()); + $this->assertTrue($form[0]->getData()); + $this->assertTrue($form[1]->getData()); + $this->assertFalse($form[2]->getData()); + $this->assertFalse($form[3]->getData()); + $this->assertFalse($form[4]->getData()); + $this->assertSame('1', $form[0]->getViewData()); + $this->assertSame('2', $form[1]->getViewData()); + $this->assertNull($form[2]->getViewData()); + $this->assertNull($form[3]->getViewData()); + $this->assertNull($form[4]->getViewData()); + } + + public function testSubmitMultipleExpandedObjectChoicesBc() { $form = $this->factory->create('choice', null, array( 'multiple' => true, @@ -1134,10 +1444,10 @@ public function testPassChoicesToView() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('a', 'a', 'A'), - new ChoiceView('b', 'b', 'B'), - new ChoiceView('c', 'c', 'C'), - new ChoiceView('d', 'd', 'D'), + new ChoiceView('A', 'a', 'a'), + new ChoiceView('B', 'b', 'b'), + new ChoiceView('C', 'c', 'c'), + new ChoiceView('D', 'd', 'd'), ), $view->vars['choices']); } @@ -1151,12 +1461,12 @@ public function testPassPreferredChoicesToView() $view = $form->createView(); $this->assertEquals(array( - 0 => new ChoiceView('a', 'a', 'A'), - 2 => new ChoiceView('c', 'c', 'C'), + 0 => new ChoiceView('A', 'a', 'a'), + 2 => new ChoiceView('C', 'c', 'c'), ), $view->vars['choices']); $this->assertEquals(array( - 1 => new ChoiceView('b', 'b', 'B'), - 3 => new ChoiceView('d', 'd', 'D'), + 1 => new ChoiceView('B', 'b', 'b'), + 3 => new ChoiceView('D', 'd', 'd'), ), $view->vars['preferred_choices']); } @@ -1169,21 +1479,21 @@ public function testPassHierarchicalChoicesToView() $view = $form->createView(); $this->assertEquals(array( - 'Symfony' => array( - 0 => new ChoiceView('a', 'a', 'Bernhard'), - 2 => new ChoiceView('c', 'c', 'Kris'), - ), - 'Doctrine' => array( - 4 => new ChoiceView('e', 'e', 'Roman'), - ), + 'Symfony' => new ChoiceGroupView('Symfony', array( + 0 => new ChoiceView('Bernhard', 'a', 'a'), + 2 => new ChoiceView('Kris', 'c', 'c'), + )), + 'Doctrine' => new ChoiceGroupView('Doctrine', array( + 4 => new ChoiceView('Roman', 'e', 'e'), + )), ), $view->vars['choices']); $this->assertEquals(array( - 'Symfony' => array( - 1 => new ChoiceView('b', 'b', 'Fabien'), - ), - 'Doctrine' => array( - 3 => new ChoiceView('d', 'd', 'Jon'), - ), + 'Symfony' => new ChoiceGroupView('Symfony', array( + 1 => new ChoiceView('Fabien', 'b', 'b'), + )), + 'Doctrine' => new ChoiceGroupView('Doctrine', array( + 3 => new ChoiceView('Jon', 'd', 'd'), + )), ), $view->vars['preferred_choices']); } @@ -1194,15 +1504,18 @@ public function testPassChoiceDataToView() $obj3 = (object) array('value' => 'c', 'label' => 'C'); $obj4 = (object) array('value' => 'd', 'label' => 'D'); $form = $this->factory->create('choice', null, array( - 'choice_list' => new ObjectChoiceList(array($obj1, $obj2, $obj3, $obj4), 'label', array(), null, 'value'), + 'choices' => array($obj1, $obj2, $obj3, $obj4), + 'choices_as_values' => true, + 'choice_label' => 'label', + 'choice_value' => 'value', )); $view = $form->createView(); $this->assertEquals(array( - new ChoiceView($obj1, 'a', 'A'), - new ChoiceView($obj2, 'b', 'B'), - new ChoiceView($obj3, 'c', 'C'), - new ChoiceView($obj4, 'd', 'D'), + new ChoiceView('A', 'a', $obj1), + new ChoiceView('B', 'b', $obj2), + new ChoiceView('C', 'c', $obj3), + new ChoiceView('D', 'd', $obj4), ), $view->vars['choices']); } @@ -1226,47 +1539,6 @@ public function testInitializeWithEmptyChoices() )); } - // https://github.com/symfony/symfony/issues/10409 - public function testReuseNonUtf8ChoiceLists() - { - $form1 = $this->factory->createNamed('name', 'choice', null, array( - 'choices' => array( - 'meter' => 'm', - 'millimeter' => 'mm', - 'micrometer' => chr(181).'meter', - ), - )); - - $form2 = $this->factory->createNamed('name', 'choice', null, array( - 'choices' => array( - 'meter' => 'm', - 'millimeter' => 'mm', - 'micrometer' => chr(181).'meter', - ), - )); - - $form3 = $this->factory->createNamed('name', 'choice', null, array( - 'choices' => array( - 'meter' => 'm', - 'millimeter' => 'mm', - 'micrometer' => null, - ), - )); - - // $form1 and $form2 use the same ChoiceList - $this->assertSame( - $form1->getConfig()->getOption('choice_list'), - $form2->getConfig()->getOption('choice_list') - ); - - // $form3 doesn't, but used to use the same when using json_encode() - // instead of serialize for the hashing algorithm - $this->assertNotSame( - $form1->getConfig()->getOption('choice_list'), - $form3->getConfig()->getOption('choice_list') - ); - } - public function testInitializeWithDefaultObjectChoice() { $obj1 = (object) array('value' => 'a', 'label' => 'A'); @@ -1275,7 +1547,10 @@ public function testInitializeWithDefaultObjectChoice() $obj4 = (object) array('value' => 'd', 'label' => 'D'); $form = $this->factory->create('choice', null, array( - 'choice_list' => new ObjectChoiceList(array($obj1, $obj2, $obj3, $obj4), 'label', array(), null, 'value'), + 'choices' => array($obj1, $obj2, $obj3, $obj4), + 'choices_as_values' => true, + 'choice_label' => 'label', + 'choice_value' => 'value', // Used to break because "data_class" was inferred, which needs to // remain null in every case (because it refers to the view format) 'data' => $obj3, diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/CountryTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/CountryTypeTest.php index 7c2cebb542064..3b684f133edd5 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/CountryTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/CountryTypeTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Test\TypeTestCase as TestCase; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Intl\Util\IntlTestHelper; class CountryTypeTest extends TestCase @@ -31,11 +31,11 @@ public function testCountriesAreSelectable() $choices = $view->vars['choices']; // Don't check objects for identity - $this->assertContains(new ChoiceView('DE', 'DE', 'Germany'), $choices, '', false, false); - $this->assertContains(new ChoiceView('GB', 'GB', 'United Kingdom'), $choices, '', false, false); - $this->assertContains(new ChoiceView('US', 'US', 'United States'), $choices, '', false, false); - $this->assertContains(new ChoiceView('FR', 'FR', 'France'), $choices, '', false, false); - $this->assertContains(new ChoiceView('MY', 'MY', 'Malaysia'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Germany', 'DE', 'DE'), $choices, '', false, false); + $this->assertContains(new ChoiceView('United Kingdom', 'GB', 'GB'), $choices, '', false, false); + $this->assertContains(new ChoiceView('United States', 'US', 'US'), $choices, '', false, false); + $this->assertContains(new ChoiceView('France', 'FR', 'FR'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Malaysia', 'MY', 'MY'), $choices, '', false, false); } public function testUnknownCountryIsNotIncluded() diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/CurrencyTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/CurrencyTypeTest.php index 702262f580382..802c715b0c4a9 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/CurrencyTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/CurrencyTypeTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Test\TypeTestCase as TestCase; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Intl\Util\IntlTestHelper; class CurrencyTypeTest extends TestCase @@ -30,8 +30,8 @@ public function testCurrenciesAreSelectable() $view = $form->createView(); $choices = $view->vars['choices']; - $this->assertContains(new ChoiceView('EUR', 'EUR', 'Euro'), $choices, '', false, false); - $this->assertContains(new ChoiceView('USD', 'USD', 'US Dollar'), $choices, '', false, false); - $this->assertContains(new ChoiceView('SIT', 'SIT', 'Slovenian Tolar'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Euro', 'EUR', 'EUR'), $choices, '', false, false); + $this->assertContains(new ChoiceView('US Dollar', 'USD', 'USD'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Slovenian Tolar', 'SIT', 'SIT'), $choices, '', false, false); } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php index d8b3312b1f6bd..a658b90465f12 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\FormError; use Symfony\Component\Form\Test\TypeTestCase as TestCase; use Symfony\Component\Intl\Util\IntlTestHelper; @@ -490,8 +490,8 @@ public function testMonthsOption() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['month']->vars['choices']); } @@ -505,8 +505,8 @@ public function testMonthsOptionShortFormat() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('1', '1', 'Jän'), - new ChoiceView('4', '4', 'Apr.'), + new ChoiceView('Jän', '1', '1'), + new ChoiceView('Apr.', '4', '4'), ), $view['month']->vars['choices']); } @@ -520,8 +520,8 @@ public function testMonthsOptionLongFormat() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('1', '1', 'Jänner'), - new ChoiceView('4', '4', 'April'), + new ChoiceView('Jänner', '1', '1'), + new ChoiceView('April', '4', '4'), ), $view['month']->vars['choices']); } @@ -535,8 +535,8 @@ public function testMonthsOptionLongFormatWithDifferentTimezone() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('1', '1', 'Jänner'), - new ChoiceView('4', '4', 'April'), + new ChoiceView('Jänner', '1', '1'), + new ChoiceView('April', '4', '4'), ), $view['month']->vars['choices']); } @@ -549,8 +549,8 @@ public function testIsDayWithinRangeReturnsTrueIfWithin() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['day']->vars['choices']); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/LanguageTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/LanguageTypeTest.php index e234811887293..9445c74fd6f31 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/LanguageTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/LanguageTypeTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Test\TypeTestCase as TestCase; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Intl\Util\IntlTestHelper; class LanguageTypeTest extends TestCase @@ -30,11 +30,11 @@ public function testCountriesAreSelectable() $view = $form->createView(); $choices = $view->vars['choices']; - $this->assertContains(new ChoiceView('en', 'en', 'English'), $choices, '', false, false); - $this->assertContains(new ChoiceView('en_GB', 'en_GB', 'British English'), $choices, '', false, false); - $this->assertContains(new ChoiceView('en_US', 'en_US', 'American English'), $choices, '', false, false); - $this->assertContains(new ChoiceView('fr', 'fr', 'French'), $choices, '', false, false); - $this->assertContains(new ChoiceView('my', 'my', 'Burmese'), $choices, '', false, false); + $this->assertContains(new ChoiceView('English', 'en', 'en'), $choices, '', false, false); + $this->assertContains(new ChoiceView('British English', 'en_GB', 'en_GB'), $choices, '', false, false); + $this->assertContains(new ChoiceView('American English', 'en_US', 'en_US'), $choices, '', false, false); + $this->assertContains(new ChoiceView('French', 'fr', 'fr'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Burmese', 'my', 'my'), $choices, '', false, false); } public function testMultipleLanguagesIsNotIncluded() @@ -43,6 +43,6 @@ public function testMultipleLanguagesIsNotIncluded() $view = $form->createView(); $choices = $view->vars['choices']; - $this->assertNotContains(new ChoiceView('mul', 'mul', 'Mehrsprachig'), $choices, '', false, false); + $this->assertNotContains(new ChoiceView('Mehrsprachig', 'mul', 'mul'), $choices, '', false, false); } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/LocaleTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/LocaleTypeTest.php index 6c1951a4e91df..0b729a3b31507 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/LocaleTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/LocaleTypeTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Test\TypeTestCase as TestCase; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Intl\Util\IntlTestHelper; class LocaleTypeTest extends TestCase @@ -30,8 +30,8 @@ public function testLocalesAreSelectable() $view = $form->createView(); $choices = $view->vars['choices']; - $this->assertContains(new ChoiceView('en', 'en', 'English'), $choices, '', false, false); - $this->assertContains(new ChoiceView('en_GB', 'en_GB', 'English (United Kingdom)'), $choices, '', false, false); - $this->assertContains(new ChoiceView('zh_Hant_MO', 'zh_Hant_MO', 'Chinese (Traditional, Macau SAR China)'), $choices, '', false, false); + $this->assertContains(new ChoiceView('English', 'en', 'en'), $choices, '', false, false); + $this->assertContains(new ChoiceView('English (United Kingdom)', 'en_GB', 'en_GB'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Chinese (Traditional, Macau SAR China)', 'zh_Hant_MO', 'zh_Hant_MO'), $choices, '', false, false); } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php index dfa8fbc5a1a53..c3754695b16b9 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\FormError; use Symfony\Component\Form\Test\TypeTestCase as TestCase; use Symfony\Component\Intl\Util\IntlTestHelper; @@ -319,8 +319,8 @@ public function testHoursOption() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['hour']->vars['choices']); } @@ -333,8 +333,8 @@ public function testIsMinuteWithinRangeReturnsTrueIfWithin() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['minute']->vars['choices']); } @@ -348,8 +348,8 @@ public function testIsSecondWithinRangeReturnsTrueIfWithin() $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['second']->vars['choices']); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php index 81df20cbb902f..78399547400fc 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; class TimezoneTypeTest extends \Symfony\Component\Form\Test\TypeTestCase { @@ -22,9 +22,9 @@ public function testTimezonesAreSelectable() $choices = $view->vars['choices']; $this->assertArrayHasKey('Africa', $choices); - $this->assertContains(new ChoiceView('Africa/Kinshasa', 'Africa/Kinshasa', 'Kinshasa'), $choices['Africa'], '', false, false); + $this->assertContains(new ChoiceView('Kinshasa', 'Africa/Kinshasa', 'Africa/Kinshasa'), $choices['Africa'], '', false, false); $this->assertArrayHasKey('America', $choices); - $this->assertContains(new ChoiceView('America/New_York', 'America/New_York', 'New York'), $choices['America'], '', false, false); + $this->assertContains(new ChoiceView('New York', 'America/New_York', 'America/New_York'), $choices['America'], '', false, false); } } 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 | 61 ++++++----- .../Bridge/Doctrine/Form/Type/EntityType.php | 65 ++--------- 4 files changed, 96 insertions(+), 133 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 d1f7971e63cb3..c00c258ca56ba 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -23,7 +23,7 @@ * * @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 @@ public static function getIdValue(ObjectManager $om, ClassMetadata $classMetadat * 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 @@ public function loadChoiceList($value = null) 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 @@ public function loadValuesForChoices(array $entities, $value = null) 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 @@ public function loadChoicesForValues(array $values, $value = null) 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 3566a33d7d583..f3d4ff48f6e61 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -24,7 +24,7 @@ * @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 6c90a2eeb098d..76e78cb1f0bda 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -13,7 +13,7 @@ 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 @@ public function configureOptions(OptionsResolver $resolver) $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'], - )); + // 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'], + )); - 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']); + if (isset($choiceLoaders[$hash])) { + return $choiceLoaders[$hash]; } + } - $choiceLoaders[$hash] = new EntityChoiceLoader( - $choiceListFactory, - $options['em'], - $options['class'], - $loader - ); + 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 $choiceLoaders[$hash]; + return $choiceLoader; } }; @@ -131,7 +142,7 @@ public function configureOptions(OptionsResolver $resolver) }; // 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 @@ public function configureOptions(OptionsResolver $resolver) $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 e9b89302dbb33..675c289c76de3 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -17,71 +17,22 @@ 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 | 58 ++++++++++++------- .../Bridge/Doctrine/Form/Type/EntityType.php | 6 +- .../Tests/Form/Type/EntityTypeTest.php | 31 +++++----- 4 files changed, 58 insertions(+), 39 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index 9cfdd1fe4855b..9d34601c9f309 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -51,6 +51,8 @@ public function __construct($queryBuilder, $manager = null, $class = null) 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 76e78cb1f0bda..ed8ded5badbbf 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -13,6 +13,7 @@ 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\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 @@ public function configureOptions(OptionsResolver $resolver) $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'], - )); - - if (isset($choiceLoaders[$hash])) { - return $choiceLoaders[$hash]; - } + // 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; + + $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 @@ public function configureOptions(OptionsResolver $resolver) $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 @@ public function configureOptions(OptionsResolver $resolver) 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 @@ public function configureOptions(OptionsResolver $resolver) $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 675c289c76de3..236b9290c7f8d 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 9f1591f308253..7d80819f6b69e 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 @@ 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\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 @@ public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder() } /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */ public function testConfigureQueryBuilderWithNonQueryBuilderAndNonClosure() { @@ -786,8 +788,7 @@ public function testLoaderCaching() $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 @@ public function testLoaderCaching() $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 @@ public function testLoaderCaching() 'property3' => 2, )); - $reflectionClass = new \ReflectionObject($entityType); - $reflectionProperty = $reflectionClass->getProperty('loaderCache'); - $reflectionProperty->setAccessible(true); - - $loaders = $reflectionProperty->getValue($entityType); - - $reflectionProperty->setAccessible(false); + $choiceList1 = $form->get('property1')->getConfig()->getOption('choice_list'); + $choiceList2 = $form->get('property2')->getConfig()->getOption('choice_list'); + $choiceList3 = $form->get('property3')->getConfig()->getOption('choice_list'); - $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 | 86 +++++++------------ .../Factory/DefaultChoiceListFactory.php | 48 ++--------- .../Tests/ChoiceList/ArrayChoiceListTest.php | 45 +++++++++- .../ChoiceList/ArrayKeyChoiceListTest.php | 57 ++++++------ .../Factory/DefaultChoiceListFactoryTest.php | 3 +- 6 files changed, 156 insertions(+), 143 deletions(-) diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php index 0dfc0f9945a0d..a3987cc02cff7 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 @@ public function getValuesForChoices(array $choices) { $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 d79747e0485b8..918c278f0608b 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php @@ -43,21 +43,14 @@ * @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 @@ public static function toArrayKey($choice) * 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); - - 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); - } + $choices = array_map(array(__CLASS__, 'toArrayKey'), $choices); - /** - * {@inheritdoc} - */ - public function getChoices() - { - return $this->choices; - } + if (null === $value) { + $value = function ($choice) { + return (string) $choice; + }; + $this->useChoicesAsValues = true; + } - /** - * {@inheritdoc} - */ - public function getValues() - { - return $this->values; + parent::__construct($choices, $value); } /** @@ -152,11 +120,15 @@ public function getValues() */ public function getChoicesForValues(array $values) { - $values = array_map('strval', $values); + if ($this->useChoicesAsValues) { + $values = array_map('strval', $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)); + } - // 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 @@ 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)); + 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 dd191eea39a1e..f0ea07cdf6337 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -89,10 +89,6 @@ public function createListFromChoices($choices, $value = null) 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 @@ public function createListFromChoices($choices, $value = null) // 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 @@ public function createListFromFlippedChoices($choices, $value = null) 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 @@ public function createListFromFlippedChoices($choices, $value = null) // 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); + $value = function ($choice) { + return (string) $choice; + }; } - 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 34b22fe04177b..0dffd08374934 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php @@ -29,7 +29,9 @@ protected function setUp() protected function createChoiceList() { - return new ArrayChoiceList($this->getChoices(), $this->getValues()); + $i = 0; + + return new ArrayChoiceList($this->getChoices()); } protected function getChoices() @@ -49,4 +51,45 @@ public function testFailIfKeyMismatch() { 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 74cf2afb4a2af..78263502d6dd0 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php @@ -29,7 +29,7 @@ protected function setUp() protected function createChoiceList() { - return new ArrayKeyChoiceList($this->getChoices(), $this->getValues()); + return new ArrayKeyChoiceList($this->getChoices()); } protected function getChoices() @@ -42,14 +42,6 @@ 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)); @@ -102,7 +94,7 @@ public function testGetValuesForChoicesConvertsChoicesToArrayKeys() */ 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 @@ public function provideConvertibleChoices() */ public function testFailIfInvalidChoices(array $choices) { - new ArrayKeyChoiceList($choices, range(0, count($choices) - 1)); + new ArrayKeyChoiceList($choices); } /** @@ -157,31 +149,48 @@ public function provideInvalidChoices() /** * @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 42f745e29b73e..b144699892c34 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 @@ protected function setUp() $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 | 132 +++++++++----- .../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, 362 insertions(+), 260 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 c00c258ca56ba..4b10b45855dd6 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 @@ public static function getIdValue(ObjectManager $om, ClassMetadata $classMetadat * @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 @@ public function loadChoiceList($value = null) ? $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 @@ public function loadValuesForChoices(array $objects, $value = null) // 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 @@ public function loadChoicesForValues(array $values, $value = null) // 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 @@ public function loadChoicesForValues(array $values, $value = null) // "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 0000000000000..f6164725fdb4e --- /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 ed8ded5badbbf..6020d95e925c3 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\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 @@ public function configureOptions(OptionsResolver $resolver) { $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 @@ public function configureOptions(OptionsResolver $resolver) $choiceListFactory, $options['em'], $options['class'], + $options['id_reader'], $entityLoader ); @@ -120,24 +187,18 @@ public function configureOptions(OptionsResolver $resolver) } // 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)); - - // 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; - }; + /** @var IdReader $idReader */ + $idReader = $options['id_reader']; + + // 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 @@ public function configureOptions(OptionsResolver $resolver) // 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 @@ public function configureOptions(OptionsResolver $resolver) return $em; }; - $choicesNormalizer = function (Options $options, $entities) { - if (null === $entities || 0 === count($entities)) { - return $entities; - } - - // Make sure that the entities are indexed by their ID - /** @var ObjectManager $om */ - $om = $options['em']; - $classMetadata = $om->getClassMetadata($options['class']); - $ids = $classMetadata->getIdentifierFieldNames(); - - // We cannot use composite IDs as indices. In that case, keep the - // given indices - if (count($ids) > 1) { - return $entities; - } - - $entitiesById = array(); - - foreach ($entities as $entity) { - $id = 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 @@ public function configureOptions(OptionsResolver $resolver) '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 7d80819f6b69e..95e11aff6ffa2 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\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 a3987cc02cff7..515cd15a830b9 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 @@ public function __construct(array $choices, $value = null, $compareByValue = fal $this->choices = $choices; $this->values = array(); - $this->valueCallback = $compareByValue ? $value : null; + $this->valueCallback = $value; if (null === $value) { $i = 0; @@ -76,7 +73,7 @@ public function __construct(array $choices, $value = null, $compareByValue = fal } } 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 @@ public function getValuesForChoices(array $choices) // 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 f0ea07cdf6337..d974bf7d4f22e 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -191,10 +191,7 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, // 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 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, 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 @@ private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $val } } - 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 bf91d85eea64b..131690a6ff7aa 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -91,7 +91,15 @@ public function createListFromChoices($choices, $value = null) 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 91e6bfe4088de..3dea398c6d4e2 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 0dffd08374934..129a093b89c68 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php @@ -54,15 +54,15 @@ public function testFailIfKeyMismatch() 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 @@ public function testCompareChoicesByIdentityByDefault() $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 78263502d6dd0..5024a60db73bf 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php @@ -183,14 +183,14 @@ public function provideConvertibleValues() 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 b144699892c34..360d46729fdf5 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 @@ 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( @@ -217,26 +200,6 @@ 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 */ @@ -306,23 +269,6 @@ function ($choice) { $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 @@ function ($choice) { $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 @@ function ($object) use ($obj2, $obj3) { 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 @@ function ($object, $key) use ($obj2, $obj3) { $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 @@ function ($object, $key) { $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 @@ function ($object, $key) { $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 @@ function ($object, $key) { $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 @@ function ($object, $key) { $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 4b10b45855dd6..5456c0eedb537 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -97,25 +97,12 @@ public function loadChoiceList($value = null) } /** - * 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 @@ public function loadValuesForChoices(array $objects, $value = null) $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 @@ public function loadValuesForChoices(array $objects, $value = null) 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 f3d4ff48f6e61..1d4232306df9b 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -129,6 +129,8 @@ public function __construct(ObjectManager $manager, $class, $labelPath = null, E } 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 9d34601c9f309..92f00cb24332e 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 @@ public function __construct($queryBuilder, $manager = null, $class = null) 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 6020d95e925c3..b8d03c0eb1872 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -116,27 +116,10 @@ public function configureOptions(OptionsResolver $resolver) $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 @@ public function configureOptions(OptionsResolver $resolver) 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 @@ public function configureOptions(OptionsResolver $resolver) 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 @@ public function configureOptions(OptionsResolver $resolver) '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 236b9290c7f8d..87b3ee42cb956 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 515cd15a830b9..f55154b08528c 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 918c278f0608b..7973072f3205c 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php @@ -41,7 +41,7 @@ * @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 @@ public function __construct(array $choices, $value = null) } 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 fb43ac87594c1..3a2702a335e3f 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php @@ -91,10 +91,6 @@ public function getDecoratedFactory() */ 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 @@ public function createListFromChoices($choices, $value = null) */ 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 d974bf7d4f22e..31527a9f349aa 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -85,10 +85,6 @@ public static function flattenFlipped(array $array, &$output) */ 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 @@ public function createListFromChoices($choices, $value = null) */ 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 @@ public function createListFromFlippedChoices($choices, $value = null) */ 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 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = nul */ 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 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, ); } - // 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 131690a6ff7aa..f6fd823784c6e 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -153,16 +153,12 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = nul /** * {@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 3dea398c6d4e2..092e2c4644b0a 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 2f7b287b63e8b..817e03ec72168 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 @@ * @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 @@ public function __construct($choices, array $labels, array $preferredChoices = a } $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 22354e09d852e..aef70aef87448 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 @@ * @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 24232bc1d67af..f3a7cc028aff8 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 @@ * @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 606de43af3ef5..c356ce466bc1d 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 @@ * @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 @@ public function __construct($choices, $labelPath = null, array $preferredChoices $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 50a3eb5f4a29f..6dd8fb091e26d 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 @@ * @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 aecdb2fad0c73..19db183a28394 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 @@ public function mapFormsToData($radios, &$choice) 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 a0b5039317b51..108c1ca6a3c43 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 @@ * @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 @@ public function __construct(ChoiceListInterface $choiceList, $placeholderPresent { $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 c38c363329012..a632bc03c7000 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 @@ * @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 297987f799729..85b08c7b32aa7 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 @@ * 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 d5067b6e33500..8641ea725dca4 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 @@ * 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 @@ public function __construct(ChoiceListInterface $choiceList, $placeholderPresent { $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 7e80a00bdec99..a950be8d8903b 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 @@ public function configureOptions(OptionsResolver $resolver) { $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 @@ public function configureOptions(OptionsResolver $resolver) // 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 @@ public function configureOptions(OptionsResolver $resolver) 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 @@ public function configureOptions(OptionsResolver $resolver) '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 65d7af246478d..0cbeecab9f94a 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 031cced280287..716468276a7b9 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 @@ protected function setUp() $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 @@ public function testCreateFromChoicesDifferentValueClosure() $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 360d46729fdf5..a2b817ed8d8ba 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 @@ protected function setUp() $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 @@ function ($object) { return $object->value; } $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 @@ public function testCreateFromLoaderWithValues() $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 b8d03c0eb1872..a478574190d64 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -228,7 +228,9 @@ public function configureOptions(OptionsResolver $resolver) // 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 @@ public function configureOptions(OptionsResolver $resolver) // 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 @@ public function configureOptions(OptionsResolver $resolver) $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 9b60c87661402..3226d69d1a3b0 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\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 @@ protected function tearDown() * @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 @@ public function testEntitiesMustHaveAToStringMethod() /** * @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 @@ public function testChoicesMustBeManaged() $choiceList->getChoices(); } - public function testInitExplicitChoices() + public function testLegacyInitExplicitChoices() { $entity1 = new SingleIntIdEntity(1, 'Foo'); $entity2 = new SingleIntIdEntity(2, 'Bar'); @@ -141,7 +146,7 @@ public function testInitExplicitChoices() $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 @@ public function testInitEmptyChoices() $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 @@ public function testInitNestedChoices() ), $choiceList->getRemainingViews()); } - public function testGroupByPropertyPath() + public function testLegacyGroupByPropertyPath() { $item1 = new GroupableEntity(1, 'Foo', 'Group1'); $item2 = new GroupableEntity(2, 'Bar', 'Group1'); @@ -224,7 +229,7 @@ public function testGroupByPropertyPath() ), $choiceList->getRemainingViews()); } - public function testGroupByInvalidPropertyPathReturnsFlatChoices() + public function testLegacyGroupByInvalidPropertyPathReturnsFlatChoices() { $item1 = new GroupableEntity(1, 'Foo', 'Group1'); $item2 = new GroupableEntity(2, 'Bar', 'Group1'); @@ -251,7 +256,7 @@ public function testGroupByInvalidPropertyPathReturnsFlatChoices() ), $choiceList->getChoices()); } - public function testInitShorthandEntityName() + public function testLegacyInitShorthandEntityName() { $item1 = new SingleIntIdEntity(1, 'Foo'); $item2 = new SingleIntIdEntity(2, 'Bar'); @@ -267,13 +272,8 @@ public function testInitShorthandEntityName() $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 90cbf1d7c8b31..a2ee7cdc8a64f 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 @@ /** * @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 52d04c38798a5..f655784004fbb 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 @@ /** * @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 690d4b3d2300c..629b399ac36a7 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 @@ /** * @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 5740a2ff9434d..15436a86271ec 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 @@ /** * @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 9c72ccccd91a4..422295feb1e04 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 @@ /** * @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 dd53bf422615f..2fa11f0d0bb76 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 @@ /** * @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 fa5bb80ae7be8..c093782ff0ec7 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 @@ /** * @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 5b25b49a710bf..6600e49e89fdb 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 @@ /** * @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 9fba5b9295a08..23329e80df3c2 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 @@ /** * @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 95e11aff6ffa2..27d1d88e43ba3 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 @@ public function testSetDataToUninitializedEntityWithNonRequired() '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 @@ public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder() 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => 'name', 'query_builder' => $qb, )); @@ -294,7 +294,7 @@ public function testSubmitSingleNonExpandedSingleIdentifier() 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -316,7 +316,7 @@ public function testSubmitSingleNonExpandedCompositeIdentifier() 'expanded' => false, 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); // the collection key is used here @@ -340,7 +340,7 @@ public function testSubmitMultipleNonExpandedSingleIdentifier() 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit(array('1', '3')); @@ -365,7 +365,7 @@ public function testSubmitMultipleNonExpandedSingleIdentifierForExistingData() 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $existing = new ArrayCollection(array(0 => $entity2)); @@ -396,7 +396,7 @@ public function testSubmitMultipleNonExpandedCompositeIdentifier() '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 @@ public function testSubmitMultipleNonExpandedCompositeIdentifierExistingData() 'expanded' => false, 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $existing = new ArrayCollection(array(0 => $entity2)); @@ -452,7 +452,7 @@ public function testSubmitSingleExpanded() 'expanded' => true, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -478,7 +478,7 @@ public function testSubmitMultipleExpanded() 'expanded' => true, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit(array('1', '3')); @@ -508,7 +508,7 @@ public function testOverrideChoices() '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 @@ public function testGroupByChoices() '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 @@ public function testPreferredChoices() '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 @@ public function testOverrideChoicesWithPreferredChoices() '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 @@ public function testDisallowChoicesThatAreNotIncludedChoicesSingleIdentifier() 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'choices' => array($entity1, $entity2), - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('3'); @@ -623,7 +623,7 @@ public function testDisallowChoicesThatAreNotIncludedChoicesCompositeIdentifier( 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, 'choices' => array($entity1, $entity2), - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -647,7 +647,7 @@ public function testDisallowChoicesThatAreNotIncludedQueryBuilderSingleIdentifie '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 @@ public function testDisallowChoicesThatAreNotIncludedQueryBuilderAsClosureSingle return $repository->createQueryBuilder('e') ->where('e.id IN (1, 2)'); }, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('3'); @@ -695,7 +695,7 @@ public function testDisallowChoicesThatAreNotIncludedQueryBuilderAsClosureCompos return $repository->createQueryBuilder('e') ->where('e.id1 IN (10, 50)'); }, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -715,7 +715,7 @@ public function testSubmitSingleStringIdentifier() 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_STRING_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('foo'); @@ -736,7 +736,7 @@ public function testSubmitCompositeStringIdentifier() '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 @@ public function testGetManagerForClassIfNoEm() $this->factory->createNamed('name', 'entity', null, array( 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => 'name', )); } @@ -775,7 +775,7 @@ public function testExplicitEm() $this->factory->createNamed('name', 'entity', null, array( 'em' => $this->em, 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); } @@ -852,20 +852,42 @@ public function testCacheChoiceLists() '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 7973072f3205c..30709108e8fdd 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php @@ -39,9 +39,6 @@ * ``` * * @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 @@ public function __construct(array $choices, $value = null) } 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 1c8378262135c..2b4d026db7589 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 @@ public function reverseTransform($value) 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 a950be8d8903b..a597e1d4f4126 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 @@ public function configureOptions(OptionsResolver $resolver) { $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 @@ public function configureOptions(OptionsResolver $resolver) 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 @@ public function configureOptions(OptionsResolver $resolver) $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 68ef4dca4f263..710c30c6c50f7 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 @@ protected function setUp() } } - 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 @@ public function testLegacyGetIndicesForChoices() $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 @@ public function testLegacyGetIndicesForChoicesPreservesKeys() $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 @@ public function testLegacyGetIndicesForChoicesPreservesOrder() $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 @@ public function testLegacyGetIndicesForChoicesIgnoresNonExistingChoices() $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 @@ public function testLegacyGetIndicesForChoicesEmpty() $this->assertSame(array(), $this->list->getIndicesForChoices(array())); } - /** - * @group legacy - */ public function testLegacyGetIndicesForValues() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -227,9 +211,6 @@ public function testLegacyGetIndicesForValues() $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 @@ public function testLegacyGetIndicesForValuesPreservesKeys() $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 @@ public function testLegacyGetIndicesForValuesPreservesOrder() $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 @@ public function testLegacyGetIndicesForValuesIgnoresNonExistingValues() $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 @@ public function testLegacyGetIndicesForValuesEmpty() $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 538bbc1b3d3a4..25b4fdd45be0e 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 @@ 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 @@ protected function setUp() 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 @@ public function testInitArray() * 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 @@ public function testInitNestedTraversable() ), $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 @@ public function testInitNestedArray() /** * @expectedException \InvalidArgumentException */ - public function testInitWithInsufficientLabels() + public function testLegacyInitWithInsufficientLabels() { $this->list = new ChoiceList( array($this->obj1, $this->obj2), @@ -105,7 +108,7 @@ public function testInitWithInsufficientLabels() ); } - 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 0e5e2e6527465..15018b2830302 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\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 @@ protected function tearDown() $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 @@ public function testLegacyGetIndicesForValues() $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 @@ public function testGetValuesForChoices() /** * @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 2bb06349aef86..63dc8a9ea1975 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 @@ public function __toString() } } +/** + * @group legacy + */ class ObjectChoiceListTest extends AbstractChoiceListTest { private $obj1; @@ -49,7 +52,7 @@ protected function setUp() 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 @@ public function testInitArray() $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 @@ public function testInitNestedArray() ), $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 @@ public function testInitArrayWithGroupPath() /** * @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 @@ public function testInitArrayWithGroupPathThrowsExceptionIfNestedArray() ); } - 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 @@ public function testInitArrayWithValuePath() $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 @@ public function testInitArrayUsesToString() /** * @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 @@ public function testLegacyGetIndicesForChoicesWithValuePathIgnoresNonExistingCho $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 @@ public function testGetValuesForChoicesWithValuePath() $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 @@ public function testGetValuesForChoicesWithValuePathPreservesKeys() $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 @@ public function testGetValuesForChoicesWithValuePathPreservesOrder() $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 3a5804ef28e03..ddf714f793b62 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 @@ 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 @@ public function testInitArray() $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 @@ public function testInitNestedArray() /** * @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 b351790c458ec..0fd5fb92d97e1 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 @@ 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 @@ public function testLegacyGetIndicesForChoicesDealsWithNumericChoices() $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 @@ public function testLegacyGetIndicesForValuesDealsWithNumericValues() $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 bbae0621ce724..c58d072f47434 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 @@ public function transformProvider() { 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 @@ public function reverseTransformProvider() 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 87f5018b0483e..a7dc40aca225f 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 @@ protected function tearDown() 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 @@ public function testReverseTransform() { // 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 426293395c9f3..cae43b6e79109 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\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 @@ protected function tearDown() $listener = null; } - public function testFixRadio() + public function testLegacyFixRadio() { $data = '1'; $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); @@ -46,7 +51,7 @@ public function testFixRadio() $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 @@ public function testFixZero() $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 @@ public function testFixEmptyString() $this->assertEquals(array(0 => ''), $event->getData()); } - public function testConvertEmptyStringToPlaceholderIfNotFound() + public function testLegacyConvertEmptyStringToPlaceholderIfNotFound() { $list = new SimpleChoiceList(array(0 => 'A', 1 => 'B')); @@ -86,7 +91,7 @@ public function testConvertEmptyStringToPlaceholderIfNotFound() $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 6a0b6db2eceb2..d34d5b2184120 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 @@ public function testSubmitSingleNonExpandedObjectChoices() $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 @@ public function testSubmitMultipleNonExpandedObjectChoices() $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 @@ public function testSubmitSingleExpandedObjectChoices() $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 @@ public function testSubmitMultipleExpandedObjectChoices() $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 0c25ad44cd2f4..334abd70928b3 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\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 @@ public function isSelectedChoiceProvider() */ 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 b5354e0bd20f7..6cc7edd7a38e7 100644 --- a/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php @@ -231,6 +231,29 @@ public function testSingleChoice() ); } + public function testSingleChoiceAttributes() + { + $form = $this->factory->createNamed('name', 'choice', '&a', array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => false, + 'expanded' => false, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array('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 @@ public function testMultipleChoice() ); } + public function testMultipleChoiceAttributes() + { + $form = $this->factory->createNamed('name', 'choice', array('&a'), array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'required' => true, + 'multiple' => true, + 'expanded' => false, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array('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 @@ public function testSingleChoiceExpanded() ); } + public function testSingleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', 'choice', '&a', array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => false, + 'expanded' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./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 @@ public function testMultipleChoiceExpanded() ); } + public function testMultipleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', 'choice', array('&a', '&c'), array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B', '&c' => 'Choice&C'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => true, + 'expanded' => true, + 'required' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./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 f6164725fdb4e..7b48005408eed 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php @@ -96,7 +96,7 @@ public function isIntId() 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 3a2702a335e3f..f9848c2d0e682 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php @@ -14,7 +14,6 @@ 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 31527a9f349aa..907829be0e99f 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\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 f6fd823784c6e..0f4bcaa14e1f5 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -99,7 +99,7 @@ public function createListFromChoices($choices, $value = null) return $accessor->getValue($choice, $value); } - return null; + return; }; } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy