[Form] Greatly improved ChoiceListInterface and all of its implementations

Fixes #2869, fixes #3021, fixes #1919, fixes #3153.
This commit is contained in:
Bernhard Schussek 2012-01-17 00:46:26 +01:00
parent fbbea2f369
commit 87b16e7015
58 changed files with 3083 additions and 1322 deletions

View File

@ -166,6 +166,37 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c
* added support for empty form name at root level, this enables rendering forms
without form name prefix in field names
* [BC BREAK] greatly improved `ChoiceListInterface` and all of its
implementations. `EntityChoiceList` was adapted, the methods `getEntities()`,
`getEntitiesByByKeys()`, `getIdentifier()` and `getIdentifierValues()` were
removed/made private. Instead of the first two you can use `getChoices()`
and `getChoicesByValues()`, for the latter two no replacement exists.
`ArrayChoiceList` was replaced by `SimpleChoiceList`.
`PaddedChoiceList`, `MonthChoiceList` and `TimezoneChoiceList` were removed.
Their functionality was merged into `DateType`, `TimeType` and `TimezoneType`.
* [BC BREAK] removed `EntitiesToArrayTransformer` and `EntityToIdTransformer`.
The former has been replaced by `CollectionToArrayTransformer` in combination
with `EntityChoiceList`, the latter is not required in the core anymore.
* [BC BREAK] renamed
* `ArrayToBooleanChoicesTransformer` to `ChoicesToBooleanArrayTransformer`
* `ScalarToBooleanChoicesTransformer` to `ChoiceToBooleanArrayTransformer`
* `ArrayToChoicesTransformer` to `ChoicesToValuesTransformer`
* `ScalarToChoiceTransformer` to `ChoiceToValueTransformer`
to be consistent with the naming in `ChoiceListInterface`.
* [BC BREAK] removed `FormUtil::toArrayKey()` and `FormUtil::toArrayKeys()`.
They were merged into `ChoiceList` and have no public equivalent anymore.
* added `ComplexChoiceList` and `ObjectChoiceList`. Both let you select amongst
objects in a choice field, but feature different constructors.
* choice fields now throw a `FormException` if neither the "choices" nor the
"choice_list" option is set
* the radio field is now a child type of the checkbox field
### HttpFoundation
* added support for streamed responses

View File

@ -76,4 +76,49 @@ UPGRADE FROM 2.0 to 2.1
If you don't want to set the `Valid` constraint, or if there is no reference
from the data of the parent form to the data of the child form, you can
enable BC behaviour by setting the option "cascade_validation" to `true` on
the parent form.
the parent form.
* In the template of the choice type, instead of a single "choices" variable
there are now two variables: "choices" and "choice_labels".
"choices" contains the choices in the values (before they were in the keys)
and "choice_labels" contains the matching labels. Both arrays have
identical keys.
Before:
{% for choice, label in choices %}
<option value="{{ choice }}"{% if _form_is_choice_selected(form, choice) %} selected="selected"{% endif %}>
{{ label }}
</option>
{% endfor %}
After:
{% for index, choice in choices %}
<option value="{{ choice }}"{% if _form_is_choice_selected(form, choice) %} selected="selected"{% endif %}>
{{ choice_labels[index] }}
</option>
{% endfor %}
* The strategy for generating the HTML attributes "id" and "name"
of choices in a choice field has changed. Instead of appending the choice
value, a generated integer is now appended by default. Take care if your
Javascript relies on that. If you can guarantee that your choice values only
contain ASCII letters, digits, letters, colons and underscores, you can
restore the old behaviour by setting the option "index_strategy" of the
choice field to `ChoiceList::COPY_CHOICE`.
* The strategy for generating the HTML attributes "value" of choices in a
choice field has changed. Instead of using the choice value, a generated
integer is now stored. Again, take care if your Javascript reads this value.
If your choice field is a non-expanded single-choice field, or if
the choices are guaranteed not to contain the empty string '' (which is the
case when you added it manually or when the field is a single-choice field
and is not required), you can restore the old behaviour by setting the
option "value_strategy" to `ChoiceList::COPY_CHOICE`.
of choices in a choice field has changed. Instead of appending the choice
value, a generated integer is now appended by default. Take care if your
Javascript relies on that. If you can guarantee that your choice values only
contain ASCII letters, digits, letters, colons and underscores, you can
restore the old behaviour by setting the option "index_strategy" of the
choice field to `ChoiceList::COPY_CHOICE`.

View File

@ -13,11 +13,17 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Exception\StringCastException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Extension\Core\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
use Doctrine\Common\Persistence\ObjectManager;
class EntityChoiceList extends ArrayChoiceList
/**
* A choice list presenting a list of Doctrine entities as choices
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class EntityChoiceList extends ObjectChoiceList
{
/**
* @var ObjectManager
@ -34,19 +40,6 @@ class EntityChoiceList extends ArrayChoiceList
*/
private $classMetadata;
/**
* The entities from which the user can choose
*
* This array is either indexed by ID (if the ID is a single field)
* or by key in the choices array (if the ID consists of multiple fields)
*
* This property is initialized by initializeChoices(). It should only
* be accessed through getEntity() and getEntities().
*
* @var array
*/
private $entities = array();
/**
* Contains the query builder that builds the query for fetching the
* entities
@ -67,223 +60,336 @@ class EntityChoiceList extends ArrayChoiceList
private $identifier = array();
/**
* Property path to access the key value of this choice-list.
* Whether the entities have already been loaded.
*
* @var PropertyPath
* @var Boolean
*/
private $propertyPath;
private $loaded = false;
/**
* Closure or PropertyPath string on Entity to use for grouping of entities
* Creates a new entity choice list.
*
* @var mixed
*/
private $groupBy;
/**
* Constructor.
*
* @param ObjectManager $manager An EntityManager instance
* @param ObjectManager $manager An EntityManager instance
* @param string $class The class name
* @param string $property The property name
* @param string $labelPath The property path used for the label
* @param EntityLoaderInterface $entityLoader An optional query builder
* @param array|\Closure $choices An array of choices or a function returning an array
* @param string $groupBy
* @param array $entities An array of choices
* @param string $groupPath
*/
public function __construct(ObjectManager $manager, $class, $property = null, EntityLoaderInterface $entityLoader = null, $choices = null, $groupBy = null)
public function __construct(ObjectManager $manager, $class, $labelPath = null, EntityLoaderInterface $entityLoader = null, $entities = null, $groupPath = null)
{
$this->em = $manager;
$this->class = $class;
$this->entityLoader = $entityLoader;
$this->classMetadata = $manager->getClassMetadata($class);
$this->identifier = $this->classMetadata->getIdentifierFieldNames();
$this->groupBy = $groupBy;
$this->loaded = is_array($entities) || $entities instanceof \Traversable;
// The property option defines, which property (path) is used for
// displaying entities as strings
if ($property) {
$this->propertyPath = new PropertyPath($property);
} elseif (!method_exists($this->classMetadata->getName(), '__toString')) {
// Otherwise expect a __toString() method in the entity
throw new FormException('Entities passed to the choice field must have a "__toString()" method defined (or you can also override the "property" option).');
if (!$this->loaded) {
// Make sure the constraints of the parent constructor are
// fulfilled
$entities = array();
}
if (!is_array($choices) && !$choices instanceof \Closure && !is_null($choices)) {
throw new UnexpectedTypeException($choices, 'array or \Closure or null');
}
$this->choices = $choices;
parent::__construct($entities, $labelPath, array(), $groupPath);
}
/**
* Initializes the choices and returns them.
* Returns the list of entities
*
* If the entities were passed in the "choices" option, this method
* does not have any significant overhead. Otherwise, if a query builder
* was passed in the "query_builder" option, this builder is now used
* to construct a query which is executed. In the last case, all entities
* for the underlying class are fetched from the repository.
* @return array
*
* @return array An array of choices
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
protected function load()
public function getChoices()
{
parent::load();
if (!$this->loaded) {
$this->load();
}
if (is_array($this->choices)) {
$entities = $this->choices;
} elseif ($entityLoader = $this->entityLoader) {
$entities = $entityLoader->getEntities();
return parent::getChoices();
}
/**
* Returns the labels for the entities
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getLabels()
{
if (!$this->loaded) {
$this->load();
}
return parent::getLabels();
}
/**
* Returns the values for the entities
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getValues()
{
if (!$this->loaded) {
$this->load();
}
return parent::getValues();
}
/**
* Returns the values of the entities that should be presented to the user
* with priority.
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getPreferredValues()
{
if (!$this->loaded) {
$this->load();
}
return parent::getPreferredValues();
}
/**
* Returns the values of the entities that should be presented to the user
* with priority as nested array with the choice groups as top-level keys.
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getPreferredValueHierarchy()
{
if (!$this->loaded) {
$this->load();
}
return parent::getPreferredValueHierarchy();
}
/**
* Returns the values of the entities that are not preferred.
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getRemainingValues()
{
if (!$this->loaded) {
$this->load();
}
return parent::getRemainingValues();
}
/**
* Returns the values of the entities that are not preferred as nested array
* with the choice groups as top-level keys.
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getRemainingValueHierarchy()
{
if (!$this->loaded) {
$this->load();
}
return parent::getRemainingValueHierarchy();
}
/**
* Returns the entities corresponding to the given values.
*
* @param array $values
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getChoicesForValues(array $values)
{
if (!$this->loaded) {
// Optimize performance in case we have an entity loader and
// a single-field identifier
if (count($this->identifier) === 1 && $this->entityLoader) {
return $this->entityLoader->getEntitiesByIds(current($this->identifier), $values);
}
$this->load();
}
return parent::getChoicesForValues($values);
}
/**
* Returns the values corresponding to the given entities.
*
* @param array $entities
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getValuesForChoices(array $entities)
{
if (!$this->loaded) {
// 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 (count($this->identifier) === 1) {
$values = array();
foreach ($entities as $entity) {
if ($entity instanceof $this->class) {
// Make sure to convert to the right format
$values[] = $this->fixValue(current($this->getIdentifierValues($entity)));
}
}
return $values;
}
$this->load();
}
return parent::getValuesForChoices($entities);
}
/**
* Returns the indices corresponding to the given entities.
*
* @param array $entities
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getIndicesForChoices(array $entities)
{
if (!$this->loaded) {
// Optimize performance for single-field identifiers. We already
// know that the IDs are used as indices
// Attention: This optimization does not check choices for existence
if (count($this->identifier) === 1) {
$indices = array();
foreach ($entities as $entity) {
if ($entity instanceof $this->class) {
// Make sure to convert to the right format
$indices[] = $this->fixIndex(current($this->getIdentifierValues($entity)));
}
}
return $indices;
}
$this->load();
}
return parent::getIndicesForChoices($entities);
}
/**
* Returns the entities corresponding to the given values.
*
* @param array $values
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getIndicesForValues(array $values)
{
if (!$this->loaded) {
// Optimize performance for single-field identifiers. We already
// know that the IDs are used as indices and values
// Attention: This optimization does not check values for existence
if (count($this->identifier) === 1) {
return $this->fixIndices($values);
}
$this->load();
}
return parent::getIndicesForValues($values);
}
/**
* Creates a new unique index for this entity.
*
* If the entity has a single-field identifier, this identifier is used.
*
* Otherwise a new integer is generated.
*
* @param mixed $choice The choice to create an index for
*
* @return integer|string A unique index containing only ASCII letters,
* digits and underscores.
*/
protected function createIndex($entity)
{
if (count($this->identifier) === 1) {
return current($this->getIdentifierValues($entity));
}
return parent::createIndex($entity);
}
/**
* Creates a new unique value for this entity.
*
* If the entity has a single-field identifier, this identifier is used.
*
* Otherwise a new integer is generated.
*
* @param mixed $choice The choice to create a value for
*
* @return integer|string A unique value without character limitations.
*/
protected function createValue($entity)
{
if (count($this->identifier) === 1) {
return current($this->getIdentifierValues($entity));
}
return parent::createValue($entity);
}
/**
* Loads the list with entities.
*/
private function load()
{
if ($this->entityLoader) {
$entities = $this->entityLoader->getEntities();
} else {
$entities = $this->em->getRepository($this->class)->findAll();
}
$this->choices = array();
$this->entities = array();
if ($this->groupBy) {
$entities = $this->groupEntities($entities, $this->groupBy);
try {
// The second parameter $labels is ignored by ObjectChoiceList
// The third parameter $preferredChoices is currently not supported
parent::initialize($entities, array(), array());
} catch (StringCastException $e) {
throw new StringCastException('Objects in the entity field must have a "__toString()" method defined. Alternatively you can set the "property" option.', null, $e);
}
$this->loadEntities($entities);
return $this->choices;
}
private function groupEntities($entities, $groupBy)
{
$grouped = array();
$path = new PropertyPath($groupBy);
foreach ($entities as $entity) {
// Get group name from property path
try {
$group = (string) $path->getValue($entity);
} catch (UnexpectedTypeException $e) {
// PropertyPath cannot traverse entity
$group = null;
}
if (empty($group)) {
$grouped[] = $entity;
} else {
$grouped[$group][] = $entity;
}
}
return $grouped;
}
/**
* Converts entities into choices with support for groups.
*
* The choices are generated from the entities. If the entities have a
* composite identifier, the choices are indexed using ascending integers.
* Otherwise the identifiers are used as indices.
*
* If the option "property" was passed, the property path in that option
* is used as option values. Otherwise this method tries to convert
* objects to strings using __toString().
*
* @param array $entities An array of entities
* @param string $group A group name
*/
private function loadEntities($entities, $group = null)
{
foreach ($entities as $key => $entity) {
if (is_array($entity)) {
// Entities are in named groups
$this->loadEntities($entity, $key);
continue;
}
if ($this->propertyPath) {
// If the property option was given, use it
$value = $this->propertyPath->getValue($entity);
} else {
$value = (string) $entity;
}
if (count($this->identifier) > 1) {
// When the identifier consists of multiple field, use
// naturally ordered keys to refer to the choices
$id = $key;
} else {
// When the identifier is a single field, index choices by
// entity ID for performance reasons
$id = current($this->getIdentifierValues($entity));
}
if (null === $group) {
// Flat list of choices
$this->choices[$id] = $value;
} else {
// Nested choices
$this->choices[$group][$id] = $value;
}
$this->entities[$id] = $entity;
}
}
/**
* Returns the fields of which the identifier of the underlying class consists.
*
* @return array
*/
public function getIdentifier()
{
return $this->identifier;
}
/**
* Returns the according entities for the choices.
*
* If the choices were not initialized, they are initialized now. This
* is an expensive operation, except if the entities were passed in the
* "choices" option.
*
* @return array An array of entities
*/
public function getEntities()
{
if (!$this->loaded) {
$this->load();
}
return $this->entities;
}
/**
* Returns the entities for the given keys.
*
* If the underlying entities have composite identifiers, the choices
* are initialized. The key is expected to be the index in the choices
* array in this case.
*
* If they have single identifiers, they are either fetched from the
* internal entity cache (if filled) or loaded from the database.
*
* @param array $keys The choice key (for entities with composite
* identifiers) or entity ID (for entities with single
* identifiers)
* @return object[] The matching entity
*/
public function getEntitiesByKeys(array $keys)
{
if (!$this->loaded) {
$this->load();
}
$found = array();
foreach ($keys as $key) {
if (isset($this->entities[$key])) {
$found[] = $this->entities[$key];
}
}
return $found;
$this->loaded = true;
}
/**
@ -299,7 +405,7 @@ class EntityChoiceList extends ArrayChoiceList
*
* @throws FormException If the entity does not exist in Doctrine's identity map
*/
public function getIdentifierValues($entity)
private function getIdentifierValues($entity)
{
if (!$this->em->contains($entity)) {
throw new FormException('Entities passed to the choice field must be managed');

View File

@ -19,9 +19,19 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList;
interface EntityLoaderInterface
{
/**
* Return an array of entities that are valid choices in the corresponding choice list.
* Returns an array of entities that are valid choices in the corresponding choice list.
*
* @return array
*/
function getEntities();
/**
* Returns an array of entities matching the given identifiers.
*
* @param string $identifier
* @param array $values
*
* @return array
*/
function getEntitiesByIds($identifier, array $values);
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Doctrine\ORM\QueryBuilder;
use Doctrine\DBAL\Connection;
/**
* Getting Entities through the ORM QueryBuilder
@ -62,4 +63,19 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface
{
return $this->queryBuilder->getQuery()->execute();
}
/**
* {@inheritDoc}
*/
public function getEntitiesByIds($identifier, array $values)
{
$qb = clone ($this->queryBuilder);
$alias = current($qb->getRootAliases());
$where = $qb->expr()->in($alias.'.'.$identifier, "?1");
return $qb->andWhere($where)
->getQuery()
->setParameter(1, $values, Connection::PARAM_STR_ARRAY)
->getResult();
}
}

View File

@ -0,0 +1,63 @@
<?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\DataTransformer;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\DataTransformerInterface;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CollectionToArrayTransformer implements DataTransformerInterface
{
/**
* Transforms a collection into an array.
*
* @param Collection|object $collection A collection of entities, a single entity or NULL
*
* @return mixed An array of choice keys, a single key or NULL
*/
public function transform($collection)
{
if (null === $collection) {
return array();
}
if (!$collection instanceof Collection) {
throw new UnexpectedTypeException($collection, 'Doctrine\Common\Collections\Collection');
}
return $collection->toArray();
}
/**
* Transforms choice keys into entities.
*
* @param mixed $keys An array of keys, a single key or NULL
*
* @return Collection|object A collection of entities, a single entity or NULL
*/
public function reverseTransform($array)
{
if ('' === $array || null === $array) {
$array = array();
} else {
$array = (array) $array;
}
return new ArrayCollection($array);
}
}

View File

@ -1,98 +0,0 @@
<?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\DataTransformer;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\DataTransformerInterface;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
class EntitiesToArrayTransformer implements DataTransformerInterface
{
private $choiceList;
public function __construct(EntityChoiceList $choiceList)
{
$this->choiceList = $choiceList;
}
/**
* Transforms entities into choice keys.
*
* @param Collection|object $collection A collection of entities, a single entity or NULL
*
* @return mixed An array of choice keys, a single key or NULL
*/
public function transform($collection)
{
if (null === $collection) {
return array();
}
if (!($collection instanceof Collection)) {
throw new UnexpectedTypeException($collection, 'Doctrine\Common\Collections\Collection');
}
$array = array();
if (count($this->choiceList->getIdentifier()) > 1) {
// load all choices
$availableEntities = $this->choiceList->getEntities();
foreach ($collection as $entity) {
// identify choices by their collection key
$key = array_search($entity, $availableEntities, true);
$array[] = $key;
}
} else {
foreach ($collection as $entity) {
$value = current($this->choiceList->getIdentifierValues($entity));
$array[] = is_numeric($value) ? (int) $value : $value;
}
}
return $array;
}
/**
* Transforms choice keys into entities.
*
* @param mixed $keys An array of keys, a single key or NULL
*
* @return Collection|object A collection of entities, a single entity or NULL
*/
public function reverseTransform($keys)
{
$collection = new ArrayCollection();
if ('' === $keys || null === $keys) {
return $collection;
}
if (!is_array($keys)) {
throw new UnexpectedTypeException($keys, 'array');
}
$entities = $this->choiceList->getEntitiesByKeys($keys);
if (count($keys) !== count($entities)) {
throw new TransformationFailedException('Not all entities matching the keys were found.');
}
foreach ($entities as $entity) {
$collection->add($entity);
}
return $collection;
}
}

View File

@ -1,83 +0,0 @@
<?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\DataTransformer;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Collections\Collection;
class EntityToIdTransformer implements DataTransformerInterface
{
private $choiceList;
public function __construct(EntityChoiceList $choiceList)
{
$this->choiceList = $choiceList;
}
/**
* Transforms entities into choice keys.
*
* @param Collection|object $entity A collection of entities, a single entity or NULL
*
* @return mixed An array of choice keys, a single key or NULL
*/
public function transform($entity)
{
if (null === $entity || '' === $entity) {
return '';
}
if (!is_object($entity)) {
throw new UnexpectedTypeException($entity, 'object');
}
if ($entity instanceof Collection) {
throw new \InvalidArgumentException('Expected an object, but got a collection. Did you forget to pass "multiple=true" to an entity field?');
}
if (count($this->choiceList->getIdentifier()) > 1) {
// load all choices
$availableEntities = $this->choiceList->getEntities();
return array_search($entity, $availableEntities);
}
return current($this->choiceList->getIdentifierValues($entity));
}
/**
* Transforms choice keys into entities.
*
* @param mixed $key An array of keys, a single key or NULL
*
* @return Collection|object A collection of entities, a single entity or NULL
*/
public function reverseTransform($key)
{
if ('' === $key || null === $key) {
return null;
}
if (count($this->choiceList->getIdentifier()) > 1 && !is_numeric($key)) {
throw new UnexpectedTypeException($key, 'numeric');
}
if (!($entities = $this->choiceList->getEntitiesByKeys(array($key)))) {
throw new TransformationFailedException(sprintf('The entity with key "%s" could not be found', $key));
}
return $entities[0];
}
}

View File

@ -17,8 +17,7 @@ use Symfony\Component\Form\FormBuilder;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface;
use Symfony\Bridge\Doctrine\Form\EventListener\MergeCollectionListener;
use Symfony\Bridge\Doctrine\Form\DataTransformer\EntitiesToArrayTransformer;
use Symfony\Bridge\Doctrine\Form\DataTransformer\EntityToIdTransformer;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Component\Form\AbstractType;
abstract class DoctrineType extends AbstractType
@ -38,10 +37,8 @@ abstract class DoctrineType extends AbstractType
if ($options['multiple']) {
$builder
->addEventSubscriber(new MergeCollectionListener())
->prependClientTransformer(new EntitiesToArrayTransformer($options['choice_list']))
->prependClientTransformer(new CollectionToArrayTransformer())
;
} else {
$builder->prependClientTransformer(new EntityToIdTransformer($options['choice_list']));
}
}
@ -61,6 +58,7 @@ abstract class DoctrineType extends AbstractType
if (!isset($options['choice_list'])) {
$manager = $this->registry->getManager($options['em']);
if (isset($options['query_builder'])) {
$options['loader'] = $this->getLoader($manager, $options);
}

View File

@ -26,15 +26,15 @@
{% block widget_choice_options %}
{% spaceless %}
{% for choice, label in options %}
{% if _form_is_choice_group(label) %}
<optgroup label="{{ choice|trans({}, translation_domain) }}">
{% for nestedChoice, nestedLabel in label %}
<option value="{{ nestedChoice }}"{% if _form_is_choice_selected(form, nestedChoice) %} selected="selected"{% endif %}>{{ nestedLabel|trans({}, translation_domain) }}</option>
{% for index, choice in options %}
{% if _form_is_choice_group(choice) %}
<optgroup label="{{ index|trans({}, translation_domain) }}">
{% for nested_index, nested_choice in choice %}
<option value="{{ nested_choice }}"{% if _form_is_choice_selected(form, nested_choice) %} selected="selected"{% endif %}>{{ choice_labels[nested_index]|trans({}, translation_domain) }}</option>
{% endfor %}
</optgroup>
{% else %}
<option value="{{ choice }}"{% if _form_is_choice_selected(form, choice) %} selected="selected"{% endif %}>{{ label|trans({}, translation_domain) }}</option>
<option value="{{ choice }}"{% if _form_is_choice_selected(form, choice) %} selected="selected"{% endif %}>{{ choice_labels[index]|trans({}, translation_domain) }}</option>
{% endif %}
{% endfor %}
{% endspaceless %}

View File

@ -1,11 +1,11 @@
<?php foreach ($options as $choice => $label): ?>
<?php if ($view['form']->isChoiceGroup($label)): ?>
<optgroup label="<?php echo $view->escape($view['translator']->trans($choice, array(), $translation_domain)) ?>">
<?php foreach ($label as $nestedChoice => $nestedLabel): ?>
<option value="<?php echo $view->escape($nestedChoice) ?>"<?php if ($view['form']->isChoiceSelected($form, $nestedChoice)): ?> selected="selected"<?php endif?>><?php echo $view->escape($view['translator']->trans($nestedLabel, array(), $translation_domain)) ?></option>
<?php foreach ($options as $index => $choice): ?>
<?php if ($view['form']->isChoiceGroup($choice)): ?>
<optgroup label="<?php echo $view->escape($view['translator']->trans($index, array(), $translation_domain)) ?>">
<?php foreach ($choice as $nested_index => $nested_choice): ?>
<option value="<?php echo $view->escape($nested_choice) ?>"<?php if ($view['form']->isChoiceSelected($form, $nested_choice)): ?> selected="selected"<?php endif?>><?php echo $view->escape($view['translator']->trans($choice_labels[$nested_index], array(), $translation_domain)) ?></option>
<?php endforeach ?>
</optgroup>
<?php else: ?>
<option value="<?php echo $view->escape($choice) ?>"<?php if ($view['form']->isChoiceSelected($form, $choice)): ?> selected="selected"<?php endif?>><?php echo $view->escape($view['translator']->trans($label, array(), $translation_domain)) ?></option>
<option value="<?php echo $view->escape($choice) ?>"<?php if ($view['form']->isChoiceSelected($form, $choice)): ?> selected="selected"<?php endif?>><?php echo $view->escape($view['translator']->trans($choice_labels[$index], array(), $translation_domain)) ?></option>
<?php endif ?>
<?php endforeach ?>

View File

@ -0,0 +1,16 @@
<?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\Exception;
class StringCastException extends FormException
{
}

View File

@ -1,69 +0,0 @@
<?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\ChoiceList;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
class ArrayChoiceList implements ChoiceListInterface
{
protected $choices;
protected $loaded = false;
/**
* Constructor.
*
* @param array|\Closure $choices An array of choices or a function returning an array
*
* @throws UnexpectedTypeException if the type of the choices parameter is not supported
*/
public function __construct($choices)
{
if (!is_array($choices) && !$choices instanceof \Closure) {
throw new UnexpectedTypeException($choices, 'array or \Closure');
}
$this->choices = $choices;
}
/**
* Returns a list of choices
*
* @return array
*/
public function getChoices()
{
if (!$this->loaded) {
$this->load();
}
return $this->choices;
}
/**
* Initializes the list of choices.
*
* @throws UnexpectedTypeException if the function does not return an array
*/
protected function load()
{
$this->loaded = true;
if ($this->choices instanceof \Closure) {
$this->choices = call_user_func($this->choices);
if (!is_array($this->choices)) {
throw new UnexpectedTypeException($this->choices, 'array');
}
}
}
}

View File

@ -0,0 +1,649 @@
<?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\ChoiceList;
use Symfony\Component\Form\Util\FormUtil;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* Base class for choice list implementations.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
abstract class ChoiceList implements ChoiceListInterface
{
/**
* Strategy creating new indices/values by creating a copy of the choice.
*
* This strategy can only be used for index creation if choices are
* guaranteed to only contain ASCII letters, digits and underscores.
*
* It can be used for value creation if choices can safely be cast into
* a (unique) string.
*
* @var integer
*/
const COPY_CHOICE = 0;
/**
* Strategy creating new indices/values by generating a new integer.
*
* This strategy can always be applied, but leads to loss of information
* in the HTML source code.
*
* @var integer
*/
const GENERATE = 1;
/**
* The choices with their indices as keys.
*
* @var array
*/
private $choices = array();
/**
* The choice values with the indices of the matching choices as keys.
*
* @var array
*/
private $values = array();
/**
* The choice labels with the indices of the matching choices as keys.
*
* @var array
*/
private $labels = array();
/**
* The preferred values as flat array with the indices of the matching
* choices as keys.
*
* @var array
*/
private $preferredValues = array();
/**
* The non-preferred values as flat array with the indices of the matching
* choices as keys.
*
* @var array
*/
private $remainingValues = array();
/**
* The preferred values as hierarchy containing also the choice groups
* with the indices of the matching choices as bottom-level keys.
*
* @var array
*/
private $preferredValueHierarchy = array();
/**
* The non-preferred values as hierarchy containing also the choice groups
* with the indices of the matching choices as bottom-level keys.
*
* @var array
*/
private $remainingValueHierarchy = array();
/**
* The strategy used for creating choice indices.
*
* @var integer
* @see COPY_CHOICE
* @see GENERATE
*/
private $indexStrategy;
/**
* The strategy used for creating choice values.
*
* @var integer
* @see COPY_CHOICE
* @see GENERATE
*/
private $valueStrategy;
/**
* Creates a new choice list.
*
* @param array|\Traversable $choices The array of choices. Choices may also be given
* as hierarchy of unlimited depth. Hierarchies are
* created by creating nested arrays. The title of
* the sub-hierarchy can be stored in the array
* key pointing to the nested array.
* @param array $labels The array of labels. The structure of this array
* should match the structure of $choices.
* @param array $preferredChoices A flat array of choices that should be
* presented to the user with priority.
* @param integer $valueStrategy The strategy used to create choice values.
* One of COPY_CHOICE and GENERATE.
* @param integer $indexStrategy The strategy used to create choice indices.
* One of COPY_CHOICE and GENERATE.
*/
public function __construct($choices, array $labels,
array $preferredChoices = array(), $valueStrategy, $indexStrategy)
{
$this->valueStrategy = $valueStrategy;
$this->indexStrategy = $indexStrategy;
$this->initialize($choices, $labels, $preferredChoices);
}
/**
* Initializes the list with choices.
*
* Safe to be called multiple times. The list is cleared on every call.
*
* @param array|\Traversable $choices The choices to write into the list.
* @param array $labels The labels belonging to the choices.
* @param array $preferredChoices The choices to display with priority.
*/
protected function initialize($choices, array $labels, array $preferredChoices)
{
if (!is_array($choices) && !$choices instanceof \Traversable) {
throw new UnexpectedTypeException($choices, 'array or \Traversable');
}
$this->choices = array();
$this->values = array();
$this->labels = array();
$this->preferredValues = array();
$this->preferredValueHierarchy = array();
$this->remainingValues = array();
$this->remainingValueHierarchy = array();
$this->addChoices(
$this->preferredValueHierarchy,
$this->remainingValueHierarchy,
$choices,
$labels,
$preferredChoices
);
}
/**
* Returns the list of choices
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getChoices()
{
return $this->choices;
}
/**
* Returns the labels for the choices
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getLabels()
{
return $this->labels;
}
/**
* Returns the values for the choices
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getValues()
{
return $this->values;
}
/**
* Returns the values of the choices that should be presented to the user
* with priority.
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getPreferredValues()
{
return $this->preferredValues;
}
/**
* Returns the values of the choices that should be presented to the user
* with priority as nested array with the choice groups as top-level keys.
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getPreferredValueHierarchy()
{
return $this->preferredValueHierarchy;
}
/**
* Returns the values of the choices that are not preferred.
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getRemainingValues()
{
return $this->remainingValues;
}
/**
* Returns the values of the choices that are not preferred as nested array
* with the choice groups as top-level keys.
*
* @return array A nested array containing the values with the corresponding
* choice indices as keys on the lower levels and the choice
* group names in the keys of the topmost level.
*
* @see getPreferredValueHierarchy
*/
public function getRemainingValueHierarchy()
{
return $this->remainingValueHierarchy;
}
/**
* Returns the choices corresponding to the given values.
*
* @param array $values
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getChoicesForValues(array $values)
{
$values = $this->fixValues($values);
// If the values are identical to the choices, we can just return them
// to improve performance a little bit
if ($this->valueStrategy === self::COPY_CHOICE) {
return $this->fixChoices(array_intersect($values, $this->values));
}
$choices = array();
foreach ($this->values as $i => $value) {
foreach ($values as $j => $givenValue) {
if ($value === $givenValue) {
$choices[] = $this->choices[$i];
unset($values[$j]);
if (count($values) === 0) {
break 2;
}
}
}
}
return $choices;
}
/**
* Returns the values corresponding to the given choices.
*
* @param array $choices
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getValuesForChoices(array $choices)
{
$choices = $this->fixChoices($choices);
// If the values are identical to the choices, we can just return them
// to improve performance a little bit
if ($this->valueStrategy === self::COPY_CHOICE) {
return $this->fixValues(array_intersect($choices, $this->choices));
}
$values = array();
foreach ($this->choices as $i => $choice) {
foreach ($choices as $j => $givenChoice) {
if ($choice === $givenChoice) {
$values[] = $this->values[$i];
unset($choices[$j]);
if (count($choices) === 0) {
break 2;
}
}
}
}
return $values;
}
/**
* Returns the indices corresponding to the given choices.
*
* @param array $choices
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getIndicesForChoices(array $choices)
{
$choices = $this->fixChoices($choices);
$indices = array();
foreach ($this->choices as $i => $choice) {
foreach ($choices as $j => $givenChoice) {
if ($choice === $this->fixChoice($givenChoice)) {
$indices[] = $i;
unset($choices[$j]);
if (count($choices) === 0) {
break 2;
}
}
}
}
return $indices;
}
/**
* Returns the indices corresponding to the given values.
*
* @param array $values
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getIndicesForValues(array $values)
{
$values = $this->fixValues($values);
$indices = array();
foreach ($this->values as $i => $value) {
foreach ($values as $j => $givenValue) {
if ($value === $givenValue) {
$indices[] = $i;
unset($values[$j]);
if (count($values) === 0) {
break 2;
}
}
}
}
return $indices;
}
/**
* Recursively adds the given choices to the list.
*
* @param array $bucketForPreferred The bucket where to store the preferred
* values.
* @param array $bucketForRemaining The bucket where to store the
* non-preferred values.
* @param array $choices The list of choices.
* @param array $labels The labels corresponding to the choices.
* @param array $preferredChoices The preferred choices.
*
* @throws UnexpectedTypeException If the structure of the $labels array
* does not match the structure of the
* $choices array.
*/
protected function addChoices(&$bucketForPreferred, &$bucketForRemaining, $choices, $labels, array $preferredChoices)
{
// Add choices to the nested buckets
foreach ($choices as $group => $choice) {
if (is_array($choice)) {
if (!is_array($labels)) {
throw new UnexpectedTypeException($labels, 'array');
}
// Don't do the work if the array is empty
if (count($choice) > 0) {
$this->addChoiceGroup(
$group,
$bucketForPreferred,
$bucketForRemaining,
$choice,
$labels[$group],
$preferredChoices
);
}
} else {
$this->addChoice(
$bucketForPreferred,
$bucketForRemaining,
$choice,
$labels[$group],
$preferredChoices
);
}
}
}
/**
* Recursively adds a choice group.
*
* @param string $group The name of the group.
* @param array $bucketForPreferred The bucket where to store the preferred
* values.
* @param array $bucketForRemaining The bucket where to store the
* non-preferred values.
* @param array $choices The list of choices in the group.
* @param array $labels The labels corresponding to the choices in the group.
* @param array $preferredChoices The preferred choices.
*/
protected function addChoiceGroup($group, &$bucketForPreferred, &$bucketForRemaining, $choices, $labels, array $preferredChoices)
{
// If this is a choice group, create a new level in the choice
// key hierarchy
$bucketForPreferred[$group] = array();
$bucketForRemaining[$group] = array();
$this->addChoices(
$bucketForPreferred[$group],
$bucketForRemaining[$group],
$choices,
$labels,
$preferredChoices
);
// Remove child levels if empty
if (empty($bucketForPreferred[$group])) {
unset($bucketForPreferred[$group]);
}
if (empty($bucketForRemaining[$group])) {
unset($bucketForRemaining[$group]);
}
}
/**
* Adds a new choice.
*
* @param array $bucketForPreferred The bucket where to store the preferred
* values.
* @param array $bucketForRemaining The bucket where to store the
* non-preferred values.
* @param mixed $choice The choice to add.
* @param string $label The label for the choice.
* @param array $preferredChoices The preferred choices.
*/
protected function addChoice(&$bucketForPreferred, &$bucketForRemaining, $choice, $label, array $preferredChoices)
{
$index = $this->createIndex($choice);
// Always store values as strings to facilitate comparisons
$value = $this->fixValue($this->createValue($choice));
$this->choices[$index] = $this->fixChoice($choice);
$this->labels[$index] = $label;
$this->values[$index] = $value;
if ($this->isPreferred($choice, $preferredChoices)) {
$bucketForPreferred[$index] = $value;
$this->preferredValues[$index] = $value;
} else {
$bucketForRemaining[$index] = $value;
$this->remainingValues[$index] = $value;
}
}
/**
* Returns whether the given choice should be preferred judging by the
* given array of preferred choices.
*
* Extension point to optimize performance by changing the structure of the
* $preferredChoices array.
*
* @param mixed $choice The choice to test.
* @param array $preferredChoices An array of preferred choices.
*/
protected function isPreferred($choice, $preferredChoices)
{
return false !== array_search($choice, $preferredChoices, true);
}
/**
* Creates a new unique index for this choice.
*
* Extension point to change the indexing strategy.
*
* @param mixed $choice The choice to create an index for
*
* @return integer|string A unique index containing only ASCII letters,
* digits and underscores.
*/
protected function createIndex($choice)
{
if ($this->indexStrategy === self::COPY_CHOICE) {
return $choice;
}
return count($this->choices);
}
/**
* Creates a new unique value for this choice.
*
* Extension point to change the value strategy.
*
* @param mixed $choice The choice to create a value for
*
* @return integer|string A unique value without character limitations.
*/
protected function createValue($choice)
{
if ($this->valueStrategy === self::COPY_CHOICE) {
return $choice;
}
return count($this->values);
}
/**
* Fixes the data type of the given choice value to avoid comparison
* problems.
*
* @param mixed $value The choice value.
*
* @return string The value as string.
*/
protected function fixValue($value)
{
return (string) $value;
}
/**
* Fixes the data types of the given choice values to avoid comparison
* problems.
*
* @param array $values The choice values.
*
* @return array The values as strings.
*/
protected function fixValues(array $values)
{
return array_map(array($this, 'fixValue'), $values);
}
/**
* Fixes the data type of the given choice index to avoid comparison
* problems.
*
* @param mixed $index The choice index.
*
* @return integer|string The index as PHP array key.
*/
protected function fixIndex($index)
{
if (is_bool($index) || (string) (int) $index === (string) $index) {
return (int) $index;
}
return (string) $index;
}
/**
* Fixes the data types of the given choice indices to avoid comparison
* problems.
*
* @param array $indices The choice indices.
*
* @return array The indices as strings.
*/
protected function fixIndices(array $indices)
{
return array_map(array($this, 'fixIndex'), $indices);
}
/**
* Fixes the data type of the given choice to avoid comparison problems.
*
* Extension point. In this implementation, choices are guaranteed to
* always maintain their type and thus can be typesafely compared.
*
* @param mixed $choice The choice.
*
* @return mixed The fixed choice.
*/
protected function fixChoice($choice)
{
return $choice;
}
/**
* Fixes the data type of the given choices to avoid comparison problems.
*
* @param array $choice The choices.
*
* @return array The fixed choices.
*
* @see fixChoice
*/
protected function fixChoices(array $choices)
{
return $choices;
}
}

View File

@ -11,12 +11,126 @@
namespace Symfony\Component\Form\Extension\Core\ChoiceList;
/**
* Contains choices that can be selected in a form field.
*
* Each choice has four different properties:
*
* - Choice: The choice that should be returned to the application by the
* choice field. Can be any scalar value or an object, but no
* array.
* - Label: A text representing the choice that is displayed to the user.
* - Index: A uniquely identifying index that should only contain ASCII
* characters, digits and underscores. This index is used to
* identify the choice in the HTML "id" and "name" attributes.
* It is also used as index of the arrays returned by the various
* getters of this class.
* - Value: A uniquely identifying value that can contain arbitrary
* characters, but no arrays or objects. This value is displayed
* in the HTML "value" attribute.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ChoiceListInterface
{
/**
* Returns a list of choices
* Returns the list of choices
*
* @return array
* @return array The choices with their indices as keys.
*/
function getChoices();
/**
* Returns the labels for the choices
*
* @return array The labels with the corresponding choice indices as keys.
*/
function getLabels();
/**
* Returns the values for the choices
*
* @return array The values with the corresponding choice indices as keys.
*/
function getValues();
/**
* Returns the values of the choices that should be presented to the user
* with priority.
*
* @return array The values with the corresponding choice indices as keys.
*/
function getPreferredValues();
/**
* Returns the values of the choices that should be presented to the user
* with priority as nested array with the choice groups as top-level keys.
*
* @return array A nested array containing the values with the corresponding
* choice indices as keys on the lower levels and the choice
* group names in the keys of the topmost level.
*/
function getPreferredValueHierarchy();
/**
* Returns the values of the choices that are not preferred.
*
* @return array The values with the corresponding choice indices as keys.
*
* @see getPreferredValues
*/
function getRemainingValues();
/**
* Returns the values of the choices that are not preferred as nested array
* with the choice groups as top-level keys.
*
* @return array A nested array containing the values with the corresponding
* choice indices as keys on the lower levels and the choice
* group names in the keys of the topmost level.
*
* @see getPreferredValueHierarchy
*/
function getRemainingValueHierarchy();
/**
* Returns the choices corresponding to the given values.
*
* @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
*/
function getChoicesForValues(array $values);
/**
* Returns the values corresponding to the given choices.
*
* @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
*/
function getValuesForChoices(array $choices);
/**
* Returns the indices corresponding to the given choices.
*
* @param array $choices An array of choices. Not existing choices in this
* array are ignored.
*
* @return array An array of indices with ascending, 0-based numeric keys
*/
function getIndicesForChoices(array $choices);
/**
* Returns the indices corresponding to the given values.
*
* @param array $values An array of choice values. Not existing values in
* this array are ignored.
*
* @return array An array of indices with ascending, 0-based numeric keys
*/
function getIndicesForValues(array $values);
}

View File

@ -0,0 +1,44 @@
<?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\ChoiceList;
use Symfony\Component\Form\Util\FormUtil;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* A choice list that can store arbitrary scalar and object choices.
*
* Arrays as choices are not supported.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ComplexChoiceList extends ChoiceList
{
/**
* Creates a new complex choice list.
*
* @param array $choices The array of choices. Choices may also be given
* as hierarchy of unlimited depth. Hierarchies are
* created by creating nested arrays. The title of
* the sub-hierarchy can be stored in the array
* key pointing to the nested array.
* @param array $labels The array of labels. The structure of this array
* should match the structure of $choices.
* @param array $preferredChoices A flat array of choices that should be
* presented to the user with priority.
*/
public function __construct(array $choices, array $labels, array $preferredChoices = array())
{
parent::__construct($choices, $labels, $preferredChoices, self::GENERATE, self::GENERATE);
}
}

View File

@ -1,58 +0,0 @@
<?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\ChoiceList;
class MonthChoiceList extends PaddedChoiceList
{
private $formatter;
/**
* Generates an array of localized month choices.
*
* @param IntlDateFormatter $formatter An IntlDateFormatter instance
* @param array $months The month numbers to generate
*/
public function __construct(\IntlDateFormatter $formatter, array $months)
{
parent::__construct(array_combine($months, $months), 2, '0', STR_PAD_LEFT);
$this->formatter = $formatter;
}
/**
* Initializes the list of months.
*
* @throws UnexpectedTypeException if the function does not return an array
*/
protected function load()
{
parent::load();
$pattern = $this->formatter->getPattern();
$timezone = $this->formatter->getTimezoneId();
$this->formatter->setTimezoneId(\DateTimeZone::UTC);
if (preg_match('/M+/', $pattern, $matches)) {
$this->formatter->setPattern($matches[0]);
foreach ($this->choices as $choice => $value) {
$this->choices[$choice] = $this->formatter->format(gmmktime(0, 0, 0, $value, 15));
}
// I'd like to clone the formatter above, but then we get a
// segmentation fault, so let's restore the old state instead
$this->formatter->setPattern($pattern);
}
$this->formatter->setTimezoneId($timezone);
}
}

View File

@ -0,0 +1,201 @@
<?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\ChoiceList;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\Exception\StringCastException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\InvalidPropertyException;
/**
* A choice list that can store object choices.
*
* Supports generation of choice labels, choice groups, choice values and
* choice indices by introspecting the properties of the object (or
* associated objects).
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ObjectChoiceList extends ChoiceList
{
/**
* The property path used to obtain the choice label.
*
* @var PropertyPath
*/
private $labelPath;
/**
* The property path used for object grouping.
*
* @var PropertyPath
*/
private $groupPath;
/**
* The property path used to obtain the choice value.
*
* @var PropertyPath
*/
private $valuePath;
/**
* The property path used to obtain the choice index.
*
* @var PropertyPath
*/
private $indexPath;
/**
* Creates a new object choice list.
*
* @param array $choices The array of choices. Choices may also be given
* as hierarchy of unlimited depth. Hierarchies are
* created by creating nested arrays. The title of
* the sub-hierarchy can be stored in the array
* key pointing to the nested array.
* @param string $labelPath A property path pointing to the property used
* for the choice labels. The value is obtained
* by calling the getter on the object. If the
* path is NULL, the object's __toString() method
* is used instead.
* @param array $preferredChoices A flat array of choices that should be
* presented to the user with priority.
* @param string $groupPath A property path pointing to the property used
* to group the choices. Only allowed if
* the choices are given as flat array.
* @param string $valuePath A property path pointing to the property used
* for the choice values. If not given, integers
* are generated instead.
* @param string $indexPath A property path pointing to the property used
* for the choice indices. If not given, integers
* are generated instead.
*/
public function __construct($choices, $labelPath = null, array $preferredChoices = array(), $groupPath = null, $valuePath = null, $indexPath = null)
{
$this->labelPath = $labelPath ? new PropertyPath($labelPath) : null;
$this->groupPath = $groupPath ? new PropertyPath($groupPath) : null;
$this->valuePath = $valuePath ? new PropertyPath($valuePath) : null;
$this->indexPath = $indexPath ? new PropertyPath($indexPath) : null;
parent::__construct($choices, array(), $preferredChoices, self::GENERATE, self::GENERATE);
}
/**
* Initializes the list with choices.
*
* Safe to be called multiple times. The list is cleared on every call.
*
* @param array|\Traversable $choices The choices to write into the list.
* @param array $labels Ignored.
* @param array $preferredChoices The choices to display with priority.
*/
protected function initialize($choices, array $labels, array $preferredChoices)
{
if (!is_array($choices) && !$choices instanceof \Traversable) {
throw new UnexpectedTypeException($choices, 'array or \Traversable');
}
if ($this->groupPath !== null) {
$groupedChoices = array();
foreach ($choices as $i => $choice) {
if (is_array($choice)) {
throw new \InvalidArgumentException('You should pass a plain object array (without groups, $code, $previous) when using the "groupPath" option');
}
try {
$group = $this->groupPath->getValue($choice);
} catch (InvalidPropertyException $e) {
// Don't group items whose group property does not exist
// see https://github.com/symfony/symfony/commit/d9b7abb7c7a0f28e0ce970afc5e305dce5dccddf
$group = null;
}
if ($group === null) {
$groupedChoices[$i] = $choice;
} else {
if (!isset($groupedChoices[$group])) {
$groupedChoices[$group] = array();
}
$groupedChoices[$group][$i] = $choice;
}
}
$choices = $groupedChoices;
}
$labels = array();
$this->extractLabels($choices, $labels);
parent::initialize($choices, $labels, $preferredChoices);
}
/**
* Creates a new unique index for this choice.
*
* If a property path for the index was given at object creation,
* the getter behind that path is now called to obtain a new value.
*
* Otherwise a new integer is generated.
*
* @param mixed $choice The choice to create an index for
* @return integer|string A unique index containing only ASCII letters,
* digits and underscores.
*/
protected function createIndex($choice)
{
if ($this->indexPath) {
return $this->indexPath->getValue($choice);
}
return parent::createIndex($choice);
}
/**
* Creates a new unique value for this choice.
*
* If a property path for the value was given at object creation,
* the getter behind that path is now called to obtain a new value.
*
* Otherwise a new integer is generated.
*
* @param mixed $choice The choice to create a value for
* @return integer|string A unique value without character limitations.
*/
protected function createValue($choice)
{
if ($this->valuePath) {
return $this->valuePath->getValue($choice);
}
return parent::createValue($choice);
}
private function extractLabels($choices, array &$labels)
{
foreach ($choices as $i => $choice) {
if (is_array($choice) || $choice instanceof \Traversable) {
$labels[$i] = array();
$this->extractLabels($choice, $labels[$i]);
} elseif ($this->labelPath) {
$labels[$i] = $this->labelPath->getValue($choice);
} elseif (method_exists($choice, '__toString')) {
$labels[$i] = (string) $choice;
} else {
throw new StringCastException('Objects passed to the choice field must have a "__toString()" method defined. Alternatively you can set the $labelPath argument to choose the property used as label.');
}
}
}
}

View File

@ -1,59 +0,0 @@
<?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\ChoiceList;
class PaddedChoiceList extends ArrayChoiceList
{
private $padLength;
private $padString;
private $padType;
/**
* Generates an array of choices for the given values
*
* If the values are shorter than $padLength characters, they are padded with
* zeros on the left side.
*
* @param array|\Closure $values The available choices
* @param integer $padLength The length to pad the choices
* @param string $padString The padding character
* @param integer $padType The direction of padding
*
* @throws UnexpectedTypeException if the type of the values parameter is not supported
*/
public function __construct($values, $padLength, $padString, $padType = STR_PAD_LEFT)
{
parent::__construct($values);
$this->padLength = $padLength;
$this->padString = $padString;
$this->padType = $padType;
}
/**
* Initializes the list of choices.
*
* Each choices is padded according to the format given in the constructor
*
* @throws UnexpectedTypeException if the function does not return an array
*/
protected function load()
{
parent::load();
foreach ($this->choices as $key => $choice) {
$this->choices[$key] = str_pad($choice, $this->padLength, $this->padString, $this->padType);
}
}
}

View File

@ -0,0 +1,133 @@
<?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\ChoiceList;
use Symfony\Component\Form\Util\FormUtil;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* A choice list that can store any choices that are allowed as PHP array keys.
*
* The value strategy of simple choice lists is fixed to ChoiceList::COPY_CHOICE,
* since array keys are always valid choice values.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class SimpleChoiceList extends ChoiceList
{
/**
* Creates a new simple choice list.
*
* @param array $choices The array of choices with the choices as keys and
* the labels as values. Choices may also be given
* as hierarchy of unlimited depth. Hierarchies are
* created by creating nested arrays. The title of
* the sub-hierarchy is stored in the array
* key pointing to the nested array.
* @param array $preferredChoices A flat array of choices that should be
* presented to the user with priority.
* @param integer $indexStrategy The strategy used to create choice indices.
* One of COPY_CHOICE and GENERATE.
*/
public function __construct(array $choices, array $preferredChoices = array(),
$valueStrategy = self::COPY_CHOICE, $indexStrategy = self::GENERATE)
{
// Flip preferred choices to speed up lookup
parent::__construct($choices, $choices, array_flip($preferredChoices), $valueStrategy, $indexStrategy);
}
/**
* Recursively adds the given choices to the list.
*
* Takes care of splitting the single $choices array passed in the
* constructor into choices and labels.
*
* @param array $bucketForPreferred
* @param array $bucketForRemaining
* @param array $choices
* @param array $labels
* @param array $preferredChoices
*
* @throws UnexpectedTypeException
*
* @see parent::addChoices
*/
protected function addChoices(&$bucketForPreferred, &$bucketForRemaining, $choices, $labels, array $preferredChoices)
{
// Add choices to the nested buckets
foreach ($choices as $choice => $label) {
if (is_array($label)) {
// Don't do the work if the array is empty
if (count($label) > 0) {
$this->addChoiceGroup(
$choice,
$bucketForPreferred,
$bucketForRemaining,
$label,
$label,
$preferredChoices
);
}
} else {
$this->addChoice(
$bucketForPreferred,
$bucketForRemaining,
$choice,
$label,
$preferredChoices
);
}
}
}
/**
* Returns whether the given choice should be preferred judging by the
* given array of preferred choices.
*
* Optimized for performance by treating the preferred choices as array
* where choices are stored in the keys.
*
* @param mixed $choice The choice to test.
* @param array $preferredChoices An array of preferred choices.
*/
protected function isPreferred($choice, $preferredChoices)
{
// Optimize performance over the default implementation
return isset($preferredChoices[$choice]);
}
/**
* Converts the choice to a valid PHP array key.
*
* @param mixed $choice The choice.
*
* @return string|integer A valid PHP array key.
*/
protected function fixChoice($choice)
{
return $this->fixIndex($choice);
}
/**
* Converts the choices to a valid PHP array keys.
*
* @param array $choices The choices.
*
* @return array Valid PHP array keys.
*/
protected function fixChoices(array $choices)
{
return $this->fixIndices($choices);
}
}

View File

@ -1,63 +0,0 @@
<?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\ChoiceList;
/**
* Represents a choice list where each timezone is broken down by continent.
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
*/
class TimezoneChoiceList implements ChoiceListInterface
{
/**
* Stores the available timezone choices
* @var array
*/
static protected $timezones;
/**
* Returns the timezone choices.
*
* The choices are generated from the ICU function
* \DateTimeZone::listIdentifiers(). They are cached during a single request,
* so multiple timezone fields on the same page don't lead to unnecessary
* overhead.
*
* @return array The timezone choices
*/
public function getChoices()
{
if (null !== static::$timezones) {
return static::$timezones;
}
static::$timezones = array();
foreach (\DateTimeZone::listIdentifiers() as $timezone) {
$parts = explode('/', $timezone);
if (count($parts) > 2) {
$region = $parts[0];
$name = $parts[1].' - '.$parts[2];
} elseif (count($parts) > 1) {
$region = $parts[0];
$name = $parts[1];
} else {
$region = 'Other';
$name = $parts[0];
}
static::$timezones[$region][$timezone] = str_replace('_', ' ', $name);
}
return static::$timezones;
}
}

View File

@ -17,7 +17,10 @@ use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Util\FormUtil;
class ScalarToBooleanChoicesTransformer implements DataTransformerInterface
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoiceToBooleanArrayTransformer implements DataTransformerInterface
{
private $choiceList;
@ -47,24 +50,21 @@ class ScalarToBooleanChoicesTransformer implements DataTransformerInterface
* @throws UnexpectedTypeException if the given value is not scalar
* @throws TransformationFailedException if the choices can not be retrieved
*/
public function transform($value)
public function transform($choice)
{
if (!is_scalar($value) && null !== $value) {
throw new UnexpectedTypeException($value, 'scalar');
}
try {
$choices = $this->choiceList->getChoices();
$values = $this->choiceList->getValues();
} catch (\Exception $e) {
throw new TransformationFailedException('Can not get the choice list', $e->getCode(), $e);
}
$value = FormUtil::toArrayKey($value);
foreach (array_keys($choices) as $key) {
$choices[$key] = $key === $value;
$index = current($this->choiceList->getIndicesForChoices(array($choice)));
foreach ($values as $i => $value) {
$values[$i] = $i === $index;
}
return $choices;
return $values;
}
/**
@ -80,15 +80,25 @@ class ScalarToBooleanChoicesTransformer implements DataTransformerInterface
*
* @throws new UnexpectedTypeException if the given value is not an array
*/
public function reverseTransform($value)
public function reverseTransform($values)
{
if (!is_array($value)) {
throw new UnexpectedTypeException($value, 'array');
if (!is_array($values)) {
throw new UnexpectedTypeException($values, 'array');
}
foreach ($value as $choice => $selected) {
try {
$choices = $this->choiceList->getChoices();
} catch (\Exception $e) {
throw new TransformationFailedException('Can not get the choice list', $e->getCode(), $e);
}
foreach ($values as $i => $selected) {
if ($selected) {
return (string) $choice;
if (isset($choices[$i])) {
return $choices[$i] === '' ? null : $choices[$i];
} else {
throw new TransformationFailedException('The choice "' . $i . '" does not exist');
}
}
}

View File

@ -0,0 +1,63 @@
<?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\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoiceToValueTransformer implements DataTransformerInterface
{
private $choiceList;
/**
* Constructor.
*
* @param ChoiceListInterface $choiceList
*/
public function __construct(ChoiceListInterface $choiceList)
{
$this->choiceList = $choiceList;
}
public function transform($choice)
{
return (string) current($this->choiceList->getValuesForChoices(array($choice)));
}
public function reverseTransform($value)
{
if (null !== $value && !is_scalar($value)) {
throw new UnexpectedTypeException($value, 'scalar');
}
// These are now valid ChoiceList values, so we can return null
// right away
if ($value === '' || $value === null) {
return null;
}
$choices = $this->choiceList->getChoicesForValues(array($value));
if (count($choices) !== 1) {
throw new TransformationFailedException('The choice "' . $value . '" does not exist');
}
$choice = current($choices);
return $choice === '' ? null : $choice;
}
}

View File

@ -16,7 +16,10 @@ use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
class ArrayToBooleanChoicesTransformer implements DataTransformerInterface
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoicesToBooleanArrayTransformer implements DataTransformerInterface
{
private $choiceList;
@ -51,16 +54,18 @@ class ArrayToBooleanChoicesTransformer implements DataTransformerInterface
}
try {
$choices = $this->choiceList->getChoices();
$values = $this->choiceList->getValues();
} catch (\Exception $e) {
throw new TransformationFailedException('Can not get the choice list', $e->getCode(), $e);
}
foreach (array_keys($choices) as $key) {
$choices[$key] = in_array($key, $array, true);
$indexMap = array_flip($this->choiceList->getIndicesForChoices($array));
foreach ($values as $i => $value) {
$values[$i] = isset($indexMap[$i]);
}
return $choices;
return $values;
}
/**
@ -76,20 +81,35 @@ class ArrayToBooleanChoicesTransformer implements DataTransformerInterface
*
* @throws UnexpectedTypeException if the given value is not an array
*/
public function reverseTransform($value)
public function reverseTransform($values)
{
if (!is_array($value)) {
throw new UnexpectedTypeException($value, 'array');
if (!is_array($values)) {
throw new UnexpectedTypeException($values, 'array');
}
$choices = array();
try {
$choices = $this->choiceList->getChoices();
} catch (\Exception $e) {
throw new TransformationFailedException('Can not get the choice list', $e->getCode(), $e);
}
foreach ($value as $choice => $selected) {
$result = array();
$unknown = array();
foreach ($values as $i => $selected) {
if ($selected) {
$choices[] = $choice;
if (isset($choices[$i])) {
$result[] = $choices[$i];
} else {
$unknown[] = $i;
}
}
}
return $choices;
if (count($unknown) > 0) {
throw new TransformationFailedException('The choices "' . implode('", "', $unknown, $code, $previous) . '" where not found');
}
return $result;
}
}

View File

@ -11,12 +11,30 @@
namespace Symfony\Component\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Util\FormUtil;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
class ArrayToChoicesTransformer implements DataTransformerInterface
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoicesToValuesTransformer implements DataTransformerInterface
{
private $choiceList;
/**
* Constructor.
*
* @param ChoiceListInterface $choiceList
*/
public function __construct(ChoiceListInterface $choiceList)
{
$this->choiceList = $choiceList;
}
/**
* @param array $array
*
@ -34,7 +52,7 @@ class ArrayToChoicesTransformer implements DataTransformerInterface
throw new UnexpectedTypeException($array, 'array');
}
return FormUtil::toArrayKeys($array);
return $this->choiceList->getValuesForChoices($array);
}
/**
@ -54,6 +72,12 @@ class ArrayToChoicesTransformer implements DataTransformerInterface
throw new UnexpectedTypeException($array, 'array');
}
return $array;
$choices = $this->choiceList->getChoicesForValues($array);
if (count($choices) !== count($array)) {
throw new TransformationFailedException('Could not find all matching choices for the given values');
}
return $choices;
}
}

View File

@ -1,37 +0,0 @@
<?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\DataTransformer;
use Symfony\Component\Form\Util\FormUtil;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
class ScalarToChoiceTransformer implements DataTransformerInterface
{
public function transform($value)
{
if (null !== $value && !is_scalar($value)) {
throw new UnexpectedTypeException($value, 'scalar');
}
return FormUtil::toArrayKey($value);
}
public function reverseTransform($value)
{
if (null !== $value && !is_scalar($value)) {
throw new UnexpectedTypeException($value, 'scalar');
}
return $value;
}
}

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\Form\Extension\Core\EventListener;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\Event\FilterDataEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
/**
* Takes care of converting the input from a single radio button
@ -23,11 +24,24 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
*/
class FixRadioInputListener implements EventSubscriberInterface
{
private $choiceList;
/**
* Constructor.
*
* @param ChoiceListInterface $choiceList
*/
public function __construct(ChoiceListInterface $choiceList)
{
$this->choiceList = $choiceList;
}
public function onBindClientData(FilterDataEvent $event)
{
$data = $event->getData();
$value = $event->getData();
$index = current($this->choiceList->getIndicesForValues(array($value)));
$event->setData(strlen($data) < 1 ? array() : array($data => true));
$event->setData($index !== false ? array($index => $value) : array());
}
static public function getSubscribedEvents()

View File

@ -15,14 +15,17 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Extension\Core\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\Loader\ChoiceListLoaderInterface;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\DataTransformer\ScalarToChoiceTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ScalarToBooleanChoicesTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToChoicesTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToBooleanChoicesTransformer;
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;
class ChoiceType extends AbstractType
{
@ -35,40 +38,38 @@ class ChoiceType extends AbstractType
throw new FormException('The "choice_list" must be an instance of "Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface".');
}
if (!$options['choice_list'] && !$options['choices']) {
throw new FormException('Either the option "choices" or "choice_list" must be set.');
}
if (!$options['choice_list']) {
$options['choice_list'] = new ArrayChoiceList($options['choices']);
$options['choice_list'] = new SimpleChoiceList(
$options['choices'],
$options['preferred_choices'],
$options['value_strategy'],
$options['index_strategy']
);
}
if ($options['expanded']) {
// Load choices already if expanded
$choices = $options['choice_list']->getChoices();
$values = $options['choice_list']->getValues();
$labels = $options['choice_list']->getLabels();
// Flatten choices
$flattened = array();
foreach ($choices as $value => $choice) {
if (is_array($choice)) {
$flattened = array_replace($flattened, $choice);
} else {
$flattened[$value] = $choice;
}
}
$options['choices'] = $flattened;
foreach ($options['choices'] as $choice => $value) {
foreach ($values as $i => $value) {
if ($options['multiple']) {
$builder->add((string) $choice, 'checkbox', array(
'value' => $choice,
'label' => $value,
$builder->add((string) $i, 'checkbox', array(
'value' => $value,
'label' => $labels[$i],
// The user can check 0 or more checkboxes. If required
// is true, he is required to check all of them.
'required' => false,
'translation_domain' => $options['translation_domain'],
));
} else {
$builder->add((string) $choice, 'radio', array(
'value' => $choice,
'label' => $value,
$builder->add((string) $i, 'radio', array(
'value' => $value,
'label' => $labels[$i],
'translation_domain' => $options['translation_domain'],
));
}
@ -101,18 +102,18 @@ class ChoiceType extends AbstractType
if ($options['expanded']) {
if ($options['multiple']) {
$builder->appendClientTransformer(new ArrayToBooleanChoicesTransformer($options['choice_list']));
$builder->appendClientTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list']));
} else {
$builder
->appendClientTransformer(new ScalarToBooleanChoicesTransformer($options['choice_list']))
->addEventSubscriber(new FixRadioInputListener(), 10)
->appendClientTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list']))
->addEventSubscriber(new FixRadioInputListener($options['choice_list']), 10)
;
}
} else {
if ($options['multiple']) {
$builder->appendClientTransformer(new ArrayToChoicesTransformer());
$builder->appendClientTransformer(new ChoicesToValuesTransformer($options['choice_list']));
} else {
$builder->appendClientTransformer(new ScalarToChoiceTransformer());
$builder->appendClientTransformer(new ChoiceToValueTransformer($options['choice_list']));
}
}
@ -123,14 +124,14 @@ class ChoiceType extends AbstractType
*/
public function buildView(FormView $view, FormInterface $form)
{
$choices = $form->getAttribute('choice_list')->getChoices();
$preferred = array_flip($form->getAttribute('preferred_choices'));
$choiceList = $form->getAttribute('choice_list');
$view
->set('multiple', $form->getAttribute('multiple'))
->set('expanded', $form->getAttribute('expanded'))
->set('preferred_choices', array_intersect_key($choices, $preferred))
->set('choices', array_diff_key($choices, $preferred))
->set('preferred_choices', $choiceList->getPreferredValueHierarchy())
->set('choices', $choiceList->getRemainingValueHierarchy())
->set('choice_labels', $choiceList->getLabels())
->set('separator', '-------------------')
->set('empty_value', $form->getAttribute('empty_value'))
;
@ -157,6 +158,8 @@ class ChoiceType extends AbstractType
'choice_list' => null,
'choices' => array(),
'preferred_choices' => array(),
'value_strategy' => ChoiceList::GENERATE,
'index_strategy' => ChoiceList::GENERATE,
'empty_data' => $multiple || $expanded ? array() : '',
'empty_value' => $multiple || $expanded || !isset($options['empty_value']) ? null : '',
'error_bubbling' => false,

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
use Symfony\Component\Locale\Locale;
class CountryType extends AbstractType
@ -23,6 +24,8 @@ class CountryType extends AbstractType
{
return array(
'choices' => Locale::getDisplayCountries(\Locale::getDefault()),
'value_strategy' => ChoiceList::COPY_CHOICE,
'index_strategy' => ChoiceList::COPY_CHOICE,
);
}

View File

@ -11,12 +11,13 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\Exception\CreationException;
use Symfony\Component\Form\Extension\Core\ChoiceList\PaddedChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\MonthChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\Loader\MonthChoiceListLoader;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
@ -74,23 +75,35 @@ class DateType extends AbstractType
$options['empty_value'] = array('year' => $options['empty_value'], 'month' => $options['empty_value'], 'day' => $options['empty_value']);
}
$years = $months = $days = array();
foreach ($options['years'] as $year) {
$years[$year] = str_pad($year, 4, '0', STR_PAD_LEFT);
}
foreach ($options['months'] as $month) {
$months[$month] = str_pad($month, 2, '0', STR_PAD_LEFT);
}
foreach ($options['days'] as $day) {
$days[$day] = str_pad($day, 2, '0', STR_PAD_LEFT);
}
// Only pass a subset of the options to children
$yearOptions = array(
'choice_list' => new PaddedChoiceList(
array_combine($options['years'], $options['years']), 4, '0', STR_PAD_LEFT
),
'choices' => $years,
'value_strategy' => ChoiceList::COPY_CHOICE,
'index_strategy' => ChoiceList::COPY_CHOICE,
'empty_value' => $options['empty_value']['year'],
);
$monthOptions = array(
'choice_list' => new MonthChoiceList(
$formatter, $options['months']
),
'choices' => $this->formatMonths($formatter, $months),
'value_strategy' => ChoiceList::COPY_CHOICE,
'index_strategy' => ChoiceList::COPY_CHOICE,
'empty_value' => $options['empty_value']['month'],
);
$dayOptions = array(
'choice_list' => new PaddedChoiceList(
array_combine($options['days'], $options['days']), 2, '0', STR_PAD_LEFT
),
'choices' => $days,
'value_strategy' => ChoiceList::COPY_CHOICE,
'index_strategy' => ChoiceList::COPY_CHOICE,
'empty_value' => $options['empty_value']['day'],
);
@ -209,4 +222,28 @@ class DateType extends AbstractType
{
return 'date';
}
private function formatMonths(\IntlDateFormatter $formatter, array $months)
{
$pattern = $formatter->getPattern();
$timezone = $formatter->getTimezoneId();
$formatter->setTimezoneId(\DateTimeZone::UTC);
if (preg_match('/M+/', $pattern, $matches)) {
$formatter->setPattern($matches[0]);
foreach ($months as $key => $value) {
$months[$key] = $formatter->format(gmmktime(0, 0, 0, $key, 15));
}
// I'd like to clone the formatter above, but then we get a
// segmentation fault, so let's restore the old state instead
$formatter->setPattern($pattern);
}
$formatter->setTimezoneId($timezone);
return $months;
}
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
use Symfony\Component\Locale\Locale;
class LanguageType extends AbstractType
@ -23,6 +24,7 @@ class LanguageType extends AbstractType
{
return array(
'choices' => Locale::getDisplayLanguages(\Locale::getDefault()),
'value_strategy' => ChoiceList::COPY_CHOICE,
);
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
use Symfony\Component\Locale\Locale;
class LocaleType extends AbstractType
@ -23,6 +24,7 @@ class LocaleType extends AbstractType
{
return array(
'choices' => Locale::getDisplayLocales(\Locale::getDefault()),
'value_strategy' => ChoiceList::COPY_CHOICE,
);
}

View File

@ -19,48 +19,22 @@ use Symfony\Component\Form\FormView;
class RadioType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->appendClientTransformer(new BooleanToStringTransformer())
->setAttribute('value', $options['value'])
;
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form)
{
$view
->set('value', $form->getAttribute('value'))
->set('checked', (Boolean) $form->getClientData())
;
if ($view->hasParent()) {
$view->set('full_name', $view->getParent()->get('full_name'));
}
}
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
{
return array(
'value' => null,
);
}
/**
* {@inheritdoc}
*/
public function getParent(array $options)
{
return 'field';
return 'checkbox';
}
/**

View File

@ -14,8 +14,8 @@ namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\Extension\Core\ChoiceList\PaddedChoiceList;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
@ -47,25 +47,40 @@ class TimeType extends AbstractType
$options['empty_value'] = array('hour' => $options['empty_value'], 'minute' => $options['empty_value'], 'second' => $options['empty_value']);
}
$hours = $minutes = array();
foreach ($options['hours'] as $hour) {
$hours[$hour] = str_pad($hour, 2, '0', STR_PAD_LEFT);
}
foreach ($options['minutes'] as $minute) {
$minutes[$minute] = str_pad($minute, 2, '0', STR_PAD_LEFT);
}
// Only pass a subset of the options to children
$hourOptions = array(
'choice_list' => new PaddedChoiceList(
array_combine($options['hours'], $options['hours']), 2, '0', STR_PAD_LEFT
),
'choices' => $hours,
'value_strategy' => ChoiceList::COPY_CHOICE,
'index_strategy' => ChoiceList::COPY_CHOICE,
'empty_value' => $options['empty_value']['hour'],
);
$minuteOptions = array(
'choice_list' => new PaddedChoiceList(
array_combine($options['minutes'], $options['minutes']), 2, '0', STR_PAD_LEFT
),
'choices' => $minutes,
'value_strategy' => ChoiceList::COPY_CHOICE,
'index_strategy' => ChoiceList::COPY_CHOICE,
'empty_value' => $options['empty_value']['minute'],
);
if ($options['with_seconds']) {
$seconds = array();
foreach ($options['seconds'] as $second) {
$seconds[$second] = str_pad($second, 2, '0', STR_PAD_LEFT);
}
$secondOptions = array(
'choice_list' => new PaddedChoiceList(
array_combine($options['seconds'], $options['seconds']), 2, '0', STR_PAD_LEFT
),
'choices' => $seconds,
'value_strategy' => ChoiceList::COPY_CHOICE,
'index_strategy' => ChoiceList::COPY_CHOICE,
'empty_value' => $options['empty_value']['second'],
);
}

View File

@ -12,18 +12,30 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\ChoiceList\TimezoneChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
class TimezoneType extends AbstractType
{
/**
* Stores the available timezone choices
* @var array
*/
static protected $timezones;
/**
* {@inheritdoc}
*/
public function getDefaultOptions(array $options)
{
return array(
'choice_list' => new TimezoneChoiceList(),
$defaultOptions = array(
'value_strategy' => ChoiceList::COPY_CHOICE,
);
if (!isset($options['choice_list']) && !isset($options['choices'])) {
$defaultOptions['choices'] = self::getTimezones();
}
return $defaultOptions;
}
/**
@ -41,4 +53,40 @@ class TimezoneType extends AbstractType
{
return 'timezone';
}
/**
* Returns the timezone choices.
*
* The choices are generated from the ICU function
* \DateTimeZone::listIdentifiers(). They are cached during a single request,
* so multiple timezone fields on the same page don't lead to unnecessary
* overhead.
*
* @return array The timezone choices
*/
static private function getTimezones()
{
if (null === static::$timezones) {
static::$timezones = array();
foreach (\DateTimeZone::listIdentifiers() as $timezone) {
$parts = explode('/', $timezone);
if (count($parts) > 2) {
$region = $parts[0];
$name = $parts[1].' - '.$parts[2];
} elseif (count($parts) > 1) {
$region = $parts[0];
$name = $parts[1];
} else {
$region = 'Other';
$name = $parts[0];
}
static::$timezones[$region][$timezone] = str_replace('_', ' ', $name);
}
}
return static::$timezones;
}
}

View File

@ -13,20 +13,6 @@ namespace Symfony\Component\Form\Util;
abstract class FormUtil
{
static public function toArrayKey($value)
{
if (is_bool($value) || (string) (int) $value === (string) $value) {
return (int) $value;
}
return (string) $value;
}
static public function toArrayKeys(array $array)
{
return array_map(array(__CLASS__, 'toArrayKey'), $array);
}
/**
* Returns whether the given choice is a group.
*
@ -49,10 +35,6 @@ abstract class FormUtil
*/
static public function isChoiceSelected($choice, $value)
{
$choice = static::toArrayKey($choice);
// The value should already have been converted by value transformers,
// otherwise we had to do the conversion on every call of this method
if (is_array($value)) {
return false !== array_search($choice, $value, true);
}

View File

@ -89,7 +89,7 @@ class EntityChoiceListTest extends DoctrineOrmTestCase
)
);
$this->assertSame(array(1 => 'Foo', 2 => 'Bar'), $choiceList->getChoices());
$this->assertSame(array(1 => $entity1, 2 => $entity2), $choiceList->getChoices());
}
public function testEmptyChoicesAreManaged()
@ -132,10 +132,11 @@ class EntityChoiceListTest extends DoctrineOrmTestCase
)
);
$this->assertSame(array(1 => $entity1, 2 => $entity2), $choiceList->getChoices());
$this->assertSame(array(
'group1' => array(1 => 'Foo'),
'group2' => array(2 => 'Bar')
), $choiceList->getChoices());
'group1' => array(1 => '1'),
'group2' => array(2 => '2')
), $choiceList->getRemainingValueHierarchy());
}
public function testGroupBySupportsString()
@ -164,11 +165,12 @@ class EntityChoiceListTest extends DoctrineOrmTestCase
'groupName'
);
$this->assertEquals(array(1 => $item1, 2 => $item2, 3 => $item3, 4 => $item4), $choiceList->getChoices());
$this->assertEquals(array(
'Group1' => array(1 => 'Foo', '2' => 'Bar'),
'Group2' => array(3 => 'Baz'),
'4' => 'Boo!'
), $choiceList->getChoices('choices'));
'Group1' => array(1 => '1', 2 => '2'),
'Group2' => array(3 => '3'),
4 => '4'
), $choiceList->getRemainingValueHierarchy());
}
public function testGroupByInvalidPropertyPathReturnsFlatChoices()
@ -188,13 +190,13 @@ class EntityChoiceListTest extends DoctrineOrmTestCase
$item1,
$item2,
),
'groupName.child.that.does.not.exist'
'child.that.does.not.exist'
);
$this->assertEquals(array(
1 => 'Foo',
2 => 'Bar'
), $choiceList->getChoices('choices'));
1 => $item1,
2 => $item2
), $choiceList->getChoices());
}
public function testPossibleToProvideShorthandEntityName()

View File

@ -109,7 +109,8 @@ class EntityTypeTest extends TypeTestCase
'property' => 'name'
));
$this->assertEquals(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choices'));
$this->assertSame(array(1 => '1', 2 => '2'), $field->createView()->get('choices'));
$this->assertSame(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choice_labels'));
}
public function testSetDataToUninitializedEntityWithNonRequiredToString()
@ -125,7 +126,8 @@ class EntityTypeTest extends TypeTestCase
'required' => false,
));
$this->assertEquals(array("1" => 'Foo', "2" => 'Bar'), $field->createView()->get('choices'));
$this->assertSame(array(1 => '1', 2 => '2'), $field->createView()->get('choices'));
$this->assertSame(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choice_labels'));
}
public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder()
@ -144,7 +146,8 @@ class EntityTypeTest extends TypeTestCase
'query_builder' => $qb
));
$this->assertEquals(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choices'));
$this->assertSame(array(1 => '1', 2 => '2'), $field->createView()->get('choices'));
$this->assertSame(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choice_labels'));
}
/**
@ -185,7 +188,7 @@ class EntityTypeTest extends TypeTestCase
$field->setData(null);
$this->assertNull($field->getData());
$this->assertEquals('', $field->getClientData());
$this->assertSame('', $field->getClientData());
}
public function testSetDataMultipleExpandedNull()
@ -199,7 +202,7 @@ class EntityTypeTest extends TypeTestCase
$field->setData(null);
$this->assertNull($field->getData());
$this->assertEquals(array(), $field->getClientData());
$this->assertSame(array(), $field->getClientData());
}
public function testSetDataMultipleNonExpandedNull()
@ -213,7 +216,7 @@ class EntityTypeTest extends TypeTestCase
$field->setData(null);
$this->assertNull($field->getData());
$this->assertEquals(array(), $field->getClientData());
$this->assertSame(array(), $field->getClientData());
}
public function testSubmitSingleExpandedNull()
@ -227,7 +230,7 @@ class EntityTypeTest extends TypeTestCase
$field->bind(null);
$this->assertNull($field->getData());
$this->assertEquals(array(), $field->getClientData());
$this->assertSame(array(), $field->getClientData());
}
public function testSubmitSingleNonExpandedNull()
@ -241,7 +244,7 @@ class EntityTypeTest extends TypeTestCase
$field->bind(null);
$this->assertNull($field->getData());
$this->assertEquals('', $field->getClientData());
$this->assertSame('', $field->getClientData());
}
public function testSubmitMultipleNull()
@ -254,7 +257,7 @@ class EntityTypeTest extends TypeTestCase
$field->bind(null);
$this->assertEquals(new ArrayCollection(), $field->getData());
$this->assertEquals(array(), $field->getClientData());
$this->assertSame(array(), $field->getClientData());
}
public function testSubmitSingleNonExpandedSingleIdentifier()
@ -275,8 +278,8 @@ class EntityTypeTest extends TypeTestCase
$field->bind('2');
$this->assertTrue($field->isSynchronized());
$this->assertEquals($entity2, $field->getData());
$this->assertEquals(2, $field->getClientData());
$this->assertSame($entity2, $field->getData());
$this->assertSame('2', $field->getClientData());
}
public function testSubmitSingleNonExpandedCompositeIdentifier()
@ -298,8 +301,8 @@ class EntityTypeTest extends TypeTestCase
$field->bind('1');
$this->assertTrue($field->isSynchronized());
$this->assertEquals($entity2, $field->getData());
$this->assertEquals(1, $field->getClientData());
$this->assertSame($entity2, $field->getData());
$this->assertSame('1', $field->getClientData());
}
public function testSubmitMultipleNonExpandedSingleIdentifier()
@ -324,7 +327,7 @@ class EntityTypeTest extends TypeTestCase
$this->assertTrue($field->isSynchronized());
$this->assertEquals($expected, $field->getData());
$this->assertEquals(array(1, 3), $field->getClientData());
$this->assertSame(array('1', '3'), $field->getClientData());
}
public function testSubmitMultipleNonExpandedSingleIdentifier_existingData()
@ -355,7 +358,7 @@ class EntityTypeTest extends TypeTestCase
$this->assertEquals($expected, $field->getData());
// same object still, useful if it is a PersistentCollection
$this->assertSame($existing, $field->getData());
$this->assertEquals(array(1, 3), $field->getClientData());
$this->assertSame(array('1', '3'), $field->getClientData());
}
public function testSubmitMultipleNonExpandedCompositeIdentifier()
@ -381,7 +384,7 @@ class EntityTypeTest extends TypeTestCase
$this->assertTrue($field->isSynchronized());
$this->assertEquals($expected, $field->getData());
$this->assertEquals(array(0, 2), $field->getClientData());
$this->assertSame(array('0', '2'), $field->getClientData());
}
public function testSubmitMultipleNonExpandedCompositeIdentifier_existingData()
@ -412,7 +415,7 @@ class EntityTypeTest extends TypeTestCase
$this->assertEquals($expected, $field->getData());
// same object still, useful if it is a PersistentCollection
$this->assertSame($existing, $field->getData());
$this->assertEquals(array(0, 2), $field->getClientData());
$this->assertSame(array('0', '2'), $field->getClientData());
}
public function testSubmitSingleExpanded()
@ -433,7 +436,7 @@ class EntityTypeTest extends TypeTestCase
$field->bind('2');
$this->assertTrue($field->isSynchronized());
$this->assertEquals($entity2, $field->getData());
$this->assertSame($entity2, $field->getData());
$this->assertFalse($field['1']->getData());
$this->assertTrue($field['2']->getData());
$this->assertSame('', $field['1']->getClientData());
@ -488,10 +491,11 @@ class EntityTypeTest extends TypeTestCase
$field->bind('2');
$this->assertEquals(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choices'));
$this->assertSame(array(1 => '1', 2 => '2'), $field->createView()->get('choices'));
$this->assertSame(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choice_labels'));
$this->assertTrue($field->isSynchronized());
$this->assertEquals($entity2, $field->getData());
$this->assertEquals(2, $field->getClientData());
$this->assertSame($entity2, $field->getData());
$this->assertSame('2', $field->getClientData());
}
public function testGroupByChoices()
@ -513,12 +517,13 @@ class EntityTypeTest extends TypeTestCase
$field->bind('2');
$this->assertEquals(2, $field->getClientData());
$this->assertEquals(array(
'Group1' => array(1 => 'Foo', '2' => 'Bar'),
'Group2' => array(3 => 'Baz'),
'4' => 'Boo!'
$this->assertSame('2', $field->getClientData());
$this->assertSame(array(
'Group1' => array(1 => '1', 2 => '2'),
'Group2' => array(3 => '3'),
'4' => '4'
), $field->createView()->get('choices'));
$this->assertSame(array(1 => 'Foo', 2 => 'Bar', 3 => 'Baz', 4 => 'Boo!'), $field->createView()->get('choice_labels'));
}
public function testDisallowChoicesThatAreNotIncluded_choicesSingleIdentifier()
@ -652,8 +657,8 @@ class EntityTypeTest extends TypeTestCase
$field->bind('foo');
$this->assertTrue($field->isSynchronized());
$this->assertEquals($entity1, $field->getData());
$this->assertEquals('foo', $field->getClientData());
$this->assertSame($entity1, $field->getData());
$this->assertSame('foo', $field->getClientData());
}
public function testSubmitCompositeStringIdentifier()
@ -674,8 +679,8 @@ class EntityTypeTest extends TypeTestCase
$field->bind('0');
$this->assertTrue($field->isSynchronized());
$this->assertEquals($entity1, $field->getData());
$this->assertEquals(0, $field->getClientData());
$this->assertSame($entity1, $field->getData());
$this->assertSame('0', $field->getClientData());
}
protected function createRegistryMock($name, $em)

View File

@ -309,23 +309,6 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
);
}
public function testCheckedCheckboxWithValue()
{
$form = $this->factory->createNamed('checkbox', 'na&me', true, array(
'property_path' => 'name',
'value' => 'foo&bar',
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/input
[@type="checkbox"]
[@name="na&me"]
[@checked="checked"]
[@value="foo&bar"]
'
);
}
public function testUncheckedCheckbox()
{
$form = $this->factory->createNamed('checkbox', 'na&me', false, array(
@ -341,6 +324,22 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
);
}
public function testCheckboxWithValue()
{
$form = $this->factory->createNamed('checkbox', 'na&me', false, array(
'property_path' => 'name',
'value' => 'foo&bar',
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/input
[@type="checkbox"]
[@name="na&me"]
[@value="foo&bar"]
'
);
}
public function testSingleChoice()
{
$form = $this->factory->createNamed('choice', 'na&me', '&a', array(
@ -355,8 +354,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
[@name="na&me"]
[@required="required"]
[
./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"]
]
[count(./option)=2]
'
@ -378,9 +377,9 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
[@name="na&me"]
[@required="required"]
[
./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
./option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"]
/following-sibling::option[@disabled="disabled"][not(@selected)][.="-- sep --"]
/following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"]
]
[count(./option)=3]
'
@ -403,8 +402,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
[@name="na&me"]
[@required="required"]
[
./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
/following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
./option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"]
/following-sibling::option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"]
]
[count(./option)=2]
'
@ -426,9 +425,9 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
[@name="na&me"]
[@required="required"]
[
./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
./option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"]
/following-sibling::option[@disabled="disabled"][not(@selected)][.=""]
/following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"]
]
[count(./option)=3]
'
@ -468,8 +467,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
[not(@required)]
[
./option[@value=""][.="[trans][/trans]"]
/following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
/following-sibling::option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"]
]
[count(./option)=3]
'
@ -492,8 +491,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
[not(@required)]
[
./option[@value=""][.="[trans][/trans]"]
/following-sibling::option[@value="&a"][not(@selected)][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
/following-sibling::option[@value="0"][not(@selected)][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"]
]
[count(./option)=3]
'
@ -517,8 +516,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
[not(@required)]
[
./option[@value=""][not(@selected)][.="[trans]Select&Anything&Not&Me[/trans]"]
/following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
/following-sibling::option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"]
]
[count(./option)=3]
'
@ -542,8 +541,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
[@required="required"]
[
./option[@value=""][.="[trans]Test&Me[/trans]"]
/following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
/following-sibling::option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"]
]
[count(./option)=3]
'
@ -566,8 +565,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
[@required="required"]
[
./option[@value=""][.="[trans][/trans]"]
/following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
/following-sibling::option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"]
]
[count(./option)=3]
'
@ -591,13 +590,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
[@name="na&me"]
[./optgroup[@label="[trans]Group&1[/trans]"]
[
./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"]
]
[count(./option)=2]
]
[./optgroup[@label="[trans]Group&2[/trans]"]
[./option[@value="&c"][not(@selected)][.="[trans]Choice&C[/trans]"]]
[./option[@value="2"][not(@selected)][.="[trans]Choice&C[/trans]"]]
[count(./option)=1]
]
[count(./optgroup)=2]
@ -619,8 +618,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
[@name="na&me[]"]
[@multiple="multiple"]
[
./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"]
]
[count(./option)=2]
'
@ -642,8 +641,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
[@name="na&me[]"]
[@multiple="multiple"]
[
./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"]
]
[count(./option)=2]
'
@ -665,8 +664,8 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
[@name="na&me[]"]
[@multiple="multiple"]
[
./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
./option[@value="0"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="1"][not(@selected)][.="[trans]Choice&B[/trans]"]
]
[count(./option)=2]
'
@ -685,10 +684,10 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="radio"][@name="na&me"][@id="na&me_&a"][@checked]
/following-sibling::label[@for="na&me_&a"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_&b"][not(@checked)]
/following-sibling::label[@for="na&me_&b"][.="[trans]Choice&B[/trans]"]
./input[@type="radio"][@name="na&me"][@id="na&me_0"][@value="0"][@checked]
/following-sibling::label[@for="na&me_0"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_1"][@value="1"][not(@checked)]
/following-sibling::label[@for="na&me_1"][.="[trans]Choice&B[/trans]"]
]
[count(./input)=2]
'
@ -708,10 +707,10 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="radio"][@name="na&me"][@id="na&me_&a"][@checked]
/following-sibling::label[@for="na&me_&a"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_&b"][not(@checked)]
/following-sibling::label[@for="na&me_&b"][.="[trans]Choice&B[/trans]"]
./input[@type="radio"][@name="na&me"][@id="na&me_0"][@checked]
/following-sibling::label[@for="na&me_0"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_1"][not(@checked)]
/following-sibling::label[@for="na&me_1"][.="[trans]Choice&B[/trans]"]
]
[count(./input)=2]
'
@ -730,10 +729,10 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="radio"][@name="na&me"][@id="na&me_1"][@checked]
/following-sibling::label[@for="na&me_1"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_0"][not(@checked)]
/following-sibling::label[@for="na&me_0"][.="[trans]Choice&B[/trans]"]
./input[@type="radio"][@name="na&me"][@id="na&me_0"][@checked]
/following-sibling::label[@for="na&me_0"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="radio"][@name="na&me"][@id="na&me_1"][not(@checked)]
/following-sibling::label[@for="na&me_1"][.="[trans]Choice&B[/trans]"]
]
[count(./input)=2]
'
@ -753,12 +752,12 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="checkbox"][@name="na&me[&a]"][@id="na&me_&a"][@checked][not(@required)]
/following-sibling::label[@for="na&me_&a"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="checkbox"][@name="na&me[&b]"][@id="na&me_&b"][not(@checked)][not(@required)]
/following-sibling::label[@for="na&me_&b"][.="[trans]Choice&B[/trans]"]
/following-sibling::input[@type="checkbox"][@name="na&me[&c]"][@id="na&me_&c"][@checked][not(@required)]
/following-sibling::label[@for="na&me_&c"][.="[trans]Choice&C[/trans]"]
./input[@type="checkbox"][@name="na&me[0]"][@id="na&me_0"][@checked][not(@required)]
/following-sibling::label[@for="na&me_0"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="checkbox"][@name="na&me[1]"][@id="na&me_1"][not(@checked)][not(@required)]
/following-sibling::label[@for="na&me_1"][.="[trans]Choice&B[/trans]"]
/following-sibling::input[@type="checkbox"][@name="na&me[2]"][@id="na&me_2"][@checked][not(@required)]
/following-sibling::label[@for="na&me_2"][.="[trans]Choice&C[/trans]"]
]
[count(./input)=3]
'
@ -1450,24 +1449,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
[@type="radio"]
[@name="na&me"]
[@checked="checked"]
[@value=""]
'
);
}
public function testCheckedRadioWithValue()
{
$form = $this->factory->createNamed('radio', 'na&me', true, array(
'property_path' => 'name',
'value' => 'foo&bar',
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/input
[@type="radio"]
[@name="na&me"]
[@checked="checked"]
[@value="foo&bar"]
[@value="1"]
'
);
}
@ -1487,6 +1469,22 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
);
}
public function testRadioWithValue()
{
$form = $this->factory->createNamed('radio', 'na&me', false, array(
'property_path' => 'name',
'value' => 'foo&bar',
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/input
[@type="radio"]
[@name="na&me"]
[@value="foo&bar"]
'
);
}
public function testTextarea()
{
$form = $this->factory->createNamed('textarea', 'na&me', 'foo&bar', array(

View File

@ -1,53 +0,0 @@
<?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\Tests\Component\Form\Extension\Core\ChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\ArrayChoiceList;
class ArrayChoiceListTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testConstructorExpectsArrayOrClosure()
{
new ArrayChoiceList('foobar');
}
public function testGetChoices()
{
$choices = array('a' => 'A', 'b' => 'B');
$list = new ArrayChoiceList($choices);
$this->assertSame($choices, $list->getChoices());
}
public function testGetChoicesFromClosure()
{
$choices = array('a' => 'A', 'b' => 'B');
$closure = function () use ($choices) { return $choices; };
$list = new ArrayChoiceList($closure);
$this->assertSame($choices, $list->getChoices());
}
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testClosureShouldReturnArray()
{
$closure = function () { return 'foobar'; };
$list = new ArrayChoiceList($closure);
$list->getChoices();
}
}

View File

@ -0,0 +1,143 @@
<?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\Tests\Component\Form\Extension\Core\ChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\ComplexChoiceList;
class ComplexChoiceListTest extends \PHPUnit_Framework_TestCase
{
private $obj1;
private $obj2;
private $obj3;
private $obj4;
private $list;
protected function setUp()
{
parent::setUp();
$this->obj1 = new \stdClass();
$this->obj2 = new \stdClass();
$this->obj3 = new \stdClass();
$this->obj4 = new \stdClass();
$this->list = new ComplexChoiceList(
array(
'Group 1' => array($this->obj1, $this->obj2),
'Group 2' => array($this->obj3, $this->obj4),
),
array(
'Group 1' => array('A', 'B'),
'Group 2' => array('C', 'D'),
),
array($this->obj2, $this->obj3)
);
}
protected function tearDown()
{
parent::tearDown();
$this->obj1 = null;
$this->obj2 = null;
$this->obj3 = null;
$this->obj4 = null;
$this->list = null;
}
public function testInitArray()
{
$this->list = new ComplexChoiceList(
array($this->obj1, $this->obj2, $this->obj3, $this->obj4),
array('A', 'B', 'C', 'D'),
array($this->obj2)
);
$this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
$this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels());
$this->assertSame(array(1 => '1'), $this->list->getPreferredValues());
$this->assertSame(array(1 => '1'), $this->list->getPreferredValueHierarchy());
$this->assertSame(array(0 => '0', 2 => '2', 3 => '3'), $this->list->getRemainingValues());
$this->assertSame(array(0 => '0', 2 => '2', 3 => '3'), $this->list->getRemainingValueHierarchy());
}
public function testInitNestedArray()
{
$this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
$this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels());
$this->assertSame(array(1 => '1', 2 => '2'), $this->list->getPreferredValues());
$this->assertSame(array(
'Group 1' => array(1 => '1'),
'Group 2' => array(2 => '2')
), $this->list->getPreferredValueHierarchy());
$this->assertSame(array(0 => '0', 3 => '3'), $this->list->getRemainingValues());
$this->assertSame(array(
'Group 1' => array(0 => '0'),
'Group 2' => array(3 => '3')
), $this->list->getRemainingValueHierarchy());
}
public function testGetIndicesForChoices()
{
$choices = array($this->obj2, $this->obj3);
$this->assertSame(array(1, 2), $this->list->getIndicesForChoices($choices));
}
public function testGetIndicesForChoicesIgnoresNonExistingChoices()
{
$choices = array($this->obj2, $this->obj3, 'foobar');
$this->assertSame(array(1, 2), $this->list->getIndicesForChoices($choices));
}
public function testGetIndicesForValues()
{
// values and indices are always the same
$values = array('1', '2');
$this->assertSame(array(1, 2), $this->list->getIndicesForValues($values));
}
public function testGetIndicesForValuesIgnoresNonExistingValues()
{
$values = array('1', '2', '5');
$this->assertSame(array(1, 2), $this->list->getIndicesForValues($values));
}
public function testGetChoicesForValues()
{
$values = array('1', '2');
$this->assertSame(array($this->obj2, $this->obj3), $this->list->getChoicesForValues($values));
}
public function testGetChoicesForValuesIgnoresNonExistingValues()
{
$values = array('1', '2', '5');
$this->assertSame(array($this->obj2, $this->obj3), $this->list->getChoicesForValues($values));
}
public function testGetValuesForChoices()
{
$choices = array($this->obj2, $this->obj3);
$this->assertSame(array('1', '2'), $this->list->getValuesForChoices($choices));
}
public function testGetValuesForChoicesIgnoresNonExistingChoices()
{
$choices = array($this->obj2, $this->obj3, 'foobar');
$this->assertSame(array('1', '2'), $this->list->getValuesForChoices($choices));
}
}

View File

@ -1,95 +0,0 @@
<?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\Tests\Component\Form\Extension\Core\ChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\MonthChoiceList;
class MonthChoiceListTest extends \PHPUnit_Framework_TestCase
{
private $formatter;
protected function setUp()
{
if (!extension_loaded('intl')) {
$this->markTestSkipped('The "intl" extension is not available');
}
\Locale::setDefault('en');
// I would prefer to mock the formatter, but this leads to weird bugs
// with the current version of PHPUnit
$this->formatter = new \IntlDateFormatter(
\Locale::getDefault(),
\IntlDateFormatter::SHORT,
\IntlDateFormatter::NONE,
\DateTimeZone::UTC
);
}
protected function tearDown()
{
$this->formatter = null;
}
public function testNumericMonthsIfPatternContainsNoMonth()
{
$this->formatter->setPattern('yy');
$months = array(1, 4);
$list = new MonthChoiceList($this->formatter, $months);
$names = array(1 => '01', 4 => '04');
$this->assertSame($names, $list->getChoices());
}
public function testFormattedMonthsShort()
{
$this->formatter->setPattern('dd.MMM.yy');
$months = array(1, 4);
$list = new MonthChoiceList($this->formatter, $months);
$names = array(1 => 'Jan', 4 => 'Apr');
$this->assertSame($names, $list->getChoices());
}
public function testFormattedMonthsLong()
{
$this->formatter->setPattern('dd.MMMM.yy');
$months = array(1, 4);
$list = new MonthChoiceList($this->formatter, $months);
$names = array(1 => 'January', 4 => 'April');
$this->assertSame($names, $list->getChoices());
}
public function testFormattedMonthsLongWithDifferentTimezone()
{
$this->formatter = new \IntlDateFormatter(
\Locale::getDefault(),
\IntlDateFormatter::SHORT,
\IntlDateFormatter::NONE,
'PST'
);
$this->formatter->setPattern('dd.MMMM.yy');
$months = array(1, 4);
$list = new MonthChoiceList($this->formatter, $months);
$names = array(1 => 'January', 4 => 'April');
// uses UTC internally
$this->assertSame($names, $list->getChoices());
$this->assertSame('PST', $this->formatter->getTimezoneId());
}
}

View File

@ -0,0 +1,249 @@
<?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\Tests\Component\Form\Extension\Core\ChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
class ObjectChoiceListTest_EntityWithToString
{
private $property;
public function __construct($property)
{
$this->property = $property;
}
public function __toString()
{
return $this->property;
}
}
class ObjectChoiceListTest extends \PHPUnit_Framework_TestCase
{
private $obj1;
private $obj2;
private $obj3;
private $obj4;
private $list;
protected function setUp()
{
parent::setUp();
$this->obj1 = (object) array('name' => 'A');
$this->obj2 = (object) array('name' => 'B');
$this->obj3 = (object) array('name' => 'C');
$this->obj4 = (object) array('name' => 'D');
$this->list = new ObjectChoiceList(
array(
'Group 1' => array($this->obj1, $this->obj2),
'Group 2' => array($this->obj3, $this->obj4),
),
'name',
array($this->obj2, $this->obj3)
);
}
protected function tearDown()
{
parent::tearDown();
$this->obj1 = null;
$this->obj2 = null;
$this->obj3 = null;
$this->obj4 = null;
$this->list = null;
}
public function testInitArray()
{
$this->list = new ObjectChoiceList(
array($this->obj1, $this->obj2, $this->obj3, $this->obj4),
'name',
array($this->obj2)
);
$this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
$this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels());
$this->assertSame(array(1 => '1'), $this->list->getPreferredValues());
$this->assertSame(array(1 => '1'), $this->list->getPreferredValueHierarchy());
$this->assertSame(array(0 => '0', 2 => '2', 3 => '3'), $this->list->getRemainingValues());
$this->assertSame(array(0 => '0', 2 => '2', 3 => '3'), $this->list->getRemainingValueHierarchy());
}
public function testInitNestedArray()
{
$this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
$this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels());
$this->assertSame(array(1 => '1', 2 => '2'), $this->list->getPreferredValues());
$this->assertSame(array(
'Group 1' => array(1 => '1'),
'Group 2' => array(2 => '2')
), $this->list->getPreferredValueHierarchy());
$this->assertSame(array(0 => '0', 3 => '3'), $this->list->getRemainingValues());
$this->assertSame(array(
'Group 1' => array(0 => '0'),
'Group 2' => array(3 => '3')
), $this->list->getRemainingValueHierarchy());
}
public function testInitArrayWithGroupPath()
{
$this->obj1 = (object) array('name' => 'A', 'category' => 'Group 1');
$this->obj2 = (object) array('name' => 'B', 'category' => 'Group 1');
$this->obj3 = (object) array('name' => 'C', 'category' => 'Group 2');
$this->obj4 = (object) array('name' => 'D', 'category' => 'Group 2');
// Objects with NULL groups are not grouped
$obj5 = (object) array('name' => 'E', 'category' => null);
// Objects without the group property are not grouped either
// see https://github.com/symfony/symfony/commit/d9b7abb7c7a0f28e0ce970afc5e305dce5dccddf
$obj6 = (object) array('name' => 'F');
$this->list = new ObjectChoiceList(
array($this->obj1, $this->obj2, $this->obj3, $this->obj4, $obj5, $obj6),
'name',
array($this->obj2, $this->obj3),
'category'
);
$this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4, $obj5, $obj6), $this->list->getChoices());
$this->assertSame(array('A', 'B', 'C', 'D', 'E', 'F'), $this->list->getLabels());
$this->assertSame(array(1 => '1', 2 => '2'), $this->list->getPreferredValues());
$this->assertSame(array(
'Group 1' => array(1 => '1'),
'Group 2' => array(2 => '2')
), $this->list->getPreferredValueHierarchy());
$this->assertSame(array(0 => '0', 3 => '3', 4 => '4', 5 => '5'), $this->list->getRemainingValues());
$this->assertSame(array(
'Group 1' => array(0 => '0'),
'Group 2' => array(3 => '3'),
4 => '4',
5 => '5',
), $this->list->getRemainingValueHierarchy());
}
/**
* @expectedException \InvalidArgumentException
*/
public function testInitArrayWithGroupPathThrowsExceptionIfNestedArray()
{
$this->obj1 = (object) array('name' => 'A', 'category' => 'Group 1');
$this->obj2 = (object) array('name' => 'B', 'category' => 'Group 1');
$this->obj3 = (object) array('name' => 'C', 'category' => 'Group 2');
$this->obj4 = (object) array('name' => 'D', 'category' => 'Group 2');
new ObjectChoiceList(
array(
'Group 1' => array($this->obj1, $this->obj2),
'Group 2' => array($this->obj3, $this->obj4),
),
'name',
array($this->obj2, $this->obj3),
'category'
);
}
public function testInitArrayWithValuePath()
{
$this->obj1 = (object) array('name' => 'A', 'id' => 10);
$this->obj2 = (object) array('name' => 'B', 'id' => 20);
$this->obj3 = (object) array('name' => 'C', 'id' => 30);
$this->obj4 = (object) array('name' => 'D', 'id' => 40);
$this->list = new ObjectChoiceList(
array($this->obj1, $this->obj2, $this->obj3, $this->obj4),
'name',
array($this->obj2, $this->obj3),
null,
'id'
);
$this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
$this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels());
// Values are always converted to strings to avoid problems with
// comparisons
$this->assertSame(array(1 => '20', 2 => '30'), $this->list->getPreferredValues());
$this->assertSame(array(1 => '20', 2 => '30'), $this->list->getPreferredValueHierarchy());
$this->assertSame(array(0 => '10', 3 => '40'), $this->list->getRemainingValues());
$this->assertSame(array(0 => '10', 3 => '40'), $this->list->getRemainingValueHierarchy());
}
public function testInitArrayWithIndexPath()
{
$this->obj1 = (object) array('name' => 'A', 'id' => 10);
$this->obj2 = (object) array('name' => 'B', 'id' => 20);
$this->obj3 = (object) array('name' => 'C', 'id' => 30);
$this->obj4 = (object) array('name' => 'D', 'id' => 40);
$this->list = new ObjectChoiceList(
array($this->obj1, $this->obj2, $this->obj3, $this->obj4),
'name',
array($this->obj2, $this->obj3),
null,
null,
'id'
);
$this->assertSame(array(10 => $this->obj1, 20 => $this->obj2, 30 => $this->obj3, 40 => $this->obj4), $this->list->getChoices());
$this->assertSame(array(10 => 'A', 20 => 'B', 30 => 'C', 40 => 'D'), $this->list->getLabels());
$this->assertSame(array(20 => '1', 30 => '2'), $this->list->getPreferredValues());
$this->assertSame(array(20 => '1', 30 => '2'), $this->list->getPreferredValueHierarchy());
$this->assertSame(array(10 => '0', 40 => '3'), $this->list->getRemainingValues());
$this->assertSame(array(10 => '0', 40 => '3'), $this->list->getRemainingValueHierarchy());
}
public function testInitArrayUsesToString()
{
$this->obj1 = new ObjectChoiceListTest_EntityWithToString('A');
$this->obj2 = new ObjectChoiceListTest_EntityWithToString('B');
$this->obj3 = new ObjectChoiceListTest_EntityWithToString('C');
$this->obj4 = new ObjectChoiceListTest_EntityWithToString('D');
$this->list = new ObjectChoiceList(
array($this->obj1, $this->obj2, $this->obj3, $this->obj4)
);
$this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
$this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels());
}
/**
* @expectedException Symfony\Component\Form\Exception\FormException
*/
public function testInitArrayThrowsExceptionIfToStringNotFound()
{
$this->obj1 = new ObjectChoiceListTest_EntityWithToString('A');
$this->obj2 = new ObjectChoiceListTest_EntityWithToString('B');
$this->obj3 = (object) array('name' => 'C');
$this->obj4 = new ObjectChoiceListTest_EntityWithToString('D');
$this->list = new ObjectChoiceList(
array($this->obj1, $this->obj2, $this->obj3, $this->obj4)
);
$this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
$this->assertSame(array('A', 'B', 'C', 'D'), $this->list->getLabels());
}
}

View File

@ -1,54 +0,0 @@
<?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\Tests\Component\Form\Extension\Core\ChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\PaddedChoiceList;
class PaddedChoiceListTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testConstructorExpectsArrayOrClosure()
{
$list = new PaddedChoiceList('foobar', 3, '-', STR_PAD_RIGHT);
}
public function testPaddingDirections()
{
$list = new PaddedChoiceList(array('a' => 'C', 'b' => 'D'), 3, '-', STR_PAD_RIGHT);
$this->assertSame(array('a' => 'C--', 'b' => 'D--'), $list->getChoices());
$list = new PaddedChoiceList(array('a' => 'C', 'b' => 'D'), 3, '-', STR_PAD_LEFT);
$this->assertSame(array('a' => '--C', 'b' => '--D'), $list->getChoices());
$list = new PaddedChoiceList(array('a' => 'C', 'b' => 'D'), 3, '-', STR_PAD_BOTH);
$this->assertSame(array('a' => '-C-', 'b' => '-D-'), $list->getChoices());
}
public function testGetChoicesFromClosure()
{
$closure = function () { return array('a' => 'C', 'b' => 'D'); };
$list = new PaddedChoiceList($closure, 3, '-', STR_PAD_RIGHT);
$this->assertSame(array('a' => 'C--', 'b' => 'D--'), $list->getChoices());
}
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testClosureShouldReturnArray()
{
$closure = function () { return 'foobar'; };
$list = new PaddedChoiceList($closure, 3, '-', STR_PAD_RIGHT);
$list->getChoices();
}
}

View File

@ -0,0 +1,222 @@
<?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\Tests\Component\Form\Extension\Core\ChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
class SimpleChoiceListTest extends \PHPUnit_Framework_TestCase
{
private $list;
private $numericList;
protected function setUp()
{
parent::setUp();
$choices = array(
'Group 1' => array('a' => 'A', 'b' => 'B'),
'Group 2' => array('c' => 'C', 'd' => 'D'),
);
$numericChoices = array(
'Group 1' => array(0 => 'A', 1 => 'B'),
'Group 2' => array(2 => 'C', 3 => 'D'),
);
$this->list = new SimpleChoiceList($choices, array('b', 'c'), ChoiceList::GENERATE, ChoiceList::GENERATE);
// Use COPY_CHOICE strategy to test for the various associated problems
$this->numericList = new SimpleChoiceList($numericChoices, array(1, 2), ChoiceList::COPY_CHOICE, ChoiceList::GENERATE);
}
protected function tearDown()
{
parent::tearDown();
$this->list = null;
$this->numericList = null;
}
public function testInitArray()
{
$choices = array('a' => 'A', 'b' => 'B', 'c' => 'C');
$this->list = new SimpleChoiceList($choices, array('b'), ChoiceList::GENERATE, ChoiceList::GENERATE);
$this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getChoices());
$this->assertSame(array(0 => 'A', 1 => 'B', 2 => 'C'), $this->list->getLabels());
$this->assertSame(array(1 => '1'), $this->list->getPreferredValues());
$this->assertSame(array(1 => '1'), $this->list->getPreferredValueHierarchy());
$this->assertSame(array(0 => '0', 2 => '2'), $this->list->getRemainingValues());
$this->assertSame(array(0 => '0', 2 => '2'), $this->list->getRemainingValueHierarchy());
}
public function testInitArrayValueCopyChoice()
{
$choices = array('a' => 'A', 'b' => 'B', 'c' => 'C');
$this->list = new SimpleChoiceList($choices, array('b'), ChoiceList::COPY_CHOICE, ChoiceList::GENERATE);
$this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getChoices());
$this->assertSame(array(0 => 'A', 1 => 'B', 2 => 'C'), $this->list->getLabels());
$this->assertSame(array(1 => 'b'), $this->list->getPreferredValues());
$this->assertSame(array(1 => 'b'), $this->list->getPreferredValueHierarchy());
$this->assertSame(array(0 => 'a', 2 => 'c'), $this->list->getRemainingValues());
$this->assertSame(array(0 => 'a', 2 => 'c'), $this->list->getRemainingValueHierarchy());
}
public function testInitArrayIndexCopyChoice()
{
$choices = array('a' => 'A', 'b' => 'B', 'c' => 'C');
$this->list = new SimpleChoiceList($choices, array('b'), ChoiceList::GENERATE, ChoiceList::COPY_CHOICE);
$this->assertSame(array('a' => 'a', 'b' => 'b', 'c' => 'c'), $this->list->getChoices());
$this->assertSame(array('a' => 'A', 'b' => 'B', 'c' => 'C'), $this->list->getLabels());
$this->assertSame(array('b' => '1'), $this->list->getPreferredValues());
$this->assertSame(array('b' => '1'), $this->list->getPreferredValueHierarchy());
$this->assertSame(array('a' => '0', 'c' => '2'), $this->list->getRemainingValues());
$this->assertSame(array('a' => '0', 'c' => '2'), $this->list->getRemainingValueHierarchy());
}
public function testInitNestedArray()
{
$this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd'), $this->list->getChoices());
$this->assertSame(array(0 => 'A', 1 => 'B', 2 => 'C', 3 => 'D'), $this->list->getLabels());
$this->assertSame(array(1 => '1', 2 => '2'), $this->list->getPreferredValues());
$this->assertSame(array(
'Group 1' => array(1 => '1'),
'Group 2' => array(2 => '2')
), $this->list->getPreferredValueHierarchy());
$this->assertSame(array(0 => '0', 3 => '3'), $this->list->getRemainingValues());
$this->assertSame(array(
'Group 1' => array(0 => '0'),
'Group 2' => array(3 => '3')
), $this->list->getRemainingValueHierarchy());
}
public function testGetIndicesForChoices()
{
$choices = array('b', 'c');
$this->assertSame(array(1, 2), $this->list->getIndicesForChoices($choices));
}
public function testGetIndicesForChoicesIgnoresNonExistingChoices()
{
$choices = array('b', 'c', 'foobar');
$this->assertSame(array(1, 2), $this->list->getIndicesForChoices($choices));
}
public function testGetIndicesForChoicesDealsWithNumericChoices()
{
// Pass choices as strings although they are integers
$choices = array('0', '1');
$this->assertSame(array(0, 1), $this->numericList->getIndicesForChoices($choices));
}
public function testGetIndicesForValues()
{
$values = array('1', '2');
$this->assertSame(array(1, 2), $this->list->getIndicesForValues($values));
}
public function testGetIndicesForValuesIgnoresNonExistingValues()
{
$values = array('1', '2', '100');
$this->assertSame(array(1, 2), $this->list->getIndicesForValues($values));
}
public function testGetIndicesForValuesDealsWithNumericValues()
{
// Pass values as strings although they are integers
$values = array('0', '1');
$this->assertSame(array(0, 1), $this->numericList->getIndicesForValues($values));
}
public function testGetChoicesForValues()
{
$values = array('1', '2');
$this->assertSame(array('b', 'c'), $this->list->getChoicesForValues($values));
}
public function testGetChoicesForValuesIgnoresNonExistingValues()
{
$values = array('1', '2', '100');
$this->assertSame(array('b', 'c'), $this->list->getChoicesForValues($values));
}
public function testGetChoicesForValuesDealsWithNumericValues()
{
// Pass values as strings although they are integers
$values = array('0', '1');
$this->assertSame(array(0, 1), $this->numericList->getChoicesForValues($values));
}
public function testGetValuesForChoices()
{
$choices = array('b', 'c');
$this->assertSame(array('1', '2'), $this->list->getValuesForChoices($choices));
}
public function testGetValuesForChoicesIgnoresNonExistingValues()
{
$choices = array('b', 'c', 'foobar');
$this->assertSame(array('1', '2'), $this->list->getValuesForChoices($choices));
}
public function testGetValuesForChoicesDealsWithNumericValues()
{
// Pass values as strings although they are integers
$values = array('0', '1');
$this->assertSame(array('0', '1'), $this->numericList->getValuesForChoices($values));
}
/**
* @dataProvider dirtyValuesProvider
*/
public function testGetValuesForChoicesDealsWithDirtyValues($choice, $value)
{
$choices = array(
'0' => 'Zero',
'1' => 'One',
'' => 'Empty',
'1.23' => 'Float',
'foo' => 'Foo',
'foo10' => 'Foo 10',
);
// use COPY_CHOICE strategy to test the problems
$this->list = new SimpleChoiceList($choices, array(), ChoiceList::COPY_CHOICE, ChoiceList::GENERATE);
$this->assertSame(array($value), $this->list->getValuesForChoices(array($choice)));
}
public function dirtyValuesProvider()
{
return array(
array(0, '0'),
array('0', '0'),
array('1', '1'),
array(false, '0'),
array(true, '1'),
array('', ''),
array(null, ''),
array('1.23', '1.23'),
array('foo', 'foo'),
array('foo10', 'foo10'),
);
}
}

View File

@ -11,15 +11,17 @@
namespace Symfony\Tests\Component\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ScalarToChoiceTransformer;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
class ScalarToChoiceTransformerTest extends \PHPUnit_Framework_TestCase
class ChoiceToValueTransformerTest extends \PHPUnit_Framework_TestCase
{
protected $transformer;
protected function setUp()
{
$this->transformer = new ScalarToChoiceTransformer();
$list = new SimpleChoiceList(array('' => 'A', 0 => 'B', 1 => 'C'));
$this->transformer = new ChoiceToValueTransformer($list);
}
protected function tearDown()
@ -31,8 +33,8 @@ class ScalarToChoiceTransformerTest extends \PHPUnit_Framework_TestCase
{
return array(
// more extensive test set can be found in FormUtilTest
array(0, 0),
array(false, 0),
array(0, '0'),
array(false, '0'),
array('', ''),
);
}
@ -50,8 +52,9 @@ class ScalarToChoiceTransformerTest extends \PHPUnit_Framework_TestCase
return array(
// values are expected to be valid choice keys already and stay
// the same
array(0, 0),
array('', ''),
array('0', 0),
array('', null),
array(null, null),
);
}
@ -60,15 +63,7 @@ class ScalarToChoiceTransformerTest extends \PHPUnit_Framework_TestCase
*/
public function testReverseTransform($in, $out)
{
$this->assertSame($out, $this->transformer->transform($in));
}
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testTransformExpectsScalar()
{
$this->transformer->transform(array());
$this->assertSame($out, $this->transformer->reverseTransform($in));
}
/**

View File

@ -11,15 +11,18 @@
namespace Symfony\Tests\Component\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToChoicesTransformer;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
class ArrayToChoicesTransformerTest extends \PHPUnit_Framework_TestCase
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
class ChoicesToValuesTransformerTest extends \PHPUnit_Framework_TestCase
{
protected $transformer;
protected function setUp()
{
$this->transformer = new ArrayToChoicesTransformer();
$list = new SimpleChoiceList(array(0 => 'A', 1 => 'B', 2 => 'C'));
$this->transformer = new ChoicesToValuesTransformer($list);
}
protected function tearDown()
@ -29,8 +32,9 @@ class ArrayToChoicesTransformerTest extends \PHPUnit_Framework_TestCase
public function testTransform()
{
$in = array(0, false, '');
$out = array(0, 0, '');
// Value strategy in SimpleChoiceList is to copy and convert to string
$in = array(0, 1, 2);
$out = array('0', '1', '2');
$this->assertSame($out, $this->transformer->transform($in));
}
@ -51,10 +55,10 @@ class ArrayToChoicesTransformerTest extends \PHPUnit_Framework_TestCase
public function testReverseTransform()
{
// values are expected to be valid choices and stay the same
$in = array(0, 0, '');
$out = array(0, 0, '');
$in = array('0', '1', '2');
$out = array(0, 1, 2);
$this->assertSame($out, $this->transformer->transform($in));
$this->assertSame($out, $this->transformer->reverseTransform($in));
}
public function testReverseTransformNull()

View File

@ -13,19 +13,36 @@ namespace Symfony\Tests\Component\Form\Extension\Core\EventListener;
use Symfony\Component\Form\Event\FilterDataEvent;
use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase
{
private $listener;
protected function setUp()
{
parent::setUp();
$list = new SimpleChoiceList(array(0 => 'A', 1 => 'B'));
$this->listener = new FixRadioInputListener($list);
}
protected function tearDown()
{
parent::tearDown();
$this->listener = null;
}
public function testFixRadio()
{
$data = '1';
$form = $this->getMock('Symfony\Tests\Component\Form\FormInterface');
$event = new FilterDataEvent($form, $data);
$filter = new FixRadioInputListener();
$filter->onBindClientData($event);
$this->listener->onBindClientData($event);
$this->assertEquals(array('1' => true), $event->getData());
$this->assertEquals(array(1 => '1'), $event->getData());
}
public function testFixZero()
@ -34,10 +51,9 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase
$form = $this->getMock('Symfony\Tests\Component\Form\FormInterface');
$event = new FilterDataEvent($form, $data);
$filter = new FixRadioInputListener();
$filter->onBindClientData($event);
$this->listener->onBindClientData($event);
$this->assertEquals(array('0' => true), $event->getData());
$this->assertEquals(array(0 => '0'), $event->getData());
}
public function testIgnoreEmptyString()
@ -46,8 +62,7 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase
$form = $this->getMock('Symfony\Tests\Component\Form\FormInterface');
$event = new FilterDataEvent($form, $data);
$filter = new FixRadioInputListener();
$filter->onBindClientData($event);
$this->listener->onBindClientData($event);
$this->assertEquals(array(), $event->getData());
}

View File

@ -11,6 +11,10 @@
namespace Symfony\Tests\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
class ChoiceTypeTest extends TypeTestCase
@ -31,6 +35,8 @@ class ChoiceTypeTest extends TypeTestCase
4 => 'Roman',
);
private $objectChoices;
private $stringButNumericChoices = array(
'0' => 'Bernhard',
'1' => 'Fabien',
@ -51,8 +57,29 @@ class ChoiceTypeTest extends TypeTestCase
)
);
protected function setUp()
{
parent::setUp();
$this->objectChoices = array(
(object) array('id' => 1, 'name' => 'Bernhard'),
(object) array('id' => 2, 'name' => 'Fabien'),
(object) array('id' => 3, 'name' => 'Kris'),
(object) array('id' => 4, 'name' => 'Jon'),
(object) array('id' => 5, 'name' => 'Roman'),
);
}
protected function tearDown()
{
parent::tearDown();
$this->objectChoices = null;
}
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
* @expectedException \PHPUnit_Framework_Error
*/
public function testChoicesOptionExpectsArray()
{
@ -71,6 +98,15 @@ class ChoiceTypeTest extends TypeTestCase
));
}
/**
* @expectedException Symfony\Component\Form\Exception\FormException
*/
public function testEitherChoiceListOrChoicesMustBeSet()
{
$form = $this->factory->create('choice', null, array(
));
}
public function testExpandedChoicesOptionsTurnIntoFields()
{
$form = $this->factory->create('choice', null, array(
@ -90,7 +126,7 @@ class ChoiceTypeTest extends TypeTestCase
$flattened = array();
foreach ($this->groupedChoices as $choices) {
$flattened = array_replace($flattened, $choices);
$flattened = array_merge($flattened, array_keys($choices));
}
$this->assertCount($form->count(), $flattened, 'Each nested choice should become a new field, not the groups');
@ -150,10 +186,33 @@ class ChoiceTypeTest extends TypeTestCase
'choices' => $this->choices,
));
$form->bind('b');
$form->bind('1');
$this->assertEquals('b', $form->getData());
$this->assertEquals('b', $form->getClientData());
$this->assertEquals('1', $form->getClientData());
}
public function testBindSingleNonExpandedObjectChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => false,
'choice_list' => new ObjectChoiceList(
$this->objectChoices,
// label path
'name',
array(),
null,
// value path
'id'
),
));
// "id" value of the second entry
$form->bind('2');
$this->assertEquals($this->objectChoices[1], $form->getData());
$this->assertEquals('2', $form->getClientData());
}
public function testBindMultipleNonExpanded()
@ -164,10 +223,32 @@ class ChoiceTypeTest extends TypeTestCase
'choices' => $this->choices,
));
$form->bind(array('a', 'b'));
$form->bind(array('0', '1'));
$this->assertEquals(array('a', 'b'), $form->getData());
$this->assertEquals(array('a', 'b'), $form->getClientData());
$this->assertEquals(array('0', '1'), $form->getClientData());
}
public function testBindMultipleNonExpandedObjectChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => true,
'expanded' => false,
'choice_list' => new ObjectChoiceList(
$this->objectChoices,
// label path
'name',
array(),
null,
// value path
'id'
),
));
$form->bind(array('2', '3'));
$this->assertEquals(array($this->objectChoices[1], $this->objectChoices[2]), $form->getData());
$this->assertEquals(array('2', '3'), $form->getClientData());
}
public function testBindSingleExpanded()
@ -178,19 +259,19 @@ class ChoiceTypeTest extends TypeTestCase
'choices' => $this->choices,
));
$form->bind('b');
$form->bind('1');
$this->assertSame('b', $form->getData());
$this->assertFalse($form['a']->getData());
$this->assertTrue($form['b']->getData());
$this->assertFalse($form['c']->getData());
$this->assertFalse($form['d']->getData());
$this->assertFalse($form['e']->getData());
$this->assertSame('', $form['a']->getClientData());
$this->assertSame('1', $form['b']->getClientData());
$this->assertSame('', $form['c']->getClientData());
$this->assertSame('', $form['d']->getClientData());
$this->assertSame('', $form['e']->getClientData());
$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->assertSame('', $form[0]->getClientData());
$this->assertSame('1', $form[1]->getClientData());
$this->assertSame('', $form[2]->getClientData());
$this->assertSame('', $form[3]->getClientData());
$this->assertSame('', $form[4]->getClientData());
}
public function testBindSingleExpandedWithFalseDoesNotHaveExtraFields()
@ -207,6 +288,57 @@ class ChoiceTypeTest extends TypeTestCase
$this->assertNull($form->getData());
}
public function testBindSingleExpandedWithEmptyField()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => true,
'choices' => array(
'' => 'Empty',
'1' => 'Not empty',
),
));
$form->bind('0');
$this->assertNull($form->getData());
$this->assertTrue($form[0]->getData());
$this->assertFalse($form[1]->getData());
$this->assertSame('1', $form[0]->getClientData());
$this->assertSame('', $form[1]->getClientData());
}
public function testBindSingleExpandedObjectChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => false,
'expanded' => true,
'choice_list' => new ObjectChoiceList(
$this->objectChoices,
// label path
'name',
array(),
null,
// value path
'id'
),
));
$form->bind('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->assertSame('', $form[0]->getClientData());
$this->assertSame('1', $form[1]->getClientData());
$this->assertSame('', $form[2]->getClientData());
$this->assertSame('', $form[3]->getClientData());
$this->assertSame('', $form[4]->getClientData());
}
public function testBindSingleExpandedNumericChoices()
{
$form = $this->factory->create('choice', null, array(
@ -217,7 +349,7 @@ class ChoiceTypeTest extends TypeTestCase
$form->bind('1');
$this->assertSame('1', $form->getData());
$this->assertSame(1, $form->getData());
$this->assertFalse($form[0]->getData());
$this->assertTrue($form[1]->getData());
$this->assertFalse($form[2]->getData());
@ -240,7 +372,7 @@ class ChoiceTypeTest extends TypeTestCase
$form->bind('1');
$this->assertSame('1', $form->getData());
$this->assertSame(1, $form->getData());
$this->assertFalse($form[0]->getData());
$this->assertTrue($form[1]->getData());
$this->assertFalse($form[2]->getData());
@ -261,19 +393,50 @@ class ChoiceTypeTest extends TypeTestCase
'choices' => $this->choices,
));
$form->bind(array('a' => 'a', 'b' => 'b'));
$form->bind(array(0 => 'a', 1 => 'b'));
$this->assertSame(array('a', 'b'), $form->getData());
$this->assertTrue($form['a']->getData());
$this->assertTrue($form['b']->getData());
$this->assertFalse($form['c']->getData());
$this->assertFalse($form['d']->getData());
$this->assertFalse($form['e']->getData());
$this->assertSame('1', $form['a']->getClientData());
$this->assertSame('1', $form['b']->getClientData());
$this->assertSame('', $form['c']->getClientData());
$this->assertSame('', $form['d']->getClientData());
$this->assertSame('', $form['e']->getClientData());
$this->assertSame(array(0 => 'a', 1 => 'b'), $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]->getClientData());
$this->assertSame('1', $form[1]->getClientData());
$this->assertSame('', $form[2]->getClientData());
$this->assertSame('', $form[3]->getClientData());
$this->assertSame('', $form[4]->getClientData());
}
public function testBindMultipleExpandedObjectChoices()
{
$form = $this->factory->create('choice', null, array(
'multiple' => true,
'expanded' => true,
'choice_list' => new ObjectChoiceList(
$this->objectChoices,
// label path
'name',
array(),
null,
// value path
'id'
),
));
$form->bind(array(0 => '1', 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]->getClientData());
$this->assertSame('1', $form[1]->getClientData());
$this->assertSame('', $form[2]->getClientData());
$this->assertSame('', $form[3]->getClientData());
$this->assertSame('', $form[4]->getClientData());
}
public function testBindMultipleExpandedNumericChoices()
@ -284,7 +447,7 @@ class ChoiceTypeTest extends TypeTestCase
'choices' => $this->numericChoices,
));
$form->bind(array(1 => 1, 2 => 2));
$form->bind(array(1 => '1', 2 => '2'));
$this->assertSame(array(1, 2), $form->getData());
$this->assertFalse($form[0]->getData());
@ -437,7 +600,8 @@ class ChoiceTypeTest extends TypeTestCase
));
$view = $form->createView();
$this->assertSame($choices, $view->get('choices'));
$this->assertSame(array('0', '1', '2', '3'), $view->get('choices'));
$this->assertSame(array('A', 'B', 'C', 'D'), $view->get('choice_labels'));
}
public function testPassPreferredChoicesToView()
@ -449,8 +613,22 @@ class ChoiceTypeTest extends TypeTestCase
));
$view = $form->createView();
$this->assertSame(array('a' => 'A', 'c' => 'C'), $view->get('choices'));
$this->assertSame(array('b' => 'B', 'd' => 'D'), $view->get('preferred_choices'));
$this->assertSame(array(0 => '0', 2 => '2'), $view->get('choices'));
$this->assertSame(array(1 => '1', 3 => '3'), $view->get('preferred_choices'));
$this->assertSame(array('A', 'B', 'C', 'D'), $view->get('choice_labels'));
}
public function testPassHierarchicalChoicesToView()
{
$form = $this->factory->create('choice', null, array(
'choices' => $this->groupedChoices,
'preferred_choices' => array('b', 'd'),
));
$view = $form->createView();
$this->assertSame(array('Symfony' => array(0 => '0', 2 => '2'), 'Doctrine' => array(4 => '4')), $view->get('choices'));
$this->assertSame(array('Symfony' => array(1 => '1'), 'Doctrine' => array(3 => '3')), $view->get('preferred_choices'));
$this->assertSame(array(0 => 'Bernhard', 1 => 'Fabien', 2 => 'Kris', 3 => 'Jon', 4 => 'Roman'), $view->get('choice_labels'));
}
public function testAdjustFullNameForMultipleNonExpanded()

View File

@ -21,17 +21,18 @@ class CountryTypeTest extends LocalizedTestCase
$form = $this->factory->create('country');
$view = $form->createView();
$choices = $view->get('choices');
$labels = $view->get('choice_labels');
$this->assertArrayHasKey('DE', $choices);
$this->assertEquals('Deutschland', $choices['DE']);
$this->assertArrayHasKey('GB', $choices);
$this->assertEquals('Vereinigtes Königreich', $choices['GB']);
$this->assertArrayHasKey('US', $choices);
$this->assertEquals('Vereinigte Staaten', $choices['US']);
$this->assertArrayHasKey('FR', $choices);
$this->assertEquals('Frankreich', $choices['FR']);
$this->assertArrayHasKey('MY', $choices);
$this->assertEquals('Malaysia', $choices['MY']);
$this->assertContains('DE', $choices);
$this->assertEquals('Deutschland', $labels['DE']);
$this->assertContains('GB', $choices);
$this->assertEquals('Vereinigtes Königreich', $labels['GB']);
$this->assertContains('US', $choices);
$this->assertEquals('Vereinigte Staaten', $labels['US']);
$this->assertContains('FR', $choices);
$this->assertEquals('Frankreich', $labels['FR']);
$this->assertContains('MY', $choices);
$this->assertEquals('Malaysia', $labels['MY']);
}
public function testUnknownCountryIsNotIncluded()

View File

@ -237,6 +237,9 @@ class DateTimeTypeTest extends LocalizedTestCase
{
$form = $this->factory->create('datetime', null, array(
'invalid_message' => 'Customized invalid message',
// Only possible with the "text" widget, because the "choice"
// widget automatically fields invalid values
'widget' => 'text',
));
$form->bind(array(

View File

@ -325,7 +325,7 @@ class DateTypeTest extends LocalizedTestCase
$view = $form->createView();
$this->assertSame(array(2010 => '2010', 2011 => '2011'), $view->getChild('year')->get('choices'));
$this->assertSame(array(2010 => '2010', 2011 => '2011'), $view->getChild('year')->get('choice_labels'));
}
public function testMonthsOption()
@ -336,7 +336,55 @@ class DateTypeTest extends LocalizedTestCase
$view = $form->createView();
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('month')->get('choices'));
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('month')->get('choice_labels'));
}
public function testMonthsOptionNumericIfFormatContainsNoMonth()
{
$form = $this->factory->create('date', null, array(
'months' => array(6, 7),
'format' => 'yy',
));
$view = $form->createView();
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('month')->get('choice_labels'));
}
public function testMonthsOptionShortFormat()
{
$form = $this->factory->create('date', null, array(
'months' => array(1, 4),
'format' => 'dd.MMM.yy',
));
$view = $form->createView();
$this->assertSame(array(1 => 'Jän', 4 => 'Apr'), $view->getChild('month')->get('choice_labels'));
}
public function testMonthsOptionLongFormat()
{
$form = $this->factory->create('date', null, array(
'months' => array(1, 4),
'format' => 'dd.MMMM.yy',
));
$view = $form->createView();
$this->assertSame(array(1 => 'Jänner', 4 => 'April'), $view->getChild('month')->get('choice_labels'));
}
public function testMonthsOptionLongFormatWithDifferentTimezone()
{
$form = $this->factory->create('date', null, array(
'months' => array(1, 4),
'format' => 'dd.MMMM.yy',
));
$view = $form->createView();
$this->assertSame(array(1 => 'Jänner', 4 => 'April'), $view->getChild('month')->get('choice_labels'));
}
public function testIsDayWithinRangeReturnsTrueIfWithin()
@ -347,7 +395,7 @@ class DateTypeTest extends LocalizedTestCase
$view = $form->createView();
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('day')->get('choices'));
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('day')->get('choice_labels'));
}
public function testIsPartiallyFilledReturnsFalseIfSingleText()

View File

@ -21,17 +21,18 @@ class LanguageTypeTest extends LocalizedTestCase
$form = $this->factory->create('language');
$view = $form->createView();
$choices = $view->get('choices');
$labels = $view->get('choice_labels');
$this->assertArrayHasKey('en', $choices);
$this->assertEquals('Englisch', $choices['en']);
$this->assertArrayHasKey('en_GB', $choices);
$this->assertEquals('Britisches Englisch', $choices['en_GB']);
$this->assertArrayHasKey('en_US', $choices);
$this->assertEquals('Amerikanisches Englisch', $choices['en_US']);
$this->assertArrayHasKey('fr', $choices);
$this->assertEquals('Französisch', $choices['fr']);
$this->assertArrayHasKey('my', $choices);
$this->assertEquals('Birmanisch', $choices['my']);
$this->assertContains('en', $choices);
$this->assertEquals('Englisch', $labels[array_search('en', $choices)]);
$this->assertContains('en_GB', $choices);
$this->assertEquals('Britisches Englisch', $labels[array_search('en_GB', $choices)]);
$this->assertContains('en_US', $choices);
$this->assertEquals('Amerikanisches Englisch', $labels[array_search('en_US', $choices)]);
$this->assertContains('fr', $choices);
$this->assertEquals('Französisch', $labels[array_search('fr', $choices)]);
$this->assertContains('my', $choices);
$this->assertEquals('Birmanisch', $labels[array_search('my', $choices)]);
}
public function testMultipleLanguagesIsNotIncluded()

View File

@ -21,12 +21,13 @@ class LocaleTypeTest extends LocalizedTestCase
$form = $this->factory->create('locale');
$view = $form->createView();
$choices = $view->get('choices');
$labels = $view->get('choice_labels');
$this->assertArrayHasKey('en', $choices);
$this->assertEquals('Englisch', $choices['en']);
$this->assertArrayHasKey('en_GB', $choices);
$this->assertEquals('Englisch (Vereinigtes Königreich)', $choices['en_GB']);
$this->assertArrayHasKey('zh_Hant_MO', $choices);
$this->assertEquals('Chinesisch (traditionell, Sonderverwaltungszone Macao)', $choices['zh_Hant_MO']);
$this->assertContains('en', $choices);
$this->assertEquals('Englisch', $labels[array_search('en', $choices)]);
$this->assertContains('en_GB', $choices);
$this->assertEquals('Englisch (Vereinigtes Königreich)', $labels[array_search('en_GB', $choices)]);
$this->assertContains('zh_Hant_MO', $choices);
$this->assertEquals('Chinesisch (traditionell, Sonderverwaltungszone Macao)', $labels[array_search('zh_Hant_MO', $choices)]);
}
}

View File

@ -13,14 +13,6 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type;
class RadioTypeTest extends TypeTestCase
{
public function testPassValueToView()
{
$form = $this->factory->create('radio', null, array('value' => 'foobar'));
$view = $form->createView();
$this->assertEquals('foobar', $view->get('value'));
}
public function testPassParentFullNameToView()
{
$parent = $this->factory->createNamed('field', 'parent');
@ -29,22 +21,4 @@ class RadioTypeTest extends TypeTestCase
$this->assertEquals('parent', $view['child']->get('full_name'));
}
public function testCheckedIfDataTrue()
{
$form = $this->factory->create('radio');
$form->setData(true);
$view = $form->createView();
$this->assertTrue($view->get('checked'));
}
public function testNotCheckedIfDataFalse()
{
$form = $this->factory->create('radio');
$form->setData(false);
$view = $form->createView();
$this->assertFalse($view->get('checked'));
}
}

View File

@ -243,7 +243,7 @@ class TimeTypeTest extends LocalizedTestCase
$view = $form->createView();
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('hour')->get('choices'));
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('hour')->get('choice_labels'));
}
public function testIsMinuteWithinRange_returnsTrueIfWithin()
@ -254,7 +254,7 @@ class TimeTypeTest extends LocalizedTestCase
$view = $form->createView();
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('minute')->get('choices'));
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('minute')->get('choice_labels'));
}
public function testIsSecondWithinRange_returnsTrueIfWithin()
@ -266,7 +266,7 @@ class TimeTypeTest extends LocalizedTestCase
$view = $form->createView();
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('second')->get('choices'));
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('second')->get('choice_labels'));
}
public function testIsPartiallyFilled_returnsFalseIfCompletelyEmpty()

View File

@ -19,13 +19,14 @@ class TimezoneTypeTest extends TypeTestCase
$form = $this->factory->create('timezone');
$view = $form->createView();
$choices = $view->get('choices');
$labels = $view->get('choice_labels');
$this->assertArrayHasKey('Africa', $choices);
$this->assertArrayHasKey('Africa/Kinshasa', $choices['Africa']);
$this->assertEquals('Kinshasa', $choices['Africa']['Africa/Kinshasa']);
$this->assertContains('Africa/Kinshasa', $choices['Africa']);
$this->assertEquals('Kinshasa', $labels[array_search('Africa/Kinshasa', $choices['Africa'])]);
$this->assertArrayHasKey('America', $choices);
$this->assertArrayHasKey('America/New_York', $choices['America']);
$this->assertEquals('New York', $choices['America']['America/New_York']);
$this->assertContains('America/New_York', $choices['America']);
$this->assertEquals('New York', $labels[array_search('America/New_York', $choices['America'])]);
}
}

View File

@ -15,42 +15,6 @@ use Symfony\Component\Form\Util\FormUtil;
class FormUtilTest extends \PHPUnit_Framework_TestCase
{
public function toArrayKeyProvider()
{
return array(
array(0, 0),
array('0', 0),
array('1', 1),
array(false, 0),
array(true, 1),
array('', ''),
array(null, ''),
array('1.23', '1.23'),
array('foo', 'foo'),
array('foo10', 'foo10'),
);
}
/**
* @dataProvider toArrayKeyProvider
*/
public function testToArrayKey($in, $out)
{
$this->assertSame($out, FormUtil::toArrayKey($in));
}
public function testToArrayKeys()
{
$in = $out = array();
foreach ($this->toArrayKeyProvider() as $call) {
$in[] = $call[0];
$out[] = $call[1];
}
$this->assertSame($out, FormUtil::toArrayKeys($in));
}
public function isChoiceGroupProvider()
{
return array(
@ -85,14 +49,17 @@ class FormUtilTest extends \PHPUnit_Framework_TestCase
public function isChoiceSelectedProvider()
{
// The commented cases should not be necessary anymore, because the
// choice lists should assure that both values passed here are always
// strings
return array(
array(true, 0, 0),
array(true, '0', 0),
array(true, '1', 1),
array(true, false, 0),
array(true, true, 1),
// array(true, 0, 0),
array(true, '0', '0'),
array(true, '1', '1'),
// array(true, false, 0),
// array(true, true, 1),
array(true, '', ''),
array(true, null, ''),
// array(true, null, ''),
array(true, '1.23', '1.23'),
array(true, 'foo', 'foo'),
array(true, 'foo10', 'foo10'),