/
EntityChoiceList.php
330 lines (291 loc) · 10.4 KB
/
EntityChoiceList.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Extension\Core\ChoiceList\ArrayChoiceList;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\NoResultException;
class EntityChoiceList extends ArrayChoiceList
{
/**
* @var Doctrine\ORM\EntityManager
*/
private $em;
/**
* @var Doctrine\ORM\Mapping\ClassMetadata
*/
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 $entityLoader;
/**
* 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 the UnitOfWork instance of Doctrine
*
* @var Doctrine\ORM\UnitOfWork
*/
private $unitOfWork;
/**
* Property path to access the key value of this choice-list.
*
* @var PropertyPath
*/
private $propertyPath;
/**
* Closure or PropertyPath string on Entity to use for grouping of entities
*
* @var mixed
*/
private $groupBy;
/**
* Constructor.
*
* @param ObjectManager $manager An EntityManager instance
* @param string $class The class name
* @param string $property The property name
* @param EntityLoaderInterface $entityLoader An optional query builder
* @param array|\Closure $choices An array of choices or a function returning an array
* @param string $groupBy
*/
public function __construct(ObjectManager $manager, $class, $property = null, EntityLoaderInterface $entityLoader = null, $choices = null, $groupBy = null)
{
$this->em = $manager;
$this->class = $class;
$this->entityLoader = $entityLoader;
$this->unitOfWork = $manager->getUnitOfWork();
$this->identifier = $manager->getClassMetadata($class)->getIdentifierFieldNames();
$this->groupBy = $groupBy;
// The property option defines, which property (path) is used for
// displaying entities as strings
if ($property) {
$this->propertyPath = new PropertyPath($property);
} else if (!method_exists($this->class, '__toString')) {
// Otherwise expect a __toString() method in the entity
throw new FormException('Entities passed to the choice field must have a "__toString()" method defined (or you can also override the "property" option).');
}
if (!is_array($choices) && !$choices instanceof \Closure && !is_null($choices)) {
throw new UnexpectedTypeException($choices, 'array or \Closure or null');
}
$this->choices = $choices;
}
/**
* Initializes the choices and returns them.
*
* 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.
*
* @return array An array of choices
*/
protected function load()
{
parent::load();
if (is_array($this->choices)) {
$entities = $this->choices;
} else if ($entityLoader = $this->entityLoader) {
$entities = $entityLoader->getEntities();
} else {
$entities = $this->em->getRepository($this->class)->findAll();
}
$this->choices = array();
$this->entities = array();
if ($this->groupBy) {
$entities = $this->groupEntities($entities, $this->groupBy);
}
$this->loadEntities($entities);
return $this->choices;
}
private function groupEntities($entities, $groupBy)
{
$grouped = array();
$path = new PropertyPath($groupBy);
foreach ($entities as $entity) {
// Get group name from property path
try {
$group = (string) $path->getValue($entity);
} catch (UnexpectedTypeException $e) {
// PropertyPath cannot traverse entity
$group = null;
}
if (empty($group)) {
$grouped[] = $entity;
} else {
$grouped[$group][] = $entity;
}
}
return $grouped;
}
/**
* Converts entities into choices with support for groups.
*
* 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 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().
*
* @param array $entities An array of entities
* @param string $group A group name
*/
private function loadEntities($entities, $group = null)
{
foreach ($entities as $key => $entity) {
if (is_array($entity)) {
// Entities are in named groups
$this->loadEntities($entity, $key);
continue;
}
if ($this->propertyPath) {
// If the property option was given, use it
$value = $this->propertyPath->getValue($entity);
} else {
$value = (string) $entity;
}
if (count($this->identifier) > 1) {
// When the identifier consists of multiple field, use
// naturally ordered keys to refer to the choices
$id = $key;
} else {
// When the identifier is a single field, index choices by
// entity ID for performance reasons
$id = current($this->getIdentifierValues($entity));
}
if (null === $group) {
// Flat list of choices
$this->choices[$id] = $value;
} else {
// Nested choices
$this->choices[$group][$id] = $value;
}
$this->entities[$id] = $entity;
}
}
/**
* Returns the fields of which the identifier of the underlying class consists.
*
* @return array
*/
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->loaded) {
$this->load();
}
return $this->entities;
}
/**
* Returns the entities for the given keys.
*
* If the underlying entities have composite identifiers, the choices
* are initialized. 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 array $keys The choice key (for entities with composite
* identifiers) or entity ID (for entities with single
* identifiers)
* @return object[] The matching entity
*/
public function getEntitiesByKeys($keys)
{
if (!$this->loaded) {
$this->load();
}
$found = array();
if (count($this->identifier) > 1) {
// $key is a collection index
$entities = $this->getEntities();
foreach ($keys as $key) {
if (isset($entities[$key])) {
$found[] = $entities[$key];
}
}
} else if ($this->entities) {
foreach ($keys as $key) {
if (isset($this->entities[$key])) {
$found[] = $this->entities[$key];
}
}
} else if ($entityLoader = $this->entityLoader) {
$found = $entityLoader->getEntitiesByKeys($this->identifier, $keys);
} else if ($keys) {
$identifier = current($this->identifier);
$found = $this->em->getRepository($this->class)
->findBy(array($identifier => $keys));
}
return $found;
}
/**
* 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
*
* @return array The identifier values
*
* @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);
}
}