[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;
|
namespace Symfony\Component\Form;
|
||||||
|
|
||||||
use Symfony\Component\Form\Exception\InvalidOptionsException;
|
use Symfony\Component\Form\ChoiceList\DefaultChoiceList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lets the user select between different choices.
|
* Lets the user select between different choices.
|
||||||
@ -37,18 +37,27 @@ use Symfony\Component\Form\Exception\InvalidOptionsException;
|
|||||||
*/
|
*/
|
||||||
class ChoiceField extends HybridField
|
class ChoiceField extends HybridField
|
||||||
{
|
{
|
||||||
/**
|
protected $choiceList;
|
||||||
* Stores the preferred choices with the choices as keys
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $preferredChoices = array();
|
|
||||||
|
|
||||||
/**
|
public function __construct($name = null, array $options = array())
|
||||||
* Stores the choices
|
{
|
||||||
* You should only access this property through getChoices()
|
parent::__construct($name, $options);
|
||||||
* @var array
|
|
||||||
*/
|
// until we have DI, this MUST happen after configure()
|
||||||
private $choices = array();
|
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()
|
protected function configure()
|
||||||
{
|
{
|
||||||
@ -60,37 +69,12 @@ class ChoiceField extends HybridField
|
|||||||
|
|
||||||
parent::configure();
|
parent::configure();
|
||||||
|
|
||||||
$choices = $this->getOption('choices');
|
$this->choiceList = new DefaultChoiceList(
|
||||||
|
$this->getOption('choices'),
|
||||||
if (!is_array($choices) && !$choices instanceof \Closure) {
|
$this->getOption('preferred_choices'),
|
||||||
throw new InvalidOptionsException('The choices option must be an array or a closure', array('choices'));
|
$this->getOption('empty_value'),
|
||||||
}
|
$this->isRequired()
|
||||||
|
);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getName()
|
public function getName()
|
||||||
@ -108,79 +92,29 @@ class ChoiceField extends HybridField
|
|||||||
return $name;
|
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()
|
public function getPreferredChoices()
|
||||||
{
|
{
|
||||||
return array_intersect_key($this->getChoices(), $this->preferredChoices);
|
return $this->choiceList->getPreferredChoices();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOtherChoices()
|
public function getOtherChoices()
|
||||||
{
|
{
|
||||||
return array_diff_key($this->getChoices(), $this->preferredChoices);
|
return $this->choiceList->getOtherChoices();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLabel($choice)
|
public function getLabel($choice)
|
||||||
{
|
{
|
||||||
$choices = $this->getChoices();
|
return $this->choiceList->getLabel($choice);
|
||||||
|
|
||||||
return isset($choices[$choice]) ? $choices[$choice] : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isChoiceGroup($choice)
|
public function isChoiceGroup($choice)
|
||||||
{
|
{
|
||||||
return is_array($choice) || $choice instanceof \Traversable;
|
return $this->choiceList->isChoiceGroup($choice);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isChoiceSelected($choice)
|
public function isChoiceSelected($choice)
|
||||||
{
|
{
|
||||||
return in_array((string) $choice, (array) $this->getDisplayedData(), true);
|
return $this->choiceList->isChoiceSelected($choice, $this->getDisplayedData());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isMultipleChoice()
|
public function isMultipleChoice()
|
||||||
@ -244,7 +178,7 @@ class ChoiceField extends HybridField
|
|||||||
{
|
{
|
||||||
if ($this->isExpanded()) {
|
if ($this->isExpanded()) {
|
||||||
$value = parent::transform($value);
|
$value = parent::transform($value);
|
||||||
$choices = $this->getChoices();
|
$choices = $this->choiceList->getChoices();
|
||||||
|
|
||||||
foreach ($choices as $choice => $_) {
|
foreach ($choices as $choice => $_) {
|
||||||
$choices[$choice] = $this->isMultipleChoice()
|
$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;
|
namespace Symfony\Component\Form;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\ChoiceList\EntityChoiceList;
|
||||||
use Symfony\Component\Form\ValueTransformer\TransformationFailedException;
|
use Symfony\Component\Form\ValueTransformer\TransformationFailedException;
|
||||||
use Symfony\Component\Form\Exception\FormException;
|
use Symfony\Component\Form\Exception\FormException;
|
||||||
use Symfony\Component\Form\Exception\InvalidOptionsException;
|
use Symfony\Component\Form\Exception\InvalidOptionsException;
|
||||||
@ -61,54 +62,6 @@ use Doctrine\ORM\NoResultException;
|
|||||||
*/
|
*/
|
||||||
class EntityChoiceField extends ChoiceField
|
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}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
@ -124,247 +77,16 @@ class EntityChoiceField extends ChoiceField
|
|||||||
|
|
||||||
parent::configure();
|
parent::configure();
|
||||||
|
|
||||||
// The entities can be passed directly in the "choices" option.
|
$this->choiceList = new EntityChoiceList(
|
||||||
// In this case, initializing the entity cache is a cheap operation
|
$this->getOption('em'),
|
||||||
// so do it now!
|
$this->getOption('class'),
|
||||||
if (is_array($this->getOption('choices')) && count($this->getOption('choices')) > 0) {
|
$this->getOption('property'),
|
||||||
$this->initializeChoices();
|
$this->getOption('query_builder'),
|
||||||
}
|
$this->getOption('choices'),
|
||||||
|
$this->getOption('preferred_choices'),
|
||||||
// If a query builder was passed, it must be a closure or QueryBuilder
|
$this->getOption('empty_value'),
|
||||||
// instance
|
$this->isRequired()
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -421,10 +143,10 @@ class EntityChoiceField extends ChoiceField
|
|||||||
|
|
||||||
$notFound = array();
|
$notFound = array();
|
||||||
|
|
||||||
if (count($this->getIdentifierFields()) > 1) {
|
if (count($this->choiceList->getIdentifier()) > 1) {
|
||||||
$notFound = array_diff((array)$keyOrKeys, array_keys($this->getEntities()));
|
$notFound = array_diff((array)$keyOrKeys, array_keys($this->choiceList->getEntities()));
|
||||||
} else if ($this->entities) {
|
} else if ($this->choiceList->getEntities()) {
|
||||||
$notFound = array_diff((array)$keyOrKeys, array_keys($this->entities));
|
$notFound = array_diff((array)$keyOrKeys, array_keys($this->choiceList->getEntities()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (0 === count($notFound)) {
|
if (0 === count($notFound)) {
|
||||||
@ -434,14 +156,14 @@ class EntityChoiceField extends ChoiceField
|
|||||||
// optimize this into a SELECT WHERE IN query
|
// optimize this into a SELECT WHERE IN query
|
||||||
foreach ($keyOrKeys as $key) {
|
foreach ($keyOrKeys as $key) {
|
||||||
try {
|
try {
|
||||||
$result->add($this->getEntity($key));
|
$result->add($this->choiceList->getEntity($key));
|
||||||
} catch (NoResultException $e) {
|
} catch (NoResultException $e) {
|
||||||
$notFound[] = $key;
|
$notFound[] = $key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
$result = $this->getEntity($keyOrKeys);
|
$result = $this->choiceList->getEntity($keyOrKeys);
|
||||||
} catch (NoResultException $e) {
|
} catch (NoResultException $e) {
|
||||||
$notFound[] = $keyOrKeys;
|
$notFound[] = $keyOrKeys;
|
||||||
}
|
}
|
||||||
@ -469,9 +191,9 @@ class EntityChoiceField extends ChoiceField
|
|||||||
return $this->getOption('multiple') ? array() : '';
|
return $this->getOption('multiple') ? array() : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($this->identifier) > 1) {
|
if (count($this->choiceList->getIdentifier()) > 1) {
|
||||||
// load all choices
|
// load all choices
|
||||||
$availableEntities = $this->getEntities();
|
$availableEntities = $this->choiceList->getEntities();
|
||||||
|
|
||||||
if ($collectionOrEntity instanceof Collection) {
|
if ($collectionOrEntity instanceof Collection) {
|
||||||
$result = array();
|
$result = array();
|
||||||
@ -489,14 +211,13 @@ class EntityChoiceField extends ChoiceField
|
|||||||
$result = array();
|
$result = array();
|
||||||
|
|
||||||
foreach ($collectionOrEntity as $entity) {
|
foreach ($collectionOrEntity as $entity) {
|
||||||
$result[] = current($this->getIdentifierValues($entity));
|
$result[] = current($this->choiceList->getIdentifierValues($entity));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$result = current($this->getIdentifierValues($collectionOrEntity));
|
$result = current($this->choiceList->getIdentifierValues($collectionOrEntity));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return parent::transform($result);
|
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()
|
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()
|
public function getChoicesVariants()
|
||||||
{
|
{
|
||||||
$choices = $this->choices;
|
$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()
|
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()
|
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()
|
public function testConfigureQueryBuilderWithClosureReturningNonQueryBuilder()
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user