/
EntityChoiceList.php
298 lines (260 loc) · 9.45 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
<?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\ORM\EntityManager;
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 $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 = array())
{
// 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 property option defines, which property (path) is used for
// displaying entities as strings
if ($property) {
$this->propertyPath = new PropertyPath($property);
}
parent::__construct($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 ($this->choices) {
$entities = $this->choices;
} else if ($qb = $this->queryBuilder) {
$entities = $qb->getQuery()->execute();
} else {
$entities = $this->em->getRepository($this->class)->findAll();
}
$this->choices = array();
$this->entities = array();
$this->loadEntities($entities);
return $this->choices;
}
/**
* Convert 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().
*
*/
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 {
// 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
$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;
}
}
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 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 (!$this->loaded) {
$this->load();
}
try {
if (count($this->identifier) > 1) {
// $key is a collection index
$entities = $this->getEntities();
return isset($entities[$key]) ? $entities[$key] : null;
} else if ($this->entities) {
return isset($this->entities[$key]) ? $this->entities[$key] : null;
} 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);
} catch (NoResultException $e) {
return null;
}
}
/**
* Returns the \ReflectionProperty instance for a property of the
* underlying class
*
* @param string $property The name of the property
* @return \ReflectionProperty The reflection instsance
*/
private 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);
}
}