diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php new file mode 100644 index 0000000000000..5456c0eedb537 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -0,0 +1,171 @@ + + * + * 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\ObjectManager; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; + +/** + * Loads choices using a Doctrine object manager. + * + * @author Bernhard Schussek + */ +class DoctrineChoiceLoader implements ChoiceLoaderInterface +{ + /** + * @var ChoiceListFactoryInterface + */ + private $factory; + + /** + * @var ObjectManager + */ + private $manager; + + /** + * @var string + */ + private $class; + + /** + * @var IdReader + */ + private $idReader; + + /** + * @var null|EntityLoaderInterface + */ + private $objectLoader; + + /** + * @var ChoiceListInterface + */ + private $choiceList; + + /** + * Creates a new choice loader. + * + * Optionally, an implementation of {@link EntityLoaderInterface} can be + * 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 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, IdReader $idReader, EntityLoaderInterface $objectLoader = null) + { + $this->factory = $factory; + $this->manager = $manager; + $this->class = $manager->getClassMetadata($class)->getName(); + $this->idReader = $idReader; + $this->objectLoader = $objectLoader; + } + + /** + * {@inheritdoc} + */ + public function loadChoiceList($value = null) + { + if ($this->choiceList) { + return $this->choiceList; + } + + $objects = $this->objectLoader + ? $this->objectLoader->getEntities() + : $this->manager->getRepository($this->class)->findAll(); + + $this->choiceList = $this->factory->createListFromChoices($objects, $value); + + return $this->choiceList; + } + + /** + * {@inheritdoc} + */ + public function loadValuesForChoices(array $choices, $value = null) + { + // Performance optimization + if (empty($choices)) { + 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->idReader->isSingleId()) { + $values = array(); + + // Maintain order and indices of the given objects + 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); + } + } + + return $values; + } + + return $this->loadChoiceList($value)->getValuesForChoices($choices); + } + + /** + * {@inheritdoc} + */ + public function loadChoicesForValues(array $values, $value = null) + { + // Performance optimization + // Also prevents the generation of "WHERE id IN ()" queries through the + // 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 + if (empty($values)) { + return array(); + } + + // Optimize performance in case we have an object loader and + // a single-field identifier + if (!$this->choiceList && $this->objectLoader && $this->idReader->isSingleId()) { + $unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idReader->getIdField(), $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 ($unorderedObjects as $object) { + $objectsById[$this->idReader->getIdValue($object)] = $object; + } + + foreach ($values as $i => $id) { + if (isset($objectsById[$id])) { + $objects[$i] = $objectsById[$id]; + } + } + + 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 57e57e6e25db9..1d4232306df9b 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 DoctrineChoiceLoader} instead. */ class EntityChoiceList extends ObjectChoiceList { @@ -126,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/IdReader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php new file mode 100644 index 0000000000000..7b48005408eed --- /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; + } + + 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/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index 872e77affe0b7..92f00cb24332e 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 { @@ -34,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 */ @@ -49,10 +57,15 @@ public function __construct($queryBuilder, $manager = null, $class = null) } 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/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..a478574190d64 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -12,17 +12,23 @@ 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 Doctrine\ORM\QueryBuilder; +use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; -use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; +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; +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\Exception\UnexpectedTypeException; +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 +39,63 @@ abstract class DoctrineType extends AbstractType protected $registry; /** - * @var array + * @var ChoiceListFactoryInterface + */ + 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. */ - private $choiceListCache = array(); + public static function createChoiceLabel($choice) + { + return (string) $choice; + } /** - * @var PropertyAccessorInterface + * 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. */ - private $propertyAccessor; + public static function createChoiceName($choice, $key, $value) + { + return (string) $value; + } - 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 +110,97 @@ public function buildForm(FormBuilderInterface $builder, array $options) public function configureOptions(OptionsResolver $resolver) { - $choiceListCache = &$this->choiceListCache; $registry = $this->registry; - $propertyAccessor = $this->propertyAccessor; + $choiceListFactory = $this->choiceListFactory; + $idReaders = &$this->idReaders; + $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'); + $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 + // 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'], + )); - return $type->getLoader($options['em'], $queryBuilder, $options['class']); - }; + if (isset($choiceLoaders[$hash])) { + return $choiceLoaders[$hash]; + } - $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 ($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']); } - $choiceHashes = $hashes; - } + $choiceLoaders[$hash] = new DoctrineChoiceLoader( + $choiceListFactory, + $options['em'], + $options['class'], + $options['id_reader'], + $entityLoader + ); - $preferredChoiceHashes = $options['preferred_choices']; + return $choiceLoaders[$hash]; + } + }; - 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']; + // BC: use __toString() by default + return array(__CLASS__, 'createChoiceLabel'); + }; - // Support for closures - $groupByHash = is_object($options['group_by']) - ? spl_object_hash($options['group_by']) - : $options['group_by']; + $choiceName = function (Options $options) { + /** @var IdReader $idReader */ + $idReader = $options['id_reader']; - $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 - ); + // 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 + }; + + // The choices are always indexed by ID (see "choices" normalizer + // 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 (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'); } - return $choiceListCache[$hash]; + // Otherwise, an incrementing integer is used as value automatically }; $emNormalizer = function (Options $options, $em) use ($registry) { @@ -165,22 +226,85 @@ public function configureOptions(OptionsResolver $resolver) return $em; }; + // deprecation note + $propertyNormalizer = function (Options $options, $propertyName) { + 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; + }; + + // 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; + }; + + // deprecation note + $loaderNormalizer = function (Options $options, $loader) { + 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; + }; + + // 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, + '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, + '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 e9b89302dbb33..87b3ee42cb956 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -17,71 +17,18 @@ class EntityType extends DoctrineType { - /** - * @var ORMQueryBuilderLoader[] - */ - private $loaderCache = array(); - /** * Return the default loader object. * * @param ObjectManager $manager - * @param mixed $queryBuilder + * @param QueryBuilder $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() 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 25afbed492150..27d1d88e43ba3 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -11,21 +11,24 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\Type; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Tools\SchemaTool; +use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser; 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 +40,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; @@ -128,10 +131,10 @@ public function testSetDataToUninitializedEntityWithNonRequired() 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => '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 +150,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() @@ -162,15 +165,15 @@ public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder() 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => 'name', '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']); } /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */ public function testConfigureQueryBuilderWithNonQueryBuilderAndNonClosure() { @@ -249,7 +252,7 @@ public function testSubmitSingleExpandedNull() $field->submit(null); $this->assertNull($field->getData()); - $this->assertSame(array(), $field->getViewData()); + $this->assertNull($field->getViewData()); } public function testSubmitSingleNonExpandedNull() @@ -291,7 +294,7 @@ public function testSubmitSingleNonExpandedSingleIdentifier() 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -313,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 @@ -337,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')); @@ -362,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)); @@ -393,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 @@ -419,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)); @@ -449,7 +452,7 @@ public function testSubmitSingleExpanded() 'expanded' => true, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -475,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')); @@ -505,12 +508,12 @@ 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'); - $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()); @@ -529,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', )); @@ -537,9 +540,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']); } @@ -555,11 +563,11 @@ 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($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() @@ -575,11 +583,11 @@ 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($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() @@ -594,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'); @@ -615,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'); @@ -639,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'); @@ -663,7 +671,7 @@ public function testDisallowChoicesThatAreNotIncludedQueryBuilderAsClosureSingle return $repository->createQueryBuilder('e') ->where('e.id IN (1, 2)'); }, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('3'); @@ -687,7 +695,7 @@ public function testDisallowChoicesThatAreNotIncludedQueryBuilderAsClosureCompos return $repository->createQueryBuilder('e') ->where('e.id1 IN (10, 50)'); }, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -707,7 +715,7 @@ public function testSubmitSingleStringIdentifier() 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_STRING_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('foo'); @@ -728,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 @@ -752,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', )); } @@ -767,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', )); } @@ -779,8 +787,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, @@ -799,19 +806,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(); @@ -822,15 +833,59 @@ public function testLoaderCaching() 'property3' => 2, )); - $reflectionClass = new \ReflectionObject($entityType); - $reflectionProperty = $reflectionClass->getProperty('loaderCache'); - $reflectionProperty->setAccessible(true); + $choiceList1 = $form->get('property1')->getConfig()->getOption('choice_list'); + $choiceList2 = $form->get('property2')->getConfig()->getOption('choice_list'); + $choiceList3 = $form->get('property3')->getConfig()->getOption('choice_list'); + + $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\ChoiceListInterface', $choiceList1); + $this->assertSame($choiceList1, $choiceList2); + $this->assertSame($choiceList1, $choiceList3); + } + + public function testCacheChoiceLists() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + + $this->persist(array($entity1)); - $loaders = $reflectionProperty->getValue($entityType); + $field1 = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'required' => false, + 'choice_label' => 'name', + )); - $reflectionProperty->setAccessible(false); + $field2 = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'required' => false, + '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->assertCount(1, $loaders); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); } protected function createRegistryMock($name, $em) 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/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/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..f55154b08528c --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php @@ -0,0 +1,158 @@ + + * + * 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\UnexpectedTypeException; + +/** + * 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(); + + /** + * 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 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) + { + if (null !== $value && !is_callable($value)) { + throw new UnexpectedTypeException($value, 'null or callable'); + } + + $this->choices = $choices; + $this->values = array(); + $this->valueCallback = $value; + + 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); + } + } + } + + /** + * {@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(); + + // Use the value callback to compare choices by their values, if present + if ($this->valueCallback) { + $givenValues = array(); + + foreach ($choices as $i => $givenChoice) { + $givenValues[$i] = (string) call_user_func($this->valueCallback, $givenChoice); + } + + 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) { + 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..30709108e8fdd --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php @@ -0,0 +1,146 @@ + + * + * 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 + */ +class ArrayKeyChoiceList extends ArrayChoiceList +{ + /** + * Whether the choices are used as values. + * + * @var bool + */ + private $useChoicesAsValues = false; + + /** + * 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 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, $value = null) + { + $choices = array_map(array(__CLASS__, 'toArrayKey'), $choices); + + if (null === $value) { + $value = function ($choice) { + return (string) $choice; + }; + $this->useChoicesAsValues = true; + } + + parent::__construct($choices, $value); + } + + /** + * {@inheritdoc} + */ + public function getChoicesForValues(array $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)); + } + + return parent::getChoicesForValues($values); + } + + /** + * {@inheritdoc} + */ + public function getValuesForChoices(array $choices) + { + $choices = array_map(array(__CLASS__, 'toArrayKey'), $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/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..f9848c2d0e682 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php @@ -0,0 +1,180 @@ + + * + * 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; + +/** + * 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 ($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 ($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..907829be0e99f --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -0,0 +1,343 @@ + + * + * 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\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 ($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); + + return new ArrayChoiceList($flatChoices, $value); + } + + /** + * {@inheritdoc} + * + * @deprecated Added for backwards compatibility in Symfony 2.7, to be + * removed in Symfony 3.0. + */ + public function createListFromFlippedChoices($choices, $value = null) + { + 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) { + $value = function ($choice) { + return (string) $choice; + }; + } + + return new ArrayKeyChoiceList($flatChoices, $value); + } + + /** + * {@inheritdoc} + */ + public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null) + { + return new LazyChoiceList($loader, $value); + } + + /** + * {@inheritdoc} + */ + public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null) + { + // 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) { + $index = 0; + } + + // 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 view 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) + { + $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, $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, $value) : (isset($attr[$key]) ? $attr[$key] : array()) + ); + + // $isPreferred may be null if no choices are preferred + if ($isPreferred && call_user_func($isPreferred, $choice, $key, $value)) { + $preferredViews[$nextIndex] = $view; + } else { + $otherViews[$nextIndex] = $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, $values[$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..0f4bcaa14e1f5 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -0,0 +1,230 @@ + + * + * 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) { + // 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; + }; + } + + 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|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 + */ + 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..092e2c4644b0a --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php @@ -0,0 +1,123 @@ + + * + * 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; + + /** + * Whether to use the value callback to compare choices. + * + * @var bool + */ + private $compareByValue; + + /** + * @var ChoiceListInterface|null + */ + 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, $compareByValue = false) + { + $this->loader = $loader; + $this->value = $value; + $this->compareByValue = $compareByValue; + } + + /** + * {@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..817e03ec72168 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\ChoiceList\ArrayChoiceList} instead. */ class ChoiceList implements ChoiceListInterface { @@ -89,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 8f09179a2a8fb..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. * @@ -25,23 +27,12 @@ * in the HTML "value" attribute. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link BaseChoiceListInterface} instead. */ -interface ChoiceListInterface +interface ChoiceListInterface extends BaseChoiceListInterface { - /** - * 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 +83,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..f3a7cc028aff8 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\ChoiceList\LazyChoiceList} + * instead. */ abstract class LazyChoiceList implements ChoiceListInterface { @@ -31,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 a20d19455577d..c356ce466bc1d 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\ChoiceList\ArrayChoiceList} + * instead. */ class ObjectChoiceList extends ChoiceList { @@ -93,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 8d4ddd12423e6..6dd8fb091e26d 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\ChoiceList\ArrayChoiceList} + * 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..19db183a28394 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php @@ -0,0 +1,71 @@ + + * + * 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()) { + 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..108c1ca6a3c43 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\ChoiceList\LazyChoiceList} + * instead. */ class ChoiceToBooleanArrayTransformer implements DataTransformerInterface { @@ -34,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/ChoiceToValueTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php index 087faf4d3b4eb..2b4d026db7589 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,20 +43,12 @@ public function reverseTransform($value) throw new TransformationFailedException('Expected a scalar.'); } - // These are now valid ChoiceList 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/DataTransformer/ChoicesToBooleanArrayTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php index f1f13fda28845..a632bc03c7000 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\ChoiceList\LazyChoiceList} + * instead. */ class ChoicesToBooleanArrayTransformer implements DataTransformerInterface { @@ -25,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/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..85b08c7b32aa7 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php @@ -11,17 +11,21 @@ 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 * 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 c5f871756bab4..8641ea725dca4 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php @@ -11,16 +11,20 @@ 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 * 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 5b52b4ad96a7d..a597e1d4f4126 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,21 +231,7 @@ public function finishView(FormView $view, FormInterface $form, array $options) */ public function configureOptions(OptionsResolver $resolver) { - $choiceListCache = &$this->choiceListCache; - - $choiceList = function (Options $options) use (&$choiceListCache) { - // Harden against NULL values (like in EntityType and ModelType) - $choices = null !== $options['choices'] ? $options['choices'] : array(); - - // Reuse existing choice lists in order to increase performance - $hash = hash('sha256', serialize(array($choices, $options['preferred_choices']))); - - if (!isset($choiceListCache[$hash])) { - $choiceListCache[$hash] = new SimpleChoiceList($choices, $options['preferred_choices']); - } - - return $choiceListCache[$hash]; - }; + $choiceListFactory = $this->choiceListFactory; $emptyData = function (Options $options) { if ($options['multiple'] || $options['expanded']) { @@ -196,6 +250,31 @@ public function configureOptions(OptionsResolver $resolver) return $options['empty_value']; }; + $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; + } + + 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) { if ($options['multiple']) { // never use an empty value for this case @@ -219,9 +298,16 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setDefaults(array( 'multiple' => false, 'expanded' => false, - 'choice_list' => $choiceList, + 'choice_list' => null, // 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, @@ -233,10 +319,20 @@ public function configureOptions(OptionsResolver $resolver) 'data_class' => null, )); + $resolver->setNormalizer('choice_list', $choiceListNormalizer); $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 +343,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 +367,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..0cbeecab9f94a 100644 --- a/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php +++ b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php @@ -11,34 +11,18 @@ 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 +class ChoiceView extends BaseChoiceView { - /** - * 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 +32,8 @@ class ChoiceView */ public function __construct($data, $value, $label) { - $this->data = $data; - $this->value = $value; - $this->label = $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/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'); 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..129a093b89c68 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php @@ -0,0 +1,81 @@ + + * + * 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() + { + $i = 0; + + return new ArrayChoiceList($this->getChoices()); + } + + 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')); + } + + public function testCreateChoiceListWithValueCallback() + { + $callback = function ($choice) { + return ':'.$choice; + }; + + $choiceList = new ArrayChoiceList(array(2 => 'foo', 7 => 'bar', 10 => 'baz'), $callback); + + $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() + { + $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(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 new file mode 100644 index 0000000000000..5024a60db73bf --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php @@ -0,0 +1,196 @@ + + * + * 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()); + } + + protected function getChoices() + { + return array(0, 1, 'a', 'b', ''); + } + + protected function getValues() + { + return array('0', '1', 'a', '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); + + $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); + } + + /** + * @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($value, $converted) + { + $callback = function () use ($value) { + return $value; + }; + + $list = new ArrayKeyChoiceList(array('choice'), $callback); + + $this->assertSame(array($converted), $list->getValues()); + } + + public function provideConvertibleValues() + { + return array( + 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(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(true, '1'), + array(false, ''), + ); + } + + public function testCreateChoiceListWithValueCallback() + { + $callback = function ($choice) { + return ':'.$choice; + }; + + $choiceList = new ArrayKeyChoiceList(array(2 => 'foo', 7 => 'bar', 10 => 'baz'), $callback); + + $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/CachingFactoryDecoratorTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php new file mode 100644 index 0000000000000..716468276a7b9 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php @@ -0,0 +1,652 @@ + + * + * 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); + } + + 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)); + } + + 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..a2b817ed8d8ba --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -0,0 +1,894 @@ + + * + * 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) + ); + $this->factory = new DefaultChoiceListFactory(); + } + + 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 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 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 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 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); + } + + 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() + { + $view = $this->factory->createView( + $this->list, + function ($object, $key) { + return 'B' === $key || 'C' === $key; + } + ); + + $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( + $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 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( + $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 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( + $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 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( + $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 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')); + $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/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 17972cbc0a85a..d34d5b2184120 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,8 +257,124 @@ 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()); + } + + /** + * @group legacy + */ + public function testLegacySubmitSingleNonExpandedObjectChoices() + { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + $form = $this->factory->create('choice', null, array( 'multiple' => false, 'expanded' => false, @@ -273,6 +410,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( @@ -305,6 +473,28 @@ 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()); + } + + /** + * @group legacy + */ + public function testLegacySubmitMultipleNonExpandedObjectChoices() + { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + $form = $this->factory->create('choice', null, array( 'multiple' => true, 'expanded' => false, @@ -337,13 +527,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 +583,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 +641,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 +657,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 +689,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 +705,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 +737,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 +753,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 +785,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 +803,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 +835,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 +853,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 +885,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 +903,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( @@ -687,6 +945,37 @@ 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()); + } + + /** + * @group legacy + */ + public function testLegacySubmitSingleExpandedObjectChoices() + { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + $form = $this->factory->create('choice', null, array( 'multiple' => false, 'expanded' => true, @@ -750,13 +1039,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 +1132,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( @@ -874,6 +1173,37 @@ 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()); + } + + /** + * @group legacy + */ + public function testLegacySubmitMultipleExpandedObjectChoices() + { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + $form = $this->factory->create('choice', null, array( 'multiple' => true, 'expanded' => true, @@ -1134,10 +1464,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 +1481,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 +1499,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 +1524,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 +1559,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 +1567,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); } }