1 | <?php |
||
2 | |||
3 | namespace Bankiru\Api\Doctrine; |
||
4 | |||
5 | use Bankiru\Api\Doctrine\Cache\ApiEntityCache; |
||
6 | use Bankiru\Api\Doctrine\Cache\EntityCacheAwareInterface; |
||
7 | use Bankiru\Api\Doctrine\Cache\LoggingCache; |
||
8 | use Bankiru\Api\Doctrine\Cache\VoidEntityCache; |
||
9 | use Bankiru\Api\Doctrine\Exception\MappingException; |
||
10 | use Bankiru\Api\Doctrine\Hydration\EntityHydrator; |
||
11 | use Bankiru\Api\Doctrine\Hydration\Hydrator; |
||
12 | use Bankiru\Api\Doctrine\Mapping\ApiMetadata; |
||
13 | use Bankiru\Api\Doctrine\Mapping\EntityMetadata; |
||
14 | use Bankiru\Api\Doctrine\Persister\ApiPersister; |
||
15 | use Bankiru\Api\Doctrine\Persister\CollectionMatcher; |
||
16 | use Bankiru\Api\Doctrine\Persister\CollectionPersister; |
||
17 | use Bankiru\Api\Doctrine\Persister\EntityPersister; |
||
18 | use Bankiru\Api\Doctrine\Proxy\ApiCollection; |
||
19 | use Bankiru\Api\Doctrine\Rpc\CrudsApiInterface; |
||
20 | use Bankiru\Api\Doctrine\Utility\IdentifierFlattener; |
||
21 | use Bankiru\Api\Doctrine\Utility\ReflectionPropertiesGetter; |
||
22 | use Doctrine\Common\Collections\ArrayCollection; |
||
23 | use Doctrine\Common\Collections\Collection; |
||
24 | use Doctrine\Common\NotifyPropertyChanged; |
||
25 | use Doctrine\Common\Persistence\Mapping\RuntimeReflectionService; |
||
26 | use Doctrine\Common\Persistence\ObjectManagerAware; |
||
27 | use Doctrine\Common\PropertyChangedListener; |
||
28 | use Doctrine\Common\Proxy\Proxy; |
||
29 | |||
30 | class UnitOfWork implements PropertyChangedListener |
||
31 | { |
||
32 | /** |
||
33 | * An entity is in MANAGED state when its persistence is managed by an EntityManager. |
||
34 | */ |
||
35 | const STATE_MANAGED = 1; |
||
36 | /** |
||
37 | * An entity is new if it has just been instantiated (i.e. using the "new" operator) |
||
38 | * and is not (yet) managed by an EntityManager. |
||
39 | */ |
||
40 | const STATE_NEW = 2; |
||
41 | /** |
||
42 | * A detached entity is an instance with persistent state and identity that is not |
||
43 | * (or no longer) associated with an EntityManager (and a UnitOfWork). |
||
44 | */ |
||
45 | const STATE_DETACHED = 3; |
||
46 | /** |
||
47 | * A removed entity instance is an instance with a persistent identity, |
||
48 | * associated with an EntityManager, whose persistent state will be deleted |
||
49 | * on commit. |
||
50 | */ |
||
51 | const STATE_REMOVED = 4; |
||
52 | |||
53 | /** |
||
54 | * The (cached) states of any known entities. |
||
55 | * Keys are object ids (spl_object_hash). |
||
56 | * |
||
57 | * @var array |
||
58 | */ |
||
59 | private $entityStates = []; |
||
60 | |||
61 | /** @var EntityManager */ |
||
62 | private $manager; |
||
63 | /** @var EntityPersister[] */ |
||
64 | private $persisters = []; |
||
65 | /** @var CollectionPersister[] */ |
||
66 | private $collectionPersisters = []; |
||
67 | /** @var array */ |
||
68 | private $entityIdentifiers = []; |
||
69 | /** @var object[][] */ |
||
70 | private $identityMap = []; |
||
71 | /** @var IdentifierFlattener */ |
||
72 | private $identifierFlattener; |
||
73 | /** @var array */ |
||
74 | private $originalEntityData = []; |
||
75 | /** @var array */ |
||
76 | private $entityDeletions = []; |
||
77 | /** @var array */ |
||
78 | private $entityChangeSets = []; |
||
79 | /** @var array */ |
||
80 | private $entityInsertions = []; |
||
81 | /** @var array */ |
||
82 | private $entityUpdates = []; |
||
83 | /** @var array */ |
||
84 | private $readOnlyObjects = []; |
||
85 | /** @var array */ |
||
86 | private $scheduledForSynchronization = []; |
||
87 | /** @var array */ |
||
88 | private $orphanRemovals = []; |
||
89 | /** @var ApiCollection[] */ |
||
90 | private $collectionDeletions = []; |
||
91 | /** @var array */ |
||
92 | private $extraUpdates = []; |
||
93 | /** @var ApiCollection[] */ |
||
94 | private $collectionUpdates = []; |
||
95 | /** @var ApiCollection[] */ |
||
96 | private $visitedCollections = []; |
||
97 | /** @var ReflectionPropertiesGetter */ |
||
98 | private $reflectionPropertiesGetter; |
||
99 | /** @var Hydrator[] */ |
||
100 | private $hydrators = []; |
||
101 | /** @var CrudsApiInterface[] */ |
||
102 | private $apis = []; |
||
103 | |||
104 | /** |
||
105 | * UnitOfWork constructor. |
||
106 | * |
||
107 | * @param EntityManager $manager |
||
108 | */ |
||
109 | public function __construct(EntityManager $manager) |
||
110 | { |
||
111 | $this->manager = $manager; |
||
112 | $this->identifierFlattener = new IdentifierFlattener($this->manager); |
||
113 | $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService()); |
||
114 | } |
||
115 | |||
116 | /** |
||
117 | * @param $className |
||
118 | * |
||
119 | * @return EntityPersister |
||
120 | */ |
||
121 | public function getEntityPersister($className) |
||
122 | { |
||
123 | if (!array_key_exists($className, $this->persisters)) { |
||
124 | /** @var ApiMetadata $classMetadata */ |
||
125 | $classMetadata = $this->manager->getClassMetadata($className); |
||
126 | |||
127 | $api = $this->getCrudsApi($classMetadata); |
||
128 | |||
129 | if ($api instanceof EntityCacheAwareInterface) { |
||
130 | $api->setEntityCache($this->createEntityCache($classMetadata)); |
||
131 | } |
||
132 | |||
133 | $this->persisters[$className] = new ApiPersister($this->manager, $api); |
||
134 | } |
||
135 | |||
136 | return $this->persisters[$className]; |
||
137 | } |
||
138 | |||
139 | /** |
||
140 | * Checks whether an entity is registered in the identity map of this UnitOfWork. |
||
141 | * |
||
142 | * @param object $entity |
||
143 | * |
||
144 | * @return boolean |
||
145 | */ |
||
146 | public function isInIdentityMap($entity) |
||
147 | { |
||
148 | $oid = spl_object_hash($entity); |
||
149 | |||
150 | if (!isset($this->entityIdentifiers[$oid])) { |
||
151 | return false; |
||
152 | } |
||
153 | |||
154 | /** @var EntityMetadata $classMetadata */ |
||
155 | $classMetadata = $this->manager->getClassMetadata(get_class($entity)); |
||
156 | $idHash = implode(' ', $this->entityIdentifiers[$oid]); |
||
157 | |||
158 | if ($idHash === '') { |
||
159 | return false; |
||
160 | } |
||
161 | |||
162 | return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]); |
||
163 | } |
||
164 | |||
165 | /** |
||
166 | * Gets the identifier of an entity. |
||
167 | * The returned value is always an array of identifier values. If the entity |
||
168 | * has a composite identifier then the identifier values are in the same |
||
169 | * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames(). |
||
170 | * |
||
171 | * @param object $entity |
||
172 | * |
||
173 | * @return array The identifier values. |
||
174 | */ |
||
175 | public function getEntityIdentifier($entity) |
||
176 | { |
||
177 | return $this->entityIdentifiers[spl_object_hash($entity)]; |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * @param string $className |
||
182 | * @param \stdClass $data |
||
183 | * |
||
184 | * @return ObjectManagerAware|object |
||
185 | * @throws MappingException |
||
186 | */ |
||
187 | public function getOrCreateEntity($className, \stdClass $data) |
||
188 | { |
||
189 | /** @var EntityMetadata $class */ |
||
190 | $class = $this->resolveSourceMetadataForClass($data, $className); |
||
191 | $tmpEntity = $this->getHydratorForClass($class)->hydarate($data); |
||
192 | |||
193 | $id = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($tmpEntity)); |
||
194 | $idHash = implode(' ', $id); |
||
195 | |||
196 | $overrideLocalValues = false; |
||
197 | if (isset($this->identityMap[$class->rootEntityName][$idHash])) { |
||
198 | $entity = $this->identityMap[$class->rootEntityName][$idHash]; |
||
199 | $oid = spl_object_hash($entity); |
||
200 | |||
201 | if ($entity instanceof Proxy && !$entity->__isInitialized()) { |
||
202 | $entity->__setInitialized(true); |
||
203 | |||
204 | $overrideLocalValues = true; |
||
205 | $this->originalEntityData[$oid] = $data; |
||
206 | |||
207 | if ($entity instanceof NotifyPropertyChanged) { |
||
208 | $entity->addPropertyChangedListener($this); |
||
209 | } |
||
210 | } |
||
211 | } else { |
||
212 | $entity = $this->newInstance($class); |
||
213 | $oid = spl_object_hash($entity); |
||
214 | $this->entityIdentifiers[$oid] = $id; |
||
215 | $this->entityStates[$oid] = self::STATE_MANAGED; |
||
216 | $this->originalEntityData[$oid] = $data; |
||
217 | $this->identityMap[$class->rootEntityName][$idHash] = $entity; |
||
218 | if ($entity instanceof NotifyPropertyChanged) { |
||
219 | $entity->addPropertyChangedListener($this); |
||
220 | } |
||
221 | $overrideLocalValues = true; |
||
222 | } |
||
223 | |||
224 | if (!$overrideLocalValues) { |
||
225 | return $entity; |
||
226 | } |
||
227 | |||
228 | $entity = $this->getHydratorForClass($class)->hydarate($data, $entity); |
||
229 | |||
230 | return $entity; |
||
231 | } |
||
232 | |||
233 | /** |
||
234 | * INTERNAL: |
||
235 | * Registers an entity as managed. |
||
236 | * |
||
237 | * @param Proxy $entity The entity. |
||
238 | * @param array $id The identifier values. |
||
239 | * @param \stdClass|null $data The original entity data. |
||
240 | * |
||
241 | * @return void |
||
242 | */ |
||
243 | public function registerManaged($entity, array $id, \stdClass $data = null) |
||
244 | { |
||
245 | $oid = spl_object_hash($entity); |
||
246 | |||
247 | $this->entityIdentifiers[$oid] = $id; |
||
248 | $this->entityStates[$oid] = self::STATE_MANAGED; |
||
249 | $this->originalEntityData[$oid] = $data; |
||
250 | |||
251 | $this->addToIdentityMap($entity); |
||
252 | |||
253 | if ($entity instanceof NotifyPropertyChanged && (!$entity instanceof Proxy || $entity->__isInitialized())) { |
||
254 | $entity->addPropertyChangedListener($this); |
||
255 | } |
||
256 | } |
||
257 | |||
258 | /** |
||
259 | * INTERNAL: |
||
260 | * Registers an entity in the identity map. |
||
261 | * Note that entities in a hierarchy are registered with the class name of |
||
262 | * the root entity. |
||
263 | * |
||
264 | * @ignore |
||
265 | * |
||
266 | * @param object $entity The entity to register. |
||
267 | * |
||
268 | * @return boolean TRUE if the registration was successful, FALSE if the identity of |
||
269 | * the entity in question is already managed. |
||
270 | * |
||
271 | */ |
||
272 | public function addToIdentityMap($entity) |
||
273 | { |
||
274 | /** @var EntityMetadata $classMetadata */ |
||
275 | $classMetadata = $this->manager->getClassMetadata(get_class($entity)); |
||
276 | $idHash = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]); |
||
277 | |||
278 | if ($idHash === '') { |
||
279 | throw new \InvalidArgumentException('Entitty does not have valid identifiers to be stored at identity map'); |
||
280 | } |
||
281 | |||
282 | $className = $classMetadata->rootEntityName; |
||
283 | |||
284 | if (isset($this->identityMap[$className][$idHash])) { |
||
285 | return false; |
||
286 | } |
||
287 | |||
288 | $this->identityMap[$className][$idHash] = $entity; |
||
289 | |||
290 | return true; |
||
291 | } |
||
292 | |||
293 | /** |
||
294 | * Gets the identity map of the UnitOfWork. |
||
295 | * |
||
296 | * @return array |
||
297 | */ |
||
298 | public function getIdentityMap() |
||
299 | { |
||
300 | return $this->identityMap; |
||
301 | } |
||
302 | |||
303 | /** |
||
304 | * Gets the original data of an entity. The original data is the data that was |
||
305 | * present at the time the entity was reconstituted from the database. |
||
306 | * |
||
307 | * @param object $entity |
||
308 | * |
||
309 | * @return array |
||
310 | */ |
||
311 | public function getOriginalEntityData($entity) |
||
312 | { |
||
313 | $oid = spl_object_hash($entity); |
||
314 | |||
315 | if (isset($this->originalEntityData[$oid])) { |
||
316 | return $this->originalEntityData[$oid]; |
||
317 | } |
||
318 | |||
319 | return []; |
||
320 | } |
||
321 | |||
322 | /** |
||
323 | * INTERNAL: |
||
324 | * Checks whether an identifier hash exists in the identity map. |
||
325 | * |
||
326 | * @ignore |
||
327 | * |
||
328 | * @param string $idHash |
||
329 | * @param string $rootClassName |
||
330 | * |
||
331 | * @return boolean |
||
332 | */ |
||
333 | public function containsIdHash($idHash, $rootClassName) |
||
334 | { |
||
335 | return isset($this->identityMap[$rootClassName][$idHash]); |
||
336 | } |
||
337 | |||
338 | /** |
||
339 | * INTERNAL: |
||
340 | * Gets an entity in the identity map by its identifier hash. |
||
341 | * |
||
342 | * @ignore |
||
343 | * |
||
344 | * @param string $idHash |
||
345 | * @param string $rootClassName |
||
346 | * |
||
347 | * @return object |
||
348 | */ |
||
349 | public function getByIdHash($idHash, $rootClassName) |
||
350 | { |
||
351 | return $this->identityMap[$rootClassName][$idHash]; |
||
352 | } |
||
353 | |||
354 | /** |
||
355 | * INTERNAL: |
||
356 | * Tries to get an entity by its identifier hash. If no entity is found for |
||
357 | * the given hash, FALSE is returned. |
||
358 | * |
||
359 | * @ignore |
||
360 | * |
||
361 | * @param mixed $idHash (must be possible to cast it to string) |
||
362 | * @param string $rootClassName |
||
363 | * |
||
364 | * @return object|bool The found entity or FALSE. |
||
365 | */ |
||
366 | public function tryGetByIdHash($idHash, $rootClassName) |
||
367 | { |
||
368 | $stringIdHash = (string)$idHash; |
||
369 | |||
370 | if (isset($this->identityMap[$rootClassName][$stringIdHash])) { |
||
371 | return $this->identityMap[$rootClassName][$stringIdHash]; |
||
372 | } |
||
373 | |||
374 | return false; |
||
375 | } |
||
376 | |||
377 | /** |
||
378 | * Gets the state of an entity with regard to the current unit of work. |
||
379 | * |
||
380 | * @param object $entity |
||
381 | * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED). |
||
382 | * This parameter can be set to improve performance of entity state detection |
||
383 | * by potentially avoiding a database lookup if the distinction between NEW and DETACHED |
||
384 | * is either known or does not matter for the caller of the method. |
||
385 | * |
||
386 | * @return int The entity state. |
||
387 | */ |
||
388 | public function getEntityState($entity, $assume = null) |
||
389 | { |
||
390 | $oid = spl_object_hash($entity); |
||
391 | if (isset($this->entityStates[$oid])) { |
||
392 | return $this->entityStates[$oid]; |
||
393 | } |
||
394 | if ($assume !== null) { |
||
395 | return $assume; |
||
396 | } |
||
397 | // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known. |
||
398 | // Note that you can not remember the NEW or DETACHED state in _entityStates since |
||
399 | // the UoW does not hold references to such objects and the object hash can be reused. |
||
400 | // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it. |
||
401 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
402 | $id = $class->getIdentifierValues($entity); |
||
403 | if (!$id) { |
||
404 | return self::STATE_NEW; |
||
405 | } |
||
406 | |||
407 | return self::STATE_DETACHED; |
||
408 | } |
||
409 | |||
410 | /** |
||
411 | * Tries to find an entity with the given identifier in the identity map of |
||
412 | * this UnitOfWork. |
||
413 | * |
||
414 | * @param mixed $id The entity identifier to look for. |
||
415 | * @param string $rootClassName The name of the root class of the mapped entity hierarchy. |
||
416 | * |
||
417 | * @return object|bool Returns the entity with the specified identifier if it exists in |
||
418 | * this UnitOfWork, FALSE otherwise. |
||
419 | */ |
||
420 | public function tryGetById($id, $rootClassName) |
||
421 | { |
||
422 | /** @var EntityMetadata $metadata */ |
||
423 | $metadata = $this->manager->getClassMetadata($rootClassName); |
||
424 | $idHash = implode(' ', (array)$this->identifierFlattener->flattenIdentifier($metadata, $id)); |
||
425 | |||
426 | if (isset($this->identityMap[$rootClassName][$idHash])) { |
||
427 | return $this->identityMap[$rootClassName][$idHash]; |
||
428 | } |
||
429 | |||
430 | return false; |
||
431 | } |
||
432 | |||
433 | /** |
||
434 | * Notifies this UnitOfWork of a property change in an entity. |
||
435 | * |
||
436 | * @param object $entity The entity that owns the property. |
||
437 | * @param string $propertyName The name of the property that changed. |
||
438 | * @param mixed $oldValue The old value of the property. |
||
439 | * @param mixed $newValue The new value of the property. |
||
440 | * |
||
441 | * @return void |
||
442 | */ |
||
443 | public function propertyChanged($entity, $propertyName, $oldValue, $newValue) |
||
444 | { |
||
445 | $oid = spl_object_hash($entity); |
||
446 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
447 | $isAssocField = $class->hasAssociation($propertyName); |
||
448 | if (!$isAssocField && !$class->hasField($propertyName)) { |
||
449 | return; // ignore non-persistent fields |
||
450 | } |
||
451 | // Update changeset and mark entity for synchronization |
||
452 | $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue]; |
||
453 | if (!isset($this->scheduledForSynchronization[$class->getRootEntityName()][$oid])) { |
||
454 | $this->scheduleForDirtyCheck($entity); |
||
455 | } |
||
456 | } |
||
457 | |||
458 | /** |
||
459 | * Persists an entity as part of the current unit of work. |
||
460 | * |
||
461 | * @param object $entity The entity to persist. |
||
462 | * |
||
463 | * @return void |
||
464 | */ |
||
465 | public function persist($entity) |
||
466 | { |
||
467 | $visited = []; |
||
468 | $this->doPersist($entity, $visited); |
||
469 | } |
||
470 | |||
471 | /** |
||
472 | * @param ApiMetadata $class |
||
473 | * @param $entity |
||
474 | * |
||
475 | * @throws \InvalidArgumentException |
||
476 | * @throws \RuntimeException |
||
477 | */ |
||
478 | public function recomputeSingleEntityChangeSet(ApiMetadata $class, $entity) |
||
479 | { |
||
480 | $oid = spl_object_hash($entity); |
||
481 | if (!isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) { |
||
482 | throw new \InvalidArgumentException('Entity is not managed'); |
||
483 | } |
||
484 | |||
485 | $actualData = []; |
||
486 | foreach ($class->getReflectionProperties() as $name => $refProp) { |
||
487 | if (!$class->isIdentifier($name) && !$class->isCollectionValuedAssociation($name)) { |
||
488 | $actualData[$name] = $refProp->getValue($entity); |
||
489 | } |
||
490 | } |
||
491 | if (!isset($this->originalEntityData[$oid])) { |
||
492 | throw new \RuntimeException( |
||
493 | 'Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.' |
||
494 | ); |
||
495 | } |
||
496 | $originalData = $this->originalEntityData[$oid]; |
||
497 | $changeSet = []; |
||
498 | foreach ($actualData as $propName => $actualValue) { |
||
499 | $orgValue = isset($originalData->$propName) ? $originalData->$propName : null; |
||
500 | if ($orgValue !== $actualValue) { |
||
501 | $changeSet[$propName] = [$orgValue, $actualValue]; |
||
502 | } |
||
503 | } |
||
504 | if ($changeSet) { |
||
505 | if (isset($this->entityChangeSets[$oid])) { |
||
506 | $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet); |
||
507 | } else { |
||
508 | if (!isset($this->entityInsertions[$oid])) { |
||
509 | $this->entityChangeSets[$oid] = $changeSet; |
||
510 | $this->entityUpdates[$oid] = $entity; |
||
511 | } |
||
512 | } |
||
513 | $this->originalEntityData[$oid] = (object)$actualData; |
||
514 | } |
||
515 | } |
||
516 | |||
517 | /** |
||
518 | * Schedules an entity for insertion into the database. |
||
519 | * If the entity already has an identifier, it will be added to the identity map. |
||
520 | * |
||
521 | * @param object $entity The entity to schedule for insertion. |
||
522 | * |
||
523 | * @return void |
||
524 | * |
||
525 | * @throws \InvalidArgumentException |
||
526 | */ |
||
527 | public function scheduleForInsert($entity) |
||
528 | { |
||
529 | $oid = spl_object_hash($entity); |
||
530 | if (isset($this->entityUpdates[$oid])) { |
||
531 | throw new \InvalidArgumentException('Dirty entity cannot be scheduled for insertion'); |
||
532 | } |
||
533 | if (isset($this->entityDeletions[$oid])) { |
||
534 | throw new \InvalidArgumentException('Removed entity scheduled for insertion'); |
||
535 | } |
||
536 | if (isset($this->originalEntityData[$oid]) && !isset($this->entityInsertions[$oid])) { |
||
537 | throw new \InvalidArgumentException('Managed entity scheduled for insertion'); |
||
538 | } |
||
539 | if (isset($this->entityInsertions[$oid])) { |
||
540 | throw new \InvalidArgumentException('Entity scheduled for insertion twice'); |
||
541 | } |
||
542 | $this->entityInsertions[$oid] = $entity; |
||
543 | if (isset($this->entityIdentifiers[$oid])) { |
||
544 | $this->addToIdentityMap($entity); |
||
545 | } |
||
546 | if ($entity instanceof NotifyPropertyChanged) { |
||
547 | $entity->addPropertyChangedListener($this); |
||
548 | } |
||
549 | } |
||
550 | |||
551 | /** |
||
552 | * Checks whether an entity is scheduled for insertion. |
||
553 | * |
||
554 | * @param object $entity |
||
555 | * |
||
556 | * @return boolean |
||
557 | */ |
||
558 | public function isScheduledForInsert($entity) |
||
559 | { |
||
560 | return isset($this->entityInsertions[spl_object_hash($entity)]); |
||
561 | } |
||
562 | |||
563 | /** |
||
564 | * Schedules an entity for being updated. |
||
565 | * |
||
566 | * @param object $entity The entity to schedule for being updated. |
||
567 | * |
||
568 | * @return void |
||
569 | * |
||
570 | * @throws \InvalidArgumentException |
||
571 | */ |
||
572 | public function scheduleForUpdate($entity) |
||
573 | { |
||
574 | $oid = spl_object_hash($entity); |
||
575 | if (!isset($this->entityIdentifiers[$oid])) { |
||
576 | throw new \InvalidArgumentException('Entity has no identity'); |
||
577 | } |
||
578 | if (isset($this->entityDeletions[$oid])) { |
||
579 | throw new \InvalidArgumentException('Entity is removed'); |
||
580 | } |
||
581 | if (!isset($this->entityUpdates[$oid]) && !isset($this->entityInsertions[$oid])) { |
||
582 | $this->entityUpdates[$oid] = $entity; |
||
583 | } |
||
584 | } |
||
585 | |||
586 | /** |
||
587 | * Checks whether an entity is registered as dirty in the unit of work. |
||
588 | * Note: Is not very useful currently as dirty entities are only registered |
||
589 | * at commit time. |
||
590 | * |
||
591 | * @param object $entity |
||
592 | * |
||
593 | * @return boolean |
||
594 | */ |
||
595 | public function isScheduledForUpdate($entity) |
||
596 | { |
||
597 | return isset($this->entityUpdates[spl_object_hash($entity)]); |
||
598 | } |
||
599 | |||
600 | /** |
||
601 | * Checks whether an entity is registered to be checked in the unit of work. |
||
602 | * |
||
603 | * @param object $entity |
||
604 | * |
||
605 | * @return boolean |
||
606 | */ |
||
607 | public function isScheduledForDirtyCheck($entity) |
||
608 | { |
||
609 | $rootEntityName = $this->manager->getClassMetadata(get_class($entity))->getRootEntityName(); |
||
610 | |||
611 | return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]); |
||
612 | } |
||
613 | |||
614 | /** |
||
615 | * INTERNAL: |
||
616 | * Schedules an entity for deletion. |
||
617 | * |
||
618 | * @param object $entity |
||
619 | * |
||
620 | * @return void |
||
621 | */ |
||
622 | public function scheduleForDelete($entity) |
||
623 | { |
||
624 | $oid = spl_object_hash($entity); |
||
625 | if (isset($this->entityInsertions[$oid])) { |
||
626 | if ($this->isInIdentityMap($entity)) { |
||
627 | $this->removeFromIdentityMap($entity); |
||
628 | } |
||
629 | unset($this->entityInsertions[$oid], $this->entityStates[$oid]); |
||
630 | |||
631 | return; // entity has not been persisted yet, so nothing more to do. |
||
632 | } |
||
633 | if (!$this->isInIdentityMap($entity)) { |
||
634 | return; |
||
635 | } |
||
636 | $this->removeFromIdentityMap($entity); |
||
637 | unset($this->entityUpdates[$oid]); |
||
638 | if (!isset($this->entityDeletions[$oid])) { |
||
639 | $this->entityDeletions[$oid] = $entity; |
||
640 | $this->entityStates[$oid] = self::STATE_REMOVED; |
||
641 | } |
||
642 | } |
||
643 | |||
644 | /** |
||
645 | * Checks whether an entity is registered as removed/deleted with the unit |
||
646 | * of work. |
||
647 | * |
||
648 | * @param object $entity |
||
649 | * |
||
650 | * @return boolean |
||
651 | */ |
||
652 | public function isScheduledForDelete($entity) |
||
653 | { |
||
654 | return isset($this->entityDeletions[spl_object_hash($entity)]); |
||
655 | } |
||
656 | |||
657 | /** |
||
658 | * Checks whether an entity is scheduled for insertion, update or deletion. |
||
659 | * |
||
660 | * @param object $entity |
||
661 | * |
||
662 | * @return boolean |
||
663 | */ |
||
664 | public function isEntityScheduled($entity) |
||
665 | { |
||
666 | $oid = spl_object_hash($entity); |
||
667 | |||
668 | return isset($this->entityInsertions[$oid]) |
||
669 | || isset($this->entityUpdates[$oid]) |
||
670 | || isset($this->entityDeletions[$oid]); |
||
671 | } |
||
672 | |||
673 | /** |
||
674 | * INTERNAL: |
||
675 | * Removes an entity from the identity map. This effectively detaches the |
||
676 | * entity from the persistence management of Doctrine. |
||
677 | * |
||
678 | * @ignore |
||
679 | * |
||
680 | * @param object $entity |
||
681 | * |
||
682 | * @return boolean |
||
683 | * |
||
684 | * @throws \InvalidArgumentException |
||
685 | */ |
||
686 | public function removeFromIdentityMap($entity) |
||
687 | { |
||
688 | $oid = spl_object_hash($entity); |
||
689 | $classMetadata = $this->manager->getClassMetadata(get_class($entity)); |
||
690 | $idHash = implode(' ', $this->entityIdentifiers[$oid]); |
||
691 | if ($idHash === '') { |
||
692 | throw new \InvalidArgumentException('Entity has no identity'); |
||
693 | } |
||
694 | $className = $classMetadata->getRootEntityName(); |
||
695 | if (isset($this->identityMap[$className][$idHash])) { |
||
696 | unset($this->identityMap[$className][$idHash]); |
||
697 | unset($this->readOnlyObjects[$oid]); |
||
698 | |||
699 | //$this->entityStates[$oid] = self::STATE_DETACHED; |
||
700 | return true; |
||
701 | } |
||
702 | |||
703 | return false; |
||
704 | } |
||
705 | |||
706 | /** |
||
707 | * Commits the UnitOfWork, executing all operations that have been postponed |
||
708 | * up to this point. The state of all managed entities will be synchronized with |
||
709 | * the database. |
||
710 | * |
||
711 | * The operations are executed in the following order: |
||
712 | * |
||
713 | * 1) All entity insertions |
||
714 | * 2) All entity updates |
||
715 | * 3) All collection deletions |
||
716 | * 4) All collection updates |
||
717 | * 5) All entity deletions |
||
718 | * |
||
719 | * @param null|object|array $entity |
||
720 | * |
||
721 | * @return void |
||
722 | * |
||
723 | * @throws \Exception |
||
724 | */ |
||
725 | public function commit($entity = null) |
||
726 | { |
||
727 | // Compute changes done since last commit. |
||
728 | if ($entity === null) { |
||
729 | $this->computeChangeSets(); |
||
730 | } elseif (is_object($entity)) { |
||
731 | $this->computeSingleEntityChangeSet($entity); |
||
732 | } elseif (is_array($entity)) { |
||
733 | foreach ((array)$entity as $object) { |
||
734 | $this->computeSingleEntityChangeSet($object); |
||
735 | } |
||
736 | } |
||
737 | if (!($this->entityInsertions || |
||
738 | $this->entityDeletions || |
||
739 | $this->entityUpdates || |
||
740 | $this->collectionUpdates || |
||
741 | $this->collectionDeletions || |
||
742 | $this->orphanRemovals) |
||
743 | ) { |
||
744 | return; // Nothing to do. |
||
745 | } |
||
746 | if ($this->orphanRemovals) { |
||
747 | foreach ($this->orphanRemovals as $orphan) { |
||
748 | $this->remove($orphan); |
||
749 | } |
||
750 | } |
||
751 | // Now we need a commit order to maintain referential integrity |
||
752 | $commitOrder = $this->getCommitOrder(); |
||
753 | |||
754 | // Collection deletions (deletions of complete collections) |
||
755 | // foreach ($this->collectionDeletions as $collectionToDelete) { |
||
756 | // //fixme: collection mutations |
||
757 | // $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete); |
||
758 | // } |
||
759 | if ($this->entityInsertions) { |
||
760 | foreach ($commitOrder as $class) { |
||
761 | $this->executeInserts($class); |
||
762 | } |
||
763 | } |
||
764 | if ($this->entityUpdates) { |
||
765 | foreach ($commitOrder as $class) { |
||
766 | $this->executeUpdates($class); |
||
767 | } |
||
768 | } |
||
769 | // Extra updates that were requested by persisters. |
||
770 | if ($this->extraUpdates) { |
||
771 | $this->executeExtraUpdates(); |
||
772 | } |
||
773 | // Collection updates (deleteRows, updateRows, insertRows) |
||
774 | foreach ($this->collectionUpdates as $collectionToUpdate) { |
||
775 | //fixme: decide what to do with collection mutation if API does not support this |
||
776 | //$this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate); |
||
777 | } |
||
778 | // Entity deletions come last and need to be in reverse commit order |
||
779 | if ($this->entityDeletions) { |
||
780 | for ($count = count($commitOrder), $i = $count - 1; $i >= 0 && $this->entityDeletions; --$i) { |
||
781 | $this->executeDeletions($commitOrder[$i]); |
||
782 | } |
||
783 | } |
||
784 | |||
785 | // Take new snapshots from visited collections |
||
786 | foreach ($this->visitedCollections as $coll) { |
||
787 | $coll->takeSnapshot(); |
||
788 | } |
||
789 | |||
790 | // Clear up |
||
791 | $this->entityInsertions = |
||
792 | $this->entityUpdates = |
||
793 | $this->entityDeletions = |
||
794 | $this->extraUpdates = |
||
795 | $this->entityChangeSets = |
||
796 | $this->collectionUpdates = |
||
797 | $this->collectionDeletions = |
||
798 | $this->visitedCollections = |
||
799 | $this->scheduledForSynchronization = |
||
800 | $this->orphanRemovals = []; |
||
801 | } |
||
802 | |||
803 | /** |
||
804 | * Gets the changeset for an entity. |
||
805 | * |
||
806 | * @param object $entity |
||
807 | * |
||
808 | * @return array |
||
809 | */ |
||
810 | public function & getEntityChangeSet($entity) |
||
811 | { |
||
812 | $oid = spl_object_hash($entity); |
||
813 | $data = []; |
||
814 | if (!isset($this->entityChangeSets[$oid])) { |
||
815 | return $data; |
||
816 | } |
||
817 | |||
818 | return $this->entityChangeSets[$oid]; |
||
819 | } |
||
820 | |||
821 | /** |
||
822 | * Computes the changes that happened to a single entity. |
||
823 | * |
||
824 | * Modifies/populates the following properties: |
||
825 | * |
||
826 | * {@link _originalEntityData} |
||
827 | * If the entity is NEW or MANAGED but not yet fully persisted (only has an id) |
||
828 | * then it was not fetched from the database and therefore we have no original |
||
829 | * entity data yet. All of the current entity data is stored as the original entity data. |
||
830 | * |
||
831 | * {@link _entityChangeSets} |
||
832 | * The changes detected on all properties of the entity are stored there. |
||
833 | * A change is a tuple array where the first entry is the old value and the second |
||
834 | * entry is the new value of the property. Changesets are used by persisters |
||
835 | * to INSERT/UPDATE the persistent entity state. |
||
836 | * |
||
837 | * {@link _entityUpdates} |
||
838 | * If the entity is already fully MANAGED (has been fetched from the database before) |
||
839 | * and any changes to its properties are detected, then a reference to the entity is stored |
||
840 | * there to mark it for an update. |
||
841 | * |
||
842 | * {@link _collectionDeletions} |
||
843 | * If a PersistentCollection has been de-referenced in a fully MANAGED entity, |
||
844 | * then this collection is marked for deletion. |
||
845 | * |
||
846 | * @ignore |
||
847 | * |
||
848 | * @internal Don't call from the outside. |
||
849 | * |
||
850 | * @param ApiMetadata $class The class descriptor of the entity. |
||
851 | * @param object $entity The entity for which to compute the changes. |
||
852 | * |
||
853 | * @return void |
||
854 | */ |
||
855 | public function computeChangeSet(ApiMetadata $class, $entity) |
||
856 | { |
||
857 | $oid = spl_object_hash($entity); |
||
858 | if (isset($this->readOnlyObjects[$oid])) { |
||
859 | return; |
||
860 | } |
||
861 | |||
862 | $actualData = []; |
||
863 | foreach ($class->getReflectionProperties() as $name => $refProp) { |
||
864 | $value = $refProp->getValue($entity); |
||
865 | if (null !== $value && $class->isCollectionValuedAssociation($name)) { |
||
866 | if ($value instanceof ApiCollection) { |
||
867 | if ($value->getOwner() === $entity) { |
||
868 | continue; |
||
869 | } |
||
870 | $value = new ArrayCollection($value->getValues()); |
||
871 | } |
||
872 | // If $value is not a Collection then use an ArrayCollection. |
||
873 | if (!$value instanceof Collection) { |
||
874 | $value = new ArrayCollection($value); |
||
875 | } |
||
876 | $assoc = $class->getAssociationMapping($name); |
||
877 | // Inject PersistentCollection |
||
878 | $value = new ApiCollection( |
||
879 | $this->manager, |
||
880 | $this->manager->getClassMetadata($assoc['targetEntity']), |
||
881 | $value |
||
882 | ); |
||
883 | $value->setOwner($entity, $assoc); |
||
884 | $value->setDirty(!$value->isEmpty()); |
||
885 | $class->getReflectionProperty($name)->setValue($entity, $value); |
||
886 | $actualData[$name] = $value; |
||
887 | continue; |
||
888 | } |
||
889 | if (!$class->isIdentifier($name)) { |
||
890 | $actualData[$name] = $value; |
||
891 | } |
||
892 | } |
||
893 | if (!isset($this->originalEntityData[$oid])) { |
||
894 | // Entity is either NEW or MANAGED but not yet fully persisted (only has an id). |
||
895 | // These result in an INSERT. |
||
896 | $this->originalEntityData[$oid] = (object)$actualData; |
||
897 | $changeSet = []; |
||
898 | foreach ($actualData as $propName => $actualValue) { |
||
899 | if (!$class->hasAssociation($propName)) { |
||
900 | $changeSet[$propName] = [null, $actualValue]; |
||
901 | continue; |
||
902 | } |
||
903 | $assoc = $class->getAssociationMapping($propName); |
||
904 | if ($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE) { |
||
905 | $changeSet[$propName] = [null, $actualValue]; |
||
906 | } |
||
907 | } |
||
908 | $this->entityChangeSets[$oid] = $changeSet; |
||
909 | } else { |
||
910 | |||
911 | // Entity is "fully" MANAGED: it was already fully persisted before |
||
912 | // and we have a copy of the original data |
||
913 | $originalData = $this->originalEntityData[$oid]; |
||
914 | $isChangeTrackingNotify = false; |
||
915 | $changeSet = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid])) |
||
916 | ? $this->entityChangeSets[$oid] |
||
917 | : []; |
||
918 | |||
919 | foreach ($actualData as $propName => $actualValue) { |
||
920 | // skip field, its a partially omitted one! |
||
921 | if (!property_exists($originalData, $propName)) { |
||
922 | continue; |
||
923 | } |
||
924 | $orgValue = $originalData->$propName; |
||
925 | // skip if value haven't changed |
||
926 | if ($orgValue === $actualValue) { |
||
927 | |||
928 | continue; |
||
929 | } |
||
930 | // if regular field |
||
931 | if (!$class->hasAssociation($propName)) { |
||
932 | if ($isChangeTrackingNotify) { |
||
933 | continue; |
||
934 | } |
||
935 | $changeSet[$propName] = [$orgValue, $actualValue]; |
||
936 | continue; |
||
937 | } |
||
938 | |||
939 | $assoc = $class->getAssociationMapping($propName); |
||
940 | // Persistent collection was exchanged with the "originally" |
||
941 | // created one. This can only mean it was cloned and replaced |
||
942 | // on another entity. |
||
943 | if ($actualValue instanceof ApiCollection) { |
||
944 | $owner = $actualValue->getOwner(); |
||
945 | if ($owner === null) { // cloned |
||
946 | $actualValue->setOwner($entity, $assoc); |
||
947 | } else { |
||
948 | if ($owner !== $entity) { // no clone, we have to fix |
||
949 | if (!$actualValue->isInitialized()) { |
||
950 | $actualValue->initialize(); // we have to do this otherwise the cols share state |
||
951 | } |
||
952 | $newValue = clone $actualValue; |
||
953 | $newValue->setOwner($entity, $assoc); |
||
954 | $class->getReflectionProperty($propName)->setValue($entity, $newValue); |
||
955 | } |
||
956 | } |
||
957 | } |
||
958 | if ($orgValue instanceof ApiCollection) { |
||
959 | // A PersistentCollection was de-referenced, so delete it. |
||
960 | $coid = spl_object_hash($orgValue); |
||
961 | if (isset($this->collectionDeletions[$coid])) { |
||
962 | continue; |
||
963 | } |
||
964 | $this->collectionDeletions[$coid] = $orgValue; |
||
965 | $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored. |
||
966 | continue; |
||
967 | } |
||
968 | if ($assoc['type'] & ApiMetadata::TO_ONE) { |
||
969 | if ($assoc['isOwningSide']) { |
||
970 | $changeSet[$propName] = [$orgValue, $actualValue]; |
||
971 | } |
||
972 | if ($orgValue !== null && $assoc['orphanRemoval']) { |
||
973 | $this->scheduleOrphanRemoval($orgValue); |
||
974 | } |
||
975 | } |
||
976 | } |
||
977 | if ($changeSet) { |
||
978 | $this->entityChangeSets[$oid] = $changeSet; |
||
979 | $this->originalEntityData[$oid] = (object)$actualData; |
||
980 | $this->entityUpdates[$oid] = $entity; |
||
981 | } |
||
982 | } |
||
983 | // Look for changes in associations of the entity |
||
984 | foreach ($class->getAssociationMappings() as $field => $assoc) { |
||
985 | if (($val = $class->getReflectionProperty($field)->getValue($entity)) === null) { |
||
986 | continue; |
||
987 | } |
||
988 | $this->computeAssociationChanges($assoc, $val); |
||
989 | if (!isset($this->entityChangeSets[$oid]) && |
||
990 | $assoc['isOwningSide'] && |
||
991 | $assoc['type'] == ApiMetadata::MANY_TO_MANY && |
||
992 | $val instanceof ApiCollection && |
||
993 | $val->isDirty() |
||
994 | ) { |
||
995 | $this->entityChangeSets[$oid] = []; |
||
996 | $this->originalEntityData[$oid] = (object)$actualData; |
||
997 | $this->entityUpdates[$oid] = $entity; |
||
998 | } |
||
999 | } |
||
1000 | } |
||
1001 | |||
1002 | /** |
||
1003 | * Computes all the changes that have been done to entities and collections |
||
1004 | * since the last commit and stores these changes in the _entityChangeSet map |
||
1005 | * temporarily for access by the persisters, until the UoW commit is finished. |
||
1006 | * |
||
1007 | * @return void |
||
1008 | */ |
||
1009 | public function computeChangeSets() |
||
1010 | { |
||
1011 | // Compute changes for INSERTed entities first. This must always happen. |
||
1012 | $this->computeScheduleInsertsChangeSets(); |
||
1013 | // Compute changes for other MANAGED entities. Change tracking policies take effect here. |
||
1014 | foreach ($this->identityMap as $className => $entities) { |
||
1015 | $class = $this->manager->getClassMetadata($className); |
||
1016 | // Skip class if instances are read-only |
||
1017 | if ($class->isReadOnly()) { |
||
1018 | continue; |
||
1019 | } |
||
1020 | // If change tracking is explicit or happens through notification, then only compute |
||
1021 | // changes on entities of that type that are explicitly marked for synchronization. |
||
1022 | switch (true) { |
||
1023 | case ($class->isChangeTrackingDeferredImplicit()): |
||
1024 | $entitiesToProcess = $entities; |
||
1025 | break; |
||
1026 | case (isset($this->scheduledForSynchronization[$className])): |
||
1027 | $entitiesToProcess = $this->scheduledForSynchronization[$className]; |
||
1028 | break; |
||
1029 | default: |
||
1030 | $entitiesToProcess = []; |
||
1031 | } |
||
1032 | foreach ($entitiesToProcess as $entity) { |
||
1033 | // Ignore uninitialized proxy objects |
||
1034 | if ($entity instanceof Proxy && !$entity->__isInitialized__) { |
||
1035 | continue; |
||
1036 | } |
||
1037 | // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here. |
||
1038 | $oid = spl_object_hash($entity); |
||
1039 | if (!isset($this->entityInsertions[$oid]) && |
||
1040 | !isset($this->entityDeletions[$oid]) && |
||
1041 | isset($this->entityStates[$oid]) |
||
1042 | ) { |
||
1043 | $this->computeChangeSet($class, $entity); |
||
1044 | } |
||
1045 | } |
||
1046 | } |
||
1047 | } |
||
1048 | |||
1049 | /** |
||
1050 | * INTERNAL: |
||
1051 | * Schedules an orphaned entity for removal. The remove() operation will be |
||
1052 | * invoked on that entity at the beginning of the next commit of this |
||
1053 | * UnitOfWork. |
||
1054 | * |
||
1055 | * @ignore |
||
1056 | * |
||
1057 | * @param object $entity |
||
1058 | * |
||
1059 | * @return void |
||
1060 | */ |
||
1061 | public function scheduleOrphanRemoval($entity) |
||
1062 | { |
||
1063 | $this->orphanRemovals[spl_object_hash($entity)] = $entity; |
||
1064 | } |
||
1065 | |||
1066 | public function loadCollection(ApiCollection $collection) |
||
1067 | { |
||
1068 | $assoc = $collection->getMapping(); |
||
1069 | $persister = $this->getEntityPersister($assoc['targetEntity']); |
||
1070 | switch ($assoc['type']) { |
||
1071 | case ApiMetadata::ONE_TO_MANY: |
||
1072 | $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection); |
||
1073 | break; |
||
1074 | } |
||
1075 | $collection->setInitialized(true); |
||
1076 | } |
||
1077 | |||
1078 | public function getCollectionPersister($association) |
||
1079 | { |
||
1080 | $targetMetadata = $this->manager->getClassMetadata($association['targetEntity']); |
||
1081 | $role = $association['sourceEntity'] . '::' . $association['field']; |
||
1082 | |||
1083 | if (!array_key_exists($role, $this->collectionPersisters)) { |
||
1084 | $this->collectionPersisters[$role] = new CollectionPersister( |
||
1085 | $this->manager, |
||
1086 | $targetMetadata, |
||
1087 | new CollectionMatcher($this->manager, $this->getCrudsApi($targetMetadata)), |
||
1088 | $association |
||
1089 | ); |
||
1090 | } |
||
1091 | |||
1092 | return $this->collectionPersisters[$role]; |
||
1093 | } |
||
1094 | |||
1095 | public function scheduleCollectionDeletion(Collection $collection) |
||
1096 | { |
||
1097 | } |
||
1098 | |||
1099 | public function cancelOrphanRemoval($value) |
||
1100 | { |
||
1101 | } |
||
1102 | |||
1103 | /** |
||
1104 | * INTERNAL: |
||
1105 | * Sets a property value of the original data array of an entity. |
||
1106 | * |
||
1107 | * @ignore |
||
1108 | * |
||
1109 | * @param string $oid |
||
1110 | * @param string $property |
||
1111 | * @param mixed $value |
||
1112 | * |
||
1113 | * @return void |
||
1114 | */ |
||
1115 | public function setOriginalEntityProperty($oid, $property, $value) |
||
1116 | { |
||
1117 | if (!array_key_exists($oid, $this->originalEntityData)) { |
||
1118 | $this->originalEntityData[$oid] = new \stdClass(); |
||
1119 | } |
||
1120 | |||
1121 | $this->originalEntityData[$oid]->$property = $value; |
||
1122 | } |
||
1123 | |||
1124 | public function scheduleExtraUpdate($entity, $changeset) |
||
1125 | { |
||
1126 | $oid = spl_object_hash($entity); |
||
1127 | $extraUpdate = [$entity, $changeset]; |
||
1128 | if (isset($this->extraUpdates[$oid])) { |
||
1129 | list(, $changeset2) = $this->extraUpdates[$oid]; |
||
1130 | $extraUpdate = [$entity, $changeset + $changeset2]; |
||
1131 | } |
||
1132 | $this->extraUpdates[$oid] = $extraUpdate; |
||
1133 | } |
||
1134 | |||
1135 | /** |
||
1136 | * Refreshes the state of the given entity from the database, overwriting |
||
1137 | * any local, unpersisted changes. |
||
1138 | * |
||
1139 | * @param object $entity The entity to refresh. |
||
1140 | * |
||
1141 | * @return void |
||
1142 | * |
||
1143 | * @throws InvalidArgumentException If the entity is not MANAGED. |
||
1144 | */ |
||
1145 | public function refresh($entity) |
||
1146 | { |
||
1147 | $visited = []; |
||
1148 | $this->doRefresh($entity, $visited); |
||
1149 | } |
||
1150 | |||
1151 | /** |
||
1152 | * Clears the UnitOfWork. |
||
1153 | * |
||
1154 | * @param string|null $entityName if given, only entities of this type will get detached. |
||
1155 | * |
||
1156 | * @return void |
||
1157 | */ |
||
1158 | public function clear($entityName = null) |
||
1159 | { |
||
1160 | if ($entityName === null) { |
||
1161 | $this->identityMap = |
||
1162 | $this->entityIdentifiers = |
||
1163 | $this->originalEntityData = |
||
1164 | $this->entityChangeSets = |
||
1165 | $this->entityStates = |
||
1166 | $this->scheduledForSynchronization = |
||
1167 | $this->entityInsertions = |
||
1168 | $this->entityUpdates = |
||
1169 | $this->entityDeletions = |
||
1170 | $this->collectionDeletions = |
||
1171 | $this->collectionUpdates = |
||
1172 | $this->extraUpdates = |
||
1173 | $this->readOnlyObjects = |
||
1174 | $this->visitedCollections = |
||
1175 | $this->orphanRemovals = []; |
||
1176 | } else { |
||
1177 | $this->clearIdentityMapForEntityName($entityName); |
||
1178 | $this->clearEntityInsertionsForEntityName($entityName); |
||
1179 | } |
||
1180 | } |
||
1181 | |||
1182 | /** |
||
1183 | * @param PersistentCollection $coll |
||
1184 | * |
||
1185 | * @return bool |
||
1186 | */ |
||
1187 | public function isCollectionScheduledForDeletion(PersistentCollection $coll) |
||
1188 | { |
||
1189 | return isset($this->collectionDeletions[spl_object_hash($coll)]); |
||
1190 | } |
||
1191 | |||
1192 | /** |
||
1193 | * Schedules an entity for dirty-checking at commit-time. |
||
1194 | * |
||
1195 | * @param object $entity The entity to schedule for dirty-checking. |
||
1196 | * |
||
1197 | * @return void |
||
1198 | * |
||
1199 | * @todo Rename: scheduleForSynchronization |
||
1200 | */ |
||
1201 | public function scheduleForDirtyCheck($entity) |
||
1202 | { |
||
1203 | $rootClassName = |
||
1204 | $this->manager->getClassMetadata(get_class($entity))->getRootEntityName(); |
||
1205 | $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity; |
||
1206 | } |
||
1207 | |||
1208 | /** |
||
1209 | * Deletes an entity as part of the current unit of work. |
||
1210 | * |
||
1211 | * @param object $entity The entity to remove. |
||
1212 | * |
||
1213 | * @return void |
||
1214 | */ |
||
1215 | public function remove($entity) |
||
1216 | { |
||
1217 | $visited = []; |
||
1218 | $this->doRemove($entity, $visited); |
||
1219 | } |
||
1220 | |||
1221 | /** |
||
1222 | * Merges the state of the given detached entity into this UnitOfWork. |
||
1223 | * |
||
1224 | * @param object $entity |
||
1225 | * |
||
1226 | * @return object The managed copy of the entity. |
||
1227 | */ |
||
1228 | public function merge($entity) |
||
1229 | { |
||
1230 | $visited = []; |
||
1231 | |||
1232 | return $this->doMerge($entity, $visited); |
||
1233 | } |
||
1234 | |||
1235 | /** |
||
1236 | * Detaches an entity from the persistence management. It's persistence will |
||
1237 | * no longer be managed by Doctrine. |
||
1238 | * |
||
1239 | * @param object $entity The entity to detach. |
||
1240 | * |
||
1241 | * @return void |
||
1242 | */ |
||
1243 | public function detach($entity) |
||
1244 | { |
||
1245 | $visited = []; |
||
1246 | $this->doDetach($entity, $visited); |
||
1247 | } |
||
1248 | |||
1249 | /** |
||
1250 | * Resolve metadata against source data and root class |
||
1251 | * |
||
1252 | * @param \stdClass $data |
||
1253 | * @param string $class |
||
1254 | * |
||
1255 | * @return ApiMetadata |
||
1256 | * @throws MappingException |
||
1257 | */ |
||
1258 | private function resolveSourceMetadataForClass(\stdClass $data, $class) |
||
1259 | { |
||
1260 | $metadata = $this->manager->getClassMetadata($class); |
||
1261 | $discriminatorValue = $metadata->getDiscriminatorValue(); |
||
1262 | if ($metadata->getDiscriminatorField()) { |
||
1263 | $property = $metadata->getDiscriminatorField()['fieldName']; |
||
1264 | if (isset($data->$property)) { |
||
1265 | $discriminatorValue = $data->$property; |
||
1266 | } |
||
1267 | } |
||
1268 | |||
1269 | $map = $metadata->getDiscriminatorMap(); |
||
1270 | |||
1271 | if (!array_key_exists($discriminatorValue, $map)) { |
||
1272 | throw MappingException::unknownDiscriminatorValue($discriminatorValue, $class); |
||
1273 | } |
||
1274 | |||
1275 | $realClass = $map[$discriminatorValue]; |
||
1276 | |||
1277 | return $this->manager->getClassMetadata($realClass); |
||
1278 | } |
||
1279 | |||
1280 | /** |
||
1281 | * Helper method to show an object as string. |
||
1282 | * |
||
1283 | * @param object $obj |
||
1284 | * |
||
1285 | * @return string |
||
1286 | */ |
||
1287 | private static function objToStr($obj) |
||
1288 | { |
||
1289 | return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj); |
||
1290 | } |
||
1291 | |||
1292 | /** |
||
1293 | * @param ApiMetadata $class |
||
1294 | * |
||
1295 | * @return \Doctrine\Common\Persistence\ObjectManagerAware|object |
||
1296 | */ |
||
1297 | private function newInstance(ApiMetadata $class) |
||
1298 | { |
||
1299 | $entity = $class->newInstance(); |
||
1300 | |||
1301 | if ($entity instanceof ObjectManagerAware) { |
||
1302 | $entity->injectObjectManager($this->manager, $class); |
||
1303 | } |
||
1304 | |||
1305 | return $entity; |
||
1306 | } |
||
1307 | |||
1308 | /** |
||
1309 | * @param ApiMetadata $classMetadata |
||
1310 | * |
||
1311 | * @return EntityDataCacheInterface |
||
1312 | */ |
||
1313 | private function createEntityCache(ApiMetadata $classMetadata) |
||
1314 | { |
||
1315 | $configuration = $this->manager->getConfiguration()->getCacheConfiguration($classMetadata->getName()); |
||
1316 | $cache = new VoidEntityCache($classMetadata); |
||
1317 | if ($configuration->isEnabled() && $this->manager->getConfiguration()->getApiCache()) { |
||
1318 | $cache = |
||
1319 | new LoggingCache( |
||
1320 | new ApiEntityCache( |
||
1321 | $this->manager->getConfiguration()->getApiCache(), |
||
1322 | $classMetadata, |
||
1323 | $configuration |
||
1324 | ), |
||
1325 | $this->manager->getConfiguration()->getApiCacheLogger() |
||
1326 | ); |
||
1327 | |||
1328 | return $cache; |
||
1329 | } |
||
1330 | |||
1331 | return $cache; |
||
1332 | } |
||
1333 | |||
1334 | /** |
||
1335 | * @param ApiMetadata $classMetadata |
||
1336 | * |
||
1337 | * @return CrudsApiInterface |
||
1338 | */ |
||
1339 | private function getCrudsApi(ApiMetadata $classMetadata) |
||
1340 | { |
||
1341 | if (!array_key_exists($classMetadata->getName(), $this->apis)) { |
||
1342 | $client = $this->manager->getConfiguration()->getClientRegistry()->get($classMetadata->getClientName()); |
||
1343 | |||
1344 | $api = $this->manager |
||
1345 | ->getConfiguration() |
||
1346 | ->getFactoryRegistry() |
||
1347 | ->create( |
||
1348 | $classMetadata->getApiFactory(), |
||
1349 | $client, |
||
1350 | $classMetadata |
||
1351 | ); |
||
1352 | |||
1353 | $this->apis[$classMetadata->getName()] = $api; |
||
1354 | } |
||
1355 | |||
1356 | return $this->apis[$classMetadata->getName()]; |
||
1357 | } |
||
1358 | |||
1359 | private function doPersist($entity, $visited) |
||
1360 | { |
||
1361 | $oid = spl_object_hash($entity); |
||
1362 | if (isset($visited[$oid])) { |
||
1363 | return; // Prevent infinite recursion |
||
1364 | } |
||
1365 | $visited[$oid] = $entity; // Mark visited |
||
1366 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
1367 | // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation). |
||
1368 | // If we would detect DETACHED here we would throw an exception anyway with the same |
||
1369 | // consequences (not recoverable/programming error), so just assuming NEW here |
||
1370 | // lets us avoid some database lookups for entities with natural identifiers. |
||
1371 | $entityState = $this->getEntityState($entity, self::STATE_NEW); |
||
1372 | switch ($entityState) { |
||
1373 | case self::STATE_MANAGED: |
||
1374 | $this->scheduleForDirtyCheck($entity); |
||
1375 | break; |
||
1376 | case self::STATE_NEW: |
||
1377 | $this->persistNew($class, $entity); |
||
1378 | break; |
||
1379 | case self::STATE_REMOVED: |
||
1380 | // Entity becomes managed again |
||
1381 | unset($this->entityDeletions[$oid]); |
||
1382 | $this->addToIdentityMap($entity); |
||
1383 | $this->entityStates[$oid] = self::STATE_MANAGED; |
||
1384 | break; |
||
1385 | case self::STATE_DETACHED: |
||
1386 | // Can actually not happen right now since we assume STATE_NEW. |
||
1387 | throw new \InvalidArgumentException('Detached entity cannot be persisted'); |
||
1388 | default: |
||
1389 | throw new \UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity)); |
||
1390 | } |
||
1391 | $this->cascadePersist($entity, $visited); |
||
1392 | } |
||
1393 | |||
1394 | /** |
||
1395 | * Cascades the save operation to associated entities. |
||
1396 | * |
||
1397 | * @param object $entity |
||
1398 | * @param array $visited |
||
1399 | * |
||
1400 | * @return void |
||
1401 | * @throws \InvalidArgumentException |
||
1402 | * @throws MappingException |
||
1403 | */ |
||
1404 | private function cascadePersist($entity, array &$visited) |
||
1405 | { |
||
1406 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
1407 | $associationMappings = []; |
||
1408 | foreach ($class->getAssociationNames() as $name) { |
||
1409 | $assoc = $class->getAssociationMapping($name); |
||
1410 | if ($assoc['isCascadePersist']) { |
||
1411 | $associationMappings[$name] = $assoc; |
||
1412 | } |
||
1413 | } |
||
1414 | foreach ($associationMappings as $assoc) { |
||
1415 | $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity); |
||
1416 | switch (true) { |
||
1417 | case ($relatedEntities instanceof ApiCollection): |
||
1418 | // Unwrap so that foreach() does not initialize |
||
1419 | $relatedEntities = $relatedEntities->unwrap(); |
||
1420 | // break; is commented intentionally! |
||
1421 | case ($relatedEntities instanceof Collection): |
||
1422 | case (is_array($relatedEntities)): |
||
1423 | if (($assoc['type'] & ApiMetadata::TO_MANY) <= 0) { |
||
1424 | throw new \InvalidArgumentException('Invalid association for cascade'); |
||
1425 | } |
||
1426 | foreach ($relatedEntities as $relatedEntity) { |
||
1427 | $this->doPersist($relatedEntity, $visited); |
||
1428 | } |
||
1429 | break; |
||
1430 | case ($relatedEntities !== null): |
||
1431 | if (!$relatedEntities instanceof $assoc['targetEntity']) { |
||
1432 | throw new \InvalidArgumentException('Invalid association for cascade'); |
||
1433 | } |
||
1434 | $this->doPersist($relatedEntities, $visited); |
||
1435 | break; |
||
1436 | default: |
||
1437 | // Do nothing |
||
1438 | } |
||
1439 | } |
||
1440 | } |
||
1441 | |||
1442 | /** |
||
1443 | * @param ApiMetadata $class |
||
1444 | * @param object $entity |
||
1445 | * |
||
1446 | * @return void |
||
1447 | */ |
||
1448 | private function persistNew($class, $entity) |
||
1449 | { |
||
1450 | $oid = spl_object_hash($entity); |
||
1451 | // $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist); |
||
1452 | // if ($invoke !== ListenersInvoker::INVOKE_NONE) { |
||
1453 | // $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); |
||
1454 | // } |
||
1455 | // $idGen = $class->idGenerator; |
||
1456 | // if ( ! $idGen->isPostInsertGenerator()) { |
||
1457 | // $idValue = $idGen->generate($this->em, $entity); |
||
1458 | // if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) { |
||
1459 | // $idValue = array($class->identifier[0] => $idValue); |
||
1460 | // $class->setIdentifierValues($entity, $idValue); |
||
1461 | // } |
||
1462 | // $this->entityIdentifiers[$oid] = $idValue; |
||
1463 | // } |
||
1464 | $this->entityStates[$oid] = self::STATE_MANAGED; |
||
1465 | $this->scheduleForInsert($entity); |
||
1466 | } |
||
1467 | |||
1468 | /** |
||
1469 | * Gets the commit order. |
||
1470 | * |
||
1471 | * @param array|null $entityChangeSet |
||
1472 | * |
||
1473 | * @return array |
||
1474 | */ |
||
1475 | private function getCommitOrder(array $entityChangeSet = null) |
||
1476 | { |
||
1477 | if ($entityChangeSet === null) { |
||
1478 | $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions); |
||
1479 | } |
||
1480 | $calc = $this->getCommitOrderCalculator(); |
||
1481 | // See if there are any new classes in the changeset, that are not in the |
||
1482 | // commit order graph yet (don't have a node). |
||
1483 | // We have to inspect changeSet to be able to correctly build dependencies. |
||
1484 | // It is not possible to use IdentityMap here because post inserted ids |
||
1485 | // are not yet available. |
||
1486 | /** @var ApiMetadata[] $newNodes */ |
||
1487 | $newNodes = []; |
||
1488 | foreach ((array)$entityChangeSet as $entity) { |
||
1489 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
1490 | if ($calc->hasNode($class->getName())) { |
||
1491 | continue; |
||
1492 | } |
||
1493 | $calc->addNode($class->getName(), $class); |
||
1494 | $newNodes[] = $class; |
||
1495 | } |
||
1496 | // Calculate dependencies for new nodes |
||
1497 | while ($class = array_pop($newNodes)) { |
||
1498 | foreach ($class->getAssociationMappings() as $assoc) { |
||
1499 | if (!($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE)) { |
||
1500 | continue; |
||
1501 | } |
||
1502 | $targetClass = $this->manager->getClassMetadata($assoc['targetEntity']); |
||
1503 | if (!$calc->hasNode($targetClass->getName())) { |
||
1504 | $calc->addNode($targetClass->getName(), $targetClass); |
||
1505 | $newNodes[] = $targetClass; |
||
1506 | } |
||
1507 | $calc->addDependency($targetClass->getName(), $class->name, (int)empty($assoc['nullable'])); |
||
1508 | // If the target class has mapped subclasses, these share the same dependency. |
||
1509 | if (!$targetClass->getSubclasses()) { |
||
1510 | continue; |
||
1511 | } |
||
1512 | foreach ($targetClass->getSubclasses() as $subClassName) { |
||
1513 | $targetSubClass = $this->manager->getClassMetadata($subClassName); |
||
1514 | if (!$calc->hasNode($subClassName)) { |
||
1515 | $calc->addNode($targetSubClass->name, $targetSubClass); |
||
1516 | $newNodes[] = $targetSubClass; |
||
1517 | } |
||
1518 | $calc->addDependency($targetSubClass->name, $class->name, 1); |
||
1519 | } |
||
1520 | } |
||
1521 | } |
||
1522 | |||
1523 | return $calc->sort(); |
||
1524 | } |
||
1525 | |||
1526 | private function getCommitOrderCalculator() |
||
1527 | { |
||
1528 | return new Utility\CommitOrderCalculator(); |
||
1529 | } |
||
1530 | |||
1531 | /** |
||
1532 | * Only flushes the given entity according to a ruleset that keeps the UoW consistent. |
||
1533 | * |
||
1534 | * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well! |
||
1535 | * 2. Read Only entities are skipped. |
||
1536 | * 3. Proxies are skipped. |
||
1537 | * 4. Only if entity is properly managed. |
||
1538 | * |
||
1539 | * @param object $entity |
||
1540 | * |
||
1541 | * @return void |
||
1542 | * |
||
1543 | * @throws \InvalidArgumentException |
||
1544 | */ |
||
1545 | private function computeSingleEntityChangeSet($entity) |
||
1546 | { |
||
1547 | $state = $this->getEntityState($entity); |
||
1548 | if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) { |
||
1549 | throw new \InvalidArgumentException( |
||
1550 | "Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity) |
||
1551 | ); |
||
1552 | } |
||
1553 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
1554 | // Compute changes for INSERTed entities first. This must always happen even in this case. |
||
1555 | $this->computeScheduleInsertsChangeSets(); |
||
1556 | if ($class->isReadOnly()) { |
||
1557 | return; |
||
1558 | } |
||
1559 | // Ignore uninitialized proxy objects |
||
1560 | if ($entity instanceof Proxy && !$entity->__isInitialized__) { |
||
1561 | return; |
||
1562 | } |
||
1563 | // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here. |
||
1564 | $oid = spl_object_hash($entity); |
||
1565 | if (!isset($this->entityInsertions[$oid]) && |
||
1566 | !isset($this->entityDeletions[$oid]) && |
||
1567 | isset($this->entityStates[$oid]) |
||
1568 | ) { |
||
1569 | $this->computeChangeSet($class, $entity); |
||
1570 | } |
||
1571 | } |
||
1572 | |||
1573 | /** |
||
1574 | * Computes the changesets of all entities scheduled for insertion. |
||
1575 | * |
||
1576 | * @return void |
||
1577 | */ |
||
1578 | private function computeScheduleInsertsChangeSets() |
||
1579 | { |
||
1580 | foreach ($this->entityInsertions as $entity) { |
||
1581 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
1582 | $this->computeChangeSet($class, $entity); |
||
1583 | } |
||
1584 | } |
||
1585 | |||
1586 | /** |
||
1587 | * Computes the changes of an association. |
||
1588 | * |
||
1589 | * @param array $assoc The association mapping. |
||
1590 | * @param mixed $value The value of the association. |
||
1591 | * |
||
1592 | * @throws \InvalidArgumentException |
||
1593 | * @throws \UnexpectedValueException |
||
1594 | * |
||
1595 | * @return void |
||
1596 | */ |
||
1597 | private function computeAssociationChanges($assoc, $value) |
||
1598 | { |
||
1599 | if ($value instanceof Proxy && !$value->__isInitialized__) { |
||
1600 | return; |
||
1601 | } |
||
1602 | if ($value instanceof ApiCollection && $value->isDirty()) { |
||
1603 | $coid = spl_object_hash($value); |
||
1604 | $this->collectionUpdates[$coid] = $value; |
||
1605 | $this->visitedCollections[$coid] = $value; |
||
1606 | } |
||
1607 | // Look through the entities, and in any of their associations, |
||
1608 | // for transient (new) entities, recursively. ("Persistence by reachability") |
||
1609 | // Unwrap. Uninitialized collections will simply be empty. |
||
1610 | $unwrappedValue = ($assoc['type'] & ApiMetadata::TO_ONE) ? [$value] : $value->unwrap(); |
||
1611 | $targetClass = $this->manager->getClassMetadata($assoc['targetEntity']); |
||
1612 | $targetClassName = $targetClass->getName(); |
||
1613 | foreach ($unwrappedValue as $key => $entry) { |
||
1614 | if (!($entry instanceof $targetClassName)) { |
||
1615 | throw new \InvalidArgumentException('Invalid association'); |
||
1616 | } |
||
1617 | $state = $this->getEntityState($entry, self::STATE_NEW); |
||
1618 | if (!($entry instanceof $assoc['targetEntity'])) { |
||
1619 | throw new \UnexpectedValueException('Unexpected association'); |
||
1620 | } |
||
1621 | switch ($state) { |
||
1622 | case self::STATE_NEW: |
||
1623 | if (!$assoc['isCascadePersist']) { |
||
1624 | throw new \InvalidArgumentException('New entity through relationship'); |
||
1625 | } |
||
1626 | $this->persistNew($targetClass, $entry); |
||
1627 | $this->computeChangeSet($targetClass, $entry); |
||
1628 | break; |
||
1629 | case self::STATE_REMOVED: |
||
1630 | // Consume the $value as array (it's either an array or an ArrayAccess) |
||
1631 | // and remove the element from Collection. |
||
1632 | if ($assoc['type'] & ApiMetadata::TO_MANY) { |
||
1633 | unset($value[$key]); |
||
1634 | } |
||
1635 | break; |
||
1636 | case self::STATE_DETACHED: |
||
1637 | // Can actually not happen right now as we assume STATE_NEW, |
||
1638 | // so the exception will be raised from the DBAL layer (constraint violation). |
||
1639 | throw new \InvalidArgumentException('Detached entity through relationship'); |
||
1640 | break; |
||
1641 | default: |
||
1642 | // MANAGED associated entities are already taken into account |
||
1643 | // during changeset calculation anyway, since they are in the identity map. |
||
1644 | } |
||
1645 | } |
||
1646 | } |
||
1647 | |||
1648 | private function executeInserts(ApiMetadata $class) |
||
1649 | { |
||
1650 | $className = $class->getName(); |
||
1651 | $persister = $this->getEntityPersister($className); |
||
1652 | foreach ($this->entityInsertions as $oid => $entity) { |
||
1653 | if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) { |
||
1654 | continue; |
||
1655 | } |
||
1656 | $persister->pushNewEntity($entity); |
||
1657 | unset($this->entityInsertions[$oid]); |
||
1658 | } |
||
1659 | $postInsertIds = $persister->flushNewEntities(); |
||
1660 | if ($postInsertIds) { |
||
1661 | // Persister returned post-insert IDs |
||
1662 | foreach ($postInsertIds as $postInsertId) { |
||
1663 | $id = $postInsertId['generatedId']; |
||
1664 | $entity = $postInsertId['entity']; |
||
1665 | $oid = spl_object_hash($entity); |
||
1666 | |||
1667 | if ($id instanceof \stdClass) { |
||
1668 | $id = (array)$id; |
||
1669 | } |
||
1670 | if (!is_array($id)) { |
||
1671 | $id = [$class->getApiFieldName($class->getIdentifierFieldNames()[0]) => $id]; |
||
1672 | } |
||
1673 | |||
1674 | if (!array_key_exists($oid, $this->originalEntityData)) { |
||
1675 | $this->originalEntityData[$oid] = new \stdClass(); |
||
1676 | } |
||
1677 | |||
1678 | $idValues = []; |
||
1679 | foreach ((array)$id as $apiIdField => $idValue) { |
||
1680 | $idName = $class->getFieldName($apiIdField); |
||
1681 | $typeName = $class->getTypeOfField($idName); |
||
1682 | $type = $this->manager->getConfiguration()->getTypeRegistry()->get($typeName); |
||
1683 | $idValue = $type->toApiValue($idValue, $class->getFieldOptions($idName)); |
||
1684 | $class->getReflectionProperty($idName)->setValue($entity, $idValue); |
||
1685 | $idValues[$idName] = $idValue; |
||
1686 | $this->originalEntityData[$oid]->$idName = $idValue; |
||
1687 | } |
||
1688 | |||
1689 | $this->entityIdentifiers[$oid] = $idValues; |
||
1690 | $this->entityStates[$oid] = self::STATE_MANAGED; |
||
1691 | $this->addToIdentityMap($entity); |
||
1692 | } |
||
1693 | } |
||
1694 | } |
||
1695 | |||
1696 | private function executeUpdates($class) |
||
1697 | { |
||
1698 | $className = $class->name; |
||
1699 | $persister = $this->getEntityPersister($className); |
||
1700 | foreach ($this->entityUpdates as $oid => $entity) { |
||
1701 | if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) { |
||
1702 | continue; |
||
1703 | } |
||
1704 | $this->recomputeSingleEntityChangeSet($class, $entity); |
||
1705 | |||
1706 | if (!empty($this->entityChangeSets[$oid])) { |
||
1707 | $persister->update($entity); |
||
1708 | } |
||
1709 | unset($this->entityUpdates[$oid]); |
||
1710 | } |
||
1711 | } |
||
1712 | |||
1713 | /** |
||
1714 | * Executes a refresh operation on an entity. |
||
1715 | * |
||
1716 | * @param object $entity The entity to refresh. |
||
1717 | * @param array $visited The already visited entities during cascades. |
||
1718 | * |
||
1719 | * @return void |
||
1720 | * |
||
1721 | * @throws \InvalidArgumentException If the entity is not MANAGED. |
||
1722 | */ |
||
1723 | private function doRefresh($entity, array &$visited) |
||
1724 | { |
||
1725 | $oid = spl_object_hash($entity); |
||
1726 | if (isset($visited[$oid])) { |
||
1727 | return; // Prevent infinite recursion |
||
1728 | } |
||
1729 | $visited[$oid] = $entity; // mark visited |
||
1730 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
1731 | if ($this->getEntityState($entity) !== self::STATE_MANAGED) { |
||
1732 | throw new \InvalidArgumentException('Entity not managed'); |
||
1733 | } |
||
1734 | $this->getEntityPersister($class->getName())->refresh( |
||
1735 | array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), |
||
1736 | $entity |
||
1737 | ); |
||
1738 | $this->cascadeRefresh($entity, $visited); |
||
1739 | } |
||
1740 | |||
1741 | /** |
||
1742 | * Cascades a refresh operation to associated entities. |
||
1743 | * |
||
1744 | * @param object $entity |
||
1745 | * @param array $visited |
||
1746 | * |
||
1747 | * @return void |
||
1748 | */ |
||
1749 | private function cascadeRefresh($entity, array &$visited) |
||
1750 | { |
||
1751 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
1752 | $associationMappings = array_filter( |
||
1753 | $class->getAssociationMappings(), |
||
1754 | function($assoc) { |
||
1755 | return $assoc['isCascadeRefresh']; |
||
1756 | } |
||
1757 | ); |
||
1758 | foreach ($associationMappings as $assoc) { |
||
1759 | $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity); |
||
1760 | switch (true) { |
||
1761 | case ($relatedEntities instanceof ApiCollection): |
||
1762 | // Unwrap so that foreach() does not initialize |
||
1763 | $relatedEntities = $relatedEntities->unwrap(); |
||
1764 | // break; is commented intentionally! |
||
1765 | case ($relatedEntities instanceof Collection): |
||
1766 | case (is_array($relatedEntities)): |
||
1767 | foreach ($relatedEntities as $relatedEntity) { |
||
1768 | $this->doRefresh($relatedEntity, $visited); |
||
1769 | } |
||
1770 | break; |
||
1771 | case ($relatedEntities !== null): |
||
1772 | $this->doRefresh($relatedEntities, $visited); |
||
1773 | break; |
||
1774 | default: |
||
1775 | // Do nothing |
||
1776 | } |
||
1777 | } |
||
1778 | } |
||
1779 | |||
1780 | /** |
||
1781 | * Cascades a detach operation to associated entities. |
||
1782 | * |
||
1783 | * @param object $entity |
||
1784 | * @param array $visited |
||
1785 | * |
||
1786 | * @return void |
||
1787 | */ |
||
1788 | private function cascadeDetach($entity, array &$visited) |
||
1789 | { |
||
1790 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
1791 | $associationMappings = array_filter( |
||
1792 | $class->getAssociationMappings(), |
||
1793 | function($assoc) { |
||
1794 | return $assoc['isCascadeDetach']; |
||
1795 | } |
||
1796 | ); |
||
1797 | foreach ($associationMappings as $assoc) { |
||
1798 | $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity); |
||
1799 | switch (true) { |
||
1800 | case ($relatedEntities instanceof ApiCollection): |
||
1801 | // Unwrap so that foreach() does not initialize |
||
1802 | $relatedEntities = $relatedEntities->unwrap(); |
||
1803 | // break; is commented intentionally! |
||
1804 | case ($relatedEntities instanceof Collection): |
||
1805 | case (is_array($relatedEntities)): |
||
1806 | foreach ($relatedEntities as $relatedEntity) { |
||
1807 | $this->doDetach($relatedEntity, $visited); |
||
1808 | } |
||
1809 | break; |
||
1810 | case ($relatedEntities !== null): |
||
1811 | $this->doDetach($relatedEntities, $visited); |
||
1812 | break; |
||
1813 | default: |
||
1814 | // Do nothing |
||
1815 | } |
||
1816 | } |
||
1817 | } |
||
1818 | |||
1819 | /** |
||
1820 | * Cascades a merge operation to associated entities. |
||
1821 | * |
||
1822 | * @param object $entity |
||
1823 | * @param object $managedCopy |
||
1824 | * @param array $visited |
||
1825 | * |
||
1826 | * @return void |
||
1827 | */ |
||
1828 | private function cascadeMerge($entity, $managedCopy, array &$visited) |
||
1829 | { |
||
1830 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
1831 | $associationMappings = array_filter( |
||
1832 | $class->getAssociationMappings(), |
||
1833 | function($assoc) { |
||
1834 | return $assoc['isCascadeMerge']; |
||
1835 | } |
||
1836 | ); |
||
1837 | foreach ($associationMappings as $assoc) { |
||
1838 | $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity); |
||
1839 | if ($relatedEntities instanceof Collection) { |
||
1840 | if ($relatedEntities === $class->getReflectionProperty($assoc['field'])->getValue($managedCopy)) { |
||
1841 | continue; |
||
1842 | } |
||
1843 | if ($relatedEntities instanceof ApiCollection) { |
||
1844 | // Unwrap so that foreach() does not initialize |
||
1845 | $relatedEntities = $relatedEntities->unwrap(); |
||
1846 | } |
||
1847 | foreach ($relatedEntities as $relatedEntity) { |
||
1848 | $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc); |
||
1849 | } |
||
1850 | } else { |
||
1851 | if ($relatedEntities !== null) { |
||
1852 | $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc); |
||
1853 | } |
||
1854 | } |
||
1855 | } |
||
1856 | } |
||
1857 | |||
1858 | /** |
||
1859 | * Cascades the delete operation to associated entities. |
||
1860 | * |
||
1861 | * @param object $entity |
||
1862 | * @param array $visited |
||
1863 | * |
||
1864 | * @return void |
||
1865 | */ |
||
1866 | private function cascadeRemove($entity, array &$visited) |
||
1867 | { |
||
1868 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
1869 | $associationMappings = array_filter( |
||
1870 | $class->getAssociationMappings(), |
||
1871 | function($assoc) { |
||
1872 | return $assoc['isCascadeRemove']; |
||
1873 | } |
||
1874 | ); |
||
1875 | $entitiesToCascade = []; |
||
1876 | foreach ($associationMappings as $assoc) { |
||
1877 | if ($entity instanceof Proxy && !$entity->__isInitialized__) { |
||
1878 | $entity->__load(); |
||
1879 | } |
||
1880 | $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity); |
||
1881 | switch (true) { |
||
1882 | case ($relatedEntities instanceof Collection): |
||
1883 | case (is_array($relatedEntities)): |
||
1884 | // If its a PersistentCollection initialization is intended! No unwrap! |
||
1885 | foreach ($relatedEntities as $relatedEntity) { |
||
1886 | $entitiesToCascade[] = $relatedEntity; |
||
1887 | } |
||
1888 | break; |
||
1889 | case ($relatedEntities !== null): |
||
1890 | $entitiesToCascade[] = $relatedEntities; |
||
1891 | break; |
||
1892 | default: |
||
1893 | // Do nothing |
||
1894 | } |
||
1895 | } |
||
1896 | foreach ($entitiesToCascade as $relatedEntity) { |
||
1897 | $this->doRemove($relatedEntity, $visited); |
||
1898 | } |
||
1899 | } |
||
1900 | |||
1901 | /** |
||
1902 | * Executes any extra updates that have been scheduled. |
||
1903 | */ |
||
1904 | private function executeExtraUpdates() |
||
1905 | { |
||
1906 | foreach ($this->extraUpdates as $oid => $update) { |
||
1907 | list ($entity, $changeset) = $update; |
||
1908 | $this->entityChangeSets[$oid] = $changeset; |
||
1909 | $this->getEntityPersister(get_class($entity))->update($entity); |
||
1910 | } |
||
1911 | $this->extraUpdates = []; |
||
1912 | } |
||
1913 | |||
1914 | private function executeDeletions(ApiMetadata $class) |
||
1915 | { |
||
1916 | $className = $class->getName(); |
||
1917 | $persister = $this->getEntityPersister($className); |
||
1918 | foreach ($this->entityDeletions as $oid => $entity) { |
||
1919 | if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) { |
||
1920 | continue; |
||
1921 | } |
||
1922 | $persister->delete($entity); |
||
1923 | unset( |
||
1924 | $this->entityDeletions[$oid], |
||
1925 | $this->entityIdentifiers[$oid], |
||
1926 | $this->originalEntityData[$oid], |
||
1927 | $this->entityStates[$oid] |
||
1928 | ); |
||
1929 | // Entity with this $oid after deletion treated as NEW, even if the $oid |
||
1930 | // is obtained by a new entity because the old one went out of scope. |
||
1931 | //$this->entityStates[$oid] = self::STATE_NEW; |
||
1932 | // if ( ! $class->isIdentifierNatural()) { |
||
1933 | // $class->getReflectionProperty($class->getIdentifierFieldNames()[0])->setValue($entity, null); |
||
1934 | // } |
||
1935 | } |
||
1936 | } |
||
1937 | |||
1938 | /** |
||
1939 | * @param object $entity |
||
1940 | * @param object $managedCopy |
||
1941 | */ |
||
1942 | private function mergeEntityStateIntoManagedCopy($entity, $managedCopy) |
||
1943 | { |
||
1944 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
1945 | foreach ($this->reflectionPropertiesGetter->getProperties($class->getName()) as $prop) { |
||
1946 | $name = $prop->name; |
||
1947 | $prop->setAccessible(true); |
||
1948 | if ($class->hasAssociation($name)) { |
||
1949 | if (!$class->isIdentifier($name)) { |
||
1950 | $prop->setValue($managedCopy, $prop->getValue($entity)); |
||
1951 | } |
||
1952 | } else { |
||
1953 | $assoc2 = $class->getAssociationMapping($name); |
||
1954 | if ($assoc2['type'] & ApiMetadata::TO_ONE) { |
||
1955 | $other = $prop->getValue($entity); |
||
1956 | if ($other === null) { |
||
1957 | $prop->setValue($managedCopy, null); |
||
1958 | } else { |
||
1959 | if ($other instanceof Proxy && !$other->__isInitialized()) { |
||
1960 | // do not merge fields marked lazy that have not been fetched. |
||
1961 | continue; |
||
1962 | } |
||
1963 | if (!$assoc2['isCascadeMerge']) { |
||
1964 | if ($this->getEntityState($other) === self::STATE_DETACHED) { |
||
1965 | $targetClass = $this->manager->getClassMetadata($assoc2['targetEntity']); |
||
1966 | $relatedId = $targetClass->getIdentifierValues($other); |
||
1967 | if ($targetClass->getSubclasses()) { |
||
1968 | $other = $this->manager->find($targetClass->getName(), $relatedId); |
||
1969 | } else { |
||
1970 | $other = $this->manager->getProxyFactory()->getProxy( |
||
1971 | $assoc2['targetEntity'], |
||
1972 | $relatedId |
||
1973 | ); |
||
1974 | $this->registerManaged($other, $relatedId, []); |
||
1975 | } |
||
1976 | } |
||
1977 | $prop->setValue($managedCopy, $other); |
||
1978 | } |
||
1979 | } |
||
1980 | } else { |
||
1981 | $mergeCol = $prop->getValue($entity); |
||
1982 | if ($mergeCol instanceof ApiCollection && !$mergeCol->isInitialized()) { |
||
1983 | // do not merge fields marked lazy that have not been fetched. |
||
1984 | // keep the lazy persistent collection of the managed copy. |
||
1985 | continue; |
||
1986 | } |
||
1987 | $managedCol = $prop->getValue($managedCopy); |
||
1988 | if (!$managedCol) { |
||
1989 | $managedCol = new ApiCollection( |
||
1990 | $this->manager, |
||
1991 | $this->manager->getClassMetadata($assoc2['targetEntity']), |
||
1992 | new ArrayCollection |
||
1993 | ); |
||
1994 | $managedCol->setOwner($managedCopy, $assoc2); |
||
1995 | $prop->setValue($managedCopy, $managedCol); |
||
1996 | } |
||
1997 | if ($assoc2['isCascadeMerge']) { |
||
1998 | $managedCol->initialize(); |
||
1999 | // clear and set dirty a managed collection if its not also the same collection to merge from. |
||
2000 | if (!$managedCol->isEmpty() && $managedCol !== $mergeCol) { |
||
2001 | $managedCol->unwrap()->clear(); |
||
2002 | $managedCol->setDirty(true); |
||
2003 | if ($assoc2['isOwningSide'] |
||
2004 | && $assoc2['type'] == ApiMetadata::MANY_TO_MANY |
||
2005 | && $class->isChangeTrackingNotify() |
||
2006 | ) { |
||
2007 | $this->scheduleForDirtyCheck($managedCopy); |
||
2008 | } |
||
2009 | } |
||
2010 | } |
||
2011 | } |
||
2012 | } |
||
2013 | if ($class->isChangeTrackingNotify()) { |
||
2014 | // Just treat all properties as changed, there is no other choice. |
||
2015 | $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy)); |
||
2016 | } |
||
2017 | } |
||
2018 | } |
||
2019 | |||
2020 | /** |
||
2021 | * Deletes an entity as part of the current unit of work. |
||
2022 | * |
||
2023 | * This method is internally called during delete() cascades as it tracks |
||
2024 | * the already visited entities to prevent infinite recursions. |
||
2025 | * |
||
2026 | * @param object $entity The entity to delete. |
||
2027 | * @param array $visited The map of the already visited entities. |
||
2028 | * |
||
2029 | * @return void |
||
2030 | * |
||
2031 | * @throws \InvalidArgumentException If the instance is a detached entity. |
||
2032 | * @throws \UnexpectedValueException |
||
2033 | */ |
||
2034 | private function doRemove($entity, array &$visited) |
||
2035 | { |
||
2036 | $oid = spl_object_hash($entity); |
||
2037 | if (isset($visited[$oid])) { |
||
2038 | return; // Prevent infinite recursion |
||
2039 | } |
||
2040 | $visited[$oid] = $entity; // mark visited |
||
2041 | // Cascade first, because scheduleForDelete() removes the entity from the identity map, which |
||
2042 | // can cause problems when a lazy proxy has to be initialized for the cascade operation. |
||
2043 | $this->cascadeRemove($entity, $visited); |
||
2044 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
2045 | $entityState = $this->getEntityState($entity); |
||
2046 | switch ($entityState) { |
||
2047 | case self::STATE_NEW: |
||
2048 | case self::STATE_REMOVED: |
||
2049 | // nothing to do |
||
2050 | break; |
||
2051 | case self::STATE_MANAGED: |
||
2052 | $this->scheduleForDelete($entity); |
||
2053 | break; |
||
2054 | case self::STATE_DETACHED: |
||
2055 | throw new \InvalidArgumentException('Detached entity cannot be removed'); |
||
2056 | default: |
||
2057 | throw new \UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity)); |
||
2058 | } |
||
2059 | } |
||
2060 | |||
2061 | /** |
||
2062 | * Tests if an entity is loaded - must either be a loaded proxy or not a proxy |
||
2063 | * |
||
2064 | * @param object $entity |
||
2065 | * |
||
2066 | * @return bool |
||
2067 | */ |
||
2068 | private function isLoaded($entity) |
||
2069 | { |
||
2070 | return !($entity instanceof Proxy) || $entity->__isInitialized(); |
||
2071 | } |
||
2072 | |||
2073 | /** |
||
2074 | * Sets/adds associated managed copies into the previous entity's association field |
||
2075 | * |
||
2076 | * @param object $entity |
||
2077 | * @param array $association |
||
2078 | * @param object $previousManagedCopy |
||
2079 | * @param object $managedCopy |
||
2080 | * |
||
2081 | * @return void |
||
2082 | */ |
||
2083 | private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy) |
||
2084 | { |
||
2085 | $assocField = $association['fieldName']; |
||
2086 | $prevClass = $this->manager->getClassMetadata(get_class($previousManagedCopy)); |
||
2087 | if ($association['type'] & ApiMetadata::TO_ONE) { |
||
2088 | $prevClass->getReflectionProperty($assocField)->setValue($previousManagedCopy, $managedCopy); |
||
2089 | |||
2090 | return; |
||
2091 | } |
||
2092 | /** @var array $value */ |
||
2093 | $value = $prevClass->getReflectionProperty($assocField)->getValue($previousManagedCopy); |
||
2094 | $value[] = $managedCopy; |
||
2095 | if ($association['type'] == ApiMetadata::ONE_TO_MANY) { |
||
2096 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
2097 | $class->getReflectionProperty($association['mappedBy'])->setValue($managedCopy, $previousManagedCopy); |
||
2098 | } |
||
2099 | } |
||
2100 | |||
2101 | /** |
||
2102 | * Executes a merge operation on an entity. |
||
2103 | * |
||
2104 | * @param object $entity |
||
2105 | * @param array $visited |
||
2106 | * @param object|null $prevManagedCopy |
||
2107 | * @param array|null $assoc |
||
2108 | * |
||
2109 | * @return object The managed copy of the entity. |
||
2110 | * |
||
2111 | * @throws \InvalidArgumentException If the entity instance is NEW. |
||
2112 | * @throws \OutOfBoundsException |
||
2113 | */ |
||
2114 | private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = []) |
||
2115 | { |
||
2116 | $oid = spl_object_hash($entity); |
||
2117 | if (isset($visited[$oid])) { |
||
2118 | $managedCopy = $visited[$oid]; |
||
2119 | if ($prevManagedCopy !== null) { |
||
2120 | $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy); |
||
2121 | } |
||
2122 | |||
2123 | return $managedCopy; |
||
2124 | } |
||
2125 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
2126 | // First we assume DETACHED, although it can still be NEW but we can avoid |
||
2127 | // an extra db-roundtrip this way. If it is not MANAGED but has an identity, |
||
2128 | // we need to fetch it from the db anyway in order to merge. |
||
2129 | // MANAGED entities are ignored by the merge operation. |
||
2130 | $managedCopy = $entity; |
||
2131 | if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) { |
||
2132 | // Try to look the entity up in the identity map. |
||
2133 | $id = $class->getIdentifierValues($entity); |
||
2134 | // If there is no ID, it is actually NEW. |
||
2135 | if (!$id) { |
||
2136 | $managedCopy = $this->newInstance($class); |
||
2137 | $this->persistNew($class, $managedCopy); |
||
2138 | } else { |
||
2139 | $flatId = ($class->containsForeignIdentifier()) |
||
2140 | ? $this->identifierFlattener->flattenIdentifier($class, $id) |
||
2141 | : $id; |
||
2142 | $managedCopy = $this->tryGetById($flatId, $class->getRootEntityName()); |
||
2143 | if ($managedCopy) { |
||
2144 | // We have the entity in-memory already, just make sure its not removed. |
||
2145 | if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) { |
||
2146 | throw new \InvalidArgumentException('Removed entity cannot be merged'); |
||
2147 | } |
||
2148 | } else { |
||
2149 | // We need to fetch the managed copy in order to merge. |
||
2150 | $managedCopy = $this->manager->find($class->getName(), $flatId); |
||
2151 | } |
||
2152 | if ($managedCopy === null) { |
||
2153 | // If the identifier is ASSIGNED, it is NEW, otherwise an error |
||
2154 | // since the managed entity was not found. |
||
2155 | if (!$class->isIdentifierNatural()) { |
||
2156 | throw new \OutOfBoundsException('Entity not found'); |
||
2157 | } |
||
2158 | $managedCopy = $this->newInstance($class); |
||
2159 | $class->setIdentifierValues($managedCopy, $id); |
||
2160 | $this->persistNew($class, $managedCopy); |
||
2161 | } |
||
2162 | } |
||
2163 | |||
2164 | $visited[$oid] = $managedCopy; // mark visited |
||
2165 | if ($this->isLoaded($entity)) { |
||
2166 | if ($managedCopy instanceof Proxy && !$managedCopy->__isInitialized()) { |
||
2167 | $managedCopy->__load(); |
||
2168 | } |
||
2169 | $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy); |
||
2170 | } |
||
2171 | if ($class->isChangeTrackingDeferredExplicit()) { |
||
2172 | $this->scheduleForDirtyCheck($entity); |
||
2173 | } |
||
2174 | } |
||
2175 | if ($prevManagedCopy !== null) { |
||
2176 | $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy); |
||
2177 | } |
||
2178 | // Mark the managed copy visited as well |
||
2179 | $visited[spl_object_hash($managedCopy)] = $managedCopy; |
||
2180 | $this->cascadeMerge($entity, $managedCopy, $visited); |
||
2181 | |||
2182 | return $managedCopy; |
||
2183 | } |
||
2184 | |||
2185 | /** |
||
2186 | * Executes a detach operation on the given entity. |
||
2187 | * |
||
2188 | * @param object $entity |
||
2189 | * @param array $visited |
||
2190 | * @param boolean $noCascade if true, don't cascade detach operation. |
||
2191 | * |
||
2192 | * @return void |
||
2193 | */ |
||
2194 | private function doDetach($entity, array &$visited, $noCascade = false) |
||
2195 | { |
||
2196 | $oid = spl_object_hash($entity); |
||
2197 | if (isset($visited[$oid])) { |
||
2198 | return; // Prevent infinite recursion |
||
2199 | } |
||
2200 | $visited[$oid] = $entity; // mark visited |
||
2201 | switch ($this->getEntityState($entity, self::STATE_DETACHED)) { |
||
2202 | case self::STATE_MANAGED: |
||
2203 | if ($this->isInIdentityMap($entity)) { |
||
2204 | $this->removeFromIdentityMap($entity); |
||
2205 | } |
||
2206 | unset( |
||
2207 | $this->entityInsertions[$oid], |
||
2208 | $this->entityUpdates[$oid], |
||
2209 | $this->entityDeletions[$oid], |
||
2210 | $this->entityIdentifiers[$oid], |
||
2211 | $this->entityStates[$oid], |
||
2212 | $this->originalEntityData[$oid] |
||
2213 | ); |
||
2214 | break; |
||
2215 | case self::STATE_NEW: |
||
2216 | case self::STATE_DETACHED: |
||
2217 | return; |
||
2218 | } |
||
2219 | if (!$noCascade) { |
||
2220 | $this->cascadeDetach($entity, $visited); |
||
2221 | } |
||
2222 | } |
||
2223 | |||
2224 | /** |
||
2225 | * @param ApiMetadata $class |
||
2226 | * |
||
2227 | * @return EntityHydrator |
||
2228 | */ |
||
2229 | private function getHydratorForClass(ApiMetadata $class) |
||
2230 | { |
||
2231 | if (!array_key_exists($class->getName(), $this->hydrators)) { |
||
2232 | $this->hydrators[$class->getName()] = new EntityHydrator($this->manager, $class); |
||
2233 | } |
||
2234 | |||
2235 | return $this->hydrators[$class->getName()]; |
||
2236 | } |
||
2237 | } |
||
2238 |