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