-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Form] Refactored choice lists to support dynamic label, value, index and attribute generation #14050
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Form] Refactored choice lists to support dynamic label, value, index and attribute generation #14050
Changes from all commits
03efce1
3846b37
e6739bf
a289deb
26eba76
d6179c8
1d89922
7e0960d
94d18e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Bridge\Doctrine\Form\ChoiceList; | ||
|
||
use Doctrine\Common\Persistence\ObjectManager; | ||
use Symfony\Component\Form\ChoiceList\ChoiceListInterface; | ||
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; | ||
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; | ||
|
||
/** | ||
* Loads choices using a Doctrine object manager. | ||
* | ||
* @author Bernhard Schussek <[email protected]> | ||
*/ | ||
class DoctrineChoiceLoader implements ChoiceLoaderInterface | ||
{ | ||
/** | ||
* @var ChoiceListFactoryInterface | ||
*/ | ||
private $factory; | ||
|
||
/** | ||
* @var ObjectManager | ||
*/ | ||
private $manager; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $class; | ||
|
||
/** | ||
* @var IdReader | ||
*/ | ||
private $idReader; | ||
|
||
/** | ||
* @var null|EntityLoaderInterface | ||
*/ | ||
private $objectLoader; | ||
|
||
/** | ||
* @var ChoiceListInterface | ||
*/ | ||
private $choiceList; | ||
|
||
/** | ||
* Creates a new choice loader. | ||
* | ||
* Optionally, an implementation of {@link EntityLoaderInterface} can be | ||
* passed which optimizes the object loading for one of the Doctrine | ||
* mapper implementations. | ||
* | ||
* @param ChoiceListFactoryInterface $factory The factory for creating | ||
* the loaded choice list | ||
* @param ObjectManager $manager The object manager | ||
* @param string $class The class name of the | ||
* loaded objects | ||
* @param IdReader $idReader The reader for the object | ||
* IDs. | ||
* @param null|EntityLoaderInterface $objectLoader The objects loader | ||
*/ | ||
public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, IdReader $idReader, EntityLoaderInterface $objectLoader = null) | ||
{ | ||
$this->factory = $factory; | ||
$this->manager = $manager; | ||
$this->class = $manager->getClassMetadata($class)->getName(); | ||
$this->idReader = $idReader; | ||
$this->objectLoader = $objectLoader; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function loadChoiceList($value = null) | ||
{ | ||
if ($this->choiceList) { | ||
return $this->choiceList; | ||
} | ||
|
||
$objects = $this->objectLoader | ||
? $this->objectLoader->getEntities() | ||
: $this->manager->getRepository($this->class)->findAll(); | ||
|
||
$this->choiceList = $this->factory->createListFromChoices($objects, $value); | ||
|
||
return $this->choiceList; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function loadValuesForChoices(array $choices, $value = null) | ||
{ | ||
// Performance optimization | ||
if (empty($choices)) { | ||
return array(); | ||
} | ||
|
||
// Optimize performance for single-field identifiers. We already | ||
// know that the IDs are used as values | ||
|
||
// Attention: This optimization does not check choices for existence | ||
if (!$this->choiceList && $this->idReader->isSingleId()) { | ||
$values = array(); | ||
|
||
// Maintain order and indices of the given objects | ||
foreach ($choices as $i => $object) { | ||
if ($object instanceof $this->class) { | ||
// Make sure to convert to the right format | ||
$values[$i] = (string) $this->idReader->getIdValue($object); | ||
} | ||
} | ||
|
||
return $values; | ||
} | ||
|
||
return $this->loadChoiceList($value)->getValuesForChoices($choices); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function loadChoicesForValues(array $values, $value = null) | ||
{ | ||
// Performance optimization | ||
// Also prevents the generation of "WHERE id IN ()" queries through the | ||
// object loader. At least with MySQL and on the development machine | ||
// this was tested on, no exception was thrown for such invalid | ||
// statements, consequently no test fails when this code is removed. | ||
// https://github.com/symfony/symfony/pull/8981#issuecomment-24230557 | ||
if (empty($values)) { | ||
return array(); | ||
} | ||
|
||
// Optimize performance in case we have an object loader and | ||
// a single-field identifier | ||
if (!$this->choiceList && $this->objectLoader && $this->idReader->isSingleId()) { | ||
$unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idReader->getIdField(), $values); | ||
$objectsById = array(); | ||
$objects = array(); | ||
|
||
// Maintain order and indices from the given $values | ||
// An alternative approach to the following loop is to add the | ||
// "INDEX BY" clause to the Doctrine query in the loader, | ||
// but I'm not sure whether that's doable in a generic fashion. | ||
foreach ($unorderedObjects as $object) { | ||
$objectsById[$this->idReader->getIdValue($object)] = $object; | ||
} | ||
|
||
foreach ($values as $i => $id) { | ||
if (isset($objectsById[$id])) { | ||
$objects[$i] = $objectsById[$id]; | ||
} | ||
} | ||
|
||
return $objects; | ||
} | ||
|
||
return $this->loadChoiceList($value)->getChoicesForValues($values); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -11,17 +11,20 @@ | |||
|
||||
namespace Symfony\Bridge\Doctrine\Form\ChoiceList; | ||||
|
||||
use Doctrine\Common\Persistence\Mapping\ClassMetadata; | ||||
use Doctrine\Common\Persistence\ObjectManager; | ||||
use Symfony\Component\Form\Exception\RuntimeException; | ||||
use Symfony\Component\Form\Exception\StringCastException; | ||||
use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; | ||||
use Doctrine\Common\Persistence\ObjectManager; | ||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface; | ||||
use Doctrine\Common\Persistence\Mapping\ClassMetadata; | ||||
|
||||
/** | ||||
* A choice list presenting a list of Doctrine entities as choices. | ||||
* | ||||
* @author Bernhard Schussek <[email protected]> | ||||
* | ||||
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. | ||||
* Use {@link DoctrineChoiceLoader} instead. | ||||
*/ | ||||
class EntityChoiceList extends ObjectChoiceList | ||||
{ | ||||
|
@@ -126,6 +129,8 @@ public function __construct(ObjectManager $manager, $class, $labelPath = null, E | |||
} | ||||
|
||||
parent::__construct($entities, $labelPath, $preferredEntities, $groupPath, null, $propertyAccessor); | ||||
|
||||
trigger_error('The '.__CLASS__.' class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader instead.', E_USER_DEPRECATED); | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't we move this near the namespace declaration ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what do you mean? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
this way the trigger happens the first time the class is autoloaded There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in #14201. |
||||
} | ||||
|
||||
/** | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Bridge\Doctrine\Form\ChoiceList; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about moving the class out of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that doesn't really make sense. ChoiceList is very form specific (values are always strings, names must follow the naming conventions of forms, ...). Making ChoiceList more generic would just make maintenance harder, and I'd like to avoid that - maintenance of the Form component is hard enough as it is. |
||
|
||
use Doctrine\Common\Persistence\Mapping\ClassMetadata; | ||
use Doctrine\Common\Persistence\ObjectManager; | ||
use Symfony\Component\Form\Exception\RuntimeException; | ||
|
||
/** | ||
* A utility for reading object IDs. | ||
* | ||
* @since 1.0 | ||
* @author Bernhard Schussek <[email protected]> | ||
* | ||
* @internal This class is meant for internal use only. | ||
*/ | ||
class IdReader | ||
{ | ||
/** | ||
* @var ObjectManager | ||
*/ | ||
private $om; | ||
|
||
/** | ||
* @var ClassMetadata | ||
*/ | ||
private $classMetadata; | ||
|
||
/** | ||
* @var bool | ||
*/ | ||
private $singleId; | ||
|
||
/** | ||
* @var bool | ||
*/ | ||
private $intId; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $idField; | ||
|
||
public function __construct(ObjectManager $om, ClassMetadata $classMetadata) | ||
{ | ||
$ids = $classMetadata->getIdentifierFieldNames(); | ||
$idType = $classMetadata->getTypeOfField(current($ids)); | ||
|
||
$this->om = $om; | ||
$this->classMetadata = $classMetadata; | ||
$this->singleId = 1 === count($ids); | ||
$this->intId = $this->singleId && 1 === count($ids) && in_array($idType, array('integer', 'smallint', 'bigint')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in #14202. |
||
$this->idField = current($ids); | ||
} | ||
|
||
/** | ||
* Returns whether the class has a single-column ID. | ||
* | ||
* @return bool Returns `true` if the class has a single-column ID and | ||
* `false` otherwise. | ||
*/ | ||
public function isSingleId() | ||
{ | ||
return $this->singleId; | ||
} | ||
|
||
/** | ||
* Returns whether the class has a single-column integer ID. | ||
* | ||
* @return bool Returns `true` if the class has a single-column integer ID | ||
* and `false` otherwise. | ||
*/ | ||
public function isIntId() | ||
{ | ||
return $this->intId; | ||
} | ||
|
||
/** | ||
* Returns the ID value for an object. | ||
* | ||
* This method assumes that the object has a single-column ID. | ||
* | ||
* @param object $object The object. | ||
* | ||
* @return mixed The ID value. | ||
*/ | ||
public function getIdValue($object) | ||
{ | ||
if (!$object) { | ||
return; | ||
} | ||
|
||
if (!$this->om->contains($object)) { | ||
throw new RuntimeException( | ||
'Entities passed to the choice field must be managed. Maybe '. | ||
'persist them in the entity manager?' | ||
); | ||
} | ||
|
||
$this->om->initializeObject($object); | ||
|
||
return current($this->classMetadata->getIdentifierValues($object)); | ||
} | ||
|
||
/** | ||
* Returns the name of the ID field. | ||
* | ||
* This method assumes that the object has a single-column ID. | ||
* | ||
* @return string The name of the ID field. | ||
*/ | ||
public function getIdField() | ||
{ | ||
return $this->idField; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,7 +17,10 @@ | |
use Doctrine\ORM\EntityManager; | ||
|
||
/** | ||
* Getting Entities through the ORM QueryBuilder. | ||
* Loads entities using a {@link QueryBuilder} instance. | ||
* | ||
* @author Benjamin Eberlei <[email protected]> | ||
* @author Bernhard Schussek <[email protected]> | ||
*/ | ||
class ORMQueryBuilderLoader implements EntityLoaderInterface | ||
{ | ||
|
@@ -34,9 +37,14 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface | |
/** | ||
* Construct an ORM Query Builder Loader. | ||
* | ||
* @param QueryBuilder|\Closure $queryBuilder | ||
* @param EntityManager $manager | ||
* @param string $class | ||
* @param QueryBuilder|\Closure $queryBuilder The query builder or a closure | ||
* for creating the query builder. | ||
* Passing a closure is | ||
* deprecated and will not be | ||
* supported anymore as of | ||
* Symfony 3.0. | ||
* @param EntityManager $manager Deprecated. | ||
* @param string $class Deprecated. | ||
* | ||
* @throws UnexpectedTypeException | ||
*/ | ||
|
@@ -49,10 +57,15 @@ public function __construct($queryBuilder, $manager = null, $class = null) | |
} | ||
|
||
if ($queryBuilder instanceof \Closure) { | ||
trigger_error('Passing a QueryBuilder closure to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a very hard deprecation. All my entity fields in my project are relying on this feature for instance. Providing an alternative for the upgrade is absolutely necessary. The message does not explain what to do instead There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is only for the ORMQueryBuilderLoader which you probably do not use standalone. If you pass a closure to the correspoding option, it still works. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, nevermind. the form itself still accepts closures. the resolution has only been moved to a different place |
||
|
||
if (!$manager instanceof EntityManager) { | ||
throw new UnexpectedTypeException($manager, 'Doctrine\ORM\EntityManager'); | ||
} | ||
|
||
trigger_error('Passing an EntityManager to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); | ||
trigger_error('Passing a class to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); | ||
|
||
$queryBuilder = $queryBuilder($manager->getRepository($class)); | ||
|
||
if (!$queryBuilder instanceof QueryBuilder) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inheritdoc? same for loadValuesForChoices
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed