Ver código fonte

[Form] Refactored parts of the choice fields into ChoiceList instances

Bernhard Schussek 14 anos atrás
pai
commit
813ec54fa1

+ 33 - 99
src/Symfony/Component/Form/ChoiceField.php

@@ -11,7 +11,7 @@
 
 namespace Symfony\Component\Form;
 
-use Symfony\Component\Form\Exception\InvalidOptionsException;
+use Symfony\Component\Form\ChoiceList\DefaultChoiceList;
 
 /**
  * Lets the user select between different choices.
@@ -37,62 +37,46 @@ use Symfony\Component\Form\Exception\InvalidOptionsException;
  */
 class ChoiceField extends HybridField
 {
-    /**
-     * Stores the preferred choices with the choices as keys
-     * @var array
-     */
-    protected $preferredChoices = array();
-
-    /**
-     * Stores the choices
-     * You should only access this property through getChoices()
-     * @var array
-     */
-    private $choices = array();
+    protected $choiceList;
 
-    protected function configure()
+    public function __construct($name = null, array $options = array())
     {
-        $this->addRequiredOption('choices');
-        $this->addOption('preferred_choices', array());
-        $this->addOption('multiple', false);
-        $this->addOption('expanded', false);
-        $this->addOption('empty_value', '');
-
-        parent::configure();
-
-        $choices = $this->getOption('choices');
-
-        if (!is_array($choices) && !$choices instanceof \Closure) {
-            throw new InvalidOptionsException('The choices option must be an array or a closure', array('choices'));
-        }
-
-        if (!is_array($this->getOption('preferred_choices'))) {
-            throw new InvalidOptionsException('The preferred_choices option must be an array', array('preferred_choices'));
-        }
-
-        if (count($this->getOption('preferred_choices')) > 0) {
-            $this->preferredChoices = array_flip($this->getOption('preferred_choices'));
-        }
+        parent::__construct($name, $options);
 
+        // until we have DI, this MUST happen after configure()
         if ($this->isExpanded()) {
             $this->setFieldMode(self::FORM);
 
-            $choices = $this->getChoices();
-
-            foreach ($this->preferredChoices as $choice => $_) {
-                $this->add($this->newChoiceField($choice, $choices[$choice]));
+            foreach ($this->choiceList->getPreferredChoices() as $choice => $value) {
+                $this->add($this->newChoiceField($choice, $value));
             }
 
-            foreach ($choices as $choice => $value) {
-                if (!isset($this->preferredChoices[$choice])) {
-                    $this->add($this->newChoiceField($choice, $value));
-                }
+            foreach ($this->choiceList->getOtherChoices() as $choice => $value) {
+                $this->add($this->newChoiceField($choice, $value));
             }
         } else {
             $this->setFieldMode(self::FIELD);
         }
     }
 
+    protected function configure()
+    {
+        $this->addRequiredOption('choices');
+        $this->addOption('preferred_choices', array());
+        $this->addOption('multiple', false);
+        $this->addOption('expanded', false);
+        $this->addOption('empty_value', '');
+
+        parent::configure();
+
+        $this->choiceList = new DefaultChoiceList(
+            $this->getOption('choices'),
+            $this->getOption('preferred_choices'),
+            $this->getOption('empty_value'),
+            $this->isRequired()
+        );
+    }
+
     public function getName()
     {
         // TESTME
@@ -108,79 +92,29 @@ class ChoiceField extends HybridField
         return $name;
     }
 
-    /**
-     * Initializes the choices
-     *
-     * If the choices were given as a closure, the closure is executed now.
-     *
-     * @return array
-     */
-    protected function initializeChoices()
-    {
-        if (!$this->choices) {
-            $this->choices = $this->getInitializedChoices();
-
-            if (!$this->isRequired()) {
-                $this->choices = array('' => $this->getOption('empty_value')) + $this->choices;
-            }
-        }
-    }
-
-    protected function getInitializedChoices()
-    {
-        $choices = $this->getOption('choices');
-
-        if ($choices instanceof \Closure) {
-            $choices = $choices->__invoke();
-        }
-
-        if (!is_array($choices)) {
-            throw new InvalidOptionsException('The "choices" option must be an array or a closure returning an array', array('choices'));
-        }
-
-        return $choices;
-    }
-
-    /**
-     * Returns the choices
-     *
-     * If the choices were given as a closure, the closure is executed on
-     * the first call of this method.
-     *
-     * @return array
-     */
-    protected function getChoices()
-    {
-        $this->initializeChoices();
-
-        return $this->choices;
-    }
-
     public function getPreferredChoices()
     {
-        return array_intersect_key($this->getChoices(), $this->preferredChoices);
+        return $this->choiceList->getPreferredChoices();
     }
 
     public function getOtherChoices()
     {
-        return array_diff_key($this->getChoices(), $this->preferredChoices);
+        return $this->choiceList->getOtherChoices();
     }
 
     public function getLabel($choice)
     {
-        $choices = $this->getChoices();
-
-        return isset($choices[$choice]) ? $choices[$choice] : null;
+        return $this->choiceList->getLabel($choice);
     }
 
     public function isChoiceGroup($choice)
     {
-        return is_array($choice) || $choice instanceof \Traversable;
+        return $this->choiceList->isChoiceGroup($choice);
     }
 
     public function isChoiceSelected($choice)
     {
-        return in_array((string) $choice, (array) $this->getDisplayedData(), true);
+        return $this->choiceList->isChoiceSelected($choice, $this->getDisplayedData());
     }
 
     public function isMultipleChoice()
@@ -244,7 +178,7 @@ class ChoiceField extends HybridField
     {
         if ($this->isExpanded()) {
             $value = parent::transform($value);
-            $choices = $this->getChoices();
+            $choices = $this->choiceList->getChoices();
 
             foreach ($choices as $choice => $_) {
                 $choices[$choice] = $this->isMultipleChoice()

+ 27 - 0
src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php

@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\ChoiceList;
+
+interface ChoiceListInterface
+{
+    function getLabel($choice);
+
+    function getChoices();
+
+    function getOtherChoices();
+
+    function getPreferredChoices();
+
+    function isChoiceGroup($choice);
+
+    function isChoiceSelected($choice, $displayedData);
+}

+ 138 - 0
src/Symfony/Component/Form/ChoiceList/DefaultChoiceList.php

@@ -0,0 +1,138 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\ChoiceList;
+
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+
+class DefaultChoiceList implements ChoiceListInterface
+{
+    /**
+     * Stores the preferred choices with the choices as keys
+     * @var array
+     */
+    private $preferredChoices = array();
+
+    /**
+     * Stores the choices
+     * You should only access this property through getChoices()
+     * @var array
+     */
+    private $choices = array();
+
+    private $initialized = false;
+
+    private $emptyValue;
+
+    private $required;
+
+    public function __construct($choices, array $preferredChoices = array(), $emptyValue = '', $required = false)
+    {
+        if (!is_array($choices) && !$choices instanceof \Closure) {
+            throw new UnexpectedTypeException($choices, 'array or \Closure');
+        }
+
+        $this->choices = $choices;
+        $this->preferredChoices = array_flip($preferredChoices);
+        $this->emptyValue = $emptyValue;
+        $this->required = $required;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getLabel($choice)
+    {
+        $choices = $this->getChoices();
+
+        return isset($choices[$choice]) ? $choices[$choice] : null;
+    }
+
+    /**
+     * Returns the choices
+     *
+     * If the choices were given as a closure, the closure is executed on
+     * the first call of this method.
+     *
+     * @return array
+     */
+    public function getChoices()
+    {
+        $this->initializeChoices();
+
+        return $this->choices;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getOtherChoices()
+    {
+        return array_diff_key($this->getChoices(), $this->preferredChoices);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getPreferredChoices()
+    {
+        return array_intersect_key($this->getChoices(), $this->preferredChoices);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function isChoiceGroup($choice)
+    {
+        return is_array($choice) || $choice instanceof \Traversable;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function isChoiceSelected($choice, $displayedData)
+    {
+        return in_array((string) $choice, (array) $displayedData, true);
+    }
+
+    /**
+     * Initializes the choices
+     *
+     * If the choices were given as a closure, the closure is executed now.
+     *
+     * @return array
+     */
+    protected function initializeChoices()
+    {
+        if (!$this->initialized) {
+            $this->choices = $this->getInitializedChoices($this->choices);
+
+            if (!$this->required) {
+                $this->choices = array('' => $this->emptyValue) + $this->choices;
+            }
+
+            $this->initialized = true;
+        }
+    }
+
+    protected function getInitializedChoices($choices)
+    {
+        if ($choices instanceof \Closure) {
+            $choices = $choices->__invoke();
+
+            if (!is_array($choices)) {
+                throw new UnexpectedTypeException($choices, 'array');
+            }
+        }
+
+        return $choices;
+    }
+}

+ 268 - 0
src/Symfony/Component/Form/ChoiceList/EntityChoiceList.php

@@ -0,0 +1,268 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\ChoiceList;
+
+use Symfony\Component\Form\PropertyPath;
+use Symfony\Component\Form\Exception\FormException;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Doctrine\ORM\EntityManager;
+use Doctrine\ORM\QueryBuilder;
+use Doctrine\ORM\NoResultException;
+
+class EntityChoiceList extends DefaultChoiceList
+{
+    private $em;
+
+    private $class;
+
+    /**
+     * 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 Collection
+     */
+    private $entities = array();
+
+    /**
+     * Contains the query builder that builds the query for fetching the
+     * entities
+     *
+     * This property should only be accessed through queryBuilder.
+     *
+     * @var Doctrine\ORM\QueryBuilder
+     */
+    private $queryBuilder;
+
+    /**
+     * The fields of which the identifier of the underlying class consists
+     *
+     * This property should only be accessed through identifier.
+     *
+     * @var array
+     */
+    private $identifier = array();
+
+    /**
+     * A cache for \ReflectionProperty instances for the underlying class
+     *
+     * This property should only be accessed through getReflProperty().
+     *
+     * @var array
+     */
+    private $reflProperties = array();
+
+    /**
+     * A cache for the UnitOfWork instance of Doctrine
+     *
+     * @var Doctrine\ORM\UnitOfWork
+     */
+    private $unitOfWork;
+
+    private $propertyPath;
+
+    public function __construct(EntityManager $em, $class, $property = null, $queryBuilder = null, $choices = null, array $preferredChoices = array(), $emptyValue = '', $required = false)
+    {
+        // If a query builder was passed, it must be a closure or QueryBuilder
+        // instance
+        if (!(null === $queryBuilder || $queryBuilder instanceof QueryBuilder || $queryBuilder instanceof \Closure)) {
+            throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure');
+        }
+
+        if ($queryBuilder instanceof \Closure) {
+            $queryBuilder = $queryBuilder($em->getRepository($class));
+
+            if (!$queryBuilder instanceof QueryBuilder) {
+                throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder');
+            }
+        }
+
+        $this->em = $em;
+        $this->class = $class;
+        $this->queryBuilder = $queryBuilder;
+        $this->unitOfWork = $em->getUnitOfWork();
+        $this->identifier = $em->getClassMetadata($class)->getIdentifierFieldNames();
+
+        // The propery option defines, which property (path) is used for
+        // displaying entities as strings
+        if ($property) {
+            $this->propertyPath = new PropertyPath($property);
+        }
+
+        parent::__construct($choices, $preferredChoices, $emptyValue, $required);
+
+        // The entities can be passed directly in the "choices" option.
+        // In this case, initializing the entity cache is a cheap operation
+        // so do it now!
+        if (is_array($choices) && count($choices) > 0) {
+            $this->initializeChoices();
+        }
+    }
+
+    /**
+     * Initializes the choices and returns them
+     *
+     * 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 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.
+     *
+     * 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().
+     *
+     * @return array  An array of choices
+     */
+    protected function getInitializedChoices($choices)
+    {
+        if ($choices) {
+            $entities = parent::getInitializedChoices($choices);
+        } else if ($qb = $this->queryBuilder) {
+            $entities = $qb->getQuery()->execute();
+        } else {
+            $entities = $this->em->getRepository($this->class)->findAll();
+        }
+
+        $propertyPath = null;
+        $choices = array();
+        $this->entities = array();
+
+        foreach ($entities as $key => $entity) {
+            if ($this->propertyPath) {
+                // If the property option was given, use it
+                $value = $this->propertyPath->getValue($entity);
+            } else {
+                // Otherwise expect a __toString() method in the entity
+                $value = (string)$entity;
+            }
+
+            if (count($this->identifier) > 1) {
+                // When the identifier consists of multiple field, use
+                // naturally ordered keys to refer to the choices
+                $choices[$key] = $value;
+                $this->entities[$key] = $entity;
+            } else {
+                // When the identifier is a single field, index choices by
+                // entity ID for performance reasons
+                $id = current($this->getIdentifierValues($entity));
+                $choices[$id] = $value;
+                $this->entities[$id] = $entity;
+            }
+        }
+
+        return $choices;
+    }
+
+    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->entities) {
+            // indirectly initializes the entities property
+            $this->initializeChoices();
+        }
+
+        return $this->entities;
+    }
+
+    /**
+     * Returns the entity for the given key
+     *
+     * If the underlying entities have composite identifiers, the choices
+     * are intialized. 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  string $key  The choice key (for entities with composite
+     *                      identifiers) or entity ID (for entities with single
+     *                      identifiers)
+     * @return object       The matching entity
+     */
+    public function getEntity($key)
+    {
+        if (count($this->identifier) > 1) {
+            // $key is a collection index
+            $entities = $this->getEntities();
+            return $entities[$key];
+        } else if ($this->entities) {
+            return $this->entities[$key];
+        } else if ($qb = $this->queryBuilder) {
+            // should we clone the builder?
+            $alias = $qb->getRootAlias();
+            $where = $qb->expr()->eq($alias.'.'.current($this->identifier), $key);
+
+            return $qb->andWhere($where)->getQuery()->getSingleResult();
+        }
+
+        return $this->em->find($this->class, $key);
+    }
+
+    /**
+     * Returns the \ReflectionProperty instance for a property of the
+     * underlying class
+     *
+     * @param  string $property     The name of the property
+     * @return \ReflectionProperty  The reflection instsance
+     */
+    protected function getReflProperty($property)
+    {
+        if (!isset($this->reflProperties[$property])) {
+            $this->reflProperties[$property] = new \ReflectionProperty($this->class, $property);
+            $this->reflProperties[$property]->setAccessible(true);
+        }
+
+        return $this->reflProperties[$property];
+    }
+
+    /**
+     * Returns the values of the identifier fields of an entity
+     *
+     * Doctrine must know about this entity, that is, the entity must already
+     * be persisted or added to the identity map before. Otherwise an
+     * exception is thrown.
+     *
+     * @param  object $entity  The entity for which to get the identifier
+     * @throws FormException   If the entity does not exist in Doctrine's
+     *                         identity map
+     */
+    public function getIdentifierValues($entity)
+    {
+        if (!$this->unitOfWork->isInIdentityMap($entity)) {
+            throw new FormException('Entities passed to the choice field must be managed');
+        }
+
+        return $this->unitOfWork->getEntityIdentifier($entity);
+    }
+}

+ 21 - 300
src/Symfony/Component/Form/EntityChoiceField.php

@@ -11,6 +11,7 @@
 
 namespace Symfony\Component\Form;
 
+use Symfony\Component\Form\ChoiceList\EntityChoiceList;
 use Symfony\Component\Form\ValueTransformer\TransformationFailedException;
 use Symfony\Component\Form\Exception\FormException;
 use Symfony\Component\Form\Exception\InvalidOptionsException;
@@ -61,54 +62,6 @@ use Doctrine\ORM\NoResultException;
  */
 class EntityChoiceField extends ChoiceField
 {
-    /**
-     * 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 Collection
-     */
-    protected $entities = null;
-
-    /**
-     * Contains the query builder that builds the query for fetching the
-     * entities
-     *
-     * This property should only be accessed through getQueryBuilder().
-     *
-     * @var Doctrine\ORM\QueryBuilder
-     */
-    protected $queryBuilder = null;
-
-    /**
-     * The fields of which the identifier of the underlying class consists
-     *
-     * This property should only be accessed through getIdentifierFields().
-     *
-     * @var array
-     */
-    protected $identifier = array();
-
-    /**
-     * A cache for \ReflectionProperty instances for the underlying class
-     *
-     * This property should only be accessed through getReflProperty().
-     *
-     * @var array
-     */
-    protected $reflProperties = array();
-
-    /**
-     * A cache for the UnitOfWork instance of Doctrine
-     *
-     * @var Doctrine\ORM\UnitOfWork
-     */
-    protected $unitOfWork = null;
-
     /**
      * {@inheritDoc}
      */
@@ -124,247 +77,16 @@ class EntityChoiceField extends ChoiceField
 
         parent::configure();
 
-        // The entities can be passed directly in the "choices" option.
-        // In this case, initializing the entity cache is a cheap operation
-        // so do it now!
-        if (is_array($this->getOption('choices')) && count($this->getOption('choices')) > 0) {
-            $this->initializeChoices();
-        }
-
-        // If a query builder was passed, it must be a closure or QueryBuilder
-        // instance
-        if ($qb = $this->getOption('query_builder')) {
-            if (!($qb instanceof QueryBuilder || $qb instanceof \Closure)) {
-                throw new InvalidOptionsException(
-                    'The option "query_builder" most contain a closure or a QueryBuilder instance',
-                    array('query_builder'));
-            }
-        }
-    }
-
-    /**
-     * Returns the query builder instance for the choices of this field
-     *
-     * @return Doctrine\ORM\QueryBuilder  The query builder
-     * @throws InvalidOptionsException    When the query builder was passed as
-     *                                    closure and that closure does not
-     *                                    return a QueryBuilder instance
-     */
-    protected function getQueryBuilder()
-    {
-        if (!$this->getOption('query_builder')) {
-            return null;
-        }
-
-        if (!$this->queryBuilder) {
-            $qb = $this->getOption('query_builder');
-
-            if ($qb instanceof \Closure) {
-                $class = $this->getOption('class');
-                $em = $this->getOption('em');
-                $qb = $qb($em->getRepository($class));
-
-                if (!$qb instanceof QueryBuilder) {
-                    throw new InvalidOptionsException(
-                        'The closure in the option "query_builder" should return a QueryBuilder instance',
-                        array('query_builder'));
-                }
-            }
-
-            $this->queryBuilder = $qb;
-        }
-
-        return $this->queryBuilder;
-    }
-
-    /**
-     * Returns the unit of work of the entity manager
-     *
-     * This object is cached for faster lookups.
-     *
-     * @return Doctrine\ORM\UnitOfWork  The unit of work
-     */
-    protected function getUnitOfWork()
-    {
-        if (!$this->unitOfWork) {
-            $this->unitOfWork = $this->getOption('em')->getUnitOfWork();
-        }
-
-        return $this->unitOfWork;
-    }
-
-    /**
-     * Initializes the choices and returns them
-     *
-     * 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 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.
-     *
-     * 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().
-     *
-     * @return array  An array of choices
-     */
-    protected function getInitializedChoices()
-    {
-        if ($this->getOption('choices')) {
-            $entities = parent::getInitializedChoices();
-        } else if ($qb = $this->getQueryBuilder()) {
-            $entities = $qb->getQuery()->execute();
-        } else {
-            $class = $this->getOption('class');
-            $em = $this->getOption('em');
-            $entities = $em->getRepository($class)->findAll();
-        }
-
-        $propertyPath = null;
-        $choices = array();
-        $this->entities = array();
-
-        // The propery option defines, which property (path) is used for
-        // displaying entities as strings
-        if ($this->getOption('property')) {
-            $propertyPath = new PropertyPath($this->getOption('property'));
-        }
-
-        foreach ($entities as $key => $entity) {
-            if ($propertyPath) {
-                // If the property option was given, use it
-                $value = $propertyPath->getValue($entity);
-            } else {
-                // Otherwise expect a __toString() method in the entity
-                $value = (string)$entity;
-            }
-
-            if (count($this->getIdentifierFields()) > 1) {
-                // When the identifier consists of multiple field, use
-                // naturally ordered keys to refer to the choices
-                $choices[$key] = $value;
-                $this->entities[$key] = $entity;
-            } else {
-                // When the identifier is a single field, index choices by
-                // entity ID for performance reasons
-                $id = current($this->getIdentifierValues($entity));
-                $choices[$id] = $value;
-                $this->entities[$id] = $entity;
-            }
-        }
-
-        return $choices;
-    }
-
-    /**
-     * 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
-     */
-    protected function getEntities()
-    {
-        if (!$this->entities) {
-            // indirectly initializes the entities property
-            $this->initializeChoices();
-        }
-
-        return $this->entities;
-    }
-
-    /**
-     * Returns the entity for the given key
-     *
-     * If the underlying entities have composite identifiers, the choices
-     * are intialized. 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  string $key  The choice key (for entities with composite
-     *                      identifiers) or entity ID (for entities with single
-     *                      identifiers)
-     * @return object       The matching entity
-     */
-    protected function getEntity($key)
-    {
-        $id = $this->getIdentifierFields();
-
-        if (count($id) > 1) {
-            // $key is a collection index
-            $entities = $this->getEntities();
-            return $entities[$key];
-        } else if ($this->entities) {
-            return $this->entities[$key];
-        } else if ($qb = $this->getQueryBuilder()) {
-            // should we clone the builder?
-            $alias = $qb->getRootAlias();
-            $where = $qb->expr()->eq($alias.'.'.current($id), $key);
-
-            return $qb->andWhere($where)->getQuery()->getSingleResult();
-        }
-
-        return $this->getOption('em')->find($this->getOption('class'), $key);
-    }
-
-    /**
-     * Returns the \ReflectionProperty instance for a property of the
-     * underlying class
-     *
-     * @param  string $property     The name of the property
-     * @return \ReflectionProperty  The reflection instsance
-     */
-    protected function getReflProperty($property)
-    {
-        if (!isset($this->reflProperties[$property])) {
-            $this->reflProperties[$property] = new \ReflectionProperty($this->getOption('class'), $property);
-            $this->reflProperties[$property]->setAccessible(true);
-        }
-
-        return $this->reflProperties[$property];
-    }
-
-    /**
-     * Returns the fields included in the identifier of the underlying class
-     *
-     * @return array  An array of field names
-     */
-    protected function getIdentifierFields()
-    {
-        if (!$this->identifier) {
-            $metadata = $this->getOption('em')->getClassMetadata($this->getOption('class'));
-            $this->identifier = $metadata->getIdentifierFieldNames();
-        }
-
-        return $this->identifier;
-    }
-
-    /**
-     * Returns the values of the identifier fields of an entity
-     *
-     * Doctrine must know about this entity, that is, the entity must already
-     * be persisted or added to the identity map before. Otherwise an
-     * exception is thrown.
-     *
-     * @param  object $entity  The entity for which to get the identifier
-     * @throws FormException   If the entity does not exist in Doctrine's
-     *                         identity map
-     */
-    protected function getIdentifierValues($entity)
-    {
-        if (!$this->getUnitOfWork()->isInIdentityMap($entity)) {
-            throw new FormException('Entities passed to the choice field must be managed');
-        }
-
-        return $this->getUnitOfWork()->getEntityIdentifier($entity);
+        $this->choiceList = new EntityChoiceList(
+            $this->getOption('em'),
+            $this->getOption('class'),
+            $this->getOption('property'),
+            $this->getOption('query_builder'),
+            $this->getOption('choices'),
+            $this->getOption('preferred_choices'),
+            $this->getOption('empty_value'),
+            $this->isRequired()
+        );
     }
 
     /**
@@ -421,10 +143,10 @@ class EntityChoiceField extends ChoiceField
 
         $notFound = array();
 
-        if (count($this->getIdentifierFields()) > 1) {
-            $notFound = array_diff((array)$keyOrKeys, array_keys($this->getEntities()));
-        } else if ($this->entities) {
-            $notFound = array_diff((array)$keyOrKeys, array_keys($this->entities));
+        if (count($this->choiceList->getIdentifier()) > 1) {
+            $notFound = array_diff((array)$keyOrKeys, array_keys($this->choiceList->getEntities()));
+        } else if ($this->choiceList->getEntities()) {
+            $notFound = array_diff((array)$keyOrKeys, array_keys($this->choiceList->getEntities()));
         }
 
         if (0 === count($notFound)) {
@@ -434,14 +156,14 @@ class EntityChoiceField extends ChoiceField
                 // optimize this into a SELECT WHERE IN query
                 foreach ($keyOrKeys as $key) {
                     try {
-                        $result->add($this->getEntity($key));
+                        $result->add($this->choiceList->getEntity($key));
                     } catch (NoResultException $e) {
                         $notFound[] = $key;
                     }
                 }
             } else {
                 try {
-                    $result = $this->getEntity($keyOrKeys);
+                    $result = $this->choiceList->getEntity($keyOrKeys);
                 } catch (NoResultException $e) {
                     $notFound[] = $keyOrKeys;
                 }
@@ -469,9 +191,9 @@ class EntityChoiceField extends ChoiceField
             return $this->getOption('multiple') ? array() : '';
         }
 
-        if (count($this->identifier) > 1) {
+        if (count($this->choiceList->getIdentifier()) > 1) {
             // load all choices
-            $availableEntities = $this->getEntities();
+            $availableEntities = $this->choiceList->getEntities();
 
             if ($collectionOrEntity instanceof Collection) {
                 $result = array();
@@ -489,14 +211,13 @@ class EntityChoiceField extends ChoiceField
                 $result = array();
 
                 foreach ($collectionOrEntity as $entity) {
-                    $result[] = current($this->getIdentifierValues($entity));
+                    $result[] = current($this->choiceList->getIdentifierValues($entity));
                 }
             } else {
-                $result = current($this->getIdentifierValues($collectionOrEntity));
+                $result = current($this->choiceList->getIdentifierValues($collectionOrEntity));
             }
         }
 
-
         return parent::transform($result);
     }
 }

+ 2 - 13
tests/Symfony/Tests/Component/Form/ChoiceFieldTest.php

@@ -103,7 +103,7 @@ class ChoiceFieldTest extends \PHPUnit_Framework_TestCase
     }
 
     /**
-     * @expectedException Symfony\Component\Form\Exception\InvalidOptionsException
+     * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
      */
     public function testConfigureChoicesWithNonArray()
     {
@@ -112,17 +112,6 @@ class ChoiceFieldTest extends \PHPUnit_Framework_TestCase
         ));
     }
 
-    /**
-     * @expectedException Symfony\Component\Form\Exception\InvalidOptionsException
-     */
-    public function testConfigurePreferredChoicesWithNonArray()
-    {
-        $field = new ChoiceField('name', array(
-            'choices' => $this->choices,
-            'preferred_choices' => new \ArrayObject(),
-        ));
-    }
-
     public function getChoicesVariants()
     {
         $choices = $this->choices;
@@ -144,7 +133,7 @@ class ChoiceFieldTest extends \PHPUnit_Framework_TestCase
     }
 
     /**
-     * @expectedException Symfony\Component\Form\Exception\InvalidOptionsException
+     * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
      */
     public function testClosureShouldReturnArray()
     {

+ 2 - 2
tests/Symfony/Tests/Component/Form/EntityChoiceFieldTest.php

@@ -103,7 +103,7 @@ class EntityChoiceFieldTest extends DoctrineOrmTestCase
 //    }
 
     /**
-     * @expectedException Symfony\Component\Form\Exception\InvalidOptionsException
+     * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
      */
     public function testConfigureQueryBuilderWithNonQueryBuilderAndNonClosure()
     {
@@ -115,7 +115,7 @@ class EntityChoiceFieldTest extends DoctrineOrmTestCase
     }
 
     /**
-     * @expectedException Symfony\Component\Form\Exception\InvalidOptionsException
+     * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
      */
     public function testConfigureQueryBuilderWithClosureReturningNonQueryBuilder()
     {