[Form] Refactored parts of the choice fields into ChoiceList instances
This commit is contained in:
parent
d2840aaad3
commit
813ec54fa1
@ -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,18 +37,27 @@ use Symfony\Component\Form\Exception\InvalidOptionsException;
|
||||
*/
|
||||
class ChoiceField extends HybridField
|
||||
{
|
||||
/**
|
||||
* Stores the preferred choices with the choices as keys
|
||||
* @var array
|
||||
*/
|
||||
protected $preferredChoices = array();
|
||||
protected $choiceList;
|
||||
|
||||
/**
|
||||
* Stores the choices
|
||||
* You should only access this property through getChoices()
|
||||
* @var array
|
||||
*/
|
||||
private $choices = array();
|
||||
public function __construct($name = null, array $options = array())
|
||||
{
|
||||
parent::__construct($name, $options);
|
||||
|
||||
// until we have DI, this MUST happen after configure()
|
||||
if ($this->isExpanded()) {
|
||||
$this->setFieldMode(self::FORM);
|
||||
|
||||
foreach ($this->choiceList->getPreferredChoices() as $choice => $value) {
|
||||
$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()
|
||||
{
|
||||
@ -60,37 +69,12 @@ class ChoiceField extends HybridField
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
if ($this->isExpanded()) {
|
||||
$this->setFieldMode(self::FORM);
|
||||
|
||||
$choices = $this->getChoices();
|
||||
|
||||
foreach ($this->preferredChoices as $choice => $_) {
|
||||
$this->add($this->newChoiceField($choice, $choices[$choice]));
|
||||
}
|
||||
|
||||
foreach ($choices as $choice => $value) {
|
||||
if (!isset($this->preferredChoices[$choice])) {
|
||||
$this->add($this->newChoiceField($choice, $value));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->setFieldMode(self::FIELD);
|
||||
}
|
||||
$this->choiceList = new DefaultChoiceList(
|
||||
$this->getOption('choices'),
|
||||
$this->getOption('preferred_choices'),
|
||||
$this->getOption('empty_value'),
|
||||
$this->isRequired()
|
||||
);
|
||||
}
|
||||
|
||||
public function getName()
|
||||
@ -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()
|
||||
|
@ -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
src/Symfony/Component/Form/ChoiceList/DefaultChoiceList.php
Normal file
138
src/Symfony/Component/Form/ChoiceList/DefaultChoiceList.php
Normal file
@ -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
src/Symfony/Component/Form/ChoiceList/EntityChoiceList.php
Normal file
268
src/Symfony/Component/Form/ChoiceList/EntityChoiceList.php
Normal file
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
Reference in New Issue
Block a user