[Form] Refactored choice lists to support dynamic label, value, index and attribute generation

This commit is contained in:
Bernhard Schussek 2014-09-26 23:28:34 +02:00
parent 75c8a2ba21
commit 03efce1b56
54 changed files with 5716 additions and 450 deletions

View File

@ -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 <bschussek@gmail.com>
*
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
* Use {@link EntityChoiceLoader} instead.
*/
class EntityChoiceList extends ObjectChoiceList
{

View File

@ -0,0 +1,267 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Doctrine\Form\ChoiceList;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\Exception\RuntimeException;
/**
* Loads choices using a Doctrine object manager.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class EntityChoiceLoader implements ChoiceLoaderInterface
{
/**
* @var ChoiceListFactoryInterface
*/
private $factory;
/**
* @var ObjectManager
*/
private $manager;
/**
* @var string
*/
private $class;
/**
* @var ClassMetadata
*/
private $classMetadata;
/**
* @var null|EntityLoaderInterface
*/
private $entityLoader;
/**
* The identifier field, unless the identifier is composite
*
* @var null|string
*/
private $idField = null;
/**
* Whether to use the identifier for value generation
*
* @var bool
*/
private $compositeId = true;
/**
* @var ChoiceListInterface
*/
private $choiceList;
/**
* Returns the value of the identifier field of an entity.
*
* Doctrine must know about this entity, that is, the entity must already
* be persisted or added to the identity map before. Otherwise an
* exception is thrown.
*
* This method assumes that the entity has a single-column identifier and
* will return a single value instead of an array.
*
* @param object $object The entity for which to get the identifier
*
* @return int|string The identifier value
*
* @throws RuntimeException If the entity does not exist in Doctrine's identity map
*
* @internal Should not be accessed by user-land code. This method is public
* only to be usable as callback.
*/
public static function getIdValue(ObjectManager $om, ClassMetadata $classMetadata, $object)
{
if (!$om->contains($object)) {
throw new RuntimeException(
'Entities passed to the choice field must be managed. Maybe '.
'persist them in the entity manager?'
);
}
$om->initializeObject($object);
return current($classMetadata->getIdentifierValues($object));
}
/**
* Creates a new choice loader.
*
* Optionally, an implementation of {@link EntityLoaderInterface} can be
* passed which optimizes the entity loading for one of the Doctrine
* mapper implementations.
*
* @param ChoiceListFactoryInterface $factory The factory for creating
* the loaded choice list
* @param ObjectManager $manager The object manager
* @param string $class The entity class name
* @param null|EntityLoaderInterface $entityLoader The entity loader
*/
public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, EntityLoaderInterface $entityLoader = null)
{
$this->factory = $factory;
$this->manager = $manager;
$this->classMetadata = $manager->getClassMetadata($class);
$this->class = $this->classMetadata->getName();
$this->entityLoader = $entityLoader;
$identifier = $this->classMetadata->getIdentifierFieldNames();
if (1 === count($identifier)) {
$this->idField = $identifier[0];
$this->compositeId = false;
}
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if ($this->choiceList) {
return $this->choiceList;
}
$entities = $this->entityLoader
? $this->entityLoader->getEntities()
: $this->manager->getRepository($this->class)->findAll();
// If the class has a multi-column identifier, we cannot index the
// entities by their IDs
if ($this->compositeId) {
$this->choiceList = $this->factory->createListFromChoices($entities, $value);
return $this->choiceList;
}
// Index the entities by ID
$entitiesById = array();
foreach ($entities as $entity) {
$id = self::getIdValue($this->manager, $this->classMetadata, $entity);
$entitiesById[$id] = $entity;
}
$this->choiceList = $this->factory->createListFromChoices($entitiesById, $value);
return $this->choiceList;
}
/**
* Loads the values corresponding to the given entities.
*
* The values are returned with the same keys and in the same order as the
* corresponding entities in the given array.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the entity as first and the array key as the second
* argument.
*
* @param array $entities An array of entities. Non-existing entities
* in this array are ignored
* @param null|callable $value The callable generating the choice values
*
* @return string[] An array of choice values
*/
public function loadValuesForChoices(array $entities, $value = null)
{
// Performance optimization
if (empty($entities)) {
return array();
}
// Optimize performance for single-field identifiers. We already
// know that the IDs are used as values
// Attention: This optimization does not check choices for existence
if (!$this->choiceList && !$this->compositeId) {
$values = array();
// Maintain order and indices of the given entities
foreach ($entities as $i => $entity) {
if ($entity instanceof $this->class) {
// Make sure to convert to the right format
$values[$i] = (string) self::getIdValue($this->manager, $this->classMetadata, $entity);
}
}
return $values;
}
return $this->loadChoiceList($value)->getValuesForChoices($entities);
}
/**
* Loads the entities corresponding to the given values.
*
* The entities are returned with the same keys and in the same order as the
* corresponding values in the given array.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the entity as first and the array key as the second
* argument.
*
* @param string[] $values An array of choice values. Non-existing
* values in this array are ignored
* @param null|callable $value The callable generating the choice values
*
* @return array An array of entities
*/
public function loadChoicesForValues(array $values, $value = null)
{
// Performance optimization
// Also prevents the generation of "WHERE id IN ()" queries through the
// entity loader. At least with MySQL and on the development machine
// this was tested on, no exception was thrown for such invalid
// statements, consequently no test fails when this code is removed.
// https://github.com/symfony/symfony/pull/8981#issuecomment-24230557
if (empty($values)) {
return array();
}
// Optimize performance in case we have an entity loader and
// a single-field identifier
if (!$this->choiceList && !$this->compositeId && $this->entityLoader) {
$unorderedEntities = $this->entityLoader->getEntitiesByIds($this->idField, $values);
$entitiesById = array();
$entities = array();
// Maintain order and indices from the given $values
// An alternative approach to the following loop is to add the
// "INDEX BY" clause to the Doctrine query in the loader,
// but I'm not sure whether that's doable in a generic fashion.
foreach ($unorderedEntities as $entity) {
$id = self::getIdValue($this->manager, $this->classMetadata, $entity);
$entitiesById[$id] = $entity;
}
foreach ($values as $i => $id) {
if (isset($entitiesById[$id])) {
$entities[$i] = $entitiesById[$id];
}
}
return $entities;
}
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
}

View File

@ -17,7 +17,10 @@ use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManager;
/**
* Getting Entities through the ORM QueryBuilder.
* Loads entities using a {@link QueryBuilder} instance.
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ORMQueryBuilderLoader implements EntityLoaderInterface
{

View File

@ -14,21 +14,38 @@ namespace Symfony\Bridge\Doctrine\Form;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class DoctrineOrmExtension extends AbstractExtension
{
protected $registry;
public function __construct(ManagerRegistry $registry)
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* @var ChoiceListFactoryInterface
*/
private $choiceListFactory;
public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null)
{
$this->registry = $registry;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
}
protected function loadTypes()
{
return array(
new EntityType($this->registry, PropertyAccess::createPropertyAccessor()),
new EntityType($this->registry, $this->propertyAccessor, $this->choiceListFactory),
);
}

View File

@ -12,17 +12,20 @@
namespace Symfony\Bridge\Doctrine\Form\Type;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Component\Form\Exception\RuntimeException;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceLoader;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface;
use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
use Symfony\Component\Form\Exception\RuntimeException;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
abstract class DoctrineType extends AbstractType
@ -33,19 +36,19 @@ abstract class DoctrineType extends AbstractType
protected $registry;
/**
* @var array
* @var ChoiceListFactoryInterface
*/
private $choiceListCache = array();
private $choiceListFactory;
/**
* @var PropertyAccessorInterface
* @var EntityChoiceLoader[]
*/
private $propertyAccessor;
private $choiceLoaders = array();
public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null)
public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null)
{
$this->registry = $registry;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->choiceListFactory = $choiceListFactory ?: new PropertyAccessDecorator(new DefaultChoiceListFactory(), $propertyAccessor);
}
public function buildForm(FormBuilderInterface $builder, array $options)
@ -60,86 +63,79 @@ abstract class DoctrineType extends AbstractType
public function configureOptions(OptionsResolver $resolver)
{
$choiceListCache = &$this->choiceListCache;
$registry = $this->registry;
$propertyAccessor = $this->propertyAccessor;
$choiceListFactory = $this->choiceListFactory;
$choiceLoaders = &$this->choiceLoaders;
$type = $this;
$loader = function (Options $options) use ($type) {
$queryBuilder = (null !== $options['query_builder'])
? $options['query_builder']
: $options['em']->getRepository($options['class'])->createQueryBuilder('e');
return $type->getLoader($options['em'], $queryBuilder, $options['class']);
};
$choiceList = function (Options $options) use (&$choiceListCache, $propertyAccessor) {
// Support for closures
$propertyHash = is_object($options['property'])
? spl_object_hash($options['property'])
: $options['property'];
$choiceHashes = $options['choices'];
// Support for recursive arrays
if (is_array($choiceHashes)) {
// A second parameter ($key) is passed, so we cannot use
// spl_object_hash() directly (which strictly requires
// one parameter)
array_walk_recursive($choiceHashes, function (&$value) {
$value = spl_object_hash($value);
});
} elseif ($choiceHashes instanceof \Traversable) {
$hashes = array();
foreach ($choiceHashes as $value) {
$hashes[] = spl_object_hash($value);
}
$choiceHashes = $hashes;
}
$preferredChoiceHashes = $options['preferred_choices'];
if (is_array($preferredChoiceHashes)) {
array_walk_recursive($preferredChoiceHashes, function (&$value) {
$value = spl_object_hash($value);
});
}
// Support for custom loaders (with query builders)
$loaderHash = is_object($options['loader'])
? spl_object_hash($options['loader'])
: $options['loader'];
// Support for closures
$groupByHash = is_object($options['group_by'])
? spl_object_hash($options['group_by'])
: $options['group_by'];
$hash = hash('sha256', json_encode(array(
spl_object_hash($options['em']),
$options['class'],
$propertyHash,
$loaderHash,
$choiceHashes,
$preferredChoiceHashes,
$groupByHash,
)));
if (!isset($choiceListCache[$hash])) {
$choiceListCache[$hash] = new EntityChoiceList(
$choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) {
// Unless the choices are given explicitly, load them on demand
if (null === $options['choices']) {
$hash = CachingFactoryDecorator::generateHash(array(
$options['em'],
$options['class'],
$options['property'],
$options['query_builder'],
$options['loader'],
$options['choices'],
$options['preferred_choices'],
$options['group_by'],
$propertyAccessor
);
));
if (!isset($choiceLoaders[$hash])) {
if ($options['loader']) {
$loader = $options['loader'];
} elseif (null !== $options['query_builder']) {
$loader = $type->getLoader($options['em'], $options['query_builder'], $options['class']);
} else {
$queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e');
$loader = $type->getLoader($options['em'], $queryBuilder, $options['class']);
}
$choiceLoaders[$hash] = new EntityChoiceLoader(
$choiceListFactory,
$options['em'],
$options['class'],
$loader
);
}
return $choiceLoaders[$hash];
}
};
$choiceLabel = function (Options $options) {
// BC with the "property" option
if ($options['property']) {
return $options['property'];
}
return $choiceListCache[$hash];
// BC: use __toString() by default
return function ($entity) {
return (string) $entity;
};
};
$choiceName = function (Options $options) {
/** @var ObjectManager $om */
$om = $options['em'];
$classMetadata = $om->getClassMetadata($options['class']);
$ids = $classMetadata->getIdentifierFieldNames();
$idType = $classMetadata->getTypeOfField(current($ids));
// If the entity has a single-column, numeric ID, use that ID as
// field name
if (1 === count($ids) && in_array($idType, array('integer', 'smallint', 'bigint'))) {
return function ($entity, $id) {
return $id;
};
}
// Otherwise, an incrementing integer is used as name automatically
};
// The choices are always indexed by ID (see "choices" normalizer
// and EntityChoiceLoader), unless the ID is composite. Then they
// are indexed by an incrementing integer.
// Use the ID/incrementing integer as choice value.
$choiceValue = function ($entity, $key) {
return $key;
};
$emNormalizer = function (Options $options, $em) use ($registry) {
@ -165,19 +161,50 @@ abstract class DoctrineType extends AbstractType
return $em;
};
$choicesNormalizer = function (Options $options, $entities) {
if (null === $entities || 0 === count($entities)) {
return $entities;
}
// Make sure that the entities are indexed by their ID
/** @var ObjectManager $om */
$om = $options['em'];
$classMetadata = $om->getClassMetadata($options['class']);
$ids = $classMetadata->getIdentifierFieldNames();
// We cannot use composite IDs as indices. In that case, keep the
// given indices
if (count($ids) > 1) {
return $entities;
}
$entitiesById = array();
foreach ($entities as $entity) {
$id = EntityChoiceLoader::getIdValue($om, $classMetadata, $entity);
$entitiesById[$id] = $entity;
}
return $entitiesById;
};
$resolver->setDefaults(array(
'em' => null,
'property' => null,
'property' => null, // deprecated, use "choice_label"
'query_builder' => null,
'loader' => $loader,
'loader' => null, // deprecated, use "choice_loader"
'choices' => null,
'choice_list' => $choiceList,
'group_by' => null,
'choices_as_values' => true,
'choice_loader' => $choiceLoader,
'choice_label' => $choiceLabel,
'choice_name' => $choiceName,
'choice_value' => $choiceValue,
));
$resolver->setRequired(array('class'));
$resolver->setNormalizer('em', $emNormalizer);
$resolver->setNormalizer('choices', $choicesNormalizer);
$resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager'));
$resolver->setAllowedTypes('loader', array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface'));

View File

@ -11,21 +11,23 @@
namespace Symfony\Bridge\Doctrine\Tests\Form\Type;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\GroupableEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\PropertyAccess\PropertyAccess;
class EntityTypeTest extends TypeTestCase
@ -37,12 +39,12 @@ class EntityTypeTest extends TypeTestCase
const COMPOSITE_STRING_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity';
/**
* @var \Doctrine\ORM\EntityManager
* @var EntityManager
*/
private $em;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
* @var \PHPUnit_Framework_MockObject_MockObject|ManagerRegistry
*/
private $emRegistry;
@ -131,7 +133,7 @@ class EntityTypeTest extends TypeTestCase
'property' => 'name',
));
$this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']);
$this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']);
}
public function testSetDataToUninitializedEntityWithNonRequiredToString()
@ -147,7 +149,7 @@ class EntityTypeTest extends TypeTestCase
'required' => false,
));
$this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']);
$this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']);
}
public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder()
@ -166,7 +168,7 @@ class EntityTypeTest extends TypeTestCase
'query_builder' => $qb,
));
$this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']);
$this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']);
}
/**
@ -249,7 +251,7 @@ class EntityTypeTest extends TypeTestCase
$field->submit(null);
$this->assertNull($field->getData());
$this->assertSame(array(), $field->getViewData());
$this->assertNull($field->getViewData());
}
public function testSubmitSingleNonExpandedNull()
@ -510,7 +512,7 @@ class EntityTypeTest extends TypeTestCase
$field->submit('2');
$this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']);
$this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']);
$this->assertTrue($field->isSynchronized());
$this->assertSame($entity2, $field->getData());
$this->assertSame('2', $field->getViewData());
@ -537,9 +539,14 @@ class EntityTypeTest extends TypeTestCase
$this->assertSame('2', $field->getViewData());
$this->assertEquals(array(
'Group1' => array(1 => new ChoiceView($item1, '1', 'Foo'), 2 => new ChoiceView($item2, '2', 'Bar')),
'Group2' => array(3 => new ChoiceView($item3, '3', 'Baz')),
'4' => new ChoiceView($item4, '4', 'Boo!'),
'Group1' => new ChoiceGroupView('Group1', array(
1 => new ChoiceView('Foo', '1', $item1),
2 => new ChoiceView('Bar', '2', $item2),
)),
'Group2' => new ChoiceGroupView('Group2', array(
3 => new ChoiceView('Baz', '3', $item3),
)),
4 => new ChoiceView('Boo!', '4', $item4),
), $field->createView()->vars['choices']);
}
@ -558,8 +565,8 @@ class EntityTypeTest extends TypeTestCase
'property' => 'name',
));
$this->assertEquals(array(3 => new ChoiceView($entity3, '3', 'Baz'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['preferred_choices']);
$this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo')), $field->createView()->vars['choices']);
$this->assertEquals(array(3 => new ChoiceView('Baz', '3', $entity3), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['preferred_choices']);
$this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1)), $field->createView()->vars['choices']);
}
public function testOverrideChoicesWithPreferredChoices()
@ -578,8 +585,8 @@ class EntityTypeTest extends TypeTestCase
'property' => 'name',
));
$this->assertEquals(array(3 => new ChoiceView($entity3, '3', 'Baz')), $field->createView()->vars['preferred_choices']);
$this->assertEquals(array(2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']);
$this->assertEquals(array(3 => new ChoiceView('Baz', '3', $entity3)), $field->createView()->vars['preferred_choices']);
$this->assertEquals(array(2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']);
}
public function testDisallowChoicesThatAreNotIncludedChoicesSingleIdentifier()
@ -833,6 +840,30 @@ class EntityTypeTest extends TypeTestCase
$this->assertCount(1, $loaders);
}
public function testCacheChoiceLists()
{
$entity1 = new SingleIntIdEntity(1, 'Foo');
$this->persist(array($entity1));
$field1 = $this->factory->createNamed('name', 'entity', null, array(
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'required' => false,
'property' => 'name',
));
$field2 = $this->factory->createNamed('name', 'entity', null, array(
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'required' => false,
'property' => 'name',
));
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\ChoiceListInterface', $field1->getConfig()->getOption('choice_list'));
$this->assertSame($field1->getConfig()->getOption('choice_list'), $field2->getConfig()->getOption('choice_list'));
}
protected function createRegistryMock($name, $em)
{
$registry = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry');

View File

@ -13,7 +13,7 @@ namespace Symfony\Bridge\Twig\Extension;
use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser;
use Symfony\Bridge\Twig\Form\TwigRendererInterface;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
/**
* FormExtension extends Twig with form capabilities.

View File

@ -79,7 +79,8 @@
{{- block('choice_widget_options') -}}
</optgroup>
{%- else -%}
<option value="{{ choice.value }}"{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice.label|trans({}, translation_domain) }}</option>
{% set attr = choice.attr %}
<option value="{{ choice.value }}" {{ block('attributes') }}{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice.label|trans({}, translation_domain) }}</option>
{%- 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 -%}

View File

@ -1,11 +1,13 @@
<?php $translatorHelper = $view['translator']; // outside of the loop for performance reasons! ?>
<?php use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
$translatorHelper = $view['translator']; // outside of the loop for performance reasons! ?>
<?php $formHelper = $view['form']; ?>
<?php foreach ($choices as $index => $choice): ?>
<?php if (is_array($choice)): ?>
<?php if (is_array($choice) || $choice instanceof ChoiceGroupView): ?>
<optgroup label="<?php echo $view->escape($translatorHelper->trans($index, array(), $translation_domain)) ?>">
<?php echo $formHelper->block($form, 'choice_widget_options', array('choices' => $choice)) ?>
</optgroup>
<?php else: ?>
<option value="<?php echo $view->escape($choice->value) ?>"<?php if ($is_selected($choice->value, $value)): ?> selected="selected"<?php endif?>><?php echo $view->escape($translatorHelper->trans($choice->label, array(), $translation_domain)) ?></option>
<option value="<?php echo $view->escape($choice->value) ?>" <?php echo $view['form']->block($form, 'attributes', array('attr' => $choice->attr)) ?><?php if ($is_selected($choice->value, $value)): ?> selected="selected"<?php endif?>><?php echo $view->escape($translatorHelper->trans($choice->label, array(), $translation_domain)) ?></option>
<?php endif ?>
<?php endforeach ?>

View File

@ -0,0 +1,136 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\ChoiceList;
use Symfony\Component\Form\Exception\InvalidArgumentException;
/**
* A list of choices with arbitrary data types.
*
* The user of this class is responsible for assigning string values to the
* choices. Both the choices and their values are passed to the constructor.
* Each choice must have a corresponding value (with the same array key) in
* the value array.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ArrayChoiceList implements ChoiceListInterface
{
/**
* The choices in the list.
*
* @var array
*/
protected $choices = array();
/**
* The values of the choices.
*
* @var string[]
*/
protected $values = array();
/**
* Creates a list with the given choices and values.
*
* The given choice array must have the same array keys as the value array.
*
* @param array $choices The selectable choices
* @param string[] $values The string values of the choices
*
* @throws InvalidArgumentException If the keys of the choices don't match
* the keys of the values
*/
public function __construct(array $choices, array $values)
{
$choiceKeys = array_keys($choices);
$valueKeys = array_keys($values);
if ($choiceKeys !== $valueKeys) {
throw new InvalidArgumentException(sprintf(
'The keys of the choices and the values must match. The choice '.
'keys are: "%s". The value keys are: "%s".',
implode('", "', $choiceKeys),
implode('", "', $valueKeys)
));
}
$this->choices = $choices;
$this->values = array_map('strval', $values);
}
/**
* {@inheritdoc}
*/
public function getChoices()
{
return $this->choices;
}
/**
* {@inheritdoc}
*/
public function getValues()
{
return $this->values;
}
/**
* {@inheritdoc}
*/
public function getChoicesForValues(array $values)
{
$choices = array();
foreach ($values as $i => $givenValue) {
foreach ($this->values as $j => $value) {
if ($value !== (string) $givenValue) {
continue;
}
$choices[$i] = $this->choices[$j];
unset($values[$i]);
if (0 === count($values)) {
break 2;
}
}
}
return $choices;
}
/**
* {@inheritdoc}
*/
public function getValuesForChoices(array $choices)
{
$values = array();
foreach ($choices as $i => $givenChoice) {
foreach ($this->choices as $j => $choice) {
if ($choice !== $givenChoice) {
continue;
}
$values[$i] = $this->values[$j];
unset($choices[$i]);
if (0 === count($choices)) {
break 2;
}
}
}
return $values;
}
}

View File

@ -0,0 +1,173 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*
* @deprecated Added for backwards compatibility in Symfony 2.7, to be removed
* in Symfony 3.0.
*/
class ArrayKeyChoiceList implements ChoiceListInterface
{
/**
* The selectable choices.
*
* @var array
*/
private $choices = array();
/**
* The values of the choices.
*
* @var string[]
*/
private $values = array();
/**
* Casts the given choice to an array key.
*
* PHP arrays accept only strings and integers as array keys. Integer
* strings such as "42" are automatically cast to integers. The boolean
* values "true" and "false" are cast to the integers 1 and 0. Every other
* scalar value is cast to a string.
*
* @param mixed $choice The choice
*
* @return int|string The choice as PHP array key
*
* @throws InvalidArgumentException If the choice is not scalar
*/
public static function toArrayKey($choice)
{
if (!is_scalar($choice) && null !== $choice) {
throw new InvalidArgumentException(sprintf(
'The value of type "%s" cannot be converted to a valid array key.',
gettype($choice)
));
}
if (is_bool($choice) || (string) (int) $choice === (string) $choice) {
return (int) $choice;
}
return (string) $choice;
}
/**
* Creates a list with the given choices and values.
*
* The given choice array must have the same array keys as the value array.
* Each choice must be castable to an integer/string according to the
* casting rules described in {@link toArrayKey()}.
*
* If no values are given, the choices are cast to strings and used as
* values.
*
* @param array $choices The selectable choices
* @param string[] $values Optional. The string values of the choices
*
* @throws InvalidArgumentException If the keys of the choices don't match
* the keys of the values or if any of the
* choices is not scalar
*/
public function __construct(array $choices, array $values = array())
{
if (empty($values)) {
// The cast to strings happens later
$values = $choices;
} else {
$choiceKeys = array_keys($choices);
$valueKeys = array_keys($values);
if ($choiceKeys !== $valueKeys) {
throw new InvalidArgumentException(
sprintf(
'The keys of the choices and the values must match. The choice '.
'keys are: "%s". The value keys are: "%s".',
implode('", "', $choiceKeys),
implode('", "', $valueKeys)
)
);
}
}
$this->choices = array_map(array(__CLASS__, 'toArrayKey'), $choices);
$this->values = array_map('strval', $values);
}
/**
* {@inheritdoc}
*/
public function getChoices()
{
return $this->choices;
}
/**
* {@inheritdoc}
*/
public function getValues()
{
return $this->values;
}
/**
* {@inheritdoc}
*/
public function getChoicesForValues(array $values)
{
$values = array_map('strval', $values);
// The values are identical to the choices, so we can just return them
// to improve performance a little bit
return array_map(array(__CLASS__, 'toArrayKey'), array_intersect($values, $this->values));
}
/**
* {@inheritdoc}
*/
public function getValuesForChoices(array $choices)
{
$choices = array_map(array(__CLASS__, 'toArrayKey'), $choices);
// The choices are identical to the values, so we can just return them
// to improve performance a little bit
return array_map('strval', array_intersect($choices, $this->choices));
}
}

View File

@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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);
}

View File

@ -0,0 +1,189 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\ChoiceList\Factory;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* Caches the choice lists created by the decorated factory.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CachingFactoryDecorator implements ChoiceListFactoryInterface
{
/**
* @var ChoiceListFactoryInterface
*/
private $decoratedFactory;
/**
* @var ChoiceListInterface[]
*/
private $lists = array();
/**
* @var ChoiceListView[]
*/
private $views = array();
/**
* Generates a SHA-256 hash for the given value.
*
* Optionally, a namespace string can be passed. Calling this method will
* the same values, but different namespaces, will return different hashes.
*
* @param mixed $value The value to hash
* @param string $namespace Optional. The namespace
*
* @return string The SHA-256 hash
*
* @internal Should not be used by user-land code.
*/
public static function generateHash($value, $namespace = '')
{
if (is_object($value)) {
$value = spl_object_hash($value);
} elseif (is_array($value)) {
array_walk_recursive($value, function (&$v) {
if (is_object($v)) {
$v = spl_object_hash($v);
}
});
}
return hash('sha256', $namespace.':'.json_encode($value));
}
/**
* Decorates the given factory.
*
* @param ChoiceListFactoryInterface $decoratedFactory The decorated factory
*/
public function __construct(ChoiceListFactoryInterface $decoratedFactory)
{
$this->decoratedFactory = $decoratedFactory;
}
/**
* Returns the decorated factory.
*
* @return ChoiceListFactoryInterface The decorated factory
*/
public function getDecoratedFactory()
{
return $this->decoratedFactory;
}
/**
* {@inheritdoc}
*/
public function createListFromChoices($choices, $value = null)
{
if (!is_array($choices) && !$choices instanceof \Traversable) {
throw new UnexpectedTypeException($choices, 'array or \Traversable');
}
if ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}
// The value is not validated on purpose. The decorated factory may
// decide which values to accept and which not.
// We ignore the choice groups for caching. If two choice lists are
// requested with the same choices, but a different grouping, the same
// choice list is returned.
DefaultChoiceListFactory::flatten($choices, $flatChoices);
$hash = self::generateHash(array($flatChoices, $value), 'fromChoices');
if (!isset($this->lists[$hash])) {
$this->lists[$hash] = $this->decoratedFactory->createListFromChoices($choices, $value);
}
return $this->lists[$hash];
}
/**
* {@inheritdoc}
*
* @deprecated Added for backwards compatibility in Symfony 2.7, to be
* removed in Symfony 3.0.
*/
public function createListFromFlippedChoices($choices, $value = null)
{
if (!is_array($choices) && !$choices instanceof \Traversable) {
throw new UnexpectedTypeException($choices, 'array or \Traversable');
}
if ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}
// The value is not validated on purpose. The decorated factory may
// decide which values to accept and which not.
// We ignore the choice groups for caching. If two choice lists are
// requested with the same choices, but a different grouping, the same
// choice list is returned.
DefaultChoiceListFactory::flattenFlipped($choices, $flatChoices);
$hash = self::generateHash(array($flatChoices, $value), 'fromFlippedChoices');
if (!isset($this->lists[$hash])) {
$this->lists[$hash] = $this->decoratedFactory->createListFromFlippedChoices($choices, $value);
}
return $this->lists[$hash];
}
/**
* {@inheritdoc}
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null)
{
$hash = self::generateHash(array($loader, $value), 'fromLoader');
if (!isset($this->lists[$hash])) {
$this->lists[$hash] = $this->decoratedFactory->createListFromLoader($loader, $value);
}
return $this->lists[$hash];
}
/**
* {@inheritdoc}
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
{
// The input is not validated on purpose. This way, the decorated
// factory may decide which input to accept and which not.
$hash = self::generateHash(array($list, $preferredChoices, $label, $index, $groupBy, $attr));
if (!isset($this->views[$hash])) {
$this->views[$hash] = $this->decoratedFactory->createView(
$list,
$preferredChoices,
$label,
$index,
$groupBy,
$attr
);
}
return $this->views[$hash];
}
}

View File

@ -0,0 +1,124 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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);
}

View File

@ -0,0 +1,414 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\ChoiceList\Factory;
use Symfony\Component\Form\ChoiceList\ArrayKeyChoiceList;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\LazyChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface as LegacyChoiceListInterface;
/**
* Default implementation of {@link ChoiceListFactoryInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class DefaultChoiceListFactory implements ChoiceListFactoryInterface
{
/**
* Flattens an array into the given output variable.
*
* @param array $array The array to flatten
* @param array $output The flattened output
*
* @internal Should not be used by user-land code
*/
public static function flatten(array $array, &$output)
{
if (null === $output) {
$output = array();
}
foreach ($array as $key => $value) {
if (is_array($value)) {
self::flatten($value, $output);
continue;
}
$output[$key] = $value;
}
}
/**
* Flattens and flips an array into the given output variable.
*
* During the flattening, the keys and values of the input array are
* flipped.
*
* @param array $array The array to flatten
* @param array $output The flattened output
*
* @internal Should not be used by user-land code
*/
public static function flattenFlipped(array $array, &$output)
{
if (null === $output) {
$output = array();
}
foreach ($array as $key => $value) {
if (is_array($value)) {
self::flattenFlipped($value, $output);
continue;
}
$output[$value] = $key;
}
}
/**
* {@inheritdoc}
*/
public function createListFromChoices($choices, $value = null)
{
if (!is_array($choices) && !$choices instanceof \Traversable) {
throw new UnexpectedTypeException($choices, 'array or \Traversable');
}
if (null !== $value && !is_callable($value)) {
throw new UnexpectedTypeException($value, 'null or callable');
}
if ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}
// If the choices are given as recursive array (i.e. with explicit
// choice groups), flatten the array. The grouping information is needed
// in the view only.
self::flatten($choices, $flatChoices);
// If no values are given, use incrementing integers as values
// We can not use the choices themselves, because we don't know whether
// choices can be converted to (duplicate-free) strings
if (null === $value) {
$values = $flatChoices;
$i = 0;
foreach ($values as $key => $value) {
$values[$key] = (string) $i++;
}
return new ArrayChoiceList($flatChoices, $values);
}
// Can't use array_map(), because array_map() doesn't pass the key
// Can't use array_walk(), which ignores the return value of the
// closure
$values = array();
foreach ($flatChoices as $key => $choice) {
$values[$key] = call_user_func($value, $choice, $key);
}
return new ArrayChoiceList($flatChoices, $values);
}
/**
* {@inheritdoc}
*
* @deprecated Added for backwards compatibility in Symfony 2.7, to be
* removed in Symfony 3.0.
*/
public function createListFromFlippedChoices($choices, $value = null)
{
if (!is_array($choices) && !$choices instanceof \Traversable) {
throw new UnexpectedTypeException($choices, 'array or \Traversable');
}
if (null !== $value && !is_callable($value)) {
throw new UnexpectedTypeException($value, 'null or callable');
}
if ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}
// If the choices are given as recursive array (i.e. with explicit
// choice groups), flatten the array. The grouping information is needed
// in the view only.
self::flattenFlipped($choices, $flatChoices);
// If no values are given, use the choices as values
// Since the choices are stored in the collection keys, i.e. they are
// strings or integers, we are guaranteed to be able to convert them
// to strings
if (null === $value) {
$values = array_map('strval', $flatChoices);
return new ArrayKeyChoiceList($flatChoices, $values);
}
// Can't use array_map(), because array_map() doesn't pass the key
// Can't use array_walk(), which ignores the return value of the
// closure
$values = array();
foreach ($flatChoices as $key => $choice) {
$values[$key] = call_user_func($value, $choice, $key);
}
return new ArrayKeyChoiceList($flatChoices, $values);
}
/**
* {@inheritdoc}
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null)
{
if (null !== $value && !is_callable($value)) {
throw new UnexpectedTypeException($value, 'null or callable');
}
return new LazyChoiceList($loader, $value);
}
/**
* {@inheritdoc}
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
{
if (null !== $preferredChoices && !is_array($preferredChoices) && !is_callable($preferredChoices)) {
throw new UnexpectedTypeException($preferredChoices, 'null, array or callable');
}
if (null !== $label && !is_callable($label)) {
throw new UnexpectedTypeException($label, 'null or callable');
}
if (null !== $index && !is_callable($index)) {
throw new UnexpectedTypeException($index, 'null or callable');
}
if (null !== $groupBy && !is_array($groupBy) && !$groupBy instanceof \Traversable && !is_callable($groupBy)) {
throw new UnexpectedTypeException($groupBy, 'null, array, \Traversable or callable');
}
if (null !== $attr && !is_array($attr) && !is_callable($attr)) {
throw new UnexpectedTypeException($attr, 'null, array or callable');
}
// Backwards compatibility
if ($list instanceof LegacyChoiceListInterface && null === $preferredChoices
&& null === $label && null === $index && null === $groupBy && null === $attr) {
return new ChoiceListView($list->getRemainingViews(), $list->getPreferredViews());
}
$preferredViews = array();
$otherViews = array();
$choices = $list->getChoices();
$values = $list->getValues();
if (!is_callable($preferredChoices) && !empty($preferredChoices)) {
$preferredChoices = function ($choice) use ($preferredChoices) {
return false !== array_search($choice, $preferredChoices, true);
};
}
// The names are generated from an incrementing integer by default
if (null === $index) {
$i = 0;
$index = function () use (&$i) {
return $i++;
};
}
// If $groupBy is not given, no grouping is done
if (empty($groupBy)) {
foreach ($choices as $key => $choice) {
self::addChoiceView(
$choice,
$key,
$label,
$values,
$index,
$attr,
$preferredChoices,
$preferredViews,
$otherViews
);
}
return new ChoiceListView($otherViews, $preferredViews);
}
// If $groupBy is a callable, choices are added to the group with the
// name returned by the callable. If the callable returns null, the
// choice is not added to any group
if (is_callable($groupBy)) {
foreach ($choices as $key => $choice) {
self::addChoiceViewGroupedBy(
$groupBy,
$choice,
$key,
$label,
$values,
$index,
$attr,
$preferredChoices,
$preferredViews,
$otherViews
);
}
} else {
// If $groupBy is passed as array, use that array as template for
// constructing the groups
self::addChoiceViewsGroupedBy(
$groupBy,
$label,
$choices,
$values,
$index,
$attr,
$preferredChoices,
$preferredViews,
$otherViews
);
}
// Remove any empty group views that may have been created by
// addChoiceViewGroupedBy()
foreach ($preferredViews as $key => $view) {
if ($view instanceof ChoiceGroupView && 0 === count($view->choices)) {
unset($preferredViews[$key]);
}
}
foreach ($otherViews as $key => $view) {
if ($view instanceof ChoiceGroupView && 0 === count($view->choices)) {
unset($otherViews[$key]);
}
}
return new ChoiceListView($otherViews, $preferredViews);
}
private static function addChoiceView($choice, $key, $label, $values, $index, $attr, $isPreferred, &$preferredViews, &$otherViews)
{
$view = new ChoiceView(
// If the labels are null, use the choice key by default
null === $label ? (string) $key : (string) call_user_func($label, $choice, $key),
$values[$key],
$choice,
// The attributes may be a callable or a mapping from choice indices
// to nested arrays
is_callable($attr) ? call_user_func($attr, $choice, $key) : (isset($attr[$key]) ? $attr[$key] : array())
);
// $isPreferred may be null if no choices are preferred
if ($isPreferred && call_user_func($isPreferred, $choice, $key)) {
$preferredViews[call_user_func($index, $choice, $key)] = $view;
} else {
$otherViews[call_user_func($index, $choice, $key)] = $view;
}
}
private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $values, $index, $attr, $isPreferred, &$preferredViews, &$otherViews)
{
foreach ($groupBy as $key => $content) {
// Add the contents of groups to new ChoiceGroupView instances
if (is_array($content)) {
$preferredViewsForGroup = array();
$otherViewsForGroup = array();
self::addChoiceViewsGroupedBy(
$content,
$label,
$choices,
$values,
$index,
$attr,
$isPreferred,
$preferredViewsForGroup,
$otherViewsForGroup
);
if (count($preferredViewsForGroup) > 0) {
$preferredViews[$key] = new ChoiceGroupView($key, $preferredViewsForGroup);
}
if (count($otherViewsForGroup) > 0) {
$otherViews[$key] = new ChoiceGroupView($key, $otherViewsForGroup);
}
continue;
}
// Add ungrouped items directly
self::addChoiceView(
$choices[$key],
$key,
$label,
$values,
$index,
$attr,
$isPreferred,
$preferredViews,
$otherViews
);
}
}
private static function addChoiceViewGroupedBy($groupBy, $choice, $key, $label, $values, $index, $attr, $isPreferred, &$preferredViews, &$otherViews)
{
$groupLabel = call_user_func($groupBy, $choice, $key);
if (null === $groupLabel) {
// If the callable returns null, don't group the choice
self::addChoiceView(
$choice,
$key,
$label,
$values,
$index,
$attr,
$isPreferred,
$preferredViews,
$otherViews
);
return;
}
// Initialize the group views if necessary. Unnnecessarily built group
// views will be cleaned up at the end of createView()
if (!isset($preferredViews[$groupLabel])) {
$preferredViews[$groupLabel] = new ChoiceGroupView($groupLabel);
$otherViews[$groupLabel] = new ChoiceGroupView($groupLabel);
}
self::addChoiceView(
$choice,
$key,
$label,
$values,
$index,
$attr,
$isPreferred,
$preferredViews[$groupLabel]->choices,
$otherViews[$groupLabel]->choices
);
}
}

View File

@ -0,0 +1,226 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class PropertyAccessDecorator implements ChoiceListFactoryInterface
{
/**
* @var ChoiceListFactoryInterface
*/
private $decoratedFactory;
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* Decorates the given factory.
*
* @param ChoiceListFactoryInterface $decoratedFactory The decorated factory
* @param null|PropertyAccessorInterface $propertyAccessor The used property accessor
*/
public function __construct(ChoiceListFactoryInterface $decoratedFactory, PropertyAccessorInterface $propertyAccessor = null)
{
$this->decoratedFactory = $decoratedFactory;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
/**
* Returns the decorated factory.
*
* @return ChoiceListFactoryInterface The decorated factory
*/
public function getDecoratedFactory()
{
return $this->decoratedFactory;
}
/**
* {@inheritdoc}
*
* @param array|\Traversable $choices The choices
* @param null|callable|string|PropertyPath $value The callable or path for
* generating the choice values
*
* @return ChoiceListInterface The choice list
*/
public function createListFromChoices($choices, $value = null)
{
if (is_string($value)) {
$value = new PropertyPath($value);
}
if ($value instanceof PropertyPath) {
$accessor = $this->propertyAccessor;
$value = function ($choice) use ($accessor, $value) {
return $accessor->getValue($choice, $value);
};
}
return $this->decoratedFactory->createListFromChoices($choices, $value);
}
/**
* {@inheritdoc}
*
* @param array|\Traversable $choices The choices
* @param null|callable|string|PropertyPath $value The callable or path for
* generating the choice values
*
* @return ChoiceListInterface The choice list
*
* @deprecated Added for backwards compatibility in Symfony 2.7, to be
* removed in Symfony 3.0.
*/
public function createListFromFlippedChoices($choices, $value = null)
{
// Property paths are not supported here, because array keys can never
// be objects
return $this->decoratedFactory->createListFromFlippedChoices($choices, $value);
}
/**
* {@inheritdoc}
*
* @param ChoiceLoaderInterface $loader The choice loader
* @param null|callable|string|PropertyPath $value The callable or path for
* generating the choice values
*
* @return ChoiceListInterface The choice list
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null)
{
if (is_string($value)) {
$value = new PropertyPath($value);
}
if ($value instanceof PropertyPath) {
$accessor = $this->propertyAccessor;
$value = function ($choice) use ($accessor, $value) {
return $accessor->getValue($choice, $value);
};
}
return $this->decoratedFactory->createListFromLoader($loader, $value);
}
/**
* {@inheritdoc}
*
* @param ChoiceListInterface $list The choice list
* @param null|array|callable|PropertyPath $preferredChoices The preferred choices
* @param null|callable|PropertyPath $label The callable or path
* generating the choice labels
* @param null|callable|PropertyPath $index The callable or path
* generating the view indices
* @param null|array|\Traversable|callable|PropertyPath $groupBy The callable or path
* generating the group names
* @param null|array|callable|PropertyPath $attr The callable or path
* generating the HTML attributes
*
* @return ChoiceListView The choice list view
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
{
$accessor = $this->propertyAccessor;
if (is_string($label)) {
$label = new PropertyPath($label);
}
if ($label instanceof PropertyPath) {
$label = function ($choice) use ($accessor, $label) {
return $accessor->getValue($choice, $label);
};
}
if (is_string($preferredChoices)) {
$preferredChoices = new PropertyPath($preferredChoices);
}
if ($preferredChoices instanceof PropertyPath) {
$preferredChoices = function ($choice) use ($accessor, $preferredChoices) {
try {
return $accessor->getValue($choice, $preferredChoices);
} catch (UnexpectedTypeException $e) {
// Assume not preferred if not readable
return false;
}
};
}
if (is_string($index)) {
$index = new PropertyPath($index);
}
if ($index instanceof PropertyPath) {
$index = function ($choice) use ($accessor, $index) {
return $accessor->getValue($choice, $index);
};
}
if (is_string($groupBy)) {
$groupBy = new PropertyPath($groupBy);
}
if ($groupBy instanceof PropertyPath) {
$groupBy = function ($choice) use ($accessor, $groupBy) {
try {
return $accessor->getValue($choice, $groupBy);
} catch (UnexpectedTypeException $e) {
// Don't group if path is not readable
}
};
}
if (is_string($attr)) {
$attr = new PropertyPath($attr);
}
if ($attr instanceof PropertyPath) {
$attr = function ($choice) use ($accessor, $attr) {
return $accessor->getValue($choice, $attr);
};
}
return $this->decoratedFactory->createView($list, $preferredChoices, $label, $index, $groupBy, $attr);
}
}

View File

@ -0,0 +1,115 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class LazyChoiceList implements ChoiceListInterface
{
/**
* The choice loader.
*
* @var ChoiceLoaderInterface
*/
private $loader;
/**
* The callable creating string values for each choice.
*
* If null, choices are simply cast to strings.
*
* @var null|callable
*/
private $value;
/**
* @var ChoiceListInterface
*/
private $loadedList;
/**
* Creates a lazily-loaded list using the given loader.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
*
* @param ChoiceLoaderInterface $loader The choice loader
* @param null|callable $value The callable generating the choice
* values
*/
public function __construct(ChoiceLoaderInterface $loader, $value = null)
{
$this->loader = $loader;
$this->value = $value;
}
/**
* {@inheritdoc}
*/
public function getChoices()
{
if (!$this->loadedList) {
$this->loadedList = $this->loader->loadChoiceList($this->value);
}
return $this->loadedList->getChoices();
}
/**
* {@inheritdoc}
*/
public function getValues()
{
if (!$this->loadedList) {
$this->loadedList = $this->loader->loadChoiceList($this->value);
}
return $this->loadedList->getValues();
}
/**
* {@inheritdoc}
*/
public function getChoicesForValues(array $values)
{
if (!$this->loadedList) {
return $this->loader->loadChoicesForValues($values, $this->value);
}
return $this->loadedList->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function getValuesForChoices(array $choices)
{
if (!$this->loadedList) {
return $this->loader->loadValuesForChoices($choices, $this->value);
}
return $this->loadedList->getValuesForChoices($choices);
}
}

View File

@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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);
}

View File

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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);
}
}

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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;
}
}

View File

@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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;
}
}

View File

@ -29,10 +29,13 @@ use Symfony\Component\Form\Extension\Core\View\ChoiceView;
* <code>
* $choices = array(true, false);
* $labels = array('Agree', 'Disagree');
* $choiceList = new ChoiceList($choices, $labels);
* $choiceList = new ArrayChoiceList($choices, $labels);
* </code>
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
* Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayChoiceList} instead.
*/
class ChoiceList implements ChoiceListInterface
{

View File

@ -25,23 +25,13 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList;
* in the HTML "value" attribute.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
* Use {@link \Symfony\Component\Form\ArrayChoiceList\ChoiceListInterface}
* instead.
*/
interface ChoiceListInterface
interface ChoiceListInterface extends \Symfony\Component\Form\ChoiceList\ChoiceListInterface
{
/**
* Returns the list of choices.
*
* @return array The choices with their indices as keys
*/
public function getChoices();
/**
* Returns the values for the choices.
*
* @return array The values with the corresponding choice indices as keys
*/
public function getValues();
/**
* Returns the choice views of the preferred choices as nested array with
* the choice groups as top-level keys.
@ -92,37 +82,6 @@ interface ChoiceListInterface
*/
public function getRemainingViews();
/**
* Returns the choices corresponding to the given values.
*
* The choices can have any data type.
*
* The choices must be returned with the same keys and in the same order
* as the corresponding values in the given array.
*
* @param array $values An array of choice values. Not existing values in
* this array are ignored
*
* @return array An array of choices with ascending, 0-based numeric keys
*/
public function getChoicesForValues(array $values);
/**
* Returns the values corresponding to the given choices.
*
* The values must be strings.
*
* The values must be returned with the same keys and in the same order
* as the corresponding choices in the given array.
*
* @param array $choices An array of choices. Not existing choices in this
* array are ignored
*
* @return array An array of choice values with ascending, 0-based numeric
* keys
*/
public function getValuesForChoices(array $choices);
/**
* Returns the indices corresponding to the given choices.
*

View File

@ -21,6 +21,10 @@ use Symfony\Component\Form\Exception\InvalidArgumentException;
* which should return a ChoiceListInterface instance.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
* Use {@link \Symfony\Component\Form\ArrayChoiceList\LazyChoiceList}
* instead.
*/
abstract class LazyChoiceList implements ChoiceListInterface
{

View File

@ -32,6 +32,10 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
* </code>
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
* Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayChoiceList}
* instead.
*/
class ObjectChoiceList extends ChoiceList
{

View File

@ -28,6 +28,10 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList;
* </code>
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
* Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayKeyChoiceList}
* instead.
*/
class SimpleChoiceList extends ChoiceList
{

View File

@ -12,7 +12,12 @@
namespace Symfony\Component\Form\Extension\Core;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* Represents the main form extension, which loads the core functionality.
@ -21,13 +26,29 @@ use Symfony\Component\PropertyAccess\PropertyAccess;
*/
class CoreExtension extends AbstractExtension
{
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* @var ChoiceListFactoryInterface
*/
private $choiceListFactory;
public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
}
protected function loadTypes()
{
return array(
new Type\FormType(PropertyAccess::createPropertyAccessor()),
new Type\FormType($this->propertyAccessor),
new Type\BirthdayType(),
new Type\CheckboxType(),
new Type\ChoiceType(),
new Type\ChoiceType($this->choiceListFactory),
new Type\CollectionType(),
new Type\CountryType(),
new Type\DateType(),

View File

@ -0,0 +1,93 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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
);
}
}
}

View File

@ -17,7 +17,7 @@ use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* A data mapper using property paths to read/write data.
* Maps arrays/objects to/from forms using property paths.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
@ -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)
{

View File

@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class RadioListMapper implements DataMapperInterface
{
/**
* @var ChoiceListInterface
*/
private $choiceList;
public function __construct(ChoiceListInterface $choiceList)
{
$this->choiceList = $choiceList;
}
/**
* {@inheritdoc}
*/
public function mapDataToForms($choice, $radios)
{
$valueMap = array_flip($this->choiceList->getValuesForChoices(array($choice)));
foreach ($radios as $radio) {
$value = $radio->getConfig()->getOption('value');
$radio->setData(isset($valueMap[$value]) ? true : false);
}
}
/**
* {@inheritdoc}
*/
public function mapFormsToData($radios, &$choice)
{
$choice = null;
foreach ($radios as $radio) {
if ($radio->getData()) {
if ('placeholder' === $radio->getName()) {
$choice = null;
return;
}
$value = $radio->getConfig()->getOption('value');
$choice = current($this->choiceList->getChoicesForValues(array($value)));
return;
}
}
}
}

View File

@ -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 <bschussek@gmail.com>
*
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
* Use {@link \Symfony\Component\Form\ArrayChoiceList\LazyChoiceList}
* instead.
*/
class ChoiceToBooleanArrayTransformer implements DataTransformerInterface
{

View File

@ -13,7 +13,7 @@ namespace Symfony\Component\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
@ -43,7 +43,7 @@ class ChoiceToValueTransformer implements DataTransformerInterface
throw new TransformationFailedException('Expected a scalar.');
}
// These are now valid ChoiceList values, so we can return null
// These are now valid ArrayChoiceList values, so we can return null
// right away
if ('' === $value || null === $value) {
return;

View File

@ -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 <bschussek@gmail.com>
*
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
* Use {@link \Symfony\Component\Form\ArrayChoiceList\LazyChoiceList}
* instead.
*/
class ChoicesToBooleanArrayTransformer implements DataTransformerInterface
{

View File

@ -13,7 +13,7 @@ namespace Symfony\Component\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>

View File

@ -11,11 +11,11 @@
namespace Symfony\Component\Form\Extension\Core\EventListener;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
/**
* Takes care of converting the input from a list of checkboxes to a correctly

View File

@ -11,10 +11,10 @@
namespace Symfony\Component\Form\Extension\Core\EventListener;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
/**
* Takes care of converting the input from a single radio button

View File

@ -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']) {
// <select> tag with "multiple" option
$builder->addViewTransformer(new ChoicesToValuesTransformer($options['choice_list']));
} else {
if ($options['multiple']) {
$builder->addViewTransformer(new ChoicesToValuesTransformer($options['choice_list']));
} else {
$builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list']));
}
// <select> tag without "multiple" option
$builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list']));
}
if ($options['multiple'] && $options['by_reference']) {
@ -95,11 +158,16 @@ class ChoiceType extends AbstractType
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
/** @var ChoiceListView $choiceListView */
$choiceListView = $form->getConfig()->hasAttribute('choice_list_view')
? $form->getConfig()->getAttribute('choice_list_view')
: $this->createChoiceListView($options['choice_list'], $options);
$view->vars = array_replace($view->vars, array(
'multiple' => $options['multiple'],
'expanded' => $options['expanded'],
'preferred_choices' => $options['choice_list']->getPreferredViews(),
'choices' => $options['choice_list']->getRemainingViews(),
'preferred_choices' => $choiceListView->preferredChoices,
'choices' => $choiceListView->choices,
'separator' => '-------------------',
'placeholder' => null,
));
@ -163,20 +231,39 @@ class ChoiceType extends AbstractType
*/
public function configureOptions(OptionsResolver $resolver)
{
$choiceListCache = &$this->choiceListCache;
$choiceListFactory = $this->choiceListFactory;
$choiceList = function (Options $options) use ($choiceListFactory) {
if (null !== $options['choice_loader']) {
// Due to a bug in OptionsResolver, the choices haven't been
// validated yet at this point. Remove the if statement once that
// bug is resolved
if (!$options['choice_loader'] instanceof ChoiceLoaderInterface) {
return;
}
return $choiceListFactory->createListFromLoader(
$options['choice_loader'],
$options['choice_value']
);
}
$choiceList = function (Options $options) use (&$choiceListCache) {
// Harden against NULL values (like in EntityType and ModelType)
$choices = null !== $options['choices'] ? $options['choices'] : array();
// Reuse existing choice lists in order to increase performance
$hash = hash('sha256', serialize(array($choices, $options['preferred_choices'])));
if (!isset($choiceListCache[$hash])) {
$choiceListCache[$hash] = new SimpleChoiceList($choices, $options['preferred_choices']);
// Due to a bug in OptionsResolver, the choices haven't been
// validated yet at this point. Remove the if statement once that
// bug is resolved
if (!is_array($choices) && !$choices instanceof \Traversable) {
return;
}
return $choiceListCache[$hash];
// BC when choices are in the keys, not in the values
if (!$options['choices_as_values']) {
return $choiceListFactory->createListFromFlippedChoices($choices, $options['choice_value']);
}
return $choiceListFactory->createListFromChoices($choices, $options['choice_value']);
};
$emptyData = function (Options $options) {
@ -219,9 +306,16 @@ class ChoiceType extends AbstractType
$resolver->setDefaults(array(
'multiple' => false,
'expanded' => false,
'choice_list' => $choiceList,
'choice_list' => $choiceList, // deprecated
'choices' => array(),
'choices_as_values' => false,
'choice_loader' => null,
'choice_label' => null,
'choice_name' => null,
'choice_value' => null,
'choice_attr' => null,
'preferred_choices' => array(),
'group_by' => null,
'empty_data' => $emptyData,
'empty_value' => $emptyValue, // deprecated
'placeholder' => $placeholder,
@ -236,7 +330,16 @@ class ChoiceType extends AbstractType
$resolver->setNormalizer('empty_value', $placeholderNormalizer);
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setAllowedTypes('choice_list', array('null', 'Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface'));
$resolver->setAllowedTypes('choice_list', array('null', 'Symfony\Component\Form\ChoiceList\ChoiceListInterface'));
$resolver->setAllowedTypes('choices', array('null', 'array', '\Traversable'));
$resolver->setAllowedTypes('choices_as_values', 'bool');
$resolver->setAllowedTypes('choice_loader', array('null', 'Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'));
$resolver->setAllowedTypes('choice_label', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('choice_name', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('choice_value', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('choice_attr', array('null', 'array', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('preferred_choices', array('array', '\Traversable', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('group_by', array('null', 'array', '\Traversable', 'string', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
}
/**
@ -247,6 +350,21 @@ class ChoiceType extends AbstractType
return 'choice';
}
private static function flipRecursive($choices, &$output = array())
{
foreach ($choices as $key => $value) {
if (is_array($value)) {
$output[$key] = array();
self::flipRecursive($value, $output[$key]);
continue;
}
$output[$value] = $key;
}
return $output;
}
/**
* Adds the sub fields for an expanded choice field.
*
@ -256,29 +374,69 @@ class ChoiceType extends AbstractType
*/
private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options)
{
foreach ($choiceViews as $i => $choiceView) {
foreach ($choiceViews as $name => $choiceView) {
// Flatten groups
if (is_array($choiceView)) {
// Flatten groups
$this->addSubForms($builder, $choiceView, $options);
} else {
$choiceOpts = array(
'value' => $choiceView->value,
'label' => $choiceView->label,
'translation_domain' => $options['translation_domain'],
'block_name' => 'entry',
);
if ($options['multiple']) {
$choiceType = 'checkbox';
// The user can check 0 or more checkboxes. If required
// is true, he is required to check all of them.
$choiceOpts['required'] = false;
} else {
$choiceType = 'radio';
}
$builder->add($i, $choiceType, $choiceOpts);
continue;
}
if ($choiceView instanceof ChoiceGroupView) {
$this->addSubForms($builder, $choiceView->choices, $options);
continue;
}
$this->addSubForm($builder, $name, $choiceView, $options);
}
}
/**
* @param FormBuilderInterface $builder
* @param $name
* @param $choiceView
* @param array $options
*
* @return mixed
*/
private function addSubForm(FormBuilderInterface $builder, $name, ChoiceView $choiceView, array $options)
{
$choiceOpts = array(
'value' => $choiceView->value,
'label' => $choiceView->label,
'attr' => $choiceView->attr,
'translation_domain' => $options['translation_domain'],
'block_name' => 'entry',
);
if ($options['multiple']) {
$choiceType = 'checkbox';
// The user can check 0 or more checkboxes. If required
// is true, he is required to check all of them.
$choiceOpts['required'] = false;
} else {
$choiceType = 'radio';
}
$builder->add($name, $choiceType, $choiceOpts);
}
private function createChoiceListView(ChoiceListInterface $choiceList, array $options)
{
// If no explicit grouping information is given, use the structural
// information from the "choices" option for creating groups
if (!$options['group_by'] && $options['choices']) {
$options['group_by'] = !$options['choices_as_values']
? ChoiceType::flipRecursive($options['choices'])
: $options['choices'];
}
return $this->choiceListFactory->createView(
$choiceList,
$options['preferred_choices'],
$options['choice_label'],
$options['choice_name'],
$options['group_by'],
$options['choice_attr']
);
}
}

View File

@ -16,29 +16,8 @@ namespace Symfony\Component\Form\Extension\Core\View;
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoiceView
class ChoiceView extends \Symfony\Component\Form\ChoiceList\View\ChoiceView
{
/**
* The original choice value.
*
* @var mixed
*/
public $data;
/**
* The view representation of the choice.
*
* @var string
*/
public $value;
/**
* The label displayed to humans.
*
* @var string
*/
public $label;
/**
* Creates a new ChoiceView.
*
@ -48,8 +27,6 @@ class ChoiceView
*/
public function __construct($data, $value, $label)
{
$this->data = $data;
$this->value = $value;
$this->label = $label;
parent::__construct($label, $value, $data);
}
}

View File

@ -516,6 +516,28 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg
);
}
public function testSingleChoiceAttributes()
{
$form = $this->factory->createNamed('name', 'choice', '&a', array(
'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')),
'multiple' => false,
'expanded' => false,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/select
[@name="name"]
[not(@required)]
[
./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"]
]
[count(./option)=2]
'
);
}
public function testSingleChoiceWithPreferred()
{
$form = $this->factory->createNamed('name', 'choice', '&a', array(
@ -776,6 +798,30 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg
);
}
public function testMultipleChoiceAttributes()
{
$form = $this->factory->createNamed('name', 'choice', array('&a'), array(
'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')),
'required' => true,
'multiple' => true,
'expanded' => false,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/select
[@name="name[]"]
[@required="required"]
[@multiple="multiple"]
[
./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"]
]
[count(./option)=2]
'
);
}
public function testMultipleChoiceSkipsPlaceholder()
{
$form = $this->factory->createNamed('name', 'choice', array('&a'), array(
@ -842,6 +888,29 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg
);
}
public function testSingleChoiceExpandedAttributes()
{
$form = $this->factory->createNamed('name', 'choice', '&a', array(
'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')),
'multiple' => false,
'expanded' => true,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked]
/following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][@class="foo&bar"][not(@checked)]
/following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"]
/following-sibling::input[@type="hidden"][@id="name__token"]
]
[count(./input)=3]
'
);
}
public function testSingleChoiceExpandedWithPlaceholder()
{
$form = $this->factory->createNamed('name', 'choice', '&a', array(
@ -914,6 +983,32 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg
);
}
public function testMultipleChoiceExpandedAttributes()
{
$form = $this->factory->createNamed('name', 'choice', array('&a', '&c'), array(
'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B', '&c' => 'Choice&C'),
'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')),
'multiple' => true,
'expanded' => true,
'required' => true,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)]
/following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][@class="foo&bar"][not(@checked)][not(@required)]
/following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"]
/following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)]
/following-sibling::label[@for="name_2"][.="[trans]Choice&C[/trans]"]
/following-sibling::input[@type="hidden"][@id="name__token"]
]
[count(./input)=4]
'
);
}
public function testCountry()
{
$form = $this->factory->createNamed('name', 'country', 'AT');

View File

@ -0,0 +1,173 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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();
}

View File

@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class ArrayChoiceListTest extends AbstractChoiceListTest
{
private $object;
protected function setUp()
{
parent::setUp();
$this->object = new \stdClass();
}
protected function createChoiceList()
{
return new ArrayChoiceList($this->getChoices(), $this->getValues());
}
protected function getChoices()
{
return array(0, 1, '1', 'a', false, true, $this->object);
}
protected function getValues()
{
return array('0', '1', '2', '3', '4', '5', '6');
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException
*/
public function testFailIfKeyMismatch()
{
new ArrayChoiceList(array(0 => 'a', 1 => 'b'), array(1 => 'a', 2 => 'b'));
}
}

View File

@ -0,0 +1,187 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class ArrayKeyChoiceListTest extends AbstractChoiceListTest
{
private $object;
protected function setUp()
{
parent::setUp();
$this->object = new \stdClass();
}
protected function createChoiceList()
{
return new ArrayKeyChoiceList($this->getChoices(), $this->getValues());
}
protected function getChoices()
{
return array(0, 1, 'a', 'b', '');
}
protected function getValues()
{
return array('0', '1', 'a', 'b', '');
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException
*/
public function testFailIfKeyMismatch()
{
new ArrayKeyChoiceList(array(0 => 'a', 1 => 'b'), array(1 => 'a', 2 => 'b'));
}
public function testUseChoicesAsValuesByDefault()
{
$list = new ArrayKeyChoiceList(array(1 => '', 3 => 0, 7 => '1', 10 => 1.23));
$this->assertSame(array(1 => '', 3 => '0', 7 => '1', 10 => '1.23'), $list->getValues());
}
public function testNoChoices()
{
$list = new ArrayKeyChoiceList(array());
$this->assertSame(array(), $list->getValues());
}
public function testGetChoicesForValuesConvertsValuesToStrings()
{
$this->assertSame(array(0), $this->list->getChoicesForValues(array(0)));
$this->assertSame(array(0), $this->list->getChoicesForValues(array('0')));
$this->assertSame(array(1), $this->list->getChoicesForValues(array(1)));
$this->assertSame(array(1), $this->list->getChoicesForValues(array('1')));
$this->assertSame(array('a'), $this->list->getChoicesForValues(array('a')));
$this->assertSame(array('b'), $this->list->getChoicesForValues(array('b')));
$this->assertSame(array(''), $this->list->getChoicesForValues(array('')));
// "1" === (string) true
$this->assertSame(array(1), $this->list->getChoicesForValues(array(true)));
// "" === (string) false
$this->assertSame(array(''), $this->list->getChoicesForValues(array(false)));
// "" === (string) null
$this->assertSame(array(''), $this->list->getChoicesForValues(array(null)));
$this->assertSame(array(), $this->list->getChoicesForValues(array(1.23)));
}
public function testGetValuesForChoicesConvertsChoicesToArrayKeys()
{
$this->assertSame(array('0'), $this->list->getValuesForChoices(array(0)));
$this->assertSame(array('0'), $this->list->getValuesForChoices(array('0')));
$this->assertSame(array('1'), $this->list->getValuesForChoices(array(1)));
$this->assertSame(array('1'), $this->list->getValuesForChoices(array('1')));
$this->assertSame(array('a'), $this->list->getValuesForChoices(array('a')));
$this->assertSame(array('b'), $this->list->getValuesForChoices(array('b')));
// Always cast booleans to 0 and 1, because:
// array(true => 'Yes', false => 'No') === array(1 => 'Yes', 0 => 'No')
// see ChoiceTypeTest::testSetDataSingleNonExpandedAcceptsBoolean
$this->assertSame(array('0'), $this->list->getValuesForChoices(array(false)));
$this->assertSame(array('1'), $this->list->getValuesForChoices(array(true)));
}
/**
* @dataProvider provideConvertibleChoices
*/
public function testConvertChoicesIfNecessary(array $choices, array $converted)
{
$list = new ArrayKeyChoiceList($choices, range(0, count($choices) - 1));
$this->assertSame($converted, $list->getChoices());
}
public function provideConvertibleChoices()
{
return array(
array(array(0), array(0)),
array(array(1), array(1)),
array(array('0'), array(0)),
array(array('1'), array(1)),
array(array('1.23'), array('1.23')),
array(array('foobar'), array('foobar')),
// The default value of choice fields is NULL. It should be treated
// like the empty value for this choice list type
array(array(null), array('')),
array(array(1.23), array('1.23')),
// Always cast booleans to 0 and 1, because:
// array(true => 'Yes', false => 'No') === array(1 => 'Yes', 0 => 'No')
// see ChoiceTypeTest::testSetDataSingleNonExpandedAcceptsBoolean
array(array(true), array(1)),
array(array(false), array(0)),
);
}
/**
* @dataProvider provideInvalidChoices
* @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException
*/
public function testFailIfInvalidChoices(array $choices)
{
new ArrayKeyChoiceList($choices, range(0, count($choices) - 1));
}
/**
* @dataProvider provideInvalidChoices
* @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException
*/
public function testGetValuesForChoicesFailsIfInvalidChoices(array $choices)
{
$this->list->getValuesForChoices($choices);
}
public function provideInvalidChoices()
{
return array(
array(array(new \stdClass())),
array(array(array(1, 2))),
);
}
/**
* @dataProvider provideConvertibleValues
*/
public function testConvertValuesToStrings(array $values, array $converted)
{
$list = new ArrayKeyChoiceList(range(0, count($values) - 1), $values);
$this->assertSame($converted, $list->getValues());
}
public function provideConvertibleValues()
{
return array(
array(array(0), array('0')),
array(array(1), array('1')),
array(array('0'), array('0')),
array(array('1'), array('1')),
array(array('1.23'), array('1.23')),
array(array('foobar'), array('foobar')),
// The default value of choice fields is NULL. It should be treated
// like the empty value for this choice list type
array(array(null), array('')),
array(array(1.23), array('1.23')),
// Always cast booleans to 0 and 1, because:
// array(true => 'Yes', false => 'No') === array(1 => 'Yes', 0 => 'No')
// see ChoiceTypeTest::testSetDataSingleNonExpandedAcceptsBoolean
array(array(true), array('1')),
array(array(false), array('')),
);
}
}

View File

@ -0,0 +1,668 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class CachingFactoryDecoratorTest extends \PHPUnit_Framework_TestCase
{
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $decoratedFactory;
/**
* @var CachingFactoryDecorator
*/
private $factory;
protected function setUp()
{
$this->decoratedFactory = $this->getMock('Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface');
$this->factory = new CachingFactoryDecorator($this->decoratedFactory);
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testCreateFromChoicesFailsIfChoicesNotArrayOrTraversable()
{
$this->factory->createListFromChoices('foobar');
}
public function testCreateFromChoicesEmpty()
{
$list = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createListFromChoices')
->with(array())
->will($this->returnValue($list));
$this->assertSame($list, $this->factory->createListFromChoices(array()));
$this->assertSame($list, $this->factory->createListFromChoices(array()));
}
public function testCreateFromChoicesComparesTraversableChoicesAsArray()
{
// The top-most traversable is converted to an array
$choices1 = new \ArrayIterator(array('A' => 'a'));
$choices2 = array('A' => 'a');
$list = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createListFromChoices')
->with($choices2)
->will($this->returnValue($list));
$this->assertSame($list, $this->factory->createListFromChoices($choices1));
$this->assertSame($list, $this->factory->createListFromChoices($choices2));
}
public function testCreateFromChoicesFlattensChoices()
{
$choices1 = array('key' => array('A' => 'a'));
$choices2 = array('A' => 'a');
$list = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createListFromChoices')
->with($choices1)
->will($this->returnValue($list));
$this->assertSame($list, $this->factory->createListFromChoices($choices1));
$this->assertSame($list, $this->factory->createListFromChoices($choices2));
}
/**
* @dataProvider provideSameChoices
*/
public function testCreateFromChoicesSameChoices($choice1, $choice2)
{
$choices1 = array($choice1);
$choices2 = array($choice2);
$list = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createListFromChoices')
->with($choices1)
->will($this->returnValue($list));
$this->assertSame($list, $this->factory->createListFromChoices($choices1));
$this->assertSame($list, $this->factory->createListFromChoices($choices2));
}
/**
* @dataProvider provideDistinguishedChoices
*/
public function testCreateFromChoicesDifferentChoices($choice1, $choice2)
{
$choices1 = array($choice1);
$choices2 = array($choice2);
$list1 = new \stdClass();
$list2 = new \stdClass();
$this->decoratedFactory->expects($this->at(0))
->method('createListFromChoices')
->with($choices1)
->will($this->returnValue($list1));
$this->decoratedFactory->expects($this->at(1))
->method('createListFromChoices')
->with($choices2)
->will($this->returnValue($list2));
$this->assertSame($list1, $this->factory->createListFromChoices($choices1));
$this->assertSame($list2, $this->factory->createListFromChoices($choices2));
}
public function testCreateFromChoicesSameValueClosure()
{
$choices = array(1);
$list = new \stdClass();
$closure = function () {};
$this->decoratedFactory->expects($this->once())
->method('createListFromChoices')
->with($choices, $closure)
->will($this->returnValue($list));
$this->assertSame($list, $this->factory->createListFromChoices($choices, $closure));
$this->assertSame($list, $this->factory->createListFromChoices($choices, $closure));
}
public function testCreateFromChoicesDifferentValueClosure()
{
$choices = array(1);
$list1 = new \stdClass();
$list2 = new \stdClass();
$closure1 = function () {};
$closure2 = function () {};
$this->decoratedFactory->expects($this->at(0))
->method('createListFromChoices')
->with($choices, $closure1)
->will($this->returnValue($list1));
$this->decoratedFactory->expects($this->at(1))
->method('createListFromChoices')
->with($choices, $closure2)
->will($this->returnValue($list2));
$this->assertSame($list1, $this->factory->createListFromChoices($choices, $closure1));
$this->assertSame($list2, $this->factory->createListFromChoices($choices, $closure2));
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testCreateFromFlippedChoicesFailsIfChoicesNotArrayOrTraversable()
{
$this->factory->createListFromFlippedChoices('foobar');
}
public function testCreateFromFlippedChoicesEmpty()
{
$list = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createListFromFlippedChoices')
->with(array())
->will($this->returnValue($list));
$this->assertSame($list, $this->factory->createListFromFlippedChoices(array()));
$this->assertSame($list, $this->factory->createListFromFlippedChoices(array()));
}
public function testCreateFromFlippedChoicesComparesTraversableChoicesAsArray()
{
// The top-most traversable is converted to an array
$choices1 = new \ArrayIterator(array('a' => 'A'));
$choices2 = array('a' => 'A');
$list = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createListFromFlippedChoices')
->with($choices2)
->will($this->returnValue($list));
$this->assertSame($list, $this->factory->createListFromFlippedChoices($choices1));
$this->assertSame($list, $this->factory->createListFromFlippedChoices($choices2));
}
public function testCreateFromFlippedChoicesFlattensChoices()
{
$choices1 = array('key' => array('a' => 'A'));
$choices2 = array('a' => 'A');
$list = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createListFromFlippedChoices')
->with($choices1)
->will($this->returnValue($list));
$this->assertSame($list, $this->factory->createListFromFlippedChoices($choices1));
$this->assertSame($list, $this->factory->createListFromFlippedChoices($choices2));
}
/**
* @dataProvider provideSameKeyChoices
*/
public function testCreateFromFlippedChoicesSameChoices($choice1, $choice2)
{
$choices1 = array($choice1);
$choices2 = array($choice2);
$list = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createListFromFlippedChoices')
->with($choices1)
->will($this->returnValue($list));
$this->assertSame($list, $this->factory->createListFromFlippedChoices($choices1));
$this->assertSame($list, $this->factory->createListFromFlippedChoices($choices2));
}
/**
* @dataProvider provideDistinguishedKeyChoices
*/
public function testCreateFromFlippedChoicesDifferentChoices($choice1, $choice2)
{
$choices1 = array($choice1);
$choices2 = array($choice2);
$list1 = new \stdClass();
$list2 = new \stdClass();
$this->decoratedFactory->expects($this->at(0))
->method('createListFromFlippedChoices')
->with($choices1)
->will($this->returnValue($list1));
$this->decoratedFactory->expects($this->at(1))
->method('createListFromFlippedChoices')
->with($choices2)
->will($this->returnValue($list2));
$this->assertSame($list1, $this->factory->createListFromFlippedChoices($choices1));
$this->assertSame($list2, $this->factory->createListFromFlippedChoices($choices2));
}
public function testCreateFromFlippedChoicesSameValueClosure()
{
$choices = array(1);
$list = new \stdClass();
$closure = function () {};
$this->decoratedFactory->expects($this->once())
->method('createListFromFlippedChoices')
->with($choices, $closure)
->will($this->returnValue($list));
$this->assertSame($list, $this->factory->createListFromFlippedChoices($choices, $closure));
$this->assertSame($list, $this->factory->createListFromFlippedChoices($choices, $closure));
}
public function testCreateFromFlippedChoicesDifferentValueClosure()
{
$choices = array(1);
$list1 = new \stdClass();
$list2 = new \stdClass();
$closure1 = function () {};
$closure2 = function () {};
$this->decoratedFactory->expects($this->at(0))
->method('createListFromFlippedChoices')
->with($choices, $closure1)
->will($this->returnValue($list1));
$this->decoratedFactory->expects($this->at(1))
->method('createListFromFlippedChoices')
->with($choices, $closure2)
->will($this->returnValue($list2));
$this->assertSame($list1, $this->factory->createListFromFlippedChoices($choices, $closure1));
$this->assertSame($list2, $this->factory->createListFromFlippedChoices($choices, $closure2));
}
public function testCreateFromLoaderSameLoader()
{
$loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface');
$list = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createListFromLoader')
->with($loader)
->will($this->returnValue($list));
$this->assertSame($list, $this->factory->createListFromLoader($loader));
$this->assertSame($list, $this->factory->createListFromLoader($loader));
}
public function testCreateFromLoaderDifferentLoader()
{
$loader1 = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface');
$loader2 = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface');
$list1 = new \stdClass();
$list2 = new \stdClass();
$this->decoratedFactory->expects($this->at(0))
->method('createListFromLoader')
->with($loader1)
->will($this->returnValue($list1));
$this->decoratedFactory->expects($this->at(1))
->method('createListFromLoader')
->with($loader2)
->will($this->returnValue($list2));
$this->assertSame($list1, $this->factory->createListFromLoader($loader1));
$this->assertSame($list2, $this->factory->createListFromLoader($loader2));
}
public function testCreateFromLoaderSameValueClosure()
{
$loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface');
$list = new \stdClass();
$closure = function () {};
$this->decoratedFactory->expects($this->once())
->method('createListFromLoader')
->with($loader, $closure)
->will($this->returnValue($list));
$this->assertSame($list, $this->factory->createListFromLoader($loader, $closure));
$this->assertSame($list, $this->factory->createListFromLoader($loader, $closure));
}
public function testCreateFromLoaderDifferentValueClosure()
{
$loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface');
$list1 = new \stdClass();
$list2 = new \stdClass();
$closure1 = function () {};
$closure2 = function () {};
$this->decoratedFactory->expects($this->at(0))
->method('createListFromLoader')
->with($loader, $closure1)
->will($this->returnValue($list1));
$this->decoratedFactory->expects($this->at(1))
->method('createListFromLoader')
->with($loader, $closure2)
->will($this->returnValue($list2));
$this->assertSame($list1, $this->factory->createListFromLoader($loader, $closure1));
$this->assertSame($list2, $this->factory->createListFromLoader($loader, $closure2));
}
public function testCreateViewSamePreferredChoices()
{
$preferred = array('a');
$list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface');
$view = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createView')
->with($list, $preferred)
->will($this->returnValue($view));
$this->assertSame($view, $this->factory->createView($list, $preferred));
$this->assertSame($view, $this->factory->createView($list, $preferred));
}
public function testCreateViewDifferentPreferredChoices()
{
$preferred1 = array('a');
$preferred2 = array('b');
$list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface');
$view1 = new \stdClass();
$view2 = new \stdClass();
$this->decoratedFactory->expects($this->at(0))
->method('createView')
->with($list, $preferred1)
->will($this->returnValue($view1));
$this->decoratedFactory->expects($this->at(1))
->method('createView')
->with($list, $preferred2)
->will($this->returnValue($view2));
$this->assertSame($view1, $this->factory->createView($list, $preferred1));
$this->assertSame($view2, $this->factory->createView($list, $preferred2));
}
public function testCreateViewSamePreferredChoicesClosure()
{
$preferred = function () {};
$list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface');
$view = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createView')
->with($list, $preferred)
->will($this->returnValue($view));
$this->assertSame($view, $this->factory->createView($list, $preferred));
$this->assertSame($view, $this->factory->createView($list, $preferred));
}
public function testCreateViewDifferentPreferredChoicesClosure()
{
$preferred1 = function () {};
$preferred2 = function () {};
$list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface');
$view1 = new \stdClass();
$view2 = new \stdClass();
$this->decoratedFactory->expects($this->at(0))
->method('createView')
->with($list, $preferred1)
->will($this->returnValue($view1));
$this->decoratedFactory->expects($this->at(1))
->method('createView')
->with($list, $preferred2)
->will($this->returnValue($view2));
$this->assertSame($view1, $this->factory->createView($list, $preferred1));
$this->assertSame($view2, $this->factory->createView($list, $preferred2));
}
public function testCreateViewSameLabelClosure()
{
$labels = function () {};
$list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface');
$view = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createView')
->with($list, null, $labels)
->will($this->returnValue($view));
$this->assertSame($view, $this->factory->createView($list, null, $labels));
$this->assertSame($view, $this->factory->createView($list, null, $labels));
}
public function testCreateViewDifferentLabelClosure()
{
$labels1 = function () {};
$labels2 = function () {};
$list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface');
$view1 = new \stdClass();
$view2 = new \stdClass();
$this->decoratedFactory->expects($this->at(0))
->method('createView')
->with($list, null, $labels1)
->will($this->returnValue($view1));
$this->decoratedFactory->expects($this->at(1))
->method('createView')
->with($list, null, $labels2)
->will($this->returnValue($view2));
$this->assertSame($view1, $this->factory->createView($list, null, $labels1));
$this->assertSame($view2, $this->factory->createView($list, null, $labels2));
}
public function testCreateViewSameIndexClosure()
{
$index = function () {};
$list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface');
$view = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createView')
->with($list, null, null, $index)
->will($this->returnValue($view));
$this->assertSame($view, $this->factory->createView($list, null, null, $index));
$this->assertSame($view, $this->factory->createView($list, null, null, $index));
}
public function testCreateViewDifferentIndexClosure()
{
$index1 = function () {};
$index2 = function () {};
$list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface');
$view1 = new \stdClass();
$view2 = new \stdClass();
$this->decoratedFactory->expects($this->at(0))
->method('createView')
->with($list, null, null, $index1)
->will($this->returnValue($view1));
$this->decoratedFactory->expects($this->at(1))
->method('createView')
->with($list, null, null, $index2)
->will($this->returnValue($view2));
$this->assertSame($view1, $this->factory->createView($list, null, null, $index1));
$this->assertSame($view2, $this->factory->createView($list, null, null, $index2));
}
public function testCreateViewSameGroupByClosure()
{
$groupBy = function () {};
$list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface');
$view = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createView')
->with($list, null, null, null, $groupBy)
->will($this->returnValue($view));
$this->assertSame($view, $this->factory->createView($list, null, null, null, $groupBy));
$this->assertSame($view, $this->factory->createView($list, null, null, null, $groupBy));
}
public function testCreateViewDifferentGroupByClosure()
{
$groupBy1 = function () {};
$groupBy2 = function () {};
$list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface');
$view1 = new \stdClass();
$view2 = new \stdClass();
$this->decoratedFactory->expects($this->at(0))
->method('createView')
->with($list, null, null, null, $groupBy1)
->will($this->returnValue($view1));
$this->decoratedFactory->expects($this->at(1))
->method('createView')
->with($list, null, null, null, $groupBy2)
->will($this->returnValue($view2));
$this->assertSame($view1, $this->factory->createView($list, null, null, null, $groupBy1));
$this->assertSame($view2, $this->factory->createView($list, null, null, null, $groupBy2));
}
public function testCreateViewSameAttributes()
{
$attr = array('class' => 'foobar');
$list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface');
$view = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createView')
->with($list, null, null, null, null, $attr)
->will($this->returnValue($view));
$this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr));
$this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr));
}
public function testCreateViewDifferentAttributes()
{
$attr1 = array('class' => 'foobar1');
$attr2 = array('class' => 'foobar2');
$list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface');
$view1 = new \stdClass();
$view2 = new \stdClass();
$this->decoratedFactory->expects($this->at(0))
->method('createView')
->with($list, null, null, null, null, $attr1)
->will($this->returnValue($view1));
$this->decoratedFactory->expects($this->at(1))
->method('createView')
->with($list, null, null, null, null, $attr2)
->will($this->returnValue($view2));
$this->assertSame($view1, $this->factory->createView($list, null, null, null, null, $attr1));
$this->assertSame($view2, $this->factory->createView($list, null, null, null, null, $attr2));
}
public function testCreateViewSameAttributesClosure()
{
$attr = function () {};
$list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface');
$view = new \stdClass();
$this->decoratedFactory->expects($this->once())
->method('createView')
->with($list, null, null, null, null, $attr)
->will($this->returnValue($view));
$this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr));
$this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr));
}
public function testCreateViewDifferentAttributesClosure()
{
$attr1 = function () {};
$attr2 = function () {};
$list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface');
$view1 = new \stdClass();
$view2 = new \stdClass();
$this->decoratedFactory->expects($this->at(0))
->method('createView')
->with($list, null, null, null, null, $attr1)
->will($this->returnValue($view1));
$this->decoratedFactory->expects($this->at(1))
->method('createView')
->with($list, null, null, null, null, $attr2)
->will($this->returnValue($view2));
$this->assertSame($view1, $this->factory->createView($list, null, null, null, null, $attr1));
$this->assertSame($view2, $this->factory->createView($list, null, null, null, null, $attr2));
}
public function provideSameChoices()
{
$object = (object) array('foo' => 'bar');
return array(
array(0, 0),
array('a', 'a'),
// https://github.com/symfony/symfony/issues/10409
array(chr(181).'meter', chr(181).'meter'), // UTF-8
array($object, $object),
);
}
public function provideDistinguishedChoices()
{
return array(
array(0, false),
array(0, null),
array(0, '0'),
array(0, ''),
array(1, true),
array(1, '1'),
array(1, 'a'),
array('', false),
array('', null),
array(false, null),
// Same properties, but not identical
array((object) array('foo' => 'bar'), (object) array('foo' => 'bar')),
);
}
public function provideSameKeyChoices()
{
// Only test types here that can be used as array keys
return array(
array(0, 0),
array(0, '0'),
array('a', 'a'),
array(chr(181).'meter', chr(181).'meter'),
);
}
public function provideDistinguishedKeyChoices()
{
// Only test types here that can be used as array keys
return array(
array(0, ''),
array(1, 'a'),
array('', 'a'),
);
}
}

View File

@ -0,0 +1,970 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\ChoiceList\Factory;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\LazyChoiceList;
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase
{
private $obj1;
private $obj2;
private $obj3;
private $obj4;
private $list;
/**
* @var DefaultChoiceListFactory
*/
private $factory;
public function getValue($object)
{
return $object->value;
}
public function getScalarValue($choice)
{
switch ($choice) {
case 'a': return 'a';
case 'b': return 'b';
case 'c': return '1';
case 'd': return '2';
}
}
public function getLabel($object)
{
return $object->label;
}
public function getFormIndex($object)
{
return $object->index;
}
public function isPreferred($object)
{
return $this->obj2 === $object || $this->obj3 === $object;
}
public function getAttr($object)
{
return $object->attr;
}
public function getGroup($object)
{
return $this->obj1 === $object || $this->obj2 === $object ? 'Group 1' : 'Group 2';
}
protected function setUp()
{
$this->obj1 = (object) array('label' => 'A', 'index' => 'w', 'value' => 'a', 'preferred' => false, 'group' => 'Group 1', 'attr' => array());
$this->obj2 = (object) array('label' => 'B', 'index' => 'x', 'value' => 'b', 'preferred' => true, 'group' => 'Group 1', 'attr' => array('attr1' => 'value1'));
$this->obj3 = (object) array('label' => 'C', 'index' => 'y', 'value' => 1, 'preferred' => true, 'group' => 'Group 2', 'attr' => array('attr2' => 'value2'));
$this->obj4 = (object) array('label' => 'D', 'index' => 'z', 'value' => 2, 'preferred' => false, 'group' => 'Group 2', 'attr' => array());
$this->list = new ArrayChoiceList(
array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4),
array('A' => '0', 'B' => '1', 'C' => '2', 'D' => '3')
);
$this->factory = new DefaultChoiceListFactory();
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testCreateFromChoicesFailsIfChoicesNotArrayOrTraversable()
{
$this->factory->createListFromChoices('foobar');
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testCreateFromChoicesFailsIfValuesNotCallableOrString()
{
$this->factory->createListFromChoices(array(), new \stdClass());
}
public function testCreateFromChoicesEmpty()
{
$list = $this->factory->createListFromChoices(array());
$this->assertSame(array(), $list->getChoices());
$this->assertSame(array(), $list->getValues());
}
public function testCreateFromChoicesFlat()
{
$list = $this->factory->createListFromChoices(
array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4)
);
$this->assertObjectListWithGeneratedValues($list);
}
public function testCreateFromChoicesFlatTraversable()
{
$list = $this->factory->createListFromChoices(
new \ArrayIterator(array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4))
);
$this->assertObjectListWithGeneratedValues($list);
}
public function testCreateFromChoicesFlatValuesAsCallable()
{
$list = $this->factory->createListFromChoices(
array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4),
array($this, 'getValue')
);
$this->assertObjectListWithCustomValues($list);
}
public function testCreateFromChoicesFlatValuesAsClosure()
{
$list = $this->factory->createListFromChoices(
array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4),
function ($object) { return $object->value; }
);
$this->assertObjectListWithCustomValues($list);
}
public function testCreateFromChoicesFlatValuesClosureReceivesKey()
{
$list = $this->factory->createListFromChoices(
array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4),
function ($object, $key) {
switch ($key) {
case 'A': return 'a';
case 'B': return 'b';
case 'C': return '1';
case 'D': return '2';
}
}
);
$this->assertObjectListWithCustomValues($list);
}
public function testCreateFromChoicesGrouped()
{
$list = $this->factory->createListFromChoices(
array(
'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2),
'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4),
)
);
$this->assertObjectListWithGeneratedValues($list);
}
public function testCreateFromChoicesGroupedTraversable()
{
$list = $this->factory->createListFromChoices(
new \ArrayIterator(array(
'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2),
'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4),
))
);
$this->assertObjectListWithGeneratedValues($list);
}
public function testCreateFromChoicesGroupedValuesAsCallable()
{
$list = $this->factory->createListFromChoices(
array(
'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2),
'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4),
),
array($this, 'getValue')
);
$this->assertObjectListWithCustomValues($list);
}
public function testCreateFromChoicesGroupedValuesAsClosure()
{
$list = $this->factory->createListFromChoices(
array(
'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2),
'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4),
),
function ($object) { return $object->value; }
);
$this->assertObjectListWithCustomValues($list);
}
public function testCreateFromChoicesGroupedValuesAsClosureReceivesKey()
{
$list = $this->factory->createListFromChoices(
array(
'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2),
'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4),
),
function ($object, $key) {
switch ($key) {
case 'A': return 'a';
case 'B': return 'b';
case 'C': return '1';
case 'D': return '2';
}
}
);
$this->assertObjectListWithCustomValues($list);
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testCreateFromFlippedChoicesFailsIfChoicesNotArrayOrTraversable()
{
$this->factory->createListFromFlippedChoices('foobar');
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testCreateFromFlippedChoicesFailsIfValuesNotCallableOrString()
{
$this->factory->createListFromFlippedChoices(array(), new \stdClass());
}
public function testCreateFromFlippedChoicesEmpty()
{
$list = $this->factory->createListFromFlippedChoices(array());
$this->assertSame(array(), $list->getChoices());
$this->assertSame(array(), $list->getValues());
}
public function testCreateFromFlippedChoicesFlat()
{
$list = $this->factory->createListFromFlippedChoices(
array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D')
);
$this->assertScalarListWithGeneratedValues($list);
}
public function testCreateFromFlippedChoicesFlatTraversable()
{
$list = $this->factory->createListFromFlippedChoices(
new \ArrayIterator(array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D'))
);
$this->assertScalarListWithGeneratedValues($list);
}
public function testCreateFromFlippedChoicesFlatValuesAsCallable()
{
$list = $this->factory->createListFromFlippedChoices(
array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D'),
array($this, 'getScalarValue')
);
$this->assertScalarListWithCustomValues($list);
}
public function testCreateFromFlippedChoicesFlatValuesAsClosure()
{
$list = $this->factory->createListFromFlippedChoices(
array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D'),
function ($choice) {
switch ($choice) {
case 'a': return 'a';
case 'b': return 'b';
case 'c': return '1';
case 'd': return '2';
}
}
);
$this->assertScalarListWithCustomValues($list);
}
public function testCreateFromFlippedChoicesFlatValuesClosureReceivesKey()
{
$list = $this->factory->createListFromFlippedChoices(
array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D'),
function ($choice, $key) {
switch ($key) {
case 'A': return 'a';
case 'B': return 'b';
case 'C': return '1';
case 'D': return '2';
}
}
);
$this->assertScalarListWithCustomValues($list);
}
public function testCreateFromFlippedChoicesGrouped()
{
$list = $this->factory->createListFromFlippedChoices(
array(
'Group 1' => array('a' => 'A', 'b' => 'B'),
'Group 2' => array('c' => 'C', 'd' => 'D'),
)
);
$this->assertScalarListWithGeneratedValues($list);
}
public function testCreateFromFlippedChoicesGroupedTraversable()
{
$list = $this->factory->createListFromFlippedChoices(
new \ArrayIterator(array(
'Group 1' => array('a' => 'A', 'b' => 'B'),
'Group 2' => array('c' => 'C', 'd' => 'D'),
))
);
$this->assertScalarListWithGeneratedValues($list);
}
public function testCreateFromFlippedChoicesGroupedValuesAsCallable()
{
$list = $this->factory->createListFromFlippedChoices(
array(
'Group 1' => array('a' => 'A', 'b' => 'B'),
'Group 2' => array('c' => 'C', 'd' => 'D'),
),
array($this, 'getScalarValue')
);
$this->assertScalarListWithCustomValues($list);
}
public function testCreateFromFlippedChoicesGroupedValuesAsClosure()
{
$list = $this->factory->createListFromFlippedChoices(
array(
'Group 1' => array('a' => 'A', 'b' => 'B'),
'Group 2' => array('c' => 'C', 'd' => 'D'),
),
function ($choice) {
switch ($choice) {
case 'a': return 'a';
case 'b': return 'b';
case 'c': return '1';
case 'd': return '2';
}
}
);
$this->assertScalarListWithCustomValues($list);
}
public function testCreateFromFlippedChoicesGroupedValuesAsClosureReceivesKey()
{
$list = $this->factory->createListFromFlippedChoices(
array(
'Group 1' => array('a' => 'A', 'b' => 'B'),
'Group 2' => array('c' => 'C', 'd' => 'D'),
),
function ($choice, $key) {
switch ($key) {
case 'A': return 'a';
case 'B': return 'b';
case 'C': return '1';
case 'D': return '2';
}
}
);
$this->assertScalarListWithCustomValues($list);
}
public function testCreateFromLoader()
{
$loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface');
$list = $this->factory->createListFromLoader($loader);
$this->assertEquals(new LazyChoiceList($loader), $list);
}
public function testCreateFromLoaderWithValues()
{
$loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface');
$value = function () {};
$list = $this->factory->createListFromLoader($loader, $value);
$this->assertEquals(new LazyChoiceList($loader, $value), $list);
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testCreateFromLoaderFailsIfValuesNotCallableOrString()
{
$loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface');
$this->factory->createListFromLoader($loader, new \stdClass());
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testCreateViewFailsIfPreferredChoicesInvalid()
{
$this->factory->createView($this->list, new \stdClass());
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testCreateViewFailsIfLabelInvalid()
{
$this->factory->createView($this->list, null, new \stdClass());
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testCreateViewFailsIfIndexInvalid()
{
$this->factory->createView($this->list, null, null, new \stdClass());
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testCreateViewFailsIfGroupByInvalid()
{
$this->factory->createView($this->list, null, null, null, new \stdClass());
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testCreateViewFailsIfAttrInvalid()
{
$this->factory->createView($this->list, null, null, null, null, new \stdClass());
}
public function testCreateViewFlat()
{
$view = $this->factory->createView($this->list);
$this->assertEquals(new ChoiceListView(
array(
0 => new ChoiceView('A', '0', $this->obj1),
1 => new ChoiceView('B', '1', $this->obj2),
2 => new ChoiceView('C', '2', $this->obj3),
3 => new ChoiceView('D', '3', $this->obj4),
), array()
), $view);
}
public function testCreateViewFlatPreferredChoices()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3)
);
$this->assertFlatView($view);
}
public function testCreateViewFlatPreferredChoicesEmptyArray()
{
$view = $this->factory->createView(
$this->list,
array()
);
$this->assertEquals(new ChoiceListView(
array(
0 => new ChoiceView('A', '0', $this->obj1),
1 => new ChoiceView('B', '1', $this->obj2),
2 => new ChoiceView('C', '2', $this->obj3),
3 => new ChoiceView('D', '3', $this->obj4),
), array()
), $view);
}
public function testCreateViewFlatPreferredChoicesAsCallable()
{
$view = $this->factory->createView(
$this->list,
array($this, 'isPreferred')
);
$this->assertFlatView($view);
}
public function testCreateViewFlatPreferredChoicesAsClosure()
{
$obj2 = $this->obj2;
$obj3 = $this->obj3;
$view = $this->factory->createView(
$this->list,
function ($object) use ($obj2, $obj3) {
return $obj2 === $object || $obj3 === $object;
}
);
$this->assertFlatView($view);
}
public function testCreateViewFlatPreferredChoicesClosureReceivesKey()
{
$obj2 = $this->obj2;
$obj3 = $this->obj3;
$view = $this->factory->createView(
$this->list,
function ($object, $key) use ($obj2, $obj3) {
return 'B' === $key || 'C' === $key;
}
);
$this->assertFlatView($view);
}
public function testCreateViewFlatLabelAsCallable()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
array($this, 'getLabel')
);
$this->assertFlatView($view);
}
public function testCreateViewFlatLabelAsClosure()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
function ($object) {
return $object->label;
}
);
$this->assertFlatView($view);
}
public function testCreateViewFlatLabelClosureReceivesKey()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
function ($object, $key) {
return $key;
}
);
$this->assertFlatView($view);
}
public function testCreateViewFlatIndexAsCallable()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
null, // label
array($this, 'getFormIndex')
);
$this->assertFlatViewWithCustomIndices($view);
}
public function testCreateViewFlatIndexAsClosure()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
null, // label
function ($object) {
return $object->index;
}
);
$this->assertFlatViewWithCustomIndices($view);
}
public function testCreateViewFlatIndexClosureReceivesKey()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
null, // label
function ($object, $key) {
switch ($key) {
case 'A': return 'w';
case 'B': return 'x';
case 'C': return 'y';
case 'D': return 'z';
}
}
);
$this->assertFlatViewWithCustomIndices($view);
}
public function testCreateViewFlatGroupByAsArray()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
null, // label
null, // index
array(
'Group 1' => array('A' => true, 'B' => true),
'Group 2' => array('C' => true, 'D' => true),
)
);
$this->assertGroupedView($view);
}
public function testCreateViewFlatGroupByAsTraversable()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
null, // label
null, // index
new \ArrayIterator(array(
'Group 1' => array('A' => true, 'B' => true),
'Group 2' => array('C' => true, 'D' => true),
))
);
$this->assertGroupedView($view);
}
public function testCreateViewFlatGroupByEmpty()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
null, // label
null, // index
array() // ignored
);
$this->assertFlatView($view);
}
public function testCreateViewFlatGroupByAsCallable()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
null, // label
null, // index
array($this, 'getGroup')
);
$this->assertGroupedView($view);
}
public function testCreateViewFlatGroupByAsClosure()
{
$obj1 = $this->obj1;
$obj2 = $this->obj2;
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
null, // label
null, // index
function ($object) use ($obj1, $obj2) {
return $obj1 === $object || $obj2 === $object ? 'Group 1'
: 'Group 2';
}
);
$this->assertGroupedView($view);
}
public function testCreateViewFlatGroupByClosureReceivesKey()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
null, // label
null, // index
function ($object, $key) {
return 'A' === $key || 'B' === $key ? 'Group 1' : 'Group 2';
}
);
$this->assertGroupedView($view);
}
public function testCreateViewFlatAttrAsArray()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
null, // label
null, // index
null, // group
array(
'B' => array('attr1' => 'value1'),
'C' => array('attr2' => 'value2')
)
);
$this->assertFlatViewWithAttr($view);
}
public function testCreateViewFlatAttrEmpty()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
null, // label
null, // index
null, // group
array()
);
$this->assertFlatView($view);
}
public function testCreateViewFlatAttrAsCallable()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
null, // label
null, // index
null, // group
array($this, 'getAttr')
);
$this->assertFlatViewWithAttr($view);
}
public function testCreateViewFlatAttrAsClosure()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
null, // label
null, // index
null, // group
function ($object) {
return $object->attr;
}
);
$this->assertFlatViewWithAttr($view);
}
public function testCreateViewFlatAttrClosureReceivesKey()
{
$view = $this->factory->createView(
$this->list,
array($this->obj2, $this->obj3),
null, // label
null, // index
null, // group
function ($object, $key) {
switch ($key) {
case 'B': return array('attr1' => 'value1');
case 'C': return array('attr2' => 'value2');
default: return array();
}
}
);
$this->assertFlatViewWithAttr($view);
}
public function testCreateViewForLegacyChoiceList()
{
$preferred = array(new ChoiceView('Preferred', 'x', 'x'));
$other = array(new ChoiceView('Other', 'y', 'y'));
$list = $this->getMock('Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface');
$list->expects($this->once())
->method('getPreferredViews')
->will($this->returnValue($preferred));
$list->expects($this->once())
->method('getRemainingViews')
->will($this->returnValue($other));
$view = $this->factory->createView($list);
$this->assertSame($other, $view->choices);
$this->assertSame($preferred, $view->preferredChoices);
}
private function assertScalarListWithGeneratedValues(ChoiceListInterface $list)
{
$this->assertSame(array(
'A' => 'a',
'B' => 'b',
'C' => 'c',
'D' => 'd',
), $list->getChoices());
$this->assertSame(array(
'A' => 'a',
'B' => 'b',
'C' => 'c',
'D' => 'd',
), $list->getValues());
}
private function assertObjectListWithGeneratedValues(ChoiceListInterface $list)
{
$this->assertSame(array(
'A' => $this->obj1,
'B' => $this->obj2,
'C' => $this->obj3,
'D' => $this->obj4,
), $list->getChoices());
$this->assertSame(array(
'A' => '0',
'B' => '1',
'C' => '2',
'D' => '3',
), $list->getValues());
}
private function assertScalarListWithCustomValues(ChoiceListInterface $list)
{
$this->assertSame(array(
'A' => 'a',
'B' => 'b',
'C' => 'c',
'D' => 'd',
), $list->getChoices());
$this->assertSame(array(
'A' => 'a',
'B' => 'b',
'C' => '1',
'D' => '2',
), $list->getValues());
}
private function assertObjectListWithCustomValues(ChoiceListInterface $list)
{
$this->assertSame(array(
'A' => $this->obj1,
'B' => $this->obj2,
'C' => $this->obj3,
'D' => $this->obj4,
), $list->getChoices());
$this->assertSame(array(
'A' => 'a',
'B' => 'b',
'C' => '1',
'D' => '2',
), $list->getValues());
}
private function assertFlatView($view)
{
$this->assertEquals(new ChoiceListView(
array(
0 => new ChoiceView('A', '0', $this->obj1),
3 => new ChoiceView('D', '3', $this->obj4),
), array(
1 => new ChoiceView('B', '1', $this->obj2),
2 => new ChoiceView('C', '2', $this->obj3),
)
), $view);
}
private function assertFlatViewWithCustomIndices($view)
{
$this->assertEquals(new ChoiceListView(
array(
'w' => new ChoiceView('A', '0', $this->obj1),
'z' => new ChoiceView('D', '3', $this->obj4),
), array(
'x' => new ChoiceView('B', '1', $this->obj2),
'y' => new ChoiceView('C', '2', $this->obj3),
)
), $view);
}
private function assertFlatViewWithAttr($view)
{
$this->assertEquals(new ChoiceListView(
array(
0 => new ChoiceView('A', '0', $this->obj1),
3 => new ChoiceView('D', '3', $this->obj4),
), array(
1 => new ChoiceView(
'B',
'1',
$this->obj2,
array('attr1' => 'value1')
),
2 => new ChoiceView(
'C',
'2',
$this->obj3,
array('attr2' => 'value2')
),
)
), $view);
}
private function assertGroupedView($view)
{
$this->assertEquals(new ChoiceListView(
array(
'Group 1' => new ChoiceGroupView(
'Group 1',
array(0 => new ChoiceView('A', '0', $this->obj1))
),
'Group 2' => new ChoiceGroupView(
'Group 2',
array(3 => new ChoiceView('D', '3', $this->obj4))
),
), array(
'Group 1' => new ChoiceGroupView(
'Group 1',
array(1 => new ChoiceView('B', '1', $this->obj2))
),
'Group 2' => new ChoiceGroupView(
'Group 2',
array(2 => new ChoiceView('C', '2', $this->obj3))
),
)
), $view);
}
}

View File

@ -0,0 +1,338 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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')
));
}
}

View File

@ -0,0 +1,141 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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')));
}
}

View File

@ -11,8 +11,9 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
{
@ -66,6 +67,16 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$this->objectChoices = null;
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testChoicesOptionExpectsArrayOrTraversable()
{
$this->factory->create('choice', null, array(
'choices' => new \stdClass(),
));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
@ -76,6 +87,16 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testChoiceLoaderOptionExpectsChoiceLoaderInterface()
{
$this->factory->create('choice', null, array(
'choice_loader' => new \stdClass(),
));
}
public function testChoiceListAndChoicesCanBeEmpty()
{
$this->factory->create('choice');
@ -236,7 +257,118 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$this->assertFalse($form->isSynchronized());
}
public function testSubmitSingleNonExpandedNull()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => false,
'choices' => $this->choices,
));
$form->submit(null);
$this->assertNull($form->getData());
$this->assertSame('', $form->getViewData());
}
// In edge cases (for example, when choices are loaded dynamically by a
// loader), the choices may be empty. Make sure to behave the same as when
// choices are available.
public function testSubmitSingleNonExpandedNullNoChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => false,
'choices' => array(),
));
$form->submit(null);
$this->assertNull($form->getData());
$this->assertSame('', $form->getViewData());
}
public function testSubmitSingleNonExpandedEmpty()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => false,
'choices' => $this->choices,
));
$form->submit('');
$this->assertNull($form->getData());
$this->assertSame('', $form->getViewData());
}
// In edge cases (for example, when choices are loaded dynamically by a
// loader), the choices may be empty. Make sure to behave the same as when
// choices are available.
public function testSubmitSingleNonExpandedEmptyNoChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => false,
'choices' => array(),
));
$form->submit('');
$this->assertNull($form->getData());
$this->assertSame('', $form->getViewData());
}
public function testSubmitSingleNonExpandedFalse()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => false,
'choices' => $this->choices,
));
$form->submit(false);
$this->assertNull($form->getData());
$this->assertSame('', $form->getViewData());
}
// In edge cases (for example, when choices are loaded dynamically by a
// loader), the choices may be empty. Make sure to behave the same as when
// choices are available.
public function testSubmitSingleNonExpandedFalseNoChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => false,
'choices' => array(),
));
$form->submit(false);
$this->assertNull($form->getData());
$this->assertSame('', $form->getViewData());
}
public function testSubmitSingleNonExpandedObjectChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => false,
'choices' => $this->objectChoices,
'choices_as_values' => true,
'choice_label' => 'name',
'choice_value' => 'id',
));
// "id" value of the second entry
$form->submit('2');
$this->assertEquals($this->objectChoices[1], $form->getData());
$this->assertEquals('2', $form->getViewData());
}
public function testSubmitSingleNonExpandedObjectChoicesBc()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
@ -273,6 +405,37 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$this->assertEquals(array('a', 'b'), $form->getViewData());
}
public function testSubmitMultipleNonExpandedEmpty()
{
$form = $this->factory->create('choice', null, array(
'multiple' => true,
'expanded' => false,
'choices' => $this->choices,
));
$form->submit(array());
$this->assertSame(array(), $form->getData());
$this->assertSame(array(), $form->getViewData());
}
// In edge cases (for example, when choices are loaded dynamically by a
// loader), the choices may be empty. Make sure to behave the same as when
// choices are available.
public function testSubmitMultipleNonExpandedEmptyNoChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => true,
'expanded' => false,
'choices' => array(),
));
$form->submit(array());
$this->assertSame(array(), $form->getData());
$this->assertSame(array(), $form->getViewData());
}
public function testSubmitMultipleNonExpandedInvalidScalarChoice()
{
$form = $this->factory->create('choice', null, array(
@ -304,6 +467,23 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
}
public function testSubmitMultipleNonExpandedObjectChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => true,
'expanded' => false,
'choices' => $this->objectChoices,
'choices_as_values' => true,
'choice_label' => 'name',
'choice_value' => 'id',
));
$form->submit(array('2', '3'));
$this->assertEquals(array($this->objectChoices[1], $this->objectChoices[2]), $form->getData());
$this->assertEquals(array('2', '3'), $form->getViewData());
}
public function testSubmitMultipleNonExpandedObjectChoicesBc()
{
$form = $this->factory->create('choice', null, array(
'multiple' => true,
@ -337,13 +517,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$form->submit('b');
$this->assertSame('b', $form->getData());
$this->assertSame(array(
0 => false,
1 => true,
2 => false,
3 => false,
4 => false,
), $form->getViewData());
$this->assertSame('b', $form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
@ -399,14 +573,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$form->submit('b');
$this->assertSame('b', $form->getData());
$this->assertSame(array(
0 => false,
1 => true,
2 => false,
3 => false,
4 => false,
'placeholder' => false,
), $form->getViewData());
$this->assertSame('b', $form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
@ -464,13 +631,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$form->submit(null);
$this->assertNull($form->getData());
$this->assertSame(array(
0 => false,
1 => false,
2 => false,
3 => false,
4 => false,
), $form->getViewData());
$this->assertNull($form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
@ -486,6 +647,26 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$this->assertNull($form[4]->getViewData());
}
// In edge cases (for example, when choices are loaded dynamically by a
// loader), the choices may be empty. Make sure to behave the same as when
// choices are available.
public function testSubmitSingleExpandedRequiredNullNoChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => true,
'required' => true,
'choices' => array(),
));
$form->submit(null);
$this->assertNull($form->getData());
$this->assertNull($form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
}
public function testSubmitSingleExpandedRequiredEmpty()
{
$form = $this->factory->create('choice', null, array(
@ -498,13 +679,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$form->submit('');
$this->assertNull($form->getData());
$this->assertSame(array(
0 => false,
1 => false,
2 => false,
3 => false,
4 => false,
), $form->getViewData());
$this->assertNull($form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
@ -520,6 +695,26 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$this->assertNull($form[4]->getViewData());
}
// In edge cases (for example, when choices are loaded dynamically by a
// loader), the choices may be empty. Make sure to behave the same as when
// choices are available.
public function testSubmitSingleExpandedRequiredEmptyNoChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => true,
'required' => true,
'choices' => array(),
));
$form->submit('');
$this->assertNull($form->getData());
$this->assertNull($form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
}
public function testSubmitSingleExpandedRequiredFalse()
{
$form = $this->factory->create('choice', null, array(
@ -532,13 +727,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$form->submit(false);
$this->assertNull($form->getData());
$this->assertSame(array(
0 => false,
1 => false,
2 => false,
3 => false,
4 => false,
), $form->getViewData());
$this->assertNull($form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
@ -554,6 +743,26 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$this->assertNull($form[4]->getViewData());
}
// In edge cases (for example, when choices are loaded dynamically by a
// loader), the choices may be empty. Make sure to behave the same as when
// choices are available.
public function testSubmitSingleExpandedRequiredFalseNoChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => true,
'required' => true,
'choices' => array(),
));
$form->submit(false);
$this->assertNull($form->getData());
$this->assertNull($form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
}
public function testSubmitSingleExpandedNonRequiredNull()
{
$form = $this->factory->create('choice', null, array(
@ -566,14 +775,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$form->submit(null);
$this->assertNull($form->getData());
$this->assertSame(array(
0 => false,
1 => false,
2 => false,
3 => false,
4 => false,
'placeholder' => true,
), $form->getViewData());
$this->assertNull($form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
@ -591,6 +793,26 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$this->assertNull($form[4]->getViewData());
}
// In edge cases (for example, when choices are loaded dynamically by a
// loader), the choices may be empty. Make sure to behave the same as when
// choices are available.
public function testSubmitSingleExpandedNonRequiredNullNoChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => true,
'required' => false,
'choices' => array(),
));
$form->submit(null);
$this->assertNull($form->getData());
$this->assertNull($form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
}
public function testSubmitSingleExpandedNonRequiredEmpty()
{
$form = $this->factory->create('choice', null, array(
@ -603,14 +825,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$form->submit('');
$this->assertNull($form->getData());
$this->assertSame(array(
0 => false,
1 => false,
2 => false,
3 => false,
4 => false,
'placeholder' => true,
), $form->getViewData());
$this->assertNull($form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
@ -628,6 +843,26 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$this->assertNull($form[4]->getViewData());
}
// In edge cases (for example, when choices are loaded dynamically by a
// loader), the choices may be empty. Make sure to behave the same as when
// choices are available.
public function testSubmitSingleExpandedNonRequiredEmptyNoChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => true,
'required' => false,
'choices' => array(),
));
$form->submit('');
$this->assertNull($form->getData());
$this->assertNull($form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
}
public function testSubmitSingleExpandedNonRequiredFalse()
{
$form = $this->factory->create('choice', null, array(
@ -640,14 +875,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$form->submit(false);
$this->assertNull($form->getData());
$this->assertSame(array(
0 => false,
1 => false,
2 => false,
3 => false,
4 => false,
'placeholder' => true,
), $form->getViewData());
$this->assertNull($form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
@ -665,6 +893,26 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$this->assertNull($form[4]->getViewData());
}
// In edge cases (for example, when choices are loaded dynamically by a
// loader), the choices may be empty. Make sure to behave the same as when
// choices are available.
public function testSubmitSingleExpandedNonRequiredFalseNoChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => true,
'required' => false,
'choices' => array(),
));
$form->submit(false);
$this->assertNull($form->getData());
$this->assertNull($form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
}
public function testSubmitSingleExpandedWithEmptyChild()
{
$form = $this->factory->create('choice', null, array(
@ -686,6 +934,32 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
}
public function testSubmitSingleExpandedObjectChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => true,
'choices' => $this->objectChoices,
'choices_as_values' => true,
'choice_label' => 'name',
'choice_value' => 'id',
));
$form->submit('2');
$this->assertSame($this->objectChoices[1], $form->getData());
$this->assertFalse($form[0]->getData());
$this->assertTrue($form[1]->getData());
$this->assertFalse($form[2]->getData());
$this->assertFalse($form[3]->getData());
$this->assertFalse($form[4]->getData());
$this->assertNull($form[0]->getViewData());
$this->assertSame('2', $form[1]->getViewData());
$this->assertNull($form[2]->getViewData());
$this->assertNull($form[3]->getViewData());
$this->assertNull($form[4]->getViewData());
}
public function testSubmitSingleExpandedObjectChoicesBc()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
@ -750,13 +1024,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$form->submit(array('a', 'c'));
$this->assertSame(array('a', 'c'), $form->getData());
$this->assertSame(array(
0 => true,
1 => false,
2 => true,
3 => false,
4 => false,
), $form->getViewData());
$this->assertSame(array('a', 'c'), $form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertTrue($form->isSynchronized());
@ -849,6 +1117,22 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$this->assertNull($form[4]->getViewData());
}
// In edge cases (for example, when choices are loaded dynamically by a
// loader), the choices may be empty. Make sure to behave the same as when
// choices are available.
public function testSubmitMultipleExpandedEmptyNoChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => true,
'expanded' => true,
'choices' => array(),
));
$form->submit(array());
$this->assertSame(array(), $form->getData());
}
public function testSubmitMultipleExpandedWithEmptyChild()
{
$form = $this->factory->create('choice', null, array(
@ -873,6 +1157,32 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
}
public function testSubmitMultipleExpandedObjectChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => true,
'expanded' => true,
'choices' => $this->objectChoices,
'choices_as_values' => true,
'choice_label' => 'name',
'choice_value' => 'id',
));
$form->submit(array('1', '2'));
$this->assertSame(array($this->objectChoices[0], $this->objectChoices[1]), $form->getData());
$this->assertTrue($form[0]->getData());
$this->assertTrue($form[1]->getData());
$this->assertFalse($form[2]->getData());
$this->assertFalse($form[3]->getData());
$this->assertFalse($form[4]->getData());
$this->assertSame('1', $form[0]->getViewData());
$this->assertSame('2', $form[1]->getViewData());
$this->assertNull($form[2]->getViewData());
$this->assertNull($form[3]->getViewData());
$this->assertNull($form[4]->getViewData());
}
public function testSubmitMultipleExpandedObjectChoicesBc()
{
$form = $this->factory->create('choice', null, array(
'multiple' => true,
@ -1134,10 +1444,10 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$view = $form->createView();
$this->assertEquals(array(
new ChoiceView('a', 'a', 'A'),
new ChoiceView('b', 'b', 'B'),
new ChoiceView('c', 'c', 'C'),
new ChoiceView('d', 'd', 'D'),
new ChoiceView('A', 'a', 'a'),
new ChoiceView('B', 'b', 'b'),
new ChoiceView('C', 'c', 'c'),
new ChoiceView('D', 'd', 'd'),
), $view->vars['choices']);
}
@ -1151,12 +1461,12 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$view = $form->createView();
$this->assertEquals(array(
0 => new ChoiceView('a', 'a', 'A'),
2 => new ChoiceView('c', 'c', 'C'),
0 => new ChoiceView('A', 'a', 'a'),
2 => new ChoiceView('C', 'c', 'c'),
), $view->vars['choices']);
$this->assertEquals(array(
1 => new ChoiceView('b', 'b', 'B'),
3 => new ChoiceView('d', 'd', 'D'),
1 => new ChoiceView('B', 'b', 'b'),
3 => new ChoiceView('D', 'd', 'd'),
), $view->vars['preferred_choices']);
}
@ -1169,21 +1479,21 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$view = $form->createView();
$this->assertEquals(array(
'Symfony' => array(
0 => new ChoiceView('a', 'a', 'Bernhard'),
2 => new ChoiceView('c', 'c', 'Kris'),
),
'Doctrine' => array(
4 => new ChoiceView('e', 'e', 'Roman'),
),
'Symfony' => new ChoiceGroupView('Symfony', array(
0 => new ChoiceView('Bernhard', 'a', 'a'),
2 => new ChoiceView('Kris', 'c', 'c'),
)),
'Doctrine' => new ChoiceGroupView('Doctrine', array(
4 => new ChoiceView('Roman', 'e', 'e'),
)),
), $view->vars['choices']);
$this->assertEquals(array(
'Symfony' => array(
1 => new ChoiceView('b', 'b', 'Fabien'),
),
'Doctrine' => array(
3 => new ChoiceView('d', 'd', 'Jon'),
),
'Symfony' => new ChoiceGroupView('Symfony', array(
1 => new ChoiceView('Fabien', 'b', 'b'),
)),
'Doctrine' => new ChoiceGroupView('Doctrine', array(
3 => new ChoiceView('Jon', 'd', 'd'),
)),
), $view->vars['preferred_choices']);
}
@ -1194,15 +1504,18 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$obj3 = (object) array('value' => 'c', 'label' => 'C');
$obj4 = (object) array('value' => 'd', 'label' => 'D');
$form = $this->factory->create('choice', null, array(
'choice_list' => new ObjectChoiceList(array($obj1, $obj2, $obj3, $obj4), 'label', array(), null, 'value'),
'choices' => array($obj1, $obj2, $obj3, $obj4),
'choices_as_values' => true,
'choice_label' => 'label',
'choice_value' => 'value',
));
$view = $form->createView();
$this->assertEquals(array(
new ChoiceView($obj1, 'a', 'A'),
new ChoiceView($obj2, 'b', 'B'),
new ChoiceView($obj3, 'c', 'C'),
new ChoiceView($obj4, 'd', 'D'),
new ChoiceView('A', 'a', $obj1),
new ChoiceView('B', 'b', $obj2),
new ChoiceView('C', 'c', $obj3),
new ChoiceView('D', 'd', $obj4),
), $view->vars['choices']);
}
@ -1226,47 +1539,6 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
));
}
// https://github.com/symfony/symfony/issues/10409
public function testReuseNonUtf8ChoiceLists()
{
$form1 = $this->factory->createNamed('name', 'choice', null, array(
'choices' => array(
'meter' => 'm',
'millimeter' => 'mm',
'micrometer' => chr(181).'meter',
),
));
$form2 = $this->factory->createNamed('name', 'choice', null, array(
'choices' => array(
'meter' => 'm',
'millimeter' => 'mm',
'micrometer' => chr(181).'meter',
),
));
$form3 = $this->factory->createNamed('name', 'choice', null, array(
'choices' => array(
'meter' => 'm',
'millimeter' => 'mm',
'micrometer' => null,
),
));
// $form1 and $form2 use the same ChoiceList
$this->assertSame(
$form1->getConfig()->getOption('choice_list'),
$form2->getConfig()->getOption('choice_list')
);
// $form3 doesn't, but used to use the same when using json_encode()
// instead of serialize for the hashing algorithm
$this->assertNotSame(
$form1->getConfig()->getOption('choice_list'),
$form3->getConfig()->getOption('choice_list')
);
}
public function testInitializeWithDefaultObjectChoice()
{
$obj1 = (object) array('value' => 'a', 'label' => 'A');
@ -1275,7 +1547,10 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$obj4 = (object) array('value' => 'd', 'label' => 'D');
$form = $this->factory->create('choice', null, array(
'choice_list' => new ObjectChoiceList(array($obj1, $obj2, $obj3, $obj4), 'label', array(), null, 'value'),
'choices' => array($obj1, $obj2, $obj3, $obj4),
'choices_as_values' => true,
'choice_label' => 'label',
'choice_value' => 'value',
// Used to break because "data_class" was inferred, which needs to
// remain null in every case (because it refers to the view format)
'data' => $obj3,

View File

@ -11,8 +11,8 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\Test\TypeTestCase as TestCase;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Intl\Util\IntlTestHelper;
class CountryTypeTest extends TestCase
@ -31,11 +31,11 @@ class CountryTypeTest extends TestCase
$choices = $view->vars['choices'];
// Don't check objects for identity
$this->assertContains(new ChoiceView('DE', 'DE', 'Germany'), $choices, '', false, false);
$this->assertContains(new ChoiceView('GB', 'GB', 'United Kingdom'), $choices, '', false, false);
$this->assertContains(new ChoiceView('US', 'US', 'United States'), $choices, '', false, false);
$this->assertContains(new ChoiceView('FR', 'FR', 'France'), $choices, '', false, false);
$this->assertContains(new ChoiceView('MY', 'MY', 'Malaysia'), $choices, '', false, false);
$this->assertContains(new ChoiceView('Germany', 'DE', 'DE'), $choices, '', false, false);
$this->assertContains(new ChoiceView('United Kingdom', 'GB', 'GB'), $choices, '', false, false);
$this->assertContains(new ChoiceView('United States', 'US', 'US'), $choices, '', false, false);
$this->assertContains(new ChoiceView('France', 'FR', 'FR'), $choices, '', false, false);
$this->assertContains(new ChoiceView('Malaysia', 'MY', 'MY'), $choices, '', false, false);
}
public function testUnknownCountryIsNotIncluded()

View File

@ -11,8 +11,8 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\Test\TypeTestCase as TestCase;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Intl\Util\IntlTestHelper;
class CurrencyTypeTest extends TestCase
@ -30,8 +30,8 @@ class CurrencyTypeTest extends TestCase
$view = $form->createView();
$choices = $view->vars['choices'];
$this->assertContains(new ChoiceView('EUR', 'EUR', 'Euro'), $choices, '', false, false);
$this->assertContains(new ChoiceView('USD', 'USD', 'US Dollar'), $choices, '', false, false);
$this->assertContains(new ChoiceView('SIT', 'SIT', 'Slovenian Tolar'), $choices, '', false, false);
$this->assertContains(new ChoiceView('Euro', 'EUR', 'EUR'), $choices, '', false, false);
$this->assertContains(new ChoiceView('US Dollar', 'USD', 'USD'), $choices, '', false, false);
$this->assertContains(new ChoiceView('Slovenian Tolar', 'SIT', 'SIT'), $choices, '', false, false);
}
}

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\Test\TypeTestCase as TestCase;
use Symfony\Component\Intl\Util\IntlTestHelper;
@ -490,8 +490,8 @@ class DateTypeTest extends TestCase
$view = $form->createView();
$this->assertEquals(array(
new ChoiceView('6', '6', '06'),
new ChoiceView('7', '7', '07'),
new ChoiceView('06', '6', '6'),
new ChoiceView('07', '7', '7'),
), $view['month']->vars['choices']);
}
@ -505,8 +505,8 @@ class DateTypeTest extends TestCase
$view = $form->createView();
$this->assertEquals(array(
new ChoiceView('1', '1', 'Jän'),
new ChoiceView('4', '4', 'Apr.'),
new ChoiceView('Jän', '1', '1'),
new ChoiceView('Apr.', '4', '4'),
), $view['month']->vars['choices']);
}
@ -520,8 +520,8 @@ class DateTypeTest extends TestCase
$view = $form->createView();
$this->assertEquals(array(
new ChoiceView('1', '1', 'Jänner'),
new ChoiceView('4', '4', 'April'),
new ChoiceView('Jänner', '1', '1'),
new ChoiceView('April', '4', '4'),
), $view['month']->vars['choices']);
}
@ -535,8 +535,8 @@ class DateTypeTest extends TestCase
$view = $form->createView();
$this->assertEquals(array(
new ChoiceView('1', '1', 'Jänner'),
new ChoiceView('4', '4', 'April'),
new ChoiceView('Jänner', '1', '1'),
new ChoiceView('April', '4', '4'),
), $view['month']->vars['choices']);
}
@ -549,8 +549,8 @@ class DateTypeTest extends TestCase
$view = $form->createView();
$this->assertEquals(array(
new ChoiceView('6', '6', '06'),
new ChoiceView('7', '7', '07'),
new ChoiceView('06', '6', '6'),
new ChoiceView('07', '7', '7'),
), $view['day']->vars['choices']);
}

View File

@ -11,8 +11,8 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\Test\TypeTestCase as TestCase;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Intl\Util\IntlTestHelper;
class LanguageTypeTest extends TestCase
@ -30,11 +30,11 @@ class LanguageTypeTest extends TestCase
$view = $form->createView();
$choices = $view->vars['choices'];
$this->assertContains(new ChoiceView('en', 'en', 'English'), $choices, '', false, false);
$this->assertContains(new ChoiceView('en_GB', 'en_GB', 'British English'), $choices, '', false, false);
$this->assertContains(new ChoiceView('en_US', 'en_US', 'American English'), $choices, '', false, false);
$this->assertContains(new ChoiceView('fr', 'fr', 'French'), $choices, '', false, false);
$this->assertContains(new ChoiceView('my', 'my', 'Burmese'), $choices, '', false, false);
$this->assertContains(new ChoiceView('English', 'en', 'en'), $choices, '', false, false);
$this->assertContains(new ChoiceView('British English', 'en_GB', 'en_GB'), $choices, '', false, false);
$this->assertContains(new ChoiceView('American English', 'en_US', 'en_US'), $choices, '', false, false);
$this->assertContains(new ChoiceView('French', 'fr', 'fr'), $choices, '', false, false);
$this->assertContains(new ChoiceView('Burmese', 'my', 'my'), $choices, '', false, false);
}
public function testMultipleLanguagesIsNotIncluded()
@ -43,6 +43,6 @@ class LanguageTypeTest extends TestCase
$view = $form->createView();
$choices = $view->vars['choices'];
$this->assertNotContains(new ChoiceView('mul', 'mul', 'Mehrsprachig'), $choices, '', false, false);
$this->assertNotContains(new ChoiceView('Mehrsprachig', 'mul', 'mul'), $choices, '', false, false);
}
}

View File

@ -11,8 +11,8 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\Test\TypeTestCase as TestCase;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Intl\Util\IntlTestHelper;
class LocaleTypeTest extends TestCase
@ -30,8 +30,8 @@ class LocaleTypeTest extends TestCase
$view = $form->createView();
$choices = $view->vars['choices'];
$this->assertContains(new ChoiceView('en', 'en', 'English'), $choices, '', false, false);
$this->assertContains(new ChoiceView('en_GB', 'en_GB', 'English (United Kingdom)'), $choices, '', false, false);
$this->assertContains(new ChoiceView('zh_Hant_MO', 'zh_Hant_MO', 'Chinese (Traditional, Macau SAR China)'), $choices, '', false, false);
$this->assertContains(new ChoiceView('English', 'en', 'en'), $choices, '', false, false);
$this->assertContains(new ChoiceView('English (United Kingdom)', 'en_GB', 'en_GB'), $choices, '', false, false);
$this->assertContains(new ChoiceView('Chinese (Traditional, Macau SAR China)', 'zh_Hant_MO', 'zh_Hant_MO'), $choices, '', false, false);
}
}

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\Test\TypeTestCase as TestCase;
use Symfony\Component\Intl\Util\IntlTestHelper;
@ -319,8 +319,8 @@ class TimeTypeTest extends TestCase
$view = $form->createView();
$this->assertEquals(array(
new ChoiceView('6', '6', '06'),
new ChoiceView('7', '7', '07'),
new ChoiceView('06', '6', '6'),
new ChoiceView('07', '7', '7'),
), $view['hour']->vars['choices']);
}
@ -333,8 +333,8 @@ class TimeTypeTest extends TestCase
$view = $form->createView();
$this->assertEquals(array(
new ChoiceView('6', '6', '06'),
new ChoiceView('7', '7', '07'),
new ChoiceView('06', '6', '6'),
new ChoiceView('07', '7', '7'),
), $view['minute']->vars['choices']);
}
@ -348,8 +348,8 @@ class TimeTypeTest extends TestCase
$view = $form->createView();
$this->assertEquals(array(
new ChoiceView('6', '6', '06'),
new ChoiceView('7', '7', '07'),
new ChoiceView('06', '6', '6'),
new ChoiceView('07', '7', '7'),
), $view['second']->vars['choices']);
}

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
class TimezoneTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
{
@ -22,9 +22,9 @@ class TimezoneTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
$choices = $view->vars['choices'];
$this->assertArrayHasKey('Africa', $choices);
$this->assertContains(new ChoiceView('Africa/Kinshasa', 'Africa/Kinshasa', 'Kinshasa'), $choices['Africa'], '', false, false);
$this->assertContains(new ChoiceView('Kinshasa', 'Africa/Kinshasa', 'Africa/Kinshasa'), $choices['Africa'], '', false, false);
$this->assertArrayHasKey('America', $choices);
$this->assertContains(new ChoiceView('America/New_York', 'America/New_York', 'New York'), $choices['America'], '', false, false);
$this->assertContains(new ChoiceView('New York', 'America/New_York', 'America/New_York'), $choices['America'], '', false, false);
}
}