merged branch bschussek/issue1919 (PR #3156)

Commits
-------

8dc78bd [Form] Fixed YODA issues
600cec7 [Form] Added missing entries to CHANGELOG and UPGRADE
b154f7c [Form] Fixed docblock and unneeded use statement
399af27 [Form] Implemented checks to assert that values and indices generated in choice lists match their requirements
5f6f75c [Form] Fixed outstanding issues mentioned in the PR
7c70976 [Form] Fixed text in UPGRADE file
c26b47a [Form] Made query parameter name generated by ORMQueryBuilderLoader unique
18f92cd [Form] Fixed double choice fixing
f533ef0 [Form] Added ChoiceView class for passing choice-related data to the view
d72900e [Form] Incorporated changes suggested in PR comments
28d2f6d Removed duplicated lines from UPGRADE file
e1fc5a5 [Form] Restricted form names to specific characters to (1) fix generation of HTML IDs and to (2) avoid problems with property paths.
87b16e7 [Form] Greatly improved ChoiceListInterface and all of its implementations

Discussion
----------

[Form] Improved ChoiceList implementation and made form naming more restrictive

Bug fix: yes
Feature addition: yes
Backwards compatibility break: **yes**
Symfony2 tests pass: yes
Fixes the following tickets: #2869, #3021, #1919, #3153
Todo: adapt documentation

![Travis Build Status](https://secure.travis-ci.org/bschussek/symfony.png?branch=issue1919)

The changes in this PR are primarily motivated by the fact that invalid form/field names lead to various problems.

1. When a name contains any characters that are not permitted in HTML "id" attributes, these are invalid
2. When a name contains periods ("."), form validation is broken, because they confuse the property path resolution
3. Since choices in expanded choice fields are directly translated to field names, choices applying to either 1. or 2. lead to problems. But choices should be unrestricted.
4. Unless a choice field is not expanded and does not allow multiple selection, it is not possible to use empty strings as choices, which might be desirable in some occasions.

The solution to these problems is to

* Restrict form names to disallow unpermitted characters (solves 1. and 2.)
* Generate integer indices to be stored in the HTML "id" and "name" attributes and map them to the choices (solves 3.). Can be reverted to the old behaviour by setting the option "index_generation" to ChoiceList::COPY_CHOICE
* Generate integer values to be stored in the HTML "value" attribute and map them to the choices (solves 4.). Can be reverted to the old behaviour by setting the option "value_generation" to ChoiceList::COPY_CHOICE

Apart from these fixes, it is now possible to write more flexible choice lists. One of these is `ObjectChoiceList`, which allows to use objects as choices and is bundled in the core. `EntityChoiceList` has been made an extension of this class.

    $form = $this->createFormBuilder()
        ->add('object', 'choice', array(
            'choice_list' => new ObjectChoiceList(
                array($obj1, $obj2, $obj3, $obj4),
                // property path determining the choice label (optional)
                'name',
                // preferred choices (optional)
                array($obj2, $obj3),
                // property path for object grouping (optional)
                'category',
                // property path for value generation (optional)
                'id',
                // property path for index generation (optional)
                'id'
            )
        ))
        ->getForm()
    ;

---------------------------------------------------------------------------

by kriswallsmith at 2012-01-19T18:09:09Z

Rather than passing `choices` and a `choice_labels` arrays to the view would it make sense to introduce a `ChoiceView` class and pass one array of objects?

---------------------------------------------------------------------------

by stof at 2012-01-22T15:32:36Z

@bschussek can you update your PR according to the feedback (and rebase it as it conflicts according to github) ?

---------------------------------------------------------------------------

by bschussek at 2012-01-24T00:15:42Z

@kriswallsmith fixed

Fixed all outstanding issues. Would be glad if someone could review again, otherwise this PR is ready to merge.

---------------------------------------------------------------------------

by fabpot at 2012-01-25T15:17:59Z

Is it ready to be merged?

---------------------------------------------------------------------------

by Tobion at 2012-01-25T15:35:50Z

Yes I think so. He said it's ready to be merged when reviewed.

---------------------------------------------------------------------------

by bschussek at 2012-01-26T02:30:36Z

Yes.

---------------------------------------------------------------------------

by bschussek at 2012-01-28T12:39:00Z

Fixed outstanding issues. Ready for merge.
This commit is contained in:
Fabien Potencier 2012-01-28 15:19:10 +01:00
commit 5e0823c99c
74 changed files with 3484 additions and 1668 deletions

View File

@ -166,6 +166,45 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c
* allowed setting different options for RepeatedType fields (like the label)
* added support for empty form name at root level, this enables rendering forms
without form name prefix in field names
* [BC BREAK] made form naming more restrictive. Form and field names must
start with a letter, digit or underscore and only contain letters, digits,
underscores, hyphens and colons
* [BC BREAK] changed default name of the prototype in the "collection" type
from "$$name$$" to "__name__". No dollars are appended/prepended to custom
names anymore.
* [BC BREAK] greatly improved `ChoiceListInterface` and all of its
implementations. `EntityChoiceList` was adapted, the methods `getEntities()`,
`getEntitiesByKeys()`, `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

View File

@ -76,4 +76,68 @@ 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.
* 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`.
* In the template of the choice type, the structure of the "choices" variable
has changed
"choices" now contains ChoiceView objects with two getters `getValue()`
and `getLabel()` to access the choice data. The indices of the array
store an index whose generation is controlled by the "index_generation"
option of the choice field.
Before:
{% for choice, label in choices %}
<option value="{{ choice }}"{% if _form_is_choice_selected(form, choice) %} selected="selected"{% endif %}>
{{ label }}
</option>
{% endfor %}
After:
{% for choice in choices %}
<option value="{{ choice.value }}"{% if _form_is_choice_selected(form, choice) %} selected="selected"{% endif %}>
{{ choice.label }}
</option>
{% endfor %}
* In the template of the collection type, the default name of the prototype
field has changed from "$$name$$" to "__name__"
For custom names, no dollars are prepended/appended anymore. You are advised
to prepend and append double underscores wherever you have configured the
prototype name manually.
Before:
$builder->add('tags', 'collection', array('prototype' => 'proto'));
// results in the name "$$proto$$" in the template
After:
$builder->add('tags', 'collection', array('prototype' => '__proto__'));
// results in the name "__proto__" in the template

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,289 @@ 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 A property path pointing to the property used
* to group the choices. Only allowed if
* the choices are given as flat array.
*/
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 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 choice views of the preferred choices as nested array with
* the choice groups as top-level keys.
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getPreferredViews()
{
if (!$this->loaded) {
$this->load();
}
return parent::getPreferredViews();
}
/**
* Returns the choice views of the choices 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 getRemainingViews()
{
if (!$this->loaded) {
$this->load();
}
return parent::getRemainingViews();
}
/**
* 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(str_replace('argument $labelPath', 'option "property"', $e->getMessage()), 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 +358,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,21 @@ 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
* @return array The entities.
*/
function getEntities();
/**
* Returns an array of entities matching the given identifiers.
*
* @param string $identifier The identifier field of the object. This method
* is not applicable for fields with multiple
* identifiers.
* @param array $values The values of the identifiers.
*
* @return array The entities.
*/
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,20 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface
{
return $this->queryBuilder->getQuery()->execute();
}
/**
* {@inheritDoc}
*/
public function getEntitiesByIds($identifier, array $values)
{
$qb = clone ($this->queryBuilder);
$alias = current($qb->getRootAliases());
$parameter = 'ORMQueryBuilderLoader_getEntitiesByIds_'.$identifier;
$where = $qb->expr()->in($alias.'.'.$identifier, ':'.$parameter);
return $qb->andWhere($where)
->getQuery()
->setParameter($parameter, $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 $collection A collection of entities
*
* @return mixed An array of entities
*/
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 entities
*
* @return Collection A collection of entities
*/
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

@ -15,6 +15,7 @@ use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\Util\FormUtil;
/**
@ -95,9 +96,9 @@ class FormExtension extends \Twig_Extension
return FormUtil::isChoiceGroup($label);
}
public function isChoiceSelected(FormView $view, $choice)
public function isChoiceSelected(FormView $view, ChoiceView $choice)
{
return FormUtil::isChoiceSelected($choice, $view->get('value'));
return FormUtil::isChoiceSelected($choice->getValue(), $view->get('value'));
}
/**

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_choice in choice %}
<option value="{{ nested_choice.value }}"{% if _form_is_choice_selected(form, nested_choice) %} selected="selected"{% endif %}>{{ nested_choice.label|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.value }}"{% if _form_is_choice_selected(form, choice) %} selected="selected"{% endif %}>{{ choice.label|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_choice): ?>
<option value="<?php echo $view->escape($nested_choice->getValue()) ?>"<?php if ($view['form']->isChoiceSelected($form, $nested_choice)): ?> selected="selected"<?php endif?>><?php echo $view->escape($view['translator']->trans($nested_choice->getLabel(), 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->getValue()) ?>"<?php if ($view['form']->isChoiceSelected($form, $choice)): ?> selected="selected"<?php endif?>><?php echo $view->escape($view['translator']->trans($choice->getLabel(), array(), $translation_domain)) ?></option>
<?php endif ?>
<?php endforeach ?>

View File

@ -16,6 +16,7 @@ use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\Util\FormUtil;
/**
@ -63,9 +64,9 @@ class FormHelper extends Helper
return FormUtil::isChoiceGroup($label);
}
public function isChoiceSelected(FormView $view, $choice)
public function isChoiceSelected(FormView $view, ChoiceView $choice)
{
return FormUtil::isChoiceSelected($choice, $view->get('value'));
return FormUtil::isChoiceSelected($choice->getValue(), $view->get('value'));
}
/**

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,602 @@
<?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\Form;
use Symfony\Component\Form\Util\FormUtil;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\InvalidConfigurationException;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
/**
* Base class for choice list implementations.
*
* @author Bernhard Schussek <bschussek@gmail.<com>
*/
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 preferred view objects as hierarchy containing also the choice groups
* with the indices of the matching choices as bottom-level keys.
*
* @var array
*/
private $preferredViews = array();
/**
* The non-preferred view objects as hierarchy containing also the choice
* groups with the indices of the matching choices as bottom-level keys.
*
* @var array
*/
private $remainingViews = 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 = self::GENERATE, $indexStrategy = self::GENERATE)
{
$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)
{
$this->choices = array();
$this->values = array();
$this->preferredViews = array();
$this->remainingViews = array();
$this->addChoices(
$this->preferredViews,
$this->remainingViews,
$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 values for the choices
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getValues()
{
return $this->values;
}
/**
* Returns the choice views of the preferred choices as nested array with
* the choice groups as top-level keys.
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
*/
public function getPreferredViews()
{
return $this->preferredViews;
}
/**
* Returns the choice views of the choices 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 getRemainingViews()
{
return $this->remainingViews;
}
/**
* 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 (self::COPY_CHOICE === $this->valueStrategy) {
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 (0 === count($values)) {
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 (self::COPY_CHOICE === $this->valueStrategy) {
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 (0 === count($choices)) {
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 === $givenChoice) {
$indices[] = $i;
unset($choices[$j]);
if (0 === count($choices)) {
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 (0 === count($values)) {
break 2;
}
}
}
}
return $indices;
}
/**
* Recursively adds the given choices to the list.
*
* @param array $bucketForPreferred The bucket where to store the preferred
* view objects.
* @param array $bucketForRemaining The bucket where to store the
* non-preferred view objects.
* @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)
{
if (!is_array($choices) && !$choices instanceof \Traversable) {
throw new UnexpectedTypeException($choices, 'array or \Traversable');
}
// 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
* view objects.
* @param array $bucketForRemaining The bucket where to store the
* non-preferred view objects.
* @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
* view objects.
* @param array $bucketForRemaining The bucket where to store the
* non-preferred view objects.
* @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);
if ('' === $index || null === $index || !Form::isValidName((string)$index)) {
throw new InvalidConfigurationException('The choice list index "' . $index . '" is invalid. Please set the choice field option "index_generation" to ChoiceList::GENERATE.');
}
$value = $this->createValue($choice);
if (!is_scalar($value)) {
throw new InvalidConfigurationException('The choice list value of type "' . gettype($value) . '" should be a scalar. Please set the choice field option "value_generation" to ChoiceList::GENERATE.');
}
// Always store values as strings to facilitate comparisons
$value = $this->fixValue($value);
$view = new ChoiceView($value, $label);
$this->choices[$index] = $this->fixChoice($choice);
$this->values[$index] = $value;
if ($this->isPreferred($choice, $preferredChoices)) {
$bucketForPreferred[$index] = $view;
} else {
$bucketForRemaining[$index] = $view;
}
}
/**
* 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 (self::COPY_CHOICE === $this->indexStrategy) {
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 (self::COPY_CHOICE === $this->valueStrategy) {
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)
{
foreach ($values as $i => $value) {
$values[$i] = $this->fixValue($value);
}
return $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)
{
foreach ($indices as $i => $index) {
$indices[$i] = $this->fixIndex($index);
}
return $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,130 @@
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 values for the choices
*
* @return array The values with the corresponding choice indices as keys.
*/
function getValues();
/**
* Returns the choice views of the preferred choices as nested array with
* the choice groups as top-level keys.
*
* Example:
*
* <source>
* array(
* 'Group 1' => array(
* 10 => ChoiceView object,
* 20 => ChoiceView object,
* ),
* 'Group 2' => array(
* 30 => ChoiceView object,
* ),
* )
* </source>
*
* @return array A nested array containing the views with the corresponding
* choice indices as keys on the lowest levels and the choice
* group names in the keys of the higher levels.
*/
function getPreferredViews();
/**
* Returns the choice views of the choices that are not preferred as nested
* array with the choice groups as top-level keys.
*
* Example:
*
* <source>
* array(
* 'Group 1' => array(
* 10 => ChoiceView object,
* 20 => ChoiceView object,
* ),
* 'Group 2' => array(
* 30 => ChoiceView object,
* ),
* )
* </source>
*
* @return array A nested array containing the views with the corresponding
* choice indices as keys on the lowest levels and the choice
* group names in the keys of the higher levels.
*
* @see getPreferredValues
*/
function getRemainingViews();
/**
* 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

@ -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 (null !== $this->groupPath) {
$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 (null === $group) {
$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('A "__toString()" method was not found on the objects of type "' . get_class($choice) . '" passed to the choice field. To read a custom getter instead, set the argument $labelPath to the desired property path.');
}
}
}
}

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 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

@ -59,7 +59,7 @@ class PropertyPathMapper implements DataMapperInterface
public function mapDataToForm($data, FormInterface $form)
{
if (!empty($data)) {
if ($form->getAttribute('property_path') !== null) {
if (null !== $form->getAttribute('property_path')) {
$form->setData($form->getAttribute('property_path')->getValue($data));
}
}
@ -77,7 +77,7 @@ class PropertyPathMapper implements DataMapperInterface
public function mapFormToData(FormInterface $form, &$data)
{
if ($form->getAttribute('property_path') !== null && $form->isSynchronized()) {
if (null !== $form->getAttribute('property_path') && $form->isSynchronized()) {
$propertyPath = $form->getAttribute('property_path');
// If the data is identical to the value in $data, we are

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 || null === $value) {
return null;
}
$choices = $this->choiceList->getChoicesForValues(array($value));
if (1 !== count($choices)) {
throw new TransformationFailedException('The choice "' . $value . '" does not exist or is not unique');
}
$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

@ -124,7 +124,7 @@ class DateTimeToArrayTransformer extends BaseDateTimeTransformer
throw new UnexpectedTypeException($value, 'array');
}
if (implode('', $value) === '') {
if ('' === implode('', $value)) {
return null;
}

View File

@ -126,7 +126,7 @@ class NumberToLocalizedStringTransformer implements DataTransformerInterface
{
$formatter = new \NumberFormatter(\Locale::getDefault(), \NumberFormatter::DECIMAL);
if ($this->precision !== null) {
if (null !== $this->precision) {
$formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->precision);
$formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode);
}

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(false !== $index ? 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,44 +38,22 @@ 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();
// 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) {
if ($options['multiple']) {
$builder->add((string) $choice, 'checkbox', array(
'value' => $choice,
'label' => $value,
// 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,
'translation_domain' => $options['translation_domain'],
));
}
}
$this->addSubFields($builder, $options['choice_list']->getPreferredViews(), $options);
$this->addSubFields($builder, $options['choice_list']->getRemainingViews(), $options);
}
// empty value
@ -101,18 +82,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 +104,13 @@ 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->getPreferredViews())
->set('choices', $choiceList->getRemainingViews())
->set('separator', '-------------------')
->set('empty_value', $form->getAttribute('empty_value'))
;
@ -157,6 +137,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,
@ -178,4 +160,36 @@ class ChoiceType extends AbstractType
{
return 'choice';
}
/**
* Adds the sub fields for an expanded choice field.
*
* @param FormBuilder $builder The form builder.
* @param array $choiceViews The choice view objects.
* @param array $options The build options.
*/
private function addSubFields(FormBuilder $builder, array $choiceViews, array $options)
{
foreach ($choiceViews as $i => $choiceView) {
if (is_array($choiceView)) {
// Flatten groups
$this->addSubFields($builder, $choiceView, $options);
} elseif ($options['multiple']) {
$builder->add((string) $i, 'checkbox', array(
'value' => $choiceView->getValue(),
'label' => $choiceView->getLabel(),
// 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) $i, 'radio', array(
'value' => $choiceView->getValue(),
'label' => $choiceView->getLabel(),
'translation_domain' => $options['translation_domain'],
));
}
}
}
}

View File

@ -25,7 +25,7 @@ class CollectionType extends AbstractType
public function buildForm(FormBuilder $builder, array $options)
{
if ($options['allow_add'] && $options['prototype']) {
$prototype = $builder->create('$$' . $options['prototype_name'] . '$$', $options['type'], $options['options']);
$prototype = $builder->create($options['prototype_name'], $options['type'], $options['options']);
$builder->setAttribute('prototype', $prototype->getForm());
}
@ -78,7 +78,7 @@ class CollectionType extends AbstractType
'allow_add' => false,
'allow_delete' => false,
'prototype' => true,
'prototype_name' => 'name',
'prototype_name' => '__name__',
'type' => 'text',
'options' => array(),
);

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

@ -45,7 +45,7 @@ class DateTimeType extends AbstractType
throw new FormException(sprintf('Options "date_widget" and "time_widget" need to be identical. Used: "date_widget" = "%s" and "time_widget" = "%s".', $options['date_widget'] ?: 'choice', $options['time_widget'] ?: 'choice'));
}
if ($options['widget'] === 'single_text') {
if ('single_text' === $options['widget']) {
$builder->appendClientTransformer(new DateTimeToStringTransformer($options['data_timezone'], $options['user_timezone'], $format));
} else {
// Only pass a subset of the options to children
@ -105,15 +105,15 @@ class DateTimeType extends AbstractType
;
}
if ($options['input'] === 'string') {
if ('string' === $options['input']) {
$builder->appendNormTransformer(new ReversedTransformer(
new DateTimeToStringTransformer($options['data_timezone'], $options['data_timezone'], $format)
));
} elseif ($options['input'] === 'timestamp') {
} elseif ('timestamp' === $options['input']) {
$builder->appendNormTransformer(new ReversedTransformer(
new DateTimeToTimestampTransformer($options['data_timezone'], $options['data_timezone'])
));
} elseif ($options['input'] === 'array') {
} elseif ('array' === $options['input']) {
$builder->appendNormTransformer(new ReversedTransformer(
new DateTimeToArrayTransformer($options['data_timezone'], $options['data_timezone'], $parts)
));
@ -200,7 +200,7 @@ class DateTimeType extends AbstractType
*/
public function getParent(array $options)
{
return $options['widget'] === 'single_text' ? 'field' : 'form';
return 'single_text' === $options['widget'] ? 'field' : 'form';
}
/**

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;
@ -62,35 +63,47 @@ class DateType extends AbstractType
$pattern
);
if ($options['widget'] === 'single_text') {
if ('single_text' === $options['widget']) {
$builder->appendClientTransformer(new DateTimeToLocalizedStringTransformer($options['data_timezone'], $options['user_timezone'], $format, \IntlDateFormatter::NONE, \IntlDateFormatter::GREGORIAN, $pattern));
} else {
$yearOptions = $monthOptions = $dayOptions = array();
if ($options['widget'] === 'choice') {
if ('choice' === $options['widget']) {
if (is_array($options['empty_value'])) {
$options['empty_value'] = array_merge(array('year' => null, 'month' => null, 'day' => null), $options['empty_value']);
} else {
$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'],
);
@ -110,15 +123,15 @@ class DateType extends AbstractType
;
}
if ($options['input'] === 'string') {
if ('string' === $options['input']) {
$builder->appendNormTransformer(new ReversedTransformer(
new DateTimeToStringTransformer($options['data_timezone'], $options['data_timezone'], 'Y-m-d')
));
} elseif ($options['input'] === 'timestamp') {
} elseif ('timestamp' === $options['input']) {
$builder->appendNormTransformer(new ReversedTransformer(
new DateTimeToTimestampTransformer($options['data_timezone'], $options['data_timezone'])
));
} elseif ($options['input'] === 'array') {
} elseif ('array' === $options['input']) {
$builder->appendNormTransformer(new ReversedTransformer(
new DateTimeToArrayTransformer($options['data_timezone'], $options['data_timezone'], array('year', 'month', 'day'))
));
@ -199,7 +212,7 @@ class DateType extends AbstractType
*/
public function getParent(array $options)
{
return $options['widget'] === 'single_text' ? 'field' : 'form';
return 'single_text' === $options['widget'] ? 'field' : 'form';
}
/**
@ -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

@ -88,6 +88,11 @@ class FieldType extends AbstractType
} else {
$id = $name;
$fullName = $name;
// Strip leading underscores and digits. These are allowed in
// form names, but not in HTML4 ID attributes.
// http://www.w3.org/TR/html401/struct/global.html#adef-id
$id = ltrim($id, '_0123456789');
}
$types = array();

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;
@ -35,37 +35,52 @@ class TimeType extends AbstractType
$parts[] = 'second';
}
if ($options['widget'] === 'single_text') {
if ('single_text' === $options['widget']) {
$builder->appendClientTransformer(new DateTimeToStringTransformer($options['data_timezone'], $options['user_timezone'], $format));
} else {
$hourOptions = $minuteOptions = $secondOptions = array();
if ($options['widget'] === 'choice') {
if ('choice' === $options['widget']) {
if (is_array($options['empty_value'])) {
$options['empty_value'] = array_merge(array('hour' => null, 'minute' => null, 'second' => null), $options['empty_value']);
} else {
$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'],
);
}
@ -88,18 +103,18 @@ class TimeType extends AbstractType
$builder->add('second', $options['widget'], $secondOptions);
}
$builder->appendClientTransformer(new DateTimeToArrayTransformer($options['data_timezone'], $options['user_timezone'], $parts, $options['widget'] === 'text'));
$builder->appendClientTransformer(new DateTimeToArrayTransformer($options['data_timezone'], $options['user_timezone'], $parts, 'text' === $options['widget']));
}
if ($options['input'] === 'string') {
if ('string' === $options['input']) {
$builder->appendNormTransformer(new ReversedTransformer(
new DateTimeToStringTransformer($options['data_timezone'], $options['data_timezone'], $format)
));
} elseif ($options['input'] === 'timestamp') {
} elseif ('timestamp' === $options['input']) {
$builder->appendNormTransformer(new ReversedTransformer(
new DateTimeToTimestampTransformer($options['data_timezone'], $options['data_timezone'])
));
} elseif ($options['input'] === 'array') {
} elseif ('array' === $options['input']) {
$builder->appendNormTransformer(new ReversedTransformer(
new DateTimeToArrayTransformer($options['data_timezone'], $options['data_timezone'], $parts)
));
@ -169,7 +184,7 @@ class TimeType extends AbstractType
*/
public function getParent(array $options)
{
return $options['widget'] === 'single_text' ? 'field' : 'form';
return 'single_text' === $options['widget'] ? 'field' : 'form';
}
/**

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

@ -0,0 +1,66 @@
<?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\View;
/**
* Represents a choice in templates.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoiceView
{
/**
* The view representation of the choice.
*
* @var string
*/
private $value;
/**
* The label displayed to humans.
*
* @var string
*/
private $label;
/**
* Creates a new ChoiceView.
*
* @param string $value The view representation of the choice.
* @param string $label The label displayed to humans.
*/
public function __construct($value, $label)
{
$this->value = $value;
$this->label = $label;
}
/**
* Returns the choice value.
*
* @return string The view representation of the choice.
*/
public function getValue()
{
return $this->value;
}
/**
* Returns the choice label.
*
* @return string The label displayed to humans.
*/
public function getLabel()
{
return $this->label;
}
}

View File

@ -229,7 +229,7 @@ class DelegatingValidator implements FormValidatorInterface
$nestedNamePath = $namePath.'.'.$child->getName();
if (strpos($path, '[') === 0) {
if (0 === strpos($path, '[')) {
$nestedDataPaths = array($dataPath.$path);
} else {
$nestedDataPaths = array($dataPath.'.'.$path);

View File

@ -193,6 +193,10 @@ class Form implements \IteratorAggregate, FormInterface
$required = false, $readOnly = false, $errorBubbling = false,
$emptyData = null, array $attributes = array())
{
$name = (string) $name;
self::validateName($name);
foreach ($clientTransformers as $transformer) {
if (!$transformer instanceof DataTransformerInterface) {
throw new UnexpectedTypeException($transformer, 'Symfony\Component\Form\DataTransformerInterface');
@ -211,7 +215,7 @@ class Form implements \IteratorAggregate, FormInterface
}
}
$this->name = (string) $name;
$this->name = $name;
$this->dispatcher = $dispatcher;
$this->types = $types;
$this->clientTransformers = $clientTransformers;
@ -1056,4 +1060,45 @@ class Form implements \IteratorAggregate, FormInterface
return $value;
}
/**
* Validates whether the given variable is a valid form name.
*
* @param string $name The tested form name.
*
* @throws UnexpectedTypeException If the name is not a string.
* @throws \InvalidArgumentException If the name contains invalid characters.
*/
static public function validateName($name)
{
if (!is_string($name)) {
throw new UnexpectedTypeException($name, 'string');
}
if (!self::isValidName($name)) {
throw new \InvalidArgumentException(sprintf(
'The name "%s" contains illegal characters. Names should start with a letter, digit or underscore and only contains letters, digits, numbers, underscores ("_"), hyphens ("-") and colons (":").',
$name
));
}
}
/**
* Returns whether the given variable contains a valid form name.
*
* A name is accepted if it
*
* * is empty
* * starts with a letter, digit or underscore
* * contains only letters, digits, numbers, underscores ("_"),
* hyphens ("-") and colons (":")
*
* @param string $name The tested form name.
*
* @return Boolean Whether the name is valid.
*/
static public function isValidName($name)
{
return '' === $name || preg_match('/^[a-zA-Z0-9_][a-zA-Z0-9_\-:]*$/D', $name);
}
}

View File

@ -123,6 +123,10 @@ class FormBuilder
*/
public function __construct($name, FormFactoryInterface $factory, EventDispatcherInterface $dispatcher, $dataClass = null)
{
$name = (string) $name;
Form::validateName($name);
$this->name = $name;
$this->factory = $factory;
$this->dispatcher = $dispatcher;

View File

@ -11,22 +11,11 @@
namespace Symfony\Component\Form\Util;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
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 +38,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

@ -67,7 +67,7 @@ class PropertyPath implements \IteratorAggregate
$pattern = '/^(([^\.\[]+)|\[([^\]]+)\])(.*)/';
while (preg_match($pattern, $remaining, $matches)) {
if ($matches[2] !== '') {
if ('' !== $matches[2]) {
$this->elements[] = $matches[2];
$this->isIndex[] = false;
} else {

View File

@ -19,6 +19,7 @@ use Symfony\Tests\Bridge\Doctrine\DoctrineOrmTestCase;
use Symfony\Tests\Bridge\Doctrine\Fixtures\ItemGroupEntity;
use Symfony\Tests\Bridge\Doctrine\Fixtures\SingleIdentEntity;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
class EntityChoiceListTest extends DoctrineOrmTestCase
{
@ -89,7 +90,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 +133,11 @@ class EntityChoiceListTest extends DoctrineOrmTestCase
)
);
$this->assertSame(array(
'group1' => array(1 => 'Foo'),
'group2' => array(2 => 'Bar')
), $choiceList->getChoices());
$this->assertSame(array(1 => $entity1, 2 => $entity2), $choiceList->getChoices());
$this->assertEquals(array(
'group1' => array(1 => new ChoiceView('1', 'Foo')),
'group2' => array(2 => new ChoiceView('2', 'Bar'))
), $choiceList->getRemainingViews());
}
public function testGroupBySupportsString()
@ -164,11 +166,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 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')),
'Group2' => array(3 => new ChoiceView('3', 'Baz')),
4 => new ChoiceView('4', 'Boo!')
), $choiceList->getRemainingViews());
}
public function testGroupByInvalidPropertyPathReturnsFlatChoices()
@ -188,13 +191,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

@ -29,6 +29,7 @@ use Symfony\Tests\Bridge\Doctrine\Fixtures\CompositeStringIdentEntity;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
class EntityTypeTest extends TypeTestCase
{
@ -109,7 +110,7 @@ class EntityTypeTest extends TypeTestCase
'property' => 'name'
));
$this->assertEquals(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choices'));
$this->assertEquals(array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), $field->createView()->get('choices'));
}
public function testSetDataToUninitializedEntityWithNonRequiredToString()
@ -125,7 +126,7 @@ class EntityTypeTest extends TypeTestCase
'required' => false,
));
$this->assertEquals(array("1" => 'Foo', "2" => 'Bar'), $field->createView()->get('choices'));
$this->assertEquals(array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), $field->createView()->get('choices'));
}
public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder()
@ -144,7 +145,7 @@ class EntityTypeTest extends TypeTestCase
'query_builder' => $qb
));
$this->assertEquals(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choices'));
$this->assertEquals(array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), $field->createView()->get('choices'));
}
/**
@ -185,7 +186,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 +200,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 +214,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 +228,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 +242,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 +255,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 +276,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 +299,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 +325,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 +356,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 +382,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 +413,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 +434,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 +489,10 @@ class EntityTypeTest extends TypeTestCase
$field->bind('2');
$this->assertEquals(array(1 => 'Foo', 2 => 'Bar'), $field->createView()->get('choices'));
$this->assertEquals(array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')), $field->createView()->get('choices'));
$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,11 +514,11 @@ class EntityTypeTest extends TypeTestCase
$field->bind('2');
$this->assertEquals(2, $field->getClientData());
$this->assertSame('2', $field->getClientData());
$this->assertEquals(array(
'Group1' => array(1 => 'Foo', '2' => 'Bar'),
'Group2' => array(3 => 'Baz'),
'4' => 'Boo!'
'Group1' => array(1 => new ChoiceView('1', 'Foo'), 2 => new ChoiceView('2', 'Bar')),
'Group2' => array(3 => new ChoiceView('3', 'Baz')),
'4' => new ChoiceView('4', 'Boo!')
), $field->createView()->get('choices'));
}
@ -652,8 +653,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 +675,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)

File diff suppressed because it is too large Load Diff

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,166 @@
<?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\View\ChoiceView;
class ChoiceListTest 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 ChoiceList(
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 ChoiceList(
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('0', '1', '2', '3'), $this->list->getValues());
$this->assertEquals(array(1 => new ChoiceView('1', 'B')), $this->list->getPreferredViews());
$this->assertEquals(array(0 => new ChoiceView('0', 'A'), 2 => new ChoiceView('2', 'C'), 3 => new ChoiceView('3', 'D')), $this->list->getRemainingViews());
}
public function testInitNestedArray()
{
$this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
$this->assertSame(array('0', '1', '2', '3'), $this->list->getValues());
$this->assertEquals(array(
'Group 1' => array(1 => new ChoiceView('1', 'B')),
'Group 2' => array(2 => new ChoiceView('2', 'C'))
), $this->list->getPreferredViews());
$this->assertEquals(array(
'Group 1' => array(0 => new ChoiceView('0', 'A')),
'Group 2' => array(3 => new ChoiceView('3', 'D'))
), $this->list->getRemainingViews());
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidConfigurationException
*/
public function testInitIndexCopyChoiceWithInvalidIndex()
{
new ChoiceList(
array('a.'),
array('A'),
array(),
ChoiceList::GENERATE,
ChoiceList::COPY_CHOICE
);
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidConfigurationException
*/
public function testInitValueCopyChoiceWithInvalidValue()
{
new ChoiceList(
array($this->obj1),
array('A'),
array(),
ChoiceList::COPY_CHOICE,
ChoiceList::GENERATE
);
}
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,231 @@
<?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;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
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('0', '1', '2', '3'), $this->list->getValues());
$this->assertEquals(array(1 => new ChoiceView('1', 'B')), $this->list->getPreferredViews());
$this->assertEquals(array(0 => new ChoiceView('0', 'A'), 2 => new ChoiceView('2', 'C'), 3 => new ChoiceView('3', 'D')), $this->list->getRemainingViews());
}
public function testInitNestedArray()
{
$this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
$this->assertSame(array('0', '1', '2', '3'), $this->list->getValues());
$this->assertEquals(array(
'Group 1' => array(1 => new ChoiceView('1', 'B')),
'Group 2' => array(2 => new ChoiceView('2', 'C'))
), $this->list->getPreferredViews());
$this->assertEquals(array(
'Group 1' => array(0 => new ChoiceView('0', 'A')),
'Group 2' => array(3 => new ChoiceView('3', 'D'))
), $this->list->getRemainingViews());
}
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('0', '1', '2', '3', '4', '5'), $this->list->getValues());
$this->assertEquals(array(
'Group 1' => array(1 => new ChoiceView('1', 'B')),
'Group 2' => array(2 => new ChoiceView('2', 'C'))
), $this->list->getPreferredViews());
$this->assertEquals(array(
'Group 1' => array(0 => new ChoiceView('0', 'A')),
'Group 2' => array(3 => new ChoiceView('3', 'D')),
4 => new ChoiceView('4', 'E'),
5 => new ChoiceView('5', 'F'),
), $this->list->getRemainingViews());
}
/**
* @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('10', '20', '30', '40'), $this->list->getValues());
$this->assertEquals(array(1 => new ChoiceView('20', 'B'), 2 => new ChoiceView('30', 'C')), $this->list->getPreferredViews());
$this->assertEquals(array(0 => new ChoiceView('10', 'A'), 3 => new ChoiceView('40', 'D')), $this->list->getRemainingViews());
}
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 => '0', 20 => '1', 30 => '2', 40 => '3'), $this->list->getValues());
$this->assertEquals(array(20 => new ChoiceView('1', 'B'), 30 => new ChoiceView('2', 'C')), $this->list->getPreferredViews());
$this->assertEquals(array(10 => new ChoiceView('0', 'A'), 40 => new ChoiceView('3', 'D')), $this->list->getRemainingViews());
}
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('0', '1', '2', '3'), $this->list->getValues());
$this->assertEquals(array(0 => new ChoiceView('0', 'A'), 1 => new ChoiceView('1', 'B'), 2 => new ChoiceView('2', 'C'), 3 => new ChoiceView('3', 'D')), $this->list->getRemainingViews());
}
/**
* @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');
new ObjectChoiceList(
array($this->obj1, $this->obj2, $this->obj3, $this->obj4)
);
}
}

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,210 @@
<?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;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
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 => '0', 1 => '1', 2 => '2'), $this->list->getValues());
$this->assertEquals(array(1 => new ChoiceView('1', 'B')), $this->list->getPreferredViews());
$this->assertEquals(array(0 => new ChoiceView('0', 'A'), 2 => new ChoiceView('2', 'C')), $this->list->getRemainingViews());
}
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->getValues());
$this->assertEquals(array(1 => new ChoiceView('b', 'B')), $this->list->getPreferredViews());
$this->assertEquals(array(0 => new ChoiceView('a', 'A'), 2 => new ChoiceView('c', 'C')), $this->list->getRemainingViews());
}
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' => '0', 'b' => '1', 'c' => '2'), $this->list->getValues());
$this->assertEquals(array('b' => new ChoiceView('1', 'B')), $this->list->getPreferredViews());
$this->assertEquals(array('a' => new ChoiceView('0', 'A'), 'c' => new ChoiceView('2', 'C')), $this->list->getRemainingViews());
}
public function testInitNestedArray()
{
$this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd'), $this->list->getChoices());
$this->assertSame(array(0 => '0', 1 => '1', 2 => '2', 3 => '3'), $this->list->getValues());
$this->assertEquals(array(
'Group 1' => array(1 => new ChoiceView('1', 'B')),
'Group 2' => array(2 => new ChoiceView('2', 'C'))
), $this->list->getPreferredViews());
$this->assertEquals(array(
'Group 1' => array(0 => new ChoiceView('0', 'A')),
'Group 2' => array(3 => new ChoiceView('3', 'D'))
), $this->list->getRemainingViews());
}
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,9 @@
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\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
class ChoiceTypeTest extends TypeTestCase
@ -31,6 +34,8 @@ class ChoiceTypeTest extends TypeTestCase
4 => 'Roman',
);
private $objectChoices;
private $stringButNumericChoices = array(
'0' => 'Bernhard',
'1' => 'Fabien',
@ -51,8 +56,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 +97,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 +125,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 +185,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 +222,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 +258,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 +287,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 +348,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 +371,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 +392,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 +446,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 +599,12 @@ class ChoiceTypeTest extends TypeTestCase
));
$view = $form->createView();
$this->assertSame($choices, $view->get('choices'));
$this->assertEquals(array(
new ChoiceView('0', 'A'),
new ChoiceView('1', 'B'),
new ChoiceView('2', 'C'),
new ChoiceView('3', 'D'),
), $view->get('choices'));
}
public function testPassPreferredChoicesToView()
@ -449,8 +616,41 @@ 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->assertEquals(array(
0 => new ChoiceView('0', 'A'),
2 => new ChoiceView('2', 'C'),
), $view->get('choices'));
$this->assertEquals(array(
1 => new ChoiceView('1', 'B'),
3 => new ChoiceView('3', 'D'),
), $view->get('preferred_choices'));
}
public function testPassHierarchicalChoicesToView()
{
$form = $this->factory->create('choice', null, array(
'choices' => $this->groupedChoices,
'preferred_choices' => array('b', 'd'),
));
$view = $form->createView();
$this->assertEquals(array(
'Symfony' => array(
0 => new ChoiceView('0', 'Bernhard'),
2 => new ChoiceView('2', 'Kris'),
),
'Doctrine' => array(
4 => new ChoiceView('4', 'Roman'),
),
), $view->get('choices'));
$this->assertEquals(array(
'Symfony' => array(
1 => new ChoiceView('1', 'Fabien'),
),
'Doctrine' => array(
3 => new ChoiceView('3', 'Jon'),
),
), $view->get('preferred_choices'));
}
public function testAdjustFullNameForMultipleNonExpanded()

View File

@ -125,7 +125,7 @@ class CollectionFormTest extends TypeTestCase
'prototype' => false,
));
$this->assertFalse($form->has('$$name$$'));
$this->assertFalse($form->has('__name__'));
}
public function testPrototypeMultipartPropagation()
@ -150,7 +150,7 @@ class CollectionFormTest extends TypeTestCase
));
$data = $form->getData();
$this->assertFalse(isset($data['$$name$$']));
$this->assertFalse(isset($data['__name__']));
}
public function testGetDataDoesNotContainsPrototypeNameAfterDataAreSet()
@ -163,7 +163,7 @@ class CollectionFormTest extends TypeTestCase
$form->setData(array('foobar.png'));
$data = $form->getData();
$this->assertFalse(isset($data['$$name$$']));
$this->assertFalse(isset($data['__name__']));
}
public function testPrototypeNameOption()
@ -174,15 +174,15 @@ class CollectionFormTest extends TypeTestCase
'allow_add' => true,
));
$this->assertSame('$$name$$', $form->getAttribute('prototype')->getName(), '$$name$$ is the default');
$this->assertSame('__name__', $form->getAttribute('prototype')->getName(), '__name__ is the default');
$form = $this->factory->create('collection', null, array(
'type' => 'field',
'prototype' => true,
'allow_add' => true,
'prototype_name' => 'test',
'prototype_name' => '__test__',
));
$this->assertSame('$$test$$', $form->getAttribute('prototype')->getName());
$this->assertSame('__test__', $form->getAttribute('prototype')->getName());
}
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Tests\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
class CountryTypeTest extends LocalizedTestCase
{
@ -22,16 +23,11 @@ class CountryTypeTest extends LocalizedTestCase
$view = $form->createView();
$choices = $view->get('choices');
$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->assertEquals(new ChoiceView('DE', 'Deutschland'), $choices['DE']);
$this->assertEquals(new ChoiceView('GB', 'Vereinigtes Königreich'), $choices['GB']);
$this->assertEquals(new ChoiceView('US', 'Vereinigte Staaten'), $choices['US']);
$this->assertEquals(new ChoiceView('FR', 'Frankreich'), $choices['FR']);
$this->assertEquals(new ChoiceView('MY', 'Malaysia'), $choices['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

@ -11,8 +11,9 @@
namespace Symfony\Tests\Component\Form\Extension\Core\Type;
require_once __DIR__ . '/LocalizedTestCase.php';
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
require_once __DIR__ . '/LocalizedTestCase.php';
class DateTypeTest extends LocalizedTestCase
{
@ -325,7 +326,10 @@ class DateTypeTest extends LocalizedTestCase
$view = $form->createView();
$this->assertSame(array(2010 => '2010', 2011 => '2011'), $view->getChild('year')->get('choices'));
$this->assertEquals(array(
2010 => new ChoiceView('2010', '2010'),
2011 => new ChoiceView('2011', '2011'),
), $view->getChild('year')->get('choices'));
}
public function testMonthsOption()
@ -336,7 +340,70 @@ class DateTypeTest extends LocalizedTestCase
$view = $form->createView();
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('month')->get('choices'));
$this->assertEquals(array(
6 => new ChoiceView('6', '06'),
7 => new ChoiceView('7', '07'),
), $view->getChild('month')->get('choices'));
}
public function testMonthsOptionNumericIfFormatContainsNoMonth()
{
$form = $this->factory->create('date', null, array(
'months' => array(6, 7),
'format' => 'yy',
));
$view = $form->createView();
$this->assertEquals(array(
6 => new ChoiceView('6', '06'),
7 => new ChoiceView('7', '07'),
), $view->getChild('month')->get('choices'));
}
public function testMonthsOptionShortFormat()
{
$form = $this->factory->create('date', null, array(
'months' => array(1, 4),
'format' => 'dd.MMM.yy',
));
$view = $form->createView();
$this->assertEquals(array(
1 => new ChoiceView('1', 'Jän'),
4 => new ChoiceView('4', 'Apr')
), $view->getChild('month')->get('choices'));
}
public function testMonthsOptionLongFormat()
{
$form = $this->factory->create('date', null, array(
'months' => array(1, 4),
'format' => 'dd.MMMM.yy',
));
$view = $form->createView();
$this->assertEquals(array(
1 => new ChoiceView('1', 'Jänner'),
4 => new ChoiceView('4', 'April'),
), $view->getChild('month')->get('choices'));
}
public function testMonthsOptionLongFormatWithDifferentTimezone()
{
$form = $this->factory->create('date', null, array(
'months' => array(1, 4),
'format' => 'dd.MMMM.yy',
));
$view = $form->createView();
$this->assertEquals(array(
1 => new ChoiceView('1', 'Jänner'),
4 => new ChoiceView('4', 'April'),
), $view->getChild('month')->get('choices'));
}
public function testIsDayWithinRangeReturnsTrueIfWithin()
@ -347,7 +414,10 @@ class DateTypeTest extends LocalizedTestCase
$view = $form->createView();
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('day')->get('choices'));
$this->assertEquals(array(
6 => new ChoiceView('6', '06'),
7 => new ChoiceView('7', '07'),
), $view->getChild('day')->get('choices'));
}
public function testIsPartiallyFilledReturnsFalseIfSingleText()

View File

@ -117,6 +117,16 @@ class FieldTypeTest extends TypeTestCase
$this->assertEquals('name', $view->get('full_name'));
}
public function testStripLeadingUnderscoresAndDigitsFromId()
{
$form = $this->factory->createNamed('field', '_09name');
$view = $form->createView();
$this->assertEquals('name', $view->get('id'));
$this->assertEquals('_09name', $view->get('name'));
$this->assertEquals('_09name', $view->get('full_name'));
}
public function testPassIdAndNameToViewWithParent()
{
$parent = $this->factory->createNamed('field', 'parent');

View File

@ -11,6 +11,7 @@
namespace Symfony\Tests\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
class LanguageTypeTest extends LocalizedTestCase
{
@ -21,17 +22,13 @@ 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(new ChoiceView('en', 'Englisch'), $choices, '', false, false);
$this->assertContains(new ChoiceView('en_GB', 'Britisches Englisch'), $choices, '', false, false);
$this->assertContains(new ChoiceView('en_US', 'Amerikanisches Englisch'), $choices, '', false, false);
$this->assertContains(new ChoiceView('fr', 'Französisch'), $choices, '', false, false);
$this->assertContains(new ChoiceView('my', 'Birmanisch'), $choices, '', false, false);
}
public function testMultipleLanguagesIsNotIncluded()
@ -40,6 +37,6 @@ class LanguageTypeTest extends LocalizedTestCase
$view = $form->createView();
$choices = $view->get('choices');
$this->assertArrayNotHasKey('mul', $choices);
$this->assertNotContains(new ChoiceView('mul', 'Mehrsprachig'), $choices, '', false, false);
}
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Tests\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
class LocaleTypeTest extends LocalizedTestCase
{
@ -22,11 +23,8 @@ class LocaleTypeTest extends LocalizedTestCase
$view = $form->createView();
$choices = $view->get('choices');
$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(new ChoiceView('en', 'Englisch'), $choices, '', false, false);
$this->assertContains(new ChoiceView('en_GB', 'Englisch (Vereinigtes Königreich)'), $choices, '', false, false);
$this->assertContains(new ChoiceView('zh_Hant_MO', 'Chinesisch (traditionell, Sonderverwaltungszone Macao)'), $choices, '', false, false);
}
}

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

@ -11,6 +11,8 @@
namespace Symfony\Tests\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
require_once __DIR__ . '/LocalizedTestCase.php';
@ -243,7 +245,10 @@ class TimeTypeTest extends LocalizedTestCase
$view = $form->createView();
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('hour')->get('choices'));
$this->assertEquals(array(
6 => new ChoiceView('6', '06'),
7 => new ChoiceView('7', '07'),
), $view->getChild('hour')->get('choices'));
}
public function testIsMinuteWithinRange_returnsTrueIfWithin()
@ -254,7 +259,10 @@ class TimeTypeTest extends LocalizedTestCase
$view = $form->createView();
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('minute')->get('choices'));
$this->assertEquals(array(
6 => new ChoiceView('6', '06'),
7 => new ChoiceView('7', '07'),
), $view->getChild('minute')->get('choices'));
}
public function testIsSecondWithinRange_returnsTrueIfWithin()
@ -266,7 +274,10 @@ class TimeTypeTest extends LocalizedTestCase
$view = $form->createView();
$this->assertSame(array(6 => '06', 7 => '07'), $view->getChild('second')->get('choices'));
$this->assertEquals(array(
6 => new ChoiceView('6', '06'),
7 => new ChoiceView('7', '07'),
), $view->getChild('second')->get('choices'));
}
public function testIsPartiallyFilled_returnsFalseIfCompletelyEmpty()

View File

@ -11,6 +11,7 @@
namespace Symfony\Tests\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
class TimezoneTypeTest extends TypeTestCase
{
@ -19,13 +20,12 @@ 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(new ChoiceView('Africa/Kinshasa', 'Kinshasa'), $choices['Africa'], '', false, false);
$this->assertArrayHasKey('America', $choices);
$this->assertArrayHasKey('America/New_York', $choices['America']);
$this->assertEquals('New York', $choices['America']['America/New_York']);
$this->assertContains(new ChoiceView('America/New_York', 'New York'), $choices['America'], '', false, false);
}
}

View File

@ -36,6 +36,37 @@ class FormBuilderTest extends \PHPUnit_Framework_TestCase
$this->builder = null;
}
public function getHtml4Ids()
{
// The full list is tested in FormTest, since both Form and FormBuilder
// use the same implementation internally
return array(
array('#', false),
array('a ', false),
array("a\t", false),
array("a\n", false),
array('a.', false),
);
}
/**
* @dataProvider getHtml4Ids
*/
public function testConstructAcceptsOnlyNamesValidAsIdsInHtml4($name, $accepted)
{
try {
new FormBuilder($name, $this->factory, $this->dispatcher);
if (!$accepted) {
$this->fail(sprintf('The value "%s" should not be accepted', $name));
}
} catch (\InvalidArgumentException $e) {
// if the value was not accepted, but should be, rethrow exception
if ($accepted) {
throw $e;
}
}
}
/**
* Changing the name is not allowed, otherwise the name and property path
* are not synchronized anymore

View File

@ -60,6 +60,62 @@ class FormTest extends \PHPUnit_Framework_TestCase
new Form('name', $this->dispatcher, array(), array(), array(), null, $validators);
}
public function getHtml4Ids()
{
return array(
array('a0', true),
array('a9', true),
array('z0', true),
array('A0', true),
array('A9', true),
array('Z0', true),
array('#', false),
array('a#', false),
array('a$', false),
array('a%', false),
array('a ', false),
array("a\t", false),
array("a\n", false),
array('a-', true),
array('a_', true),
array('a:', true),
// Periods are allowed by the HTML4 spec, but disallowed by us
// because they break the generated property paths
array('a.', false),
// Contrary to the HTML4 spec, we allow names starting with a
// number, otherwise naming fields by collection indices is not
// possible.
// For root forms, leading digits will be stripped from the
// "id" attribute to produce valid HTML4.
array('0', true),
array('9', true),
// Contrary to the HTML4 spec, we allow names starting with an
// underscore, since this is already a widely used practice in
// Symfony2.
// For root forms, leading underscores will be stripped from the
// "id" attribute to produce valid HTML4.
array('_', true),
);
}
/**
* @dataProvider getHtml4Ids
*/
public function testConstructAcceptsOnlyNamesValidAsIdsInHtml4($name, $accepted)
{
try {
new Form($name, $this->dispatcher);
if (!$accepted) {
$this->fail(sprintf('The value "%s" should not be accepted', $name));
}
} catch (\InvalidArgumentException $e) {
// if the value was not accepted, but should be, rethrow exception
if ($accepted) {
throw $e;
}
}
}
public function testDataIsInitializedEmpty()
{
$norm = new FixedDataTransformer(array(

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'),