Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3e1a9fa

Browse filesBrowse files
committed
[Form] Refactored choice lists to support dynamic label, value, index and attribute generation
1 parent cb70899 commit 3e1a9fa
Copy full SHA for 3e1a9fa

File tree

54 files changed

+5677
-444
lines changed
Filter options

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Dismiss banner

54 files changed

+5677
-444
lines changed

‎src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php
+5-2Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,20 @@
1111

1212
namespace Symfony\Bridge\Doctrine\Form\ChoiceList;
1313

14+
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
15+
use Doctrine\Common\Persistence\ObjectManager;
1416
use Symfony\Component\Form\Exception\RuntimeException;
1517
use Symfony\Component\Form\Exception\StringCastException;
1618
use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
17-
use Doctrine\Common\Persistence\ObjectManager;
1819
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
19-
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
2020

2121
/**
2222
* A choice list presenting a list of Doctrine entities as choices.
2323
*
2424
* @author Bernhard Schussek <bschussek@gmail.com>
25+
*
26+
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
27+
* Use {@link EntityChoiceLoader} instead.
2528
*/
2629
class EntityChoiceList extends ObjectChoiceList
2730
{
+267Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\Form\ChoiceList;
13+
14+
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
15+
use Doctrine\Common\Persistence\ObjectManager;
16+
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
17+
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
18+
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
19+
use Symfony\Component\Form\Exception\RuntimeException;
20+
21+
/**
22+
* Loads choices using a Doctrine object manager.
23+
*
24+
* @author Bernhard Schussek <bschussek@gmail.com>
25+
*/
26+
class EntityChoiceLoader implements ChoiceLoaderInterface
27+
{
28+
/**
29+
* @var ChoiceListFactoryInterface
30+
*/
31+
private $factory;
32+
33+
/**
34+
* @var ObjectManager
35+
*/
36+
private $manager;
37+
38+
/**
39+
* @var string
40+
*/
41+
private $class;
42+
43+
/**
44+
* @var ClassMetadata
45+
*/
46+
private $classMetadata;
47+
48+
/**
49+
* @var null|EntityLoaderInterface
50+
*/
51+
private $entityLoader;
52+
53+
/**
54+
* The identifier field, unless the identifier is composite
55+
*
56+
* @var null|string
57+
*/
58+
private $idField = null;
59+
60+
/**
61+
* Whether to use the identifier for value generation
62+
*
63+
* @var bool
64+
*/
65+
private $compositeId = true;
66+
67+
/**
68+
* @var ChoiceListInterface
69+
*/
70+
private $choiceList;
71+
72+
/**
73+
* Returns the value of the identifier field of an entity.
74+
*
75+
* Doctrine must know about this entity, that is, the entity must already
76+
* be persisted or added to the identity map before. Otherwise an
77+
* exception is thrown.
78+
*
79+
* This method assumes that the entity has a single-column identifier and
80+
* will return a single value instead of an array.
81+
*
82+
* @param object $object The entity for which to get the identifier
83+
*
84+
* @return int|string The identifier value
85+
*
86+
* @throws RuntimeException If the entity does not exist in Doctrine's identity map
87+
*
88+
* @internal Should not be accessed by user-land code. This method is public
89+
* only to be usable as callback.
90+
*/
91+
public static function getIdValue(ObjectManager $om, ClassMetadata $classMetadata, $object)
92+
{
93+
if (!$om->contains($object)) {
94+
throw new RuntimeException(
95+
'Entities passed to the choice field must be managed. Maybe '.
96+
'persist them in the entity manager?'
97+
);
98+
}
99+
100+
$om->initializeObject($object);
101+
102+
return current($classMetadata->getIdentifierValues($object));
103+
}
104+
105+
/**
106+
* Creates a new choice loader.
107+
*
108+
* Optionally, an implementation of {@link EntityLoaderInterface} can be
109+
* passed which optimizes the entity loading for one of the Doctrine
110+
* mapper implementations.
111+
*
112+
* @param ChoiceListFactoryInterface $factory The factory for creating
113+
* the loaded choice list
114+
* @param ObjectManager $manager The object manager
115+
* @param string $class The entity class name
116+
* @param null|EntityLoaderInterface $entityLoader The entity loader
117+
*/
118+
public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, EntityLoaderInterface $entityLoader = null)
119+
{
120+
$this->factory = $factory;
121+
$this->manager = $manager;
122+
$this->classMetadata = $manager->getClassMetadata($class);
123+
$this->class = $this->classMetadata->getName();
124+
$this->entityLoader = $entityLoader;
125+
126+
$identifier = $this->classMetadata->getIdentifierFieldNames();
127+
128+
if (1 === count($identifier)) {
129+
$this->idField = $identifier[0];
130+
$this->compositeId = false;
131+
}
132+
}
133+
134+
/**
135+
* {@inheritdoc}
136+
*/
137+
public function loadChoiceList($value = null)
138+
{
139+
if ($this->choiceList) {
140+
return $this->choiceList;
141+
}
142+
143+
$entities = $this->entityLoader
144+
? $this->entityLoader->getEntities()
145+
: $this->manager->getRepository($this->class)->findAll();
146+
147+
// If the class has a multi-column identifier, we cannot index the
148+
// entities by their IDs
149+
if ($this->compositeId) {
150+
$this->choiceList = $this->factory->createListFromChoices($entities, $value);
151+
152+
return $this->choiceList;
153+
}
154+
155+
// Index the entities by ID
156+
$entitiesById = array();
157+
158+
foreach ($entities as $entity) {
159+
$id = self::getIdValue($this->manager, $this->classMetadata, $entity);
160+
$entitiesById[$id] = $entity;
161+
}
162+
163+
$this->choiceList = $this->factory->createListFromChoices($entitiesById, $value);
164+
165+
return $this->choiceList;
166+
}
167+
168+
/**
169+
* Loads the values corresponding to the given entities.
170+
*
171+
* The values are returned with the same keys and in the same order as the
172+
* corresponding entities in the given array.
173+
*
174+
* Optionally, a callable can be passed for generating the choice values.
175+
* The callable receives the entity as first and the array key as the second
176+
* argument.
177+
*
178+
* @param array $entities An array of entities. Non-existing entities
179+
* in this array are ignored
180+
* @param null|callable $value The callable generating the choice values
181+
*
182+
* @return string[] An array of choice values
183+
*/
184+
public function loadValuesForChoices(array $entities, $value = null)
185+
{
186+
// Performance optimization
187+
if (empty($entities)) {
188+
return array();
189+
}
190+
191+
// Optimize performance for single-field identifiers. We already
192+
// know that the IDs are used as values
193+
194+
// Attention: This optimization does not check choices for existence
195+
if (!$this->choiceList && !$this->compositeId) {
196+
$values = array();
197+
198+
// Maintain order and indices of the given entities
199+
foreach ($entities as $i => $entity) {
200+
if ($entity instanceof $this->class) {
201+
// Make sure to convert to the right format
202+
$values[$i] = (string) self::getIdValue($this->manager, $this->classMetadata, $entity);
203+
}
204+
}
205+
206+
return $values;
207+
}
208+
209+
return $this->loadChoiceList($value)->getValuesForChoices($entities);
210+
}
211+
212+
/**
213+
* Loads the entities corresponding to the given values.
214+
*
215+
* The entities are returned with the same keys and in the same order as the
216+
* corresponding values in the given array.
217+
*
218+
* Optionally, a callable can be passed for generating the choice values.
219+
* The callable receives the entity as first and the array key as the second
220+
* argument.
221+
*
222+
* @param string[] $values An array of choice values. Non-existing
223+
* values in this array are ignored
224+
* @param null|callable $value The callable generating the choice values
225+
*
226+
* @return array An array of entities
227+
*/
228+
public function loadChoicesForValues(array $values, $value = null)
229+
{
230+
// Performance optimization
231+
// Also prevents the generation of "WHERE id IN ()" queries through the
232+
// entity loader. At least with MySQL and on the development machine
233+
// this was tested on, no exception was thrown for such invalid
234+
// statements, consequently no test fails when this code is removed.
235+
// https://github.com/symfony/symfony/pull/8981#issuecomment-24230557
236+
if (empty($values)) {
237+
return array();
238+
}
239+
240+
// Optimize performance in case we have an entity loader and
241+
// a single-field identifier
242+
if (!$this->choiceList && !$this->compositeId && $this->entityLoader) {
243+
$unorderedEntities = $this->entityLoader->getEntitiesByIds($this->idField, $values);
244+
$entitiesById = array();
245+
$entities = array();
246+
247+
// Maintain order and indices from the given $values
248+
// An alternative approach to the following loop is to add the
249+
// "INDEX BY" clause to the Doctrine query in the loader,
250+
// but I'm not sure whether that's doable in a generic fashion.
251+
foreach ($unorderedEntities as $entity) {
252+
$id = self::getIdValue($this->manager, $this->classMetadata, $entity);
253+
$entitiesById[$id] = $entity;
254+
}
255+
256+
foreach ($values as $i => $id) {
257+
if (isset($entitiesById[$id])) {
258+
$entities[$i] = $entitiesById[$id];
259+
}
260+
}
261+
262+
return $entities;
263+
}
264+
265+
return $this->loadChoiceList($value)->getChoicesForValues($values);
266+
}
267+
}

‎src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php
+4-1Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
use Doctrine\ORM\EntityManager;
1818

1919
/**
20-
* Getting Entities through the ORM QueryBuilder.
20+
* Loads entities using a {@link QueryBuilder} instance.
21+
*
22+
* @author Benjamin Eberlei <kontakt@beberlei.de>
23+
* @author Bernhard Schussek <bschussek@gmail.com>
2124
*/
2225
class ORMQueryBuilderLoader implements EntityLoaderInterface
2326
{

‎src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php
+19-2Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,38 @@
1414
use Doctrine\Common\Persistence\ManagerRegistry;
1515
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
1616
use Symfony\Component\Form\AbstractExtension;
17+
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
18+
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
19+
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
20+
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
1721
use Symfony\Component\PropertyAccess\PropertyAccess;
22+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
1823

1924
class DoctrineOrmExtension extends AbstractExtension
2025
{
2126
protected $registry;
2227

23-
public function __construct(ManagerRegistry $registry)
28+
/**
29+
* @var PropertyAccessorInterface
30+
*/
31+
private $propertyAccessor;
32+
33+
/**
34+
* @var ChoiceListFactoryInterface
35+
*/
36+
private $choiceListFactory;
37+
38+
public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null)
2439
{
2540
$this->registry = $registry;
41+
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
42+
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
2643
}
2744

2845
protected function loadTypes()
2946
{
3047
return array(
31-
new EntityType($this->registry, PropertyAccess::createPropertyAccessor()),
48+
new EntityType($this->registry, $this->propertyAccessor, $this->choiceListFactory),
3249
);
3350
}
3451

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.