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 MIT license. For more information, see 17 * <http://www.doctrine-project.org>. 18 */ 19 20namespace Doctrine\ORM; 21 22use Doctrine\Common\Collections\ArrayCollection; 23use Doctrine\Common\Collections\Collection; 24use Doctrine\DBAL\LockMode; 25use Doctrine\ORM\Cache\Persister\CachedPersister; 26use Doctrine\ORM\Event\LifecycleEventArgs; 27use Doctrine\ORM\Event\ListenersInvoker; 28use Doctrine\ORM\Event\OnFlushEventArgs; 29use Doctrine\ORM\Event\PostFlushEventArgs; 30use Doctrine\ORM\Event\PreFlushEventArgs; 31use Doctrine\ORM\Event\PreUpdateEventArgs; 32use Doctrine\ORM\Internal\HydrationCompleteHandler; 33use Doctrine\ORM\Mapping\ClassMetadata; 34use Doctrine\ORM\Mapping\Reflection\ReflectionPropertiesGetter; 35use Doctrine\ORM\Persisters\Collection\ManyToManyPersister; 36use Doctrine\ORM\Persisters\Collection\OneToManyPersister; 37use Doctrine\ORM\Persisters\Entity\BasicEntityPersister; 38use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister; 39use Doctrine\ORM\Persisters\Entity\SingleTablePersister; 40use Doctrine\ORM\Proxy\Proxy; 41use Doctrine\ORM\Utility\IdentifierFlattener; 42use Doctrine\Persistence\Mapping\RuntimeReflectionService; 43use Doctrine\Persistence\NotifyPropertyChanged; 44use Doctrine\Persistence\ObjectManagerAware; 45use Doctrine\Persistence\PropertyChangedListener; 46use InvalidArgumentException; 47use Throwable; 48use UnexpectedValueException; 49use function get_class; 50use function spl_object_hash; 51 52/** 53 * The UnitOfWork is responsible for tracking changes to objects during an 54 * "object-level" transaction and for writing out changes to the database 55 * in the correct order. 56 * 57 * Internal note: This class contains highly performance-sensitive code. 58 * 59 * @since 2.0 60 * @author Benjamin Eberlei <kontakt@beberlei.de> 61 * @author Guilherme Blanco <guilhermeblanco@hotmail.com> 62 * @author Jonathan Wage <jonwage@gmail.com> 63 * @author Roman Borschel <roman@code-factory.org> 64 * @author Rob Caiger <rob@clocal.co.uk> 65 */ 66class UnitOfWork implements PropertyChangedListener 67{ 68 /** 69 * An entity is in MANAGED state when its persistence is managed by an EntityManager. 70 */ 71 const STATE_MANAGED = 1; 72 73 /** 74 * An entity is new if it has just been instantiated (i.e. using the "new" operator) 75 * and is not (yet) managed by an EntityManager. 76 */ 77 const STATE_NEW = 2; 78 79 /** 80 * A detached entity is an instance with persistent state and identity that is not 81 * (or no longer) associated with an EntityManager (and a UnitOfWork). 82 */ 83 const STATE_DETACHED = 3; 84 85 /** 86 * A removed entity instance is an instance with a persistent identity, 87 * associated with an EntityManager, whose persistent state will be deleted 88 * on commit. 89 */ 90 const STATE_REMOVED = 4; 91 92 /** 93 * Hint used to collect all primary keys of associated entities during hydration 94 * and execute it in a dedicated query afterwards 95 * @see https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#temporarily-change-fetch-mode-in-dql 96 */ 97 const HINT_DEFEREAGERLOAD = 'deferEagerLoad'; 98 99 /** 100 * The identity map that holds references to all managed entities that have 101 * an identity. The entities are grouped by their class name. 102 * Since all classes in a hierarchy must share the same identifier set, 103 * we always take the root class name of the hierarchy. 104 * 105 * @var array 106 */ 107 private $identityMap = []; 108 109 /** 110 * Map of all identifiers of managed entities. 111 * Keys are object ids (spl_object_hash). 112 * 113 * @var array 114 */ 115 private $entityIdentifiers = []; 116 117 /** 118 * Map of the original entity data of managed entities. 119 * Keys are object ids (spl_object_hash). This is used for calculating changesets 120 * at commit time. 121 * 122 * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage. 123 * A value will only really be copied if the value in the entity is modified 124 * by the user. 125 * 126 * @var array 127 */ 128 private $originalEntityData = []; 129 130 /** 131 * Map of entity changes. Keys are object ids (spl_object_hash). 132 * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end. 133 * 134 * @var array 135 */ 136 private $entityChangeSets = []; 137 138 /** 139 * The (cached) states of any known entities. 140 * Keys are object ids (spl_object_hash). 141 * 142 * @var array 143 */ 144 private $entityStates = []; 145 146 /** 147 * Map of entities that are scheduled for dirty checking at commit time. 148 * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT. 149 * Keys are object ids (spl_object_hash). 150 * 151 * @var array 152 */ 153 private $scheduledForSynchronization = []; 154 155 /** 156 * A list of all pending entity insertions. 157 * 158 * @var array 159 */ 160 private $entityInsertions = []; 161 162 /** 163 * A list of all pending entity updates. 164 * 165 * @var array 166 */ 167 private $entityUpdates = []; 168 169 /** 170 * Any pending extra updates that have been scheduled by persisters. 171 * 172 * @var array 173 */ 174 private $extraUpdates = []; 175 176 /** 177 * A list of all pending entity deletions. 178 * 179 * @var array 180 */ 181 private $entityDeletions = []; 182 183 /** 184 * New entities that were discovered through relationships that were not 185 * marked as cascade-persist. During flush, this array is populated and 186 * then pruned of any entities that were discovered through a valid 187 * cascade-persist path. (Leftovers cause an error.) 188 * 189 * Keys are OIDs, payload is a two-item array describing the association 190 * and the entity. 191 * 192 * @var object[][]|array[][] indexed by respective object spl_object_hash() 193 */ 194 private $nonCascadedNewDetectedEntities = []; 195 196 /** 197 * All pending collection deletions. 198 * 199 * @var array 200 */ 201 private $collectionDeletions = []; 202 203 /** 204 * All pending collection updates. 205 * 206 * @var array 207 */ 208 private $collectionUpdates = []; 209 210 /** 211 * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork. 212 * At the end of the UnitOfWork all these collections will make new snapshots 213 * of their data. 214 * 215 * @var array 216 */ 217 private $visitedCollections = []; 218 219 /** 220 * The EntityManager that "owns" this UnitOfWork instance. 221 * 222 * @var EntityManagerInterface 223 */ 224 private $em; 225 226 /** 227 * The entity persister instances used to persist entity instances. 228 * 229 * @var array 230 */ 231 private $persisters = []; 232 233 /** 234 * The collection persister instances used to persist collections. 235 * 236 * @var array 237 */ 238 private $collectionPersisters = []; 239 240 /** 241 * The EventManager used for dispatching events. 242 * 243 * @var \Doctrine\Common\EventManager 244 */ 245 private $evm; 246 247 /** 248 * The ListenersInvoker used for dispatching events. 249 * 250 * @var \Doctrine\ORM\Event\ListenersInvoker 251 */ 252 private $listenersInvoker; 253 254 /** 255 * The IdentifierFlattener used for manipulating identifiers 256 * 257 * @var \Doctrine\ORM\Utility\IdentifierFlattener 258 */ 259 private $identifierFlattener; 260 261 /** 262 * Orphaned entities that are scheduled for removal. 263 * 264 * @var array 265 */ 266 private $orphanRemovals = []; 267 268 /** 269 * Read-Only objects are never evaluated 270 * 271 * @var array 272 */ 273 private $readOnlyObjects = []; 274 275 /** 276 * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested. 277 * 278 * @var array 279 */ 280 private $eagerLoadingEntities = []; 281 282 /** 283 * @var boolean 284 */ 285 protected $hasCache = false; 286 287 /** 288 * Helper for handling completion of hydration 289 * 290 * @var HydrationCompleteHandler 291 */ 292 private $hydrationCompleteHandler; 293 294 /** 295 * @var ReflectionPropertiesGetter 296 */ 297 private $reflectionPropertiesGetter; 298 299 /** 300 * Initializes a new UnitOfWork instance, bound to the given EntityManager. 301 * 302 * @param EntityManagerInterface $em 303 */ 304 public function __construct(EntityManagerInterface $em) 305 { 306 $this->em = $em; 307 $this->evm = $em->getEventManager(); 308 $this->listenersInvoker = new ListenersInvoker($em); 309 $this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled(); 310 $this->identifierFlattener = new IdentifierFlattener($this, $em->getMetadataFactory()); 311 $this->hydrationCompleteHandler = new HydrationCompleteHandler($this->listenersInvoker, $em); 312 $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService()); 313 } 314 315 /** 316 * Commits the UnitOfWork, executing all operations that have been postponed 317 * up to this point. The state of all managed entities will be synchronized with 318 * the database. 319 * 320 * The operations are executed in the following order: 321 * 322 * 1) All entity insertions 323 * 2) All entity updates 324 * 3) All collection deletions 325 * 4) All collection updates 326 * 5) All entity deletions 327 * 328 * @param null|object|array $entity 329 * 330 * @return void 331 * 332 * @throws \Exception 333 */ 334 public function commit($entity = null) 335 { 336 // Raise preFlush 337 if ($this->evm->hasListeners(Events::preFlush)) { 338 $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em)); 339 } 340 341 // Compute changes done since last commit. 342 if (null === $entity) { 343 $this->computeChangeSets(); 344 } elseif (is_object($entity)) { 345 $this->computeSingleEntityChangeSet($entity); 346 } elseif (is_array($entity)) { 347 foreach ($entity as $object) { 348 $this->computeSingleEntityChangeSet($object); 349 } 350 } 351 352 if ( ! ($this->entityInsertions || 353 $this->entityDeletions || 354 $this->entityUpdates || 355 $this->collectionUpdates || 356 $this->collectionDeletions || 357 $this->orphanRemovals)) { 358 $this->dispatchOnFlushEvent(); 359 $this->dispatchPostFlushEvent(); 360 361 $this->postCommitCleanup($entity); 362 363 return; // Nothing to do. 364 } 365 366 $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations(); 367 368 if ($this->orphanRemovals) { 369 foreach ($this->orphanRemovals as $orphan) { 370 $this->remove($orphan); 371 } 372 } 373 374 $this->dispatchOnFlushEvent(); 375 376 // Now we need a commit order to maintain referential integrity 377 $commitOrder = $this->getCommitOrder(); 378 379 $conn = $this->em->getConnection(); 380 $conn->beginTransaction(); 381 382 try { 383 // Collection deletions (deletions of complete collections) 384 foreach ($this->collectionDeletions as $collectionToDelete) { 385 if (! $collectionToDelete instanceof PersistentCollection) { 386 $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete); 387 388 continue; 389 } 390 391 // Deferred explicit tracked collections can be removed only when owning relation was persisted 392 $owner = $collectionToDelete->getOwner(); 393 394 if ($this->em->getClassMetadata(get_class($owner))->isChangeTrackingDeferredImplicit() || $this->isScheduledForDirtyCheck($owner)) { 395 $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete); 396 } 397 } 398 399 if ($this->entityInsertions) { 400 foreach ($commitOrder as $class) { 401 $this->executeInserts($class); 402 } 403 } 404 405 if ($this->entityUpdates) { 406 foreach ($commitOrder as $class) { 407 $this->executeUpdates($class); 408 } 409 } 410 411 // Extra updates that were requested by persisters. 412 if ($this->extraUpdates) { 413 $this->executeExtraUpdates(); 414 } 415 416 // Collection updates (deleteRows, updateRows, insertRows) 417 foreach ($this->collectionUpdates as $collectionToUpdate) { 418 $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate); 419 } 420 421 // Entity deletions come last and need to be in reverse commit order 422 if ($this->entityDeletions) { 423 for ($count = count($commitOrder), $i = $count - 1; $i >= 0 && $this->entityDeletions; --$i) { 424 $this->executeDeletions($commitOrder[$i]); 425 } 426 } 427 428 $conn->commit(); 429 } catch (Throwable $e) { 430 $this->em->close(); 431 $conn->rollBack(); 432 433 $this->afterTransactionRolledBack(); 434 435 throw $e; 436 } 437 438 $this->afterTransactionComplete(); 439 440 // Take new snapshots from visited collections 441 foreach ($this->visitedCollections as $coll) { 442 $coll->takeSnapshot(); 443 } 444 445 $this->dispatchPostFlushEvent(); 446 447 $this->postCommitCleanup($entity); 448 } 449 450 /** 451 * @param null|object|object[] $entity 452 */ 453 private function postCommitCleanup($entity) : void 454 { 455 $this->entityInsertions = 456 $this->entityUpdates = 457 $this->entityDeletions = 458 $this->extraUpdates = 459 $this->collectionUpdates = 460 $this->nonCascadedNewDetectedEntities = 461 $this->collectionDeletions = 462 $this->visitedCollections = 463 $this->orphanRemovals = []; 464 465 if (null === $entity) { 466 $this->entityChangeSets = $this->scheduledForSynchronization = []; 467 468 return; 469 } 470 471 $entities = \is_object($entity) 472 ? [$entity] 473 : $entity; 474 475 foreach ($entities as $object) { 476 $oid = spl_object_hash($object); 477 478 $this->clearEntityChangeSet($oid); 479 480 unset($this->scheduledForSynchronization[$this->em->getClassMetadata(\get_class($object))->rootEntityName][$oid]); 481 } 482 } 483 484 /** 485 * Computes the changesets of all entities scheduled for insertion. 486 * 487 * @return void 488 */ 489 private function computeScheduleInsertsChangeSets() 490 { 491 foreach ($this->entityInsertions as $entity) { 492 $class = $this->em->getClassMetadata(get_class($entity)); 493 494 $this->computeChangeSet($class, $entity); 495 } 496 } 497 498 /** 499 * Only flushes the given entity according to a ruleset that keeps the UoW consistent. 500 * 501 * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well! 502 * 2. Read Only entities are skipped. 503 * 3. Proxies are skipped. 504 * 4. Only if entity is properly managed. 505 * 506 * @param object $entity 507 * 508 * @return void 509 * 510 * @throws \InvalidArgumentException 511 */ 512 private function computeSingleEntityChangeSet($entity) 513 { 514 $state = $this->getEntityState($entity); 515 516 if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) { 517 throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity)); 518 } 519 520 $class = $this->em->getClassMetadata(get_class($entity)); 521 522 if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) { 523 $this->persist($entity); 524 } 525 526 // Compute changes for INSERTed entities first. This must always happen even in this case. 527 $this->computeScheduleInsertsChangeSets(); 528 529 if ($class->isReadOnly) { 530 return; 531 } 532 533 // Ignore uninitialized proxy objects 534 if ($entity instanceof Proxy && ! $entity->__isInitialized__) { 535 return; 536 } 537 538 // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here. 539 $oid = spl_object_hash($entity); 540 541 if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) { 542 $this->computeChangeSet($class, $entity); 543 } 544 } 545 546 /** 547 * Executes any extra updates that have been scheduled. 548 */ 549 private function executeExtraUpdates() 550 { 551 foreach ($this->extraUpdates as $oid => $update) { 552 [$entity, $changeset] = $update; 553 554 $this->entityChangeSets[$oid] = $changeset; 555 $this->getEntityPersister(get_class($entity))->update($entity); 556 } 557 558 $this->extraUpdates = []; 559 } 560 561 /** 562 * Gets the changeset for an entity. 563 * 564 * @param object $entity 565 * 566 * @return array 567 */ 568 public function & getEntityChangeSet($entity) 569 { 570 $oid = spl_object_hash($entity); 571 $data = []; 572 573 if (!isset($this->entityChangeSets[$oid])) { 574 return $data; 575 } 576 577 return $this->entityChangeSets[$oid]; 578 } 579 580 /** 581 * Computes the changes that happened to a single entity. 582 * 583 * Modifies/populates the following properties: 584 * 585 * {@link _originalEntityData} 586 * If the entity is NEW or MANAGED but not yet fully persisted (only has an id) 587 * then it was not fetched from the database and therefore we have no original 588 * entity data yet. All of the current entity data is stored as the original entity data. 589 * 590 * {@link _entityChangeSets} 591 * The changes detected on all properties of the entity are stored there. 592 * A change is a tuple array where the first entry is the old value and the second 593 * entry is the new value of the property. Changesets are used by persisters 594 * to INSERT/UPDATE the persistent entity state. 595 * 596 * {@link _entityUpdates} 597 * If the entity is already fully MANAGED (has been fetched from the database before) 598 * and any changes to its properties are detected, then a reference to the entity is stored 599 * there to mark it for an update. 600 * 601 * {@link _collectionDeletions} 602 * If a PersistentCollection has been de-referenced in a fully MANAGED entity, 603 * then this collection is marked for deletion. 604 * 605 * @ignore 606 * 607 * @internal Don't call from the outside. 608 * 609 * @param ClassMetadata $class The class descriptor of the entity. 610 * @param object $entity The entity for which to compute the changes. 611 * 612 * @return void 613 */ 614 public function computeChangeSet(ClassMetadata $class, $entity) 615 { 616 $oid = spl_object_hash($entity); 617 618 if (isset($this->readOnlyObjects[$oid])) { 619 return; 620 } 621 622 if ( ! $class->isInheritanceTypeNone()) { 623 $class = $this->em->getClassMetadata(get_class($entity)); 624 } 625 626 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER; 627 628 if ($invoke !== ListenersInvoker::INVOKE_NONE) { 629 $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke); 630 } 631 632 $actualData = []; 633 634 foreach ($class->reflFields as $name => $refProp) { 635 $value = $refProp->getValue($entity); 636 637 if ($class->isCollectionValuedAssociation($name) && $value !== null) { 638 if ($value instanceof PersistentCollection) { 639 if ($value->getOwner() === $entity) { 640 continue; 641 } 642 643 $value = new ArrayCollection($value->getValues()); 644 } 645 646 // If $value is not a Collection then use an ArrayCollection. 647 if ( ! $value instanceof Collection) { 648 $value = new ArrayCollection($value); 649 } 650 651 $assoc = $class->associationMappings[$name]; 652 653 // Inject PersistentCollection 654 $value = new PersistentCollection( 655 $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value 656 ); 657 $value->setOwner($entity, $assoc); 658 $value->setDirty( ! $value->isEmpty()); 659 660 $class->reflFields[$name]->setValue($entity, $value); 661 662 $actualData[$name] = $value; 663 664 continue; 665 } 666 667 if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) { 668 $actualData[$name] = $value; 669 } 670 } 671 672 if ( ! isset($this->originalEntityData[$oid])) { 673 // Entity is either NEW or MANAGED but not yet fully persisted (only has an id). 674 // These result in an INSERT. 675 $this->originalEntityData[$oid] = $actualData; 676 $changeSet = []; 677 678 foreach ($actualData as $propName => $actualValue) { 679 if ( ! isset($class->associationMappings[$propName])) { 680 $changeSet[$propName] = [null, $actualValue]; 681 682 continue; 683 } 684 685 $assoc = $class->associationMappings[$propName]; 686 687 if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { 688 $changeSet[$propName] = [null, $actualValue]; 689 } 690 } 691 692 $this->entityChangeSets[$oid] = $changeSet; 693 } else { 694 // Entity is "fully" MANAGED: it was already fully persisted before 695 // and we have a copy of the original data 696 $originalData = $this->originalEntityData[$oid]; 697 $isChangeTrackingNotify = $class->isChangeTrackingNotify(); 698 $changeSet = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid])) 699 ? $this->entityChangeSets[$oid] 700 : []; 701 702 foreach ($actualData as $propName => $actualValue) { 703 // skip field, its a partially omitted one! 704 if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) { 705 continue; 706 } 707 708 $orgValue = $originalData[$propName]; 709 710 // skip if value haven't changed 711 if ($orgValue === $actualValue) { 712 continue; 713 } 714 715 // if regular field 716 if ( ! isset($class->associationMappings[$propName])) { 717 if ($isChangeTrackingNotify) { 718 continue; 719 } 720 721 $changeSet[$propName] = [$orgValue, $actualValue]; 722 723 continue; 724 } 725 726 $assoc = $class->associationMappings[$propName]; 727 728 // Persistent collection was exchanged with the "originally" 729 // created one. This can only mean it was cloned and replaced 730 // on another entity. 731 if ($actualValue instanceof PersistentCollection) { 732 $owner = $actualValue->getOwner(); 733 if ($owner === null) { // cloned 734 $actualValue->setOwner($entity, $assoc); 735 } else if ($owner !== $entity) { // no clone, we have to fix 736 if (!$actualValue->isInitialized()) { 737 $actualValue->initialize(); // we have to do this otherwise the cols share state 738 } 739 $newValue = clone $actualValue; 740 $newValue->setOwner($entity, $assoc); 741 $class->reflFields[$propName]->setValue($entity, $newValue); 742 } 743 } 744 745 if ($orgValue instanceof PersistentCollection) { 746 // A PersistentCollection was de-referenced, so delete it. 747 $coid = spl_object_hash($orgValue); 748 749 if (isset($this->collectionDeletions[$coid])) { 750 continue; 751 } 752 753 $this->collectionDeletions[$coid] = $orgValue; 754 $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored. 755 756 continue; 757 } 758 759 if ($assoc['type'] & ClassMetadata::TO_ONE) { 760 if ($assoc['isOwningSide']) { 761 $changeSet[$propName] = [$orgValue, $actualValue]; 762 } 763 764 if ($orgValue !== null && $assoc['orphanRemoval']) { 765 $this->scheduleOrphanRemoval($orgValue); 766 } 767 } 768 } 769 770 if ($changeSet) { 771 $this->entityChangeSets[$oid] = $changeSet; 772 $this->originalEntityData[$oid] = $actualData; 773 $this->entityUpdates[$oid] = $entity; 774 } 775 } 776 777 // Look for changes in associations of the entity 778 foreach ($class->associationMappings as $field => $assoc) { 779 if (($val = $class->reflFields[$field]->getValue($entity)) === null) { 780 continue; 781 } 782 783 $this->computeAssociationChanges($assoc, $val); 784 785 if ( ! isset($this->entityChangeSets[$oid]) && 786 $assoc['isOwningSide'] && 787 $assoc['type'] == ClassMetadata::MANY_TO_MANY && 788 $val instanceof PersistentCollection && 789 $val->isDirty()) { 790 791 $this->entityChangeSets[$oid] = []; 792 $this->originalEntityData[$oid] = $actualData; 793 $this->entityUpdates[$oid] = $entity; 794 } 795 } 796 } 797 798 /** 799 * Computes all the changes that have been done to entities and collections 800 * since the last commit and stores these changes in the _entityChangeSet map 801 * temporarily for access by the persisters, until the UoW commit is finished. 802 * 803 * @return void 804 */ 805 public function computeChangeSets() 806 { 807 // Compute changes for INSERTed entities first. This must always happen. 808 $this->computeScheduleInsertsChangeSets(); 809 810 // Compute changes for other MANAGED entities. Change tracking policies take effect here. 811 foreach ($this->identityMap as $className => $entities) { 812 $class = $this->em->getClassMetadata($className); 813 814 // Skip class if instances are read-only 815 if ($class->isReadOnly) { 816 continue; 817 } 818 819 // If change tracking is explicit or happens through notification, then only compute 820 // changes on entities of that type that are explicitly marked for synchronization. 821 switch (true) { 822 case ($class->isChangeTrackingDeferredImplicit()): 823 $entitiesToProcess = $entities; 824 break; 825 826 case (isset($this->scheduledForSynchronization[$className])): 827 $entitiesToProcess = $this->scheduledForSynchronization[$className]; 828 break; 829 830 default: 831 $entitiesToProcess = []; 832 833 } 834 835 foreach ($entitiesToProcess as $entity) { 836 // Ignore uninitialized proxy objects 837 if ($entity instanceof Proxy && ! $entity->__isInitialized__) { 838 continue; 839 } 840 841 // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here. 842 $oid = spl_object_hash($entity); 843 844 if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) { 845 $this->computeChangeSet($class, $entity); 846 } 847 } 848 } 849 } 850 851 /** 852 * Computes the changes of an association. 853 * 854 * @param array $assoc The association mapping. 855 * @param mixed $value The value of the association. 856 * 857 * @throws ORMInvalidArgumentException 858 * @throws ORMException 859 * 860 * @return void 861 */ 862 private function computeAssociationChanges($assoc, $value) 863 { 864 if ($value instanceof Proxy && ! $value->__isInitialized__) { 865 return; 866 } 867 868 if ($value instanceof PersistentCollection && $value->isDirty()) { 869 $coid = spl_object_hash($value); 870 871 $this->collectionUpdates[$coid] = $value; 872 $this->visitedCollections[$coid] = $value; 873 } 874 875 // Look through the entities, and in any of their associations, 876 // for transient (new) entities, recursively. ("Persistence by reachability") 877 // Unwrap. Uninitialized collections will simply be empty. 878 $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap(); 879 $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); 880 881 foreach ($unwrappedValue as $key => $entry) { 882 if (! ($entry instanceof $targetClass->name)) { 883 throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry); 884 } 885 886 $state = $this->getEntityState($entry, self::STATE_NEW); 887 888 if ( ! ($entry instanceof $assoc['targetEntity'])) { 889 throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']); 890 } 891 892 switch ($state) { 893 case self::STATE_NEW: 894 if ( ! $assoc['isCascadePersist']) { 895 /* 896 * For now just record the details, because this may 897 * not be an issue if we later discover another pathway 898 * through the object-graph where cascade-persistence 899 * is enabled for this object. 900 */ 901 $this->nonCascadedNewDetectedEntities[spl_object_hash($entry)] = [$assoc, $entry]; 902 903 break; 904 } 905 906 $this->persistNew($targetClass, $entry); 907 $this->computeChangeSet($targetClass, $entry); 908 909 break; 910 911 case self::STATE_REMOVED: 912 // Consume the $value as array (it's either an array or an ArrayAccess) 913 // and remove the element from Collection. 914 if ($assoc['type'] & ClassMetadata::TO_MANY) { 915 unset($value[$key]); 916 } 917 break; 918 919 case self::STATE_DETACHED: 920 // Can actually not happen right now as we assume STATE_NEW, 921 // so the exception will be raised from the DBAL layer (constraint violation). 922 throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry); 923 break; 924 925 default: 926 // MANAGED associated entities are already taken into account 927 // during changeset calculation anyway, since they are in the identity map. 928 } 929 } 930 } 931 932 /** 933 * @param \Doctrine\ORM\Mapping\ClassMetadata $class 934 * @param object $entity 935 * 936 * @return void 937 */ 938 private function persistNew($class, $entity) 939 { 940 $oid = spl_object_hash($entity); 941 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist); 942 943 if ($invoke !== ListenersInvoker::INVOKE_NONE) { 944 $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); 945 } 946 947 $idGen = $class->idGenerator; 948 949 if ( ! $idGen->isPostInsertGenerator()) { 950 $idValue = $idGen->generate($this->em, $entity); 951 952 if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) { 953 $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)]; 954 955 $class->setIdentifierValues($entity, $idValue); 956 } 957 958 // Some identifiers may be foreign keys to new entities. 959 // In this case, we don't have the value yet and should treat it as if we have a post-insert generator 960 if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) { 961 $this->entityIdentifiers[$oid] = $idValue; 962 } 963 } 964 965 $this->entityStates[$oid] = self::STATE_MANAGED; 966 967 $this->scheduleForInsert($entity); 968 } 969 970 /** 971 * @param mixed[] $idValue 972 */ 973 private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool 974 { 975 foreach ($idValue as $idField => $idFieldValue) { 976 if ($idFieldValue === null && isset($class->associationMappings[$idField])) { 977 return true; 978 } 979 } 980 981 return false; 982 } 983 984 /** 985 * INTERNAL: 986 * Computes the changeset of an individual entity, independently of the 987 * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit(). 988 * 989 * The passed entity must be a managed entity. If the entity already has a change set 990 * because this method is invoked during a commit cycle then the change sets are added. 991 * whereby changes detected in this method prevail. 992 * 993 * @ignore 994 * 995 * @param ClassMetadata $class The class descriptor of the entity. 996 * @param object $entity The entity for which to (re)calculate the change set. 997 * 998 * @return void 999 * 1000 * @throws ORMInvalidArgumentException If the passed entity is not MANAGED. 1001 */ 1002 public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity) 1003 { 1004 $oid = spl_object_hash($entity); 1005 1006 if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) { 1007 throw ORMInvalidArgumentException::entityNotManaged($entity); 1008 } 1009 1010 // skip if change tracking is "NOTIFY" 1011 if ($class->isChangeTrackingNotify()) { 1012 return; 1013 } 1014 1015 if ( ! $class->isInheritanceTypeNone()) { 1016 $class = $this->em->getClassMetadata(get_class($entity)); 1017 } 1018 1019 $actualData = []; 1020 1021 foreach ($class->reflFields as $name => $refProp) { 1022 if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) 1023 && ($name !== $class->versionField) 1024 && ! $class->isCollectionValuedAssociation($name)) { 1025 $actualData[$name] = $refProp->getValue($entity); 1026 } 1027 } 1028 1029 if ( ! isset($this->originalEntityData[$oid])) { 1030 throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.'); 1031 } 1032 1033 $originalData = $this->originalEntityData[$oid]; 1034 $changeSet = []; 1035 1036 foreach ($actualData as $propName => $actualValue) { 1037 $orgValue = $originalData[$propName] ?? null; 1038 1039 if ($orgValue !== $actualValue) { 1040 $changeSet[$propName] = [$orgValue, $actualValue]; 1041 } 1042 } 1043 1044 if ($changeSet) { 1045 if (isset($this->entityChangeSets[$oid])) { 1046 $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet); 1047 } else if ( ! isset($this->entityInsertions[$oid])) { 1048 $this->entityChangeSets[$oid] = $changeSet; 1049 $this->entityUpdates[$oid] = $entity; 1050 } 1051 $this->originalEntityData[$oid] = $actualData; 1052 } 1053 } 1054 1055 /** 1056 * Executes all entity insertions for entities of the specified type. 1057 * 1058 * @param \Doctrine\ORM\Mapping\ClassMetadata $class 1059 * 1060 * @return void 1061 */ 1062 private function executeInserts($class) 1063 { 1064 $entities = []; 1065 $className = $class->name; 1066 $persister = $this->getEntityPersister($className); 1067 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist); 1068 1069 $insertionsForClass = []; 1070 1071 foreach ($this->entityInsertions as $oid => $entity) { 1072 1073 if ($this->em->getClassMetadata(get_class($entity))->name !== $className) { 1074 continue; 1075 } 1076 1077 $insertionsForClass[$oid] = $entity; 1078 1079 $persister->addInsert($entity); 1080 1081 unset($this->entityInsertions[$oid]); 1082 1083 if ($invoke !== ListenersInvoker::INVOKE_NONE) { 1084 $entities[] = $entity; 1085 } 1086 } 1087 1088 $postInsertIds = $persister->executeInserts(); 1089 1090 if ($postInsertIds) { 1091 // Persister returned post-insert IDs 1092 foreach ($postInsertIds as $postInsertId) { 1093 $idField = $class->getSingleIdentifierFieldName(); 1094 $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']); 1095 1096 $entity = $postInsertId['entity']; 1097 $oid = spl_object_hash($entity); 1098 1099 $class->reflFields[$idField]->setValue($entity, $idValue); 1100 1101 $this->entityIdentifiers[$oid] = [$idField => $idValue]; 1102 $this->entityStates[$oid] = self::STATE_MANAGED; 1103 $this->originalEntityData[$oid][$idField] = $idValue; 1104 1105 $this->addToIdentityMap($entity); 1106 } 1107 } else { 1108 foreach ($insertionsForClass as $oid => $entity) { 1109 if (! isset($this->entityIdentifiers[$oid])) { 1110 //entity was not added to identity map because some identifiers are foreign keys to new entities. 1111 //add it now 1112 $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity); 1113 } 1114 } 1115 } 1116 1117 foreach ($entities as $entity) { 1118 $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); 1119 } 1120 } 1121 1122 /** 1123 * @param object $entity 1124 */ 1125 private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, string $oid, $entity): void 1126 { 1127 $identifier = []; 1128 1129 foreach ($class->getIdentifierFieldNames() as $idField) { 1130 $value = $class->getFieldValue($entity, $idField); 1131 1132 if (isset($class->associationMappings[$idField])) { 1133 // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced. 1134 $value = $this->getSingleIdentifierValue($value); 1135 } 1136 1137 $identifier[$idField] = $this->originalEntityData[$oid][$idField] = $value; 1138 } 1139 1140 $this->entityStates[$oid] = self::STATE_MANAGED; 1141 $this->entityIdentifiers[$oid] = $identifier; 1142 1143 $this->addToIdentityMap($entity); 1144 } 1145 1146 /** 1147 * Executes all entity updates for entities of the specified type. 1148 * 1149 * @param \Doctrine\ORM\Mapping\ClassMetadata $class 1150 * 1151 * @return void 1152 */ 1153 private function executeUpdates($class) 1154 { 1155 $className = $class->name; 1156 $persister = $this->getEntityPersister($className); 1157 $preUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate); 1158 $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate); 1159 1160 foreach ($this->entityUpdates as $oid => $entity) { 1161 if ($this->em->getClassMetadata(get_class($entity))->name !== $className) { 1162 continue; 1163 } 1164 1165 if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) { 1166 $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke); 1167 1168 $this->recomputeSingleEntityChangeSet($class, $entity); 1169 } 1170 1171 if ( ! empty($this->entityChangeSets[$oid])) { 1172 $persister->update($entity); 1173 } 1174 1175 unset($this->entityUpdates[$oid]); 1176 1177 if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) { 1178 $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke); 1179 } 1180 } 1181 } 1182 1183 /** 1184 * Executes all entity deletions for entities of the specified type. 1185 * 1186 * @param \Doctrine\ORM\Mapping\ClassMetadata $class 1187 * 1188 * @return void 1189 */ 1190 private function executeDeletions($class) 1191 { 1192 $className = $class->name; 1193 $persister = $this->getEntityPersister($className); 1194 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove); 1195 1196 foreach ($this->entityDeletions as $oid => $entity) { 1197 if ($this->em->getClassMetadata(get_class($entity))->name !== $className) { 1198 continue; 1199 } 1200 1201 $persister->delete($entity); 1202 1203 unset( 1204 $this->entityDeletions[$oid], 1205 $this->entityIdentifiers[$oid], 1206 $this->originalEntityData[$oid], 1207 $this->entityStates[$oid] 1208 ); 1209 1210 // Entity with this $oid after deletion treated as NEW, even if the $oid 1211 // is obtained by a new entity because the old one went out of scope. 1212 //$this->entityStates[$oid] = self::STATE_NEW; 1213 if ( ! $class->isIdentifierNatural()) { 1214 $class->reflFields[$class->identifier[0]]->setValue($entity, null); 1215 } 1216 1217 if ($invoke !== ListenersInvoker::INVOKE_NONE) { 1218 $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); 1219 } 1220 } 1221 } 1222 1223 /** 1224 * Gets the commit order. 1225 * 1226 * @param array|null $entityChangeSet 1227 * 1228 * @return array 1229 */ 1230 private function getCommitOrder(array $entityChangeSet = null) 1231 { 1232 if ($entityChangeSet === null) { 1233 $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions); 1234 } 1235 1236 $calc = $this->getCommitOrderCalculator(); 1237 1238 // See if there are any new classes in the changeset, that are not in the 1239 // commit order graph yet (don't have a node). 1240 // We have to inspect changeSet to be able to correctly build dependencies. 1241 // It is not possible to use IdentityMap here because post inserted ids 1242 // are not yet available. 1243 $newNodes = []; 1244 1245 foreach ($entityChangeSet as $entity) { 1246 $class = $this->em->getClassMetadata(get_class($entity)); 1247 1248 if ($calc->hasNode($class->name)) { 1249 continue; 1250 } 1251 1252 $calc->addNode($class->name, $class); 1253 1254 $newNodes[] = $class; 1255 } 1256 1257 // Calculate dependencies for new nodes 1258 while ($class = array_pop($newNodes)) { 1259 foreach ($class->associationMappings as $assoc) { 1260 if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) { 1261 continue; 1262 } 1263 1264 $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); 1265 1266 if ( ! $calc->hasNode($targetClass->name)) { 1267 $calc->addNode($targetClass->name, $targetClass); 1268 1269 $newNodes[] = $targetClass; 1270 } 1271 1272 $joinColumns = reset($assoc['joinColumns']); 1273 1274 $calc->addDependency($targetClass->name, $class->name, (int)empty($joinColumns['nullable'])); 1275 1276 // If the target class has mapped subclasses, these share the same dependency. 1277 if ( ! $targetClass->subClasses) { 1278 continue; 1279 } 1280 1281 foreach ($targetClass->subClasses as $subClassName) { 1282 $targetSubClass = $this->em->getClassMetadata($subClassName); 1283 1284 if ( ! $calc->hasNode($subClassName)) { 1285 $calc->addNode($targetSubClass->name, $targetSubClass); 1286 1287 $newNodes[] = $targetSubClass; 1288 } 1289 1290 $calc->addDependency($targetSubClass->name, $class->name, 1); 1291 } 1292 } 1293 } 1294 1295 return $calc->sort(); 1296 } 1297 1298 /** 1299 * Schedules an entity for insertion into the database. 1300 * If the entity already has an identifier, it will be added to the identity map. 1301 * 1302 * @param object $entity The entity to schedule for insertion. 1303 * 1304 * @return void 1305 * 1306 * @throws ORMInvalidArgumentException 1307 * @throws \InvalidArgumentException 1308 */ 1309 public function scheduleForInsert($entity) 1310 { 1311 $oid = spl_object_hash($entity); 1312 1313 if (isset($this->entityUpdates[$oid])) { 1314 throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion."); 1315 } 1316 1317 if (isset($this->entityDeletions[$oid])) { 1318 throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity); 1319 } 1320 if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) { 1321 throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity); 1322 } 1323 1324 if (isset($this->entityInsertions[$oid])) { 1325 throw ORMInvalidArgumentException::scheduleInsertTwice($entity); 1326 } 1327 1328 $this->entityInsertions[$oid] = $entity; 1329 1330 if (isset($this->entityIdentifiers[$oid])) { 1331 $this->addToIdentityMap($entity); 1332 } 1333 1334 if ($entity instanceof NotifyPropertyChanged) { 1335 $entity->addPropertyChangedListener($this); 1336 } 1337 } 1338 1339 /** 1340 * Checks whether an entity is scheduled for insertion. 1341 * 1342 * @param object $entity 1343 * 1344 * @return boolean 1345 */ 1346 public function isScheduledForInsert($entity) 1347 { 1348 return isset($this->entityInsertions[spl_object_hash($entity)]); 1349 } 1350 1351 /** 1352 * Schedules an entity for being updated. 1353 * 1354 * @param object $entity The entity to schedule for being updated. 1355 * 1356 * @return void 1357 * 1358 * @throws ORMInvalidArgumentException 1359 */ 1360 public function scheduleForUpdate($entity) 1361 { 1362 $oid = spl_object_hash($entity); 1363 1364 if ( ! isset($this->entityIdentifiers[$oid])) { 1365 throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update"); 1366 } 1367 1368 if (isset($this->entityDeletions[$oid])) { 1369 throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update"); 1370 } 1371 1372 if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) { 1373 $this->entityUpdates[$oid] = $entity; 1374 } 1375 } 1376 1377 /** 1378 * INTERNAL: 1379 * Schedules an extra update that will be executed immediately after the 1380 * regular entity updates within the currently running commit cycle. 1381 * 1382 * Extra updates for entities are stored as (entity, changeset) tuples. 1383 * 1384 * @ignore 1385 * 1386 * @param object $entity The entity for which to schedule an extra update. 1387 * @param array $changeset The changeset of the entity (what to update). 1388 * 1389 * @return void 1390 */ 1391 public function scheduleExtraUpdate($entity, array $changeset) 1392 { 1393 $oid = spl_object_hash($entity); 1394 $extraUpdate = [$entity, $changeset]; 1395 1396 if (isset($this->extraUpdates[$oid])) { 1397 [, $changeset2] = $this->extraUpdates[$oid]; 1398 1399 $extraUpdate = [$entity, $changeset + $changeset2]; 1400 } 1401 1402 $this->extraUpdates[$oid] = $extraUpdate; 1403 } 1404 1405 /** 1406 * Checks whether an entity is registered as dirty in the unit of work. 1407 * Note: Is not very useful currently as dirty entities are only registered 1408 * at commit time. 1409 * 1410 * @param object $entity 1411 * 1412 * @return boolean 1413 */ 1414 public function isScheduledForUpdate($entity) 1415 { 1416 return isset($this->entityUpdates[spl_object_hash($entity)]); 1417 } 1418 1419 /** 1420 * Checks whether an entity is registered to be checked in the unit of work. 1421 * 1422 * @param object $entity 1423 * 1424 * @return boolean 1425 */ 1426 public function isScheduledForDirtyCheck($entity) 1427 { 1428 $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName; 1429 1430 return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]); 1431 } 1432 1433 /** 1434 * INTERNAL: 1435 * Schedules an entity for deletion. 1436 * 1437 * @param object $entity 1438 * 1439 * @return void 1440 */ 1441 public function scheduleForDelete($entity) 1442 { 1443 $oid = spl_object_hash($entity); 1444 1445 if (isset($this->entityInsertions[$oid])) { 1446 if ($this->isInIdentityMap($entity)) { 1447 $this->removeFromIdentityMap($entity); 1448 } 1449 1450 unset($this->entityInsertions[$oid], $this->entityStates[$oid]); 1451 1452 return; // entity has not been persisted yet, so nothing more to do. 1453 } 1454 1455 if ( ! $this->isInIdentityMap($entity)) { 1456 return; 1457 } 1458 1459 $this->removeFromIdentityMap($entity); 1460 1461 unset($this->entityUpdates[$oid]); 1462 1463 if ( ! isset($this->entityDeletions[$oid])) { 1464 $this->entityDeletions[$oid] = $entity; 1465 $this->entityStates[$oid] = self::STATE_REMOVED; 1466 } 1467 } 1468 1469 /** 1470 * Checks whether an entity is registered as removed/deleted with the unit 1471 * of work. 1472 * 1473 * @param object $entity 1474 * 1475 * @return boolean 1476 */ 1477 public function isScheduledForDelete($entity) 1478 { 1479 return isset($this->entityDeletions[spl_object_hash($entity)]); 1480 } 1481 1482 /** 1483 * Checks whether an entity is scheduled for insertion, update or deletion. 1484 * 1485 * @param object $entity 1486 * 1487 * @return boolean 1488 */ 1489 public function isEntityScheduled($entity) 1490 { 1491 $oid = spl_object_hash($entity); 1492 1493 return isset($this->entityInsertions[$oid]) 1494 || isset($this->entityUpdates[$oid]) 1495 || isset($this->entityDeletions[$oid]); 1496 } 1497 1498 /** 1499 * INTERNAL: 1500 * Registers an entity in the identity map. 1501 * Note that entities in a hierarchy are registered with the class name of 1502 * the root entity. 1503 * 1504 * @ignore 1505 * 1506 * @param object $entity The entity to register. 1507 * 1508 * @return boolean TRUE if the registration was successful, FALSE if the identity of 1509 * the entity in question is already managed. 1510 * 1511 * @throws ORMInvalidArgumentException 1512 */ 1513 public function addToIdentityMap($entity) 1514 { 1515 $classMetadata = $this->em->getClassMetadata(get_class($entity)); 1516 $identifier = $this->entityIdentifiers[spl_object_hash($entity)]; 1517 1518 if (empty($identifier) || in_array(null, $identifier, true)) { 1519 throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity); 1520 } 1521 1522 $idHash = implode(' ', $identifier); 1523 $className = $classMetadata->rootEntityName; 1524 1525 if (isset($this->identityMap[$className][$idHash])) { 1526 return false; 1527 } 1528 1529 $this->identityMap[$className][$idHash] = $entity; 1530 1531 return true; 1532 } 1533 1534 /** 1535 * Gets the state of an entity with regard to the current unit of work. 1536 * 1537 * @param object $entity 1538 * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED). 1539 * This parameter can be set to improve performance of entity state detection 1540 * by potentially avoiding a database lookup if the distinction between NEW and DETACHED 1541 * is either known or does not matter for the caller of the method. 1542 * 1543 * @return int The entity state. 1544 */ 1545 public function getEntityState($entity, $assume = null) 1546 { 1547 $oid = spl_object_hash($entity); 1548 1549 if (isset($this->entityStates[$oid])) { 1550 return $this->entityStates[$oid]; 1551 } 1552 1553 if ($assume !== null) { 1554 return $assume; 1555 } 1556 1557 // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known. 1558 // Note that you can not remember the NEW or DETACHED state in _entityStates since 1559 // the UoW does not hold references to such objects and the object hash can be reused. 1560 // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it. 1561 $class = $this->em->getClassMetadata(get_class($entity)); 1562 $id = $class->getIdentifierValues($entity); 1563 1564 if ( ! $id) { 1565 return self::STATE_NEW; 1566 } 1567 1568 if ($class->containsForeignIdentifier) { 1569 $id = $this->identifierFlattener->flattenIdentifier($class, $id); 1570 } 1571 1572 switch (true) { 1573 case ($class->isIdentifierNatural()): 1574 // Check for a version field, if available, to avoid a db lookup. 1575 if ($class->isVersioned) { 1576 return ($class->getFieldValue($entity, $class->versionField)) 1577 ? self::STATE_DETACHED 1578 : self::STATE_NEW; 1579 } 1580 1581 // Last try before db lookup: check the identity map. 1582 if ($this->tryGetById($id, $class->rootEntityName)) { 1583 return self::STATE_DETACHED; 1584 } 1585 1586 // db lookup 1587 if ($this->getEntityPersister($class->name)->exists($entity)) { 1588 return self::STATE_DETACHED; 1589 } 1590 1591 return self::STATE_NEW; 1592 1593 case ( ! $class->idGenerator->isPostInsertGenerator()): 1594 // if we have a pre insert generator we can't be sure that having an id 1595 // really means that the entity exists. We have to verify this through 1596 // the last resort: a db lookup 1597 1598 // Last try before db lookup: check the identity map. 1599 if ($this->tryGetById($id, $class->rootEntityName)) { 1600 return self::STATE_DETACHED; 1601 } 1602 1603 // db lookup 1604 if ($this->getEntityPersister($class->name)->exists($entity)) { 1605 return self::STATE_DETACHED; 1606 } 1607 1608 return self::STATE_NEW; 1609 1610 default: 1611 return self::STATE_DETACHED; 1612 } 1613 } 1614 1615 /** 1616 * INTERNAL: 1617 * Removes an entity from the identity map. This effectively detaches the 1618 * entity from the persistence management of Doctrine. 1619 * 1620 * @ignore 1621 * 1622 * @param object $entity 1623 * 1624 * @return boolean 1625 * 1626 * @throws ORMInvalidArgumentException 1627 */ 1628 public function removeFromIdentityMap($entity) 1629 { 1630 $oid = spl_object_hash($entity); 1631 $classMetadata = $this->em->getClassMetadata(get_class($entity)); 1632 $idHash = implode(' ', $this->entityIdentifiers[$oid]); 1633 1634 if ($idHash === '') { 1635 throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "remove from identity map"); 1636 } 1637 1638 $className = $classMetadata->rootEntityName; 1639 1640 if (isset($this->identityMap[$className][$idHash])) { 1641 unset($this->identityMap[$className][$idHash]); 1642 unset($this->readOnlyObjects[$oid]); 1643 1644 //$this->entityStates[$oid] = self::STATE_DETACHED; 1645 1646 return true; 1647 } 1648 1649 return false; 1650 } 1651 1652 /** 1653 * INTERNAL: 1654 * Gets an entity in the identity map by its identifier hash. 1655 * 1656 * @ignore 1657 * 1658 * @param string $idHash 1659 * @param string $rootClassName 1660 * 1661 * @return object 1662 */ 1663 public function getByIdHash($idHash, $rootClassName) 1664 { 1665 return $this->identityMap[$rootClassName][$idHash]; 1666 } 1667 1668 /** 1669 * INTERNAL: 1670 * Tries to get an entity by its identifier hash. If no entity is found for 1671 * the given hash, FALSE is returned. 1672 * 1673 * @ignore 1674 * 1675 * @param mixed $idHash (must be possible to cast it to string) 1676 * @param string $rootClassName 1677 * 1678 * @return object|bool The found entity or FALSE. 1679 */ 1680 public function tryGetByIdHash($idHash, $rootClassName) 1681 { 1682 $stringIdHash = (string) $idHash; 1683 1684 return isset($this->identityMap[$rootClassName][$stringIdHash]) 1685 ? $this->identityMap[$rootClassName][$stringIdHash] 1686 : false; 1687 } 1688 1689 /** 1690 * Checks whether an entity is registered in the identity map of this UnitOfWork. 1691 * 1692 * @param object $entity 1693 * 1694 * @return boolean 1695 */ 1696 public function isInIdentityMap($entity) 1697 { 1698 $oid = spl_object_hash($entity); 1699 1700 if (empty($this->entityIdentifiers[$oid])) { 1701 return false; 1702 } 1703 1704 $classMetadata = $this->em->getClassMetadata(get_class($entity)); 1705 $idHash = implode(' ', $this->entityIdentifiers[$oid]); 1706 1707 return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]); 1708 } 1709 1710 /** 1711 * INTERNAL: 1712 * Checks whether an identifier hash exists in the identity map. 1713 * 1714 * @ignore 1715 * 1716 * @param string $idHash 1717 * @param string $rootClassName 1718 * 1719 * @return boolean 1720 */ 1721 public function containsIdHash($idHash, $rootClassName) 1722 { 1723 return isset($this->identityMap[$rootClassName][$idHash]); 1724 } 1725 1726 /** 1727 * Persists an entity as part of the current unit of work. 1728 * 1729 * @param object $entity The entity to persist. 1730 * 1731 * @return void 1732 */ 1733 public function persist($entity) 1734 { 1735 $visited = []; 1736 1737 $this->doPersist($entity, $visited); 1738 } 1739 1740 /** 1741 * Persists an entity as part of the current unit of work. 1742 * 1743 * This method is internally called during persist() cascades as it tracks 1744 * the already visited entities to prevent infinite recursions. 1745 * 1746 * @param object $entity The entity to persist. 1747 * @param array $visited The already visited entities. 1748 * 1749 * @return void 1750 * 1751 * @throws ORMInvalidArgumentException 1752 * @throws UnexpectedValueException 1753 */ 1754 private function doPersist($entity, array &$visited) 1755 { 1756 $oid = spl_object_hash($entity); 1757 1758 if (isset($visited[$oid])) { 1759 return; // Prevent infinite recursion 1760 } 1761 1762 $visited[$oid] = $entity; // Mark visited 1763 1764 $class = $this->em->getClassMetadata(get_class($entity)); 1765 1766 // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation). 1767 // If we would detect DETACHED here we would throw an exception anyway with the same 1768 // consequences (not recoverable/programming error), so just assuming NEW here 1769 // lets us avoid some database lookups for entities with natural identifiers. 1770 $entityState = $this->getEntityState($entity, self::STATE_NEW); 1771 1772 switch ($entityState) { 1773 case self::STATE_MANAGED: 1774 // Nothing to do, except if policy is "deferred explicit" 1775 if ($class->isChangeTrackingDeferredExplicit()) { 1776 $this->scheduleForDirtyCheck($entity); 1777 } 1778 break; 1779 1780 case self::STATE_NEW: 1781 $this->persistNew($class, $entity); 1782 break; 1783 1784 case self::STATE_REMOVED: 1785 // Entity becomes managed again 1786 unset($this->entityDeletions[$oid]); 1787 $this->addToIdentityMap($entity); 1788 1789 $this->entityStates[$oid] = self::STATE_MANAGED; 1790 break; 1791 1792 case self::STATE_DETACHED: 1793 // Can actually not happen right now since we assume STATE_NEW. 1794 throw ORMInvalidArgumentException::detachedEntityCannot($entity, "persisted"); 1795 1796 default: 1797 throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity)); 1798 } 1799 1800 $this->cascadePersist($entity, $visited); 1801 } 1802 1803 /** 1804 * Deletes an entity as part of the current unit of work. 1805 * 1806 * @param object $entity The entity to remove. 1807 * 1808 * @return void 1809 */ 1810 public function remove($entity) 1811 { 1812 $visited = []; 1813 1814 $this->doRemove($entity, $visited); 1815 } 1816 1817 /** 1818 * Deletes an entity as part of the current unit of work. 1819 * 1820 * This method is internally called during delete() cascades as it tracks 1821 * the already visited entities to prevent infinite recursions. 1822 * 1823 * @param object $entity The entity to delete. 1824 * @param array $visited The map of the already visited entities. 1825 * 1826 * @return void 1827 * 1828 * @throws ORMInvalidArgumentException If the instance is a detached entity. 1829 * @throws UnexpectedValueException 1830 */ 1831 private function doRemove($entity, array &$visited) 1832 { 1833 $oid = spl_object_hash($entity); 1834 1835 if (isset($visited[$oid])) { 1836 return; // Prevent infinite recursion 1837 } 1838 1839 $visited[$oid] = $entity; // mark visited 1840 1841 // Cascade first, because scheduleForDelete() removes the entity from the identity map, which 1842 // can cause problems when a lazy proxy has to be initialized for the cascade operation. 1843 $this->cascadeRemove($entity, $visited); 1844 1845 $class = $this->em->getClassMetadata(get_class($entity)); 1846 $entityState = $this->getEntityState($entity); 1847 1848 switch ($entityState) { 1849 case self::STATE_NEW: 1850 case self::STATE_REMOVED: 1851 // nothing to do 1852 break; 1853 1854 case self::STATE_MANAGED: 1855 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove); 1856 1857 if ($invoke !== ListenersInvoker::INVOKE_NONE) { 1858 $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); 1859 } 1860 1861 $this->scheduleForDelete($entity); 1862 break; 1863 1864 case self::STATE_DETACHED: 1865 throw ORMInvalidArgumentException::detachedEntityCannot($entity, "removed"); 1866 default: 1867 throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity)); 1868 } 1869 1870 } 1871 1872 /** 1873 * Merges the state of the given detached entity into this UnitOfWork. 1874 * 1875 * @param object $entity 1876 * 1877 * @return object The managed copy of the entity. 1878 * 1879 * @throws OptimisticLockException If the entity uses optimistic locking through a version 1880 * attribute and the version check against the managed copy fails. 1881 * 1882 * @deprecated 2.7 This method is being removed from the ORM and won't have any replacement 1883 */ 1884 public function merge($entity) 1885 { 1886 $visited = []; 1887 1888 return $this->doMerge($entity, $visited); 1889 } 1890 1891 /** 1892 * Executes a merge operation on an entity. 1893 * 1894 * @param object $entity 1895 * @param array $visited 1896 * @param object|null $prevManagedCopy 1897 * @param string[] $assoc 1898 * 1899 * @return object The managed copy of the entity. 1900 * 1901 * @throws OptimisticLockException If the entity uses optimistic locking through a version 1902 * attribute and the version check against the managed copy fails. 1903 * @throws ORMInvalidArgumentException If the entity instance is NEW. 1904 * @throws EntityNotFoundException if an assigned identifier is used in the entity, but none is provided 1905 */ 1906 private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = []) 1907 { 1908 $oid = spl_object_hash($entity); 1909 1910 if (isset($visited[$oid])) { 1911 $managedCopy = $visited[$oid]; 1912 1913 if ($prevManagedCopy !== null) { 1914 $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy); 1915 } 1916 1917 return $managedCopy; 1918 } 1919 1920 $class = $this->em->getClassMetadata(get_class($entity)); 1921 1922 // First we assume DETACHED, although it can still be NEW but we can avoid 1923 // an extra db-roundtrip this way. If it is not MANAGED but has an identity, 1924 // we need to fetch it from the db anyway in order to merge. 1925 // MANAGED entities are ignored by the merge operation. 1926 $managedCopy = $entity; 1927 1928 if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) { 1929 // Try to look the entity up in the identity map. 1930 $id = $class->getIdentifierValues($entity); 1931 1932 // If there is no ID, it is actually NEW. 1933 if ( ! $id) { 1934 $managedCopy = $this->newInstance($class); 1935 1936 $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy); 1937 $this->persistNew($class, $managedCopy); 1938 } else { 1939 $flatId = ($class->containsForeignIdentifier) 1940 ? $this->identifierFlattener->flattenIdentifier($class, $id) 1941 : $id; 1942 1943 $managedCopy = $this->tryGetById($flatId, $class->rootEntityName); 1944 1945 if ($managedCopy) { 1946 // We have the entity in-memory already, just make sure its not removed. 1947 if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) { 1948 throw ORMInvalidArgumentException::entityIsRemoved($managedCopy, "merge"); 1949 } 1950 } else { 1951 // We need to fetch the managed copy in order to merge. 1952 $managedCopy = $this->em->find($class->name, $flatId); 1953 } 1954 1955 if ($managedCopy === null) { 1956 // If the identifier is ASSIGNED, it is NEW, otherwise an error 1957 // since the managed entity was not found. 1958 if ( ! $class->isIdentifierNatural()) { 1959 throw EntityNotFoundException::fromClassNameAndIdentifier( 1960 $class->getName(), 1961 $this->identifierFlattener->flattenIdentifier($class, $id) 1962 ); 1963 } 1964 1965 $managedCopy = $this->newInstance($class); 1966 $class->setIdentifierValues($managedCopy, $id); 1967 1968 $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy); 1969 $this->persistNew($class, $managedCopy); 1970 } else { 1971 $this->ensureVersionMatch($class, $entity, $managedCopy); 1972 $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy); 1973 } 1974 } 1975 1976 $visited[$oid] = $managedCopy; // mark visited 1977 1978 if ($class->isChangeTrackingDeferredExplicit()) { 1979 $this->scheduleForDirtyCheck($entity); 1980 } 1981 } 1982 1983 if ($prevManagedCopy !== null) { 1984 $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy); 1985 } 1986 1987 // Mark the managed copy visited as well 1988 $visited[spl_object_hash($managedCopy)] = $managedCopy; 1989 1990 $this->cascadeMerge($entity, $managedCopy, $visited); 1991 1992 return $managedCopy; 1993 } 1994 1995 /** 1996 * @param ClassMetadata $class 1997 * @param object $entity 1998 * @param object $managedCopy 1999 * 2000 * @return void 2001 * 2002 * @throws OptimisticLockException 2003 */ 2004 private function ensureVersionMatch(ClassMetadata $class, $entity, $managedCopy) 2005 { 2006 if (! ($class->isVersioned && $this->isLoaded($managedCopy) && $this->isLoaded($entity))) { 2007 return; 2008 } 2009 2010 $reflField = $class->reflFields[$class->versionField]; 2011 $managedCopyVersion = $reflField->getValue($managedCopy); 2012 $entityVersion = $reflField->getValue($entity); 2013 2014 // Throw exception if versions don't match. 2015 if ($managedCopyVersion == $entityVersion) { 2016 return; 2017 } 2018 2019 throw OptimisticLockException::lockFailedVersionMismatch($entity, $entityVersion, $managedCopyVersion); 2020 } 2021 2022 /** 2023 * Tests if an entity is loaded - must either be a loaded proxy or not a proxy 2024 * 2025 * @param object $entity 2026 * 2027 * @return bool 2028 */ 2029 private function isLoaded($entity) 2030 { 2031 return !($entity instanceof Proxy) || $entity->__isInitialized(); 2032 } 2033 2034 /** 2035 * Sets/adds associated managed copies into the previous entity's association field 2036 * 2037 * @param object $entity 2038 * @param array $association 2039 * @param object $previousManagedCopy 2040 * @param object $managedCopy 2041 * 2042 * @return void 2043 */ 2044 private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy) 2045 { 2046 $assocField = $association['fieldName']; 2047 $prevClass = $this->em->getClassMetadata(get_class($previousManagedCopy)); 2048 2049 if ($association['type'] & ClassMetadata::TO_ONE) { 2050 $prevClass->reflFields[$assocField]->setValue($previousManagedCopy, $managedCopy); 2051 2052 return; 2053 } 2054 2055 $value = $prevClass->reflFields[$assocField]->getValue($previousManagedCopy); 2056 $value[] = $managedCopy; 2057 2058 if ($association['type'] == ClassMetadata::ONE_TO_MANY) { 2059 $class = $this->em->getClassMetadata(get_class($entity)); 2060 2061 $class->reflFields[$association['mappedBy']]->setValue($managedCopy, $previousManagedCopy); 2062 } 2063 } 2064 2065 /** 2066 * Detaches an entity from the persistence management. It's persistence will 2067 * no longer be managed by Doctrine. 2068 * 2069 * @param object $entity The entity to detach. 2070 * 2071 * @return void 2072 * 2073 * @deprecated 2.7 This method is being removed from the ORM and won't have any replacement 2074 */ 2075 public function detach($entity) 2076 { 2077 $visited = []; 2078 2079 $this->doDetach($entity, $visited); 2080 } 2081 2082 /** 2083 * Executes a detach operation on the given entity. 2084 * 2085 * @param object $entity 2086 * @param array $visited 2087 * @param boolean $noCascade if true, don't cascade detach operation. 2088 * 2089 * @return void 2090 */ 2091 private function doDetach($entity, array &$visited, $noCascade = false) 2092 { 2093 $oid = spl_object_hash($entity); 2094 2095 if (isset($visited[$oid])) { 2096 return; // Prevent infinite recursion 2097 } 2098 2099 $visited[$oid] = $entity; // mark visited 2100 2101 switch ($this->getEntityState($entity, self::STATE_DETACHED)) { 2102 case self::STATE_MANAGED: 2103 if ($this->isInIdentityMap($entity)) { 2104 $this->removeFromIdentityMap($entity); 2105 } 2106 2107 unset( 2108 $this->entityInsertions[$oid], 2109 $this->entityUpdates[$oid], 2110 $this->entityDeletions[$oid], 2111 $this->entityIdentifiers[$oid], 2112 $this->entityStates[$oid], 2113 $this->originalEntityData[$oid] 2114 ); 2115 break; 2116 case self::STATE_NEW: 2117 case self::STATE_DETACHED: 2118 return; 2119 } 2120 2121 if ( ! $noCascade) { 2122 $this->cascadeDetach($entity, $visited); 2123 } 2124 } 2125 2126 /** 2127 * Refreshes the state of the given entity from the database, overwriting 2128 * any local, unpersisted changes. 2129 * 2130 * @param object $entity The entity to refresh. 2131 * 2132 * @return void 2133 * 2134 * @throws InvalidArgumentException If the entity is not MANAGED. 2135 */ 2136 public function refresh($entity) 2137 { 2138 $visited = []; 2139 2140 $this->doRefresh($entity, $visited); 2141 } 2142 2143 /** 2144 * Executes a refresh operation on an entity. 2145 * 2146 * @param object $entity The entity to refresh. 2147 * @param array $visited The already visited entities during cascades. 2148 * 2149 * @return void 2150 * 2151 * @throws ORMInvalidArgumentException If the entity is not MANAGED. 2152 */ 2153 private function doRefresh($entity, array &$visited) 2154 { 2155 $oid = spl_object_hash($entity); 2156 2157 if (isset($visited[$oid])) { 2158 return; // Prevent infinite recursion 2159 } 2160 2161 $visited[$oid] = $entity; // mark visited 2162 2163 $class = $this->em->getClassMetadata(get_class($entity)); 2164 2165 if ($this->getEntityState($entity) !== self::STATE_MANAGED) { 2166 throw ORMInvalidArgumentException::entityNotManaged($entity); 2167 } 2168 2169 $this->getEntityPersister($class->name)->refresh( 2170 array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), 2171 $entity 2172 ); 2173 2174 $this->cascadeRefresh($entity, $visited); 2175 } 2176 2177 /** 2178 * Cascades a refresh operation to associated entities. 2179 * 2180 * @param object $entity 2181 * @param array $visited 2182 * 2183 * @return void 2184 */ 2185 private function cascadeRefresh($entity, array &$visited) 2186 { 2187 $class = $this->em->getClassMetadata(get_class($entity)); 2188 2189 $associationMappings = array_filter( 2190 $class->associationMappings, 2191 function ($assoc) { return $assoc['isCascadeRefresh']; } 2192 ); 2193 2194 foreach ($associationMappings as $assoc) { 2195 $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity); 2196 2197 switch (true) { 2198 case ($relatedEntities instanceof PersistentCollection): 2199 // Unwrap so that foreach() does not initialize 2200 $relatedEntities = $relatedEntities->unwrap(); 2201 // break; is commented intentionally! 2202 2203 case ($relatedEntities instanceof Collection): 2204 case (is_array($relatedEntities)): 2205 foreach ($relatedEntities as $relatedEntity) { 2206 $this->doRefresh($relatedEntity, $visited); 2207 } 2208 break; 2209 2210 case ($relatedEntities !== null): 2211 $this->doRefresh($relatedEntities, $visited); 2212 break; 2213 2214 default: 2215 // Do nothing 2216 } 2217 } 2218 } 2219 2220 /** 2221 * Cascades a detach operation to associated entities. 2222 * 2223 * @param object $entity 2224 * @param array $visited 2225 * 2226 * @return void 2227 */ 2228 private function cascadeDetach($entity, array &$visited) 2229 { 2230 $class = $this->em->getClassMetadata(get_class($entity)); 2231 2232 $associationMappings = array_filter( 2233 $class->associationMappings, 2234 function ($assoc) { return $assoc['isCascadeDetach']; } 2235 ); 2236 2237 foreach ($associationMappings as $assoc) { 2238 $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity); 2239 2240 switch (true) { 2241 case ($relatedEntities instanceof PersistentCollection): 2242 // Unwrap so that foreach() does not initialize 2243 $relatedEntities = $relatedEntities->unwrap(); 2244 // break; is commented intentionally! 2245 2246 case ($relatedEntities instanceof Collection): 2247 case (is_array($relatedEntities)): 2248 foreach ($relatedEntities as $relatedEntity) { 2249 $this->doDetach($relatedEntity, $visited); 2250 } 2251 break; 2252 2253 case ($relatedEntities !== null): 2254 $this->doDetach($relatedEntities, $visited); 2255 break; 2256 2257 default: 2258 // Do nothing 2259 } 2260 } 2261 } 2262 2263 /** 2264 * Cascades a merge operation to associated entities. 2265 * 2266 * @param object $entity 2267 * @param object $managedCopy 2268 * @param array $visited 2269 * 2270 * @return void 2271 */ 2272 private function cascadeMerge($entity, $managedCopy, array &$visited) 2273 { 2274 $class = $this->em->getClassMetadata(get_class($entity)); 2275 2276 $associationMappings = array_filter( 2277 $class->associationMappings, 2278 function ($assoc) { return $assoc['isCascadeMerge']; } 2279 ); 2280 2281 foreach ($associationMappings as $assoc) { 2282 $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity); 2283 2284 if ($relatedEntities instanceof Collection) { 2285 if ($relatedEntities === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) { 2286 continue; 2287 } 2288 2289 if ($relatedEntities instanceof PersistentCollection) { 2290 // Unwrap so that foreach() does not initialize 2291 $relatedEntities = $relatedEntities->unwrap(); 2292 } 2293 2294 foreach ($relatedEntities as $relatedEntity) { 2295 $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc); 2296 } 2297 } else if ($relatedEntities !== null) { 2298 $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc); 2299 } 2300 } 2301 } 2302 2303 /** 2304 * Cascades the save operation to associated entities. 2305 * 2306 * @param object $entity 2307 * @param array $visited 2308 * 2309 * @return void 2310 */ 2311 private function cascadePersist($entity, array &$visited) 2312 { 2313 $class = $this->em->getClassMetadata(get_class($entity)); 2314 2315 $associationMappings = array_filter( 2316 $class->associationMappings, 2317 function ($assoc) { return $assoc['isCascadePersist']; } 2318 ); 2319 2320 foreach ($associationMappings as $assoc) { 2321 $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity); 2322 2323 switch (true) { 2324 case ($relatedEntities instanceof PersistentCollection): 2325 // Unwrap so that foreach() does not initialize 2326 $relatedEntities = $relatedEntities->unwrap(); 2327 // break; is commented intentionally! 2328 2329 case ($relatedEntities instanceof Collection): 2330 case (is_array($relatedEntities)): 2331 if (($assoc['type'] & ClassMetadata::TO_MANY) <= 0) { 2332 throw ORMInvalidArgumentException::invalidAssociation( 2333 $this->em->getClassMetadata($assoc['targetEntity']), 2334 $assoc, 2335 $relatedEntities 2336 ); 2337 } 2338 2339 foreach ($relatedEntities as $relatedEntity) { 2340 $this->doPersist($relatedEntity, $visited); 2341 } 2342 2343 break; 2344 2345 case ($relatedEntities !== null): 2346 if (! $relatedEntities instanceof $assoc['targetEntity']) { 2347 throw ORMInvalidArgumentException::invalidAssociation( 2348 $this->em->getClassMetadata($assoc['targetEntity']), 2349 $assoc, 2350 $relatedEntities 2351 ); 2352 } 2353 2354 $this->doPersist($relatedEntities, $visited); 2355 break; 2356 2357 default: 2358 // Do nothing 2359 } 2360 } 2361 } 2362 2363 /** 2364 * Cascades the delete operation to associated entities. 2365 * 2366 * @param object $entity 2367 * @param array $visited 2368 * 2369 * @return void 2370 */ 2371 private function cascadeRemove($entity, array &$visited) 2372 { 2373 $class = $this->em->getClassMetadata(get_class($entity)); 2374 2375 $associationMappings = array_filter( 2376 $class->associationMappings, 2377 function ($assoc) { return $assoc['isCascadeRemove']; } 2378 ); 2379 2380 $entitiesToCascade = []; 2381 2382 foreach ($associationMappings as $assoc) { 2383 if ($entity instanceof Proxy && !$entity->__isInitialized__) { 2384 $entity->__load(); 2385 } 2386 2387 $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity); 2388 2389 switch (true) { 2390 case ($relatedEntities instanceof Collection): 2391 case (is_array($relatedEntities)): 2392 // If its a PersistentCollection initialization is intended! No unwrap! 2393 foreach ($relatedEntities as $relatedEntity) { 2394 $entitiesToCascade[] = $relatedEntity; 2395 } 2396 break; 2397 2398 case ($relatedEntities !== null): 2399 $entitiesToCascade[] = $relatedEntities; 2400 break; 2401 2402 default: 2403 // Do nothing 2404 } 2405 } 2406 2407 foreach ($entitiesToCascade as $relatedEntity) { 2408 $this->doRemove($relatedEntity, $visited); 2409 } 2410 } 2411 2412 /** 2413 * Acquire a lock on the given entity. 2414 * 2415 * @param object $entity 2416 * @param int $lockMode 2417 * @param int $lockVersion 2418 * 2419 * @return void 2420 * 2421 * @throws ORMInvalidArgumentException 2422 * @throws TransactionRequiredException 2423 * @throws OptimisticLockException 2424 */ 2425 public function lock($entity, $lockMode, $lockVersion = null) 2426 { 2427 if ($entity === null) { 2428 throw new \InvalidArgumentException("No entity passed to UnitOfWork#lock()."); 2429 } 2430 2431 if ($this->getEntityState($entity, self::STATE_DETACHED) != self::STATE_MANAGED) { 2432 throw ORMInvalidArgumentException::entityNotManaged($entity); 2433 } 2434 2435 $class = $this->em->getClassMetadata(get_class($entity)); 2436 2437 switch (true) { 2438 case LockMode::OPTIMISTIC === $lockMode: 2439 if ( ! $class->isVersioned) { 2440 throw OptimisticLockException::notVersioned($class->name); 2441 } 2442 2443 if ($lockVersion === null) { 2444 return; 2445 } 2446 2447 if ($entity instanceof Proxy && !$entity->__isInitialized__) { 2448 $entity->__load(); 2449 } 2450 2451 $entityVersion = $class->reflFields[$class->versionField]->getValue($entity); 2452 2453 if ($entityVersion != $lockVersion) { 2454 throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion); 2455 } 2456 2457 break; 2458 2459 case LockMode::NONE === $lockMode: 2460 case LockMode::PESSIMISTIC_READ === $lockMode: 2461 case LockMode::PESSIMISTIC_WRITE === $lockMode: 2462 if (!$this->em->getConnection()->isTransactionActive()) { 2463 throw TransactionRequiredException::transactionRequired(); 2464 } 2465 2466 $oid = spl_object_hash($entity); 2467 2468 $this->getEntityPersister($class->name)->lock( 2469 array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), 2470 $lockMode 2471 ); 2472 break; 2473 2474 default: 2475 // Do nothing 2476 } 2477 } 2478 2479 /** 2480 * Gets the CommitOrderCalculator used by the UnitOfWork to order commits. 2481 * 2482 * @return \Doctrine\ORM\Internal\CommitOrderCalculator 2483 */ 2484 public function getCommitOrderCalculator() 2485 { 2486 return new Internal\CommitOrderCalculator(); 2487 } 2488 2489 /** 2490 * Clears the UnitOfWork. 2491 * 2492 * @param string|null $entityName if given, only entities of this type will get detached. 2493 * 2494 * @return void 2495 * 2496 * @throws ORMInvalidArgumentException if an invalid entity name is given 2497 */ 2498 public function clear($entityName = null) 2499 { 2500 if ($entityName === null) { 2501 $this->identityMap = 2502 $this->entityIdentifiers = 2503 $this->originalEntityData = 2504 $this->entityChangeSets = 2505 $this->entityStates = 2506 $this->scheduledForSynchronization = 2507 $this->entityInsertions = 2508 $this->entityUpdates = 2509 $this->entityDeletions = 2510 $this->nonCascadedNewDetectedEntities = 2511 $this->collectionDeletions = 2512 $this->collectionUpdates = 2513 $this->extraUpdates = 2514 $this->readOnlyObjects = 2515 $this->visitedCollections = 2516 $this->eagerLoadingEntities = 2517 $this->orphanRemovals = []; 2518 } else { 2519 $this->clearIdentityMapForEntityName($entityName); 2520 $this->clearEntityInsertionsForEntityName($entityName); 2521 } 2522 2523 if ($this->evm->hasListeners(Events::onClear)) { 2524 $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em, $entityName)); 2525 } 2526 } 2527 2528 /** 2529 * INTERNAL: 2530 * Schedules an orphaned entity for removal. The remove() operation will be 2531 * invoked on that entity at the beginning of the next commit of this 2532 * UnitOfWork. 2533 * 2534 * @ignore 2535 * 2536 * @param object $entity 2537 * 2538 * @return void 2539 */ 2540 public function scheduleOrphanRemoval($entity) 2541 { 2542 $this->orphanRemovals[spl_object_hash($entity)] = $entity; 2543 } 2544 2545 /** 2546 * INTERNAL: 2547 * Cancels a previously scheduled orphan removal. 2548 * 2549 * @ignore 2550 * 2551 * @param object $entity 2552 * 2553 * @return void 2554 */ 2555 public function cancelOrphanRemoval($entity) 2556 { 2557 unset($this->orphanRemovals[spl_object_hash($entity)]); 2558 } 2559 2560 /** 2561 * INTERNAL: 2562 * Schedules a complete collection for removal when this UnitOfWork commits. 2563 * 2564 * @param PersistentCollection $coll 2565 * 2566 * @return void 2567 */ 2568 public function scheduleCollectionDeletion(PersistentCollection $coll) 2569 { 2570 $coid = spl_object_hash($coll); 2571 2572 // TODO: if $coll is already scheduled for recreation ... what to do? 2573 // Just remove $coll from the scheduled recreations? 2574 unset($this->collectionUpdates[$coid]); 2575 2576 $this->collectionDeletions[$coid] = $coll; 2577 } 2578 2579 /** 2580 * @param PersistentCollection $coll 2581 * 2582 * @return bool 2583 */ 2584 public function isCollectionScheduledForDeletion(PersistentCollection $coll) 2585 { 2586 return isset($this->collectionDeletions[spl_object_hash($coll)]); 2587 } 2588 2589 /** 2590 * @param ClassMetadata $class 2591 * 2592 * @return ObjectManagerAware|object 2593 */ 2594 private function newInstance($class) 2595 { 2596 $entity = $class->newInstance(); 2597 2598 if ($entity instanceof ObjectManagerAware) { 2599 $entity->injectObjectManager($this->em, $class); 2600 } 2601 2602 return $entity; 2603 } 2604 2605 /** 2606 * INTERNAL: 2607 * Creates an entity. Used for reconstitution of persistent entities. 2608 * 2609 * Internal note: Highly performance-sensitive method. 2610 * 2611 * @ignore 2612 * 2613 * @param string $className The name of the entity class. 2614 * @param array $data The data for the entity. 2615 * @param array $hints Any hints to account for during reconstitution/lookup of the entity. 2616 * 2617 * @return object The managed entity instance. 2618 * 2619 * @todo Rename: getOrCreateEntity 2620 */ 2621 public function createEntity($className, array $data, &$hints = []) 2622 { 2623 $class = $this->em->getClassMetadata($className); 2624 2625 $id = $this->identifierFlattener->flattenIdentifier($class, $data); 2626 $idHash = implode(' ', $id); 2627 2628 if (isset($this->identityMap[$class->rootEntityName][$idHash])) { 2629 $entity = $this->identityMap[$class->rootEntityName][$idHash]; 2630 $oid = spl_object_hash($entity); 2631 2632 if ( 2633 isset($hints[Query::HINT_REFRESH]) 2634 && isset($hints[Query::HINT_REFRESH_ENTITY]) 2635 && ($unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY]) !== $entity 2636 && $unmanagedProxy instanceof Proxy 2637 && $this->isIdentifierEquals($unmanagedProxy, $entity) 2638 ) { 2639 // DDC-1238 - we have a managed instance, but it isn't the provided one. 2640 // Therefore we clear its identifier. Also, we must re-fetch metadata since the 2641 // refreshed object may be anything 2642 2643 foreach ($class->identifier as $fieldName) { 2644 $class->reflFields[$fieldName]->setValue($unmanagedProxy, null); 2645 } 2646 2647 return $unmanagedProxy; 2648 } 2649 2650 if ($entity instanceof Proxy && ! $entity->__isInitialized()) { 2651 $entity->__setInitialized(true); 2652 2653 if ($entity instanceof NotifyPropertyChanged) { 2654 $entity->addPropertyChangedListener($this); 2655 } 2656 } else { 2657 if ( ! isset($hints[Query::HINT_REFRESH]) 2658 || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)) { 2659 return $entity; 2660 } 2661 } 2662 2663 // inject ObjectManager upon refresh. 2664 if ($entity instanceof ObjectManagerAware) { 2665 $entity->injectObjectManager($this->em, $class); 2666 } 2667 2668 $this->originalEntityData[$oid] = $data; 2669 } else { 2670 $entity = $this->newInstance($class); 2671 $oid = spl_object_hash($entity); 2672 2673 $this->entityIdentifiers[$oid] = $id; 2674 $this->entityStates[$oid] = self::STATE_MANAGED; 2675 $this->originalEntityData[$oid] = $data; 2676 2677 $this->identityMap[$class->rootEntityName][$idHash] = $entity; 2678 2679 if ($entity instanceof NotifyPropertyChanged) { 2680 $entity->addPropertyChangedListener($this); 2681 } 2682 } 2683 2684 foreach ($data as $field => $value) { 2685 if (isset($class->fieldMappings[$field])) { 2686 $class->reflFields[$field]->setValue($entity, $value); 2687 } 2688 } 2689 2690 // Loading the entity right here, if its in the eager loading map get rid of it there. 2691 unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]); 2692 2693 if (isset($this->eagerLoadingEntities[$class->rootEntityName]) && ! $this->eagerLoadingEntities[$class->rootEntityName]) { 2694 unset($this->eagerLoadingEntities[$class->rootEntityName]); 2695 } 2696 2697 // Properly initialize any unfetched associations, if partial objects are not allowed. 2698 if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) { 2699 return $entity; 2700 } 2701 2702 foreach ($class->associationMappings as $field => $assoc) { 2703 // Check if the association is not among the fetch-joined associations already. 2704 if (isset($hints['fetchAlias']) && isset($hints['fetched'][$hints['fetchAlias']][$field])) { 2705 continue; 2706 } 2707 2708 $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); 2709 2710 switch (true) { 2711 case ($assoc['type'] & ClassMetadata::TO_ONE): 2712 if ( ! $assoc['isOwningSide']) { 2713 2714 // use the given entity association 2715 if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) { 2716 2717 $this->originalEntityData[$oid][$field] = $data[$field]; 2718 2719 $class->reflFields[$field]->setValue($entity, $data[$field]); 2720 $targetClass->reflFields[$assoc['mappedBy']]->setValue($data[$field], $entity); 2721 2722 continue 2; 2723 } 2724 2725 // Inverse side of x-to-one can never be lazy 2726 $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity)); 2727 2728 continue 2; 2729 } 2730 2731 // use the entity association 2732 if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) { 2733 $class->reflFields[$field]->setValue($entity, $data[$field]); 2734 $this->originalEntityData[$oid][$field] = $data[$field]; 2735 2736 break; 2737 } 2738 2739 $associatedId = []; 2740 2741 // TODO: Is this even computed right in all cases of composite keys? 2742 foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { 2743 $joinColumnValue = $data[$srcColumn] ?? null; 2744 2745 if ($joinColumnValue !== null) { 2746 if ($targetClass->containsForeignIdentifier) { 2747 $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue; 2748 } else { 2749 $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue; 2750 } 2751 } elseif ($targetClass->containsForeignIdentifier 2752 && in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true) 2753 ) { 2754 // the missing key is part of target's entity primary key 2755 $associatedId = []; 2756 break; 2757 } 2758 } 2759 2760 if ( ! $associatedId) { 2761 // Foreign key is NULL 2762 $class->reflFields[$field]->setValue($entity, null); 2763 $this->originalEntityData[$oid][$field] = null; 2764 2765 break; 2766 } 2767 2768 if ( ! isset($hints['fetchMode'][$class->name][$field])) { 2769 $hints['fetchMode'][$class->name][$field] = $assoc['fetch']; 2770 } 2771 2772 // Foreign key is set 2773 // Check identity map first 2774 // FIXME: Can break easily with composite keys if join column values are in 2775 // wrong order. The correct order is the one in ClassMetadata#identifier. 2776 $relatedIdHash = implode(' ', $associatedId); 2777 2778 switch (true) { 2779 case (isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash])): 2780 $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash]; 2781 2782 // If this is an uninitialized proxy, we are deferring eager loads, 2783 // this association is marked as eager fetch, and its an uninitialized proxy (wtf!) 2784 // then we can append this entity for eager loading! 2785 if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER && 2786 isset($hints[self::HINT_DEFEREAGERLOAD]) && 2787 !$targetClass->isIdentifierComposite && 2788 $newValue instanceof Proxy && 2789 $newValue->__isInitialized__ === false) { 2790 2791 $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); 2792 } 2793 2794 break; 2795 2796 case ($targetClass->subClasses): 2797 // If it might be a subtype, it can not be lazy. There isn't even 2798 // a way to solve this with deferred eager loading, which means putting 2799 // an entity with subclasses at a *-to-one location is really bad! (performance-wise) 2800 $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId); 2801 break; 2802 2803 default: 2804 switch (true) { 2805 // We are negating the condition here. Other cases will assume it is valid! 2806 case ($hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER): 2807 $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); 2808 break; 2809 2810 // Deferred eager load only works for single identifier classes 2811 case (isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite): 2812 // TODO: Is there a faster approach? 2813 $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); 2814 2815 $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); 2816 break; 2817 2818 default: 2819 // TODO: This is very imperformant, ignore it? 2820 $newValue = $this->em->find($assoc['targetEntity'], $associatedId); 2821 break; 2822 } 2823 2824 // PERF: Inlined & optimized code from UnitOfWork#registerManaged() 2825 $newValueOid = spl_object_hash($newValue); 2826 $this->entityIdentifiers[$newValueOid] = $associatedId; 2827 $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue; 2828 2829 if ( 2830 $newValue instanceof NotifyPropertyChanged && 2831 ( ! $newValue instanceof Proxy || $newValue->__isInitialized()) 2832 ) { 2833 $newValue->addPropertyChangedListener($this); 2834 } 2835 $this->entityStates[$newValueOid] = self::STATE_MANAGED; 2836 // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also! 2837 break; 2838 } 2839 2840 $this->originalEntityData[$oid][$field] = $newValue; 2841 $class->reflFields[$field]->setValue($entity, $newValue); 2842 2843 if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) { 2844 $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']]; 2845 $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity); 2846 } 2847 2848 break; 2849 2850 default: 2851 // Ignore if its a cached collection 2852 if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) { 2853 break; 2854 } 2855 2856 // use the given collection 2857 if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) { 2858 2859 $data[$field]->setOwner($entity, $assoc); 2860 2861 $class->reflFields[$field]->setValue($entity, $data[$field]); 2862 $this->originalEntityData[$oid][$field] = $data[$field]; 2863 2864 break; 2865 } 2866 2867 // Inject collection 2868 $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection); 2869 $pColl->setOwner($entity, $assoc); 2870 $pColl->setInitialized(false); 2871 2872 $reflField = $class->reflFields[$field]; 2873 $reflField->setValue($entity, $pColl); 2874 2875 if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { 2876 $this->loadCollection($pColl); 2877 $pColl->takeSnapshot(); 2878 } 2879 2880 $this->originalEntityData[$oid][$field] = $pColl; 2881 break; 2882 } 2883 } 2884 2885 // defer invoking of postLoad event to hydration complete step 2886 $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity); 2887 2888 return $entity; 2889 } 2890 2891 /** 2892 * @return void 2893 */ 2894 public function triggerEagerLoads() 2895 { 2896 if ( ! $this->eagerLoadingEntities) { 2897 return; 2898 } 2899 2900 // avoid infinite recursion 2901 $eagerLoadingEntities = $this->eagerLoadingEntities; 2902 $this->eagerLoadingEntities = []; 2903 2904 foreach ($eagerLoadingEntities as $entityName => $ids) { 2905 if ( ! $ids) { 2906 continue; 2907 } 2908 2909 $class = $this->em->getClassMetadata($entityName); 2910 2911 $this->getEntityPersister($entityName)->loadAll( 2912 array_combine($class->identifier, [array_values($ids)]) 2913 ); 2914 } 2915 } 2916 2917 /** 2918 * Initializes (loads) an uninitialized persistent collection of an entity. 2919 * 2920 * @param \Doctrine\ORM\PersistentCollection $collection The collection to initialize. 2921 * 2922 * @return void 2923 * 2924 * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733. 2925 */ 2926 public function loadCollection(PersistentCollection $collection) 2927 { 2928 $assoc = $collection->getMapping(); 2929 $persister = $this->getEntityPersister($assoc['targetEntity']); 2930 2931 switch ($assoc['type']) { 2932 case ClassMetadata::ONE_TO_MANY: 2933 $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection); 2934 break; 2935 2936 case ClassMetadata::MANY_TO_MANY: 2937 $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection); 2938 break; 2939 } 2940 2941 $collection->setInitialized(true); 2942 } 2943 2944 /** 2945 * Gets the identity map of the UnitOfWork. 2946 * 2947 * @return array 2948 */ 2949 public function getIdentityMap() 2950 { 2951 return $this->identityMap; 2952 } 2953 2954 /** 2955 * Gets the original data of an entity. The original data is the data that was 2956 * present at the time the entity was reconstituted from the database. 2957 * 2958 * @param object $entity 2959 * 2960 * @return array 2961 */ 2962 public function getOriginalEntityData($entity) 2963 { 2964 $oid = spl_object_hash($entity); 2965 2966 return isset($this->originalEntityData[$oid]) 2967 ? $this->originalEntityData[$oid] 2968 : []; 2969 } 2970 2971 /** 2972 * @ignore 2973 * 2974 * @param object $entity 2975 * @param array $data 2976 * 2977 * @return void 2978 */ 2979 public function setOriginalEntityData($entity, array $data) 2980 { 2981 $this->originalEntityData[spl_object_hash($entity)] = $data; 2982 } 2983 2984 /** 2985 * INTERNAL: 2986 * Sets a property value of the original data array of an entity. 2987 * 2988 * @ignore 2989 * 2990 * @param string $oid 2991 * @param string $property 2992 * @param mixed $value 2993 * 2994 * @return void 2995 */ 2996 public function setOriginalEntityProperty($oid, $property, $value) 2997 { 2998 $this->originalEntityData[$oid][$property] = $value; 2999 } 3000 3001 /** 3002 * Gets the identifier of an entity. 3003 * The returned value is always an array of identifier values. If the entity 3004 * has a composite identifier then the identifier values are in the same 3005 * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames(). 3006 * 3007 * @param object $entity 3008 * 3009 * @return array The identifier values. 3010 */ 3011 public function getEntityIdentifier($entity) 3012 { 3013 return $this->entityIdentifiers[spl_object_hash($entity)]; 3014 } 3015 3016 /** 3017 * Processes an entity instance to extract their identifier values. 3018 * 3019 * @param object $entity The entity instance. 3020 * 3021 * @return mixed A scalar value. 3022 * 3023 * @throws \Doctrine\ORM\ORMInvalidArgumentException 3024 */ 3025 public function getSingleIdentifierValue($entity) 3026 { 3027 $class = $this->em->getClassMetadata(get_class($entity)); 3028 3029 if ($class->isIdentifierComposite) { 3030 throw ORMInvalidArgumentException::invalidCompositeIdentifier(); 3031 } 3032 3033 $values = $this->isInIdentityMap($entity) 3034 ? $this->getEntityIdentifier($entity) 3035 : $class->getIdentifierValues($entity); 3036 3037 return isset($values[$class->identifier[0]]) ? $values[$class->identifier[0]] : null; 3038 } 3039 3040 /** 3041 * Tries to find an entity with the given identifier in the identity map of 3042 * this UnitOfWork. 3043 * 3044 * @param mixed $id The entity identifier to look for. 3045 * @param string $rootClassName The name of the root class of the mapped entity hierarchy. 3046 * 3047 * @return object|false Returns the entity with the specified identifier if it exists in 3048 * this UnitOfWork, FALSE otherwise. 3049 */ 3050 public function tryGetById($id, $rootClassName) 3051 { 3052 $idHash = implode(' ', (array) $id); 3053 3054 return isset($this->identityMap[$rootClassName][$idHash]) 3055 ? $this->identityMap[$rootClassName][$idHash] 3056 : false; 3057 } 3058 3059 /** 3060 * Schedules an entity for dirty-checking at commit-time. 3061 * 3062 * @param object $entity The entity to schedule for dirty-checking. 3063 * 3064 * @return void 3065 * 3066 * @todo Rename: scheduleForSynchronization 3067 */ 3068 public function scheduleForDirtyCheck($entity) 3069 { 3070 $rootClassName = $this->em->getClassMetadata(get_class($entity))->rootEntityName; 3071 3072 $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity; 3073 } 3074 3075 /** 3076 * Checks whether the UnitOfWork has any pending insertions. 3077 * 3078 * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise. 3079 */ 3080 public function hasPendingInsertions() 3081 { 3082 return ! empty($this->entityInsertions); 3083 } 3084 3085 /** 3086 * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the 3087 * number of entities in the identity map. 3088 * 3089 * @return integer 3090 */ 3091 public function size() 3092 { 3093 $countArray = array_map('count', $this->identityMap); 3094 3095 return array_sum($countArray); 3096 } 3097 3098 /** 3099 * Gets the EntityPersister for an Entity. 3100 * 3101 * @param string $entityName The name of the Entity. 3102 * 3103 * @return \Doctrine\ORM\Persisters\Entity\EntityPersister 3104 */ 3105 public function getEntityPersister($entityName) 3106 { 3107 if (isset($this->persisters[$entityName])) { 3108 return $this->persisters[$entityName]; 3109 } 3110 3111 $class = $this->em->getClassMetadata($entityName); 3112 3113 switch (true) { 3114 case ($class->isInheritanceTypeNone()): 3115 $persister = new BasicEntityPersister($this->em, $class); 3116 break; 3117 3118 case ($class->isInheritanceTypeSingleTable()): 3119 $persister = new SingleTablePersister($this->em, $class); 3120 break; 3121 3122 case ($class->isInheritanceTypeJoined()): 3123 $persister = new JoinedSubclassPersister($this->em, $class); 3124 break; 3125 3126 default: 3127 throw new \RuntimeException('No persister found for entity.'); 3128 } 3129 3130 if ($this->hasCache && $class->cache !== null) { 3131 $persister = $this->em->getConfiguration() 3132 ->getSecondLevelCacheConfiguration() 3133 ->getCacheFactory() 3134 ->buildCachedEntityPersister($this->em, $persister, $class); 3135 } 3136 3137 $this->persisters[$entityName] = $persister; 3138 3139 return $this->persisters[$entityName]; 3140 } 3141 3142 /** 3143 * Gets a collection persister for a collection-valued association. 3144 * 3145 * @param array $association 3146 * 3147 * @return \Doctrine\ORM\Persisters\Collection\CollectionPersister 3148 */ 3149 public function getCollectionPersister(array $association) 3150 { 3151 $role = isset($association['cache']) 3152 ? $association['sourceEntity'] . '::' . $association['fieldName'] 3153 : $association['type']; 3154 3155 if (isset($this->collectionPersisters[$role])) { 3156 return $this->collectionPersisters[$role]; 3157 } 3158 3159 $persister = ClassMetadata::ONE_TO_MANY === $association['type'] 3160 ? new OneToManyPersister($this->em) 3161 : new ManyToManyPersister($this->em); 3162 3163 if ($this->hasCache && isset($association['cache'])) { 3164 $persister = $this->em->getConfiguration() 3165 ->getSecondLevelCacheConfiguration() 3166 ->getCacheFactory() 3167 ->buildCachedCollectionPersister($this->em, $persister, $association); 3168 } 3169 3170 $this->collectionPersisters[$role] = $persister; 3171 3172 return $this->collectionPersisters[$role]; 3173 } 3174 3175 /** 3176 * INTERNAL: 3177 * Registers an entity as managed. 3178 * 3179 * @param object $entity The entity. 3180 * @param array $id The identifier values. 3181 * @param array $data The original entity data. 3182 * 3183 * @return void 3184 */ 3185 public function registerManaged($entity, array $id, array $data) 3186 { 3187 $oid = spl_object_hash($entity); 3188 3189 $this->entityIdentifiers[$oid] = $id; 3190 $this->entityStates[$oid] = self::STATE_MANAGED; 3191 $this->originalEntityData[$oid] = $data; 3192 3193 $this->addToIdentityMap($entity); 3194 3195 if ($entity instanceof NotifyPropertyChanged && ( ! $entity instanceof Proxy || $entity->__isInitialized())) { 3196 $entity->addPropertyChangedListener($this); 3197 } 3198 } 3199 3200 /** 3201 * INTERNAL: 3202 * Clears the property changeset of the entity with the given OID. 3203 * 3204 * @param string $oid The entity's OID. 3205 * 3206 * @return void 3207 */ 3208 public function clearEntityChangeSet($oid) 3209 { 3210 unset($this->entityChangeSets[$oid]); 3211 } 3212 3213 /* PropertyChangedListener implementation */ 3214 3215 /** 3216 * Notifies this UnitOfWork of a property change in an entity. 3217 * 3218 * @param object $sender The entity that owns the property. 3219 * @param string $propertyName The name of the property that changed. 3220 * @param mixed $oldValue The old value of the property. 3221 * @param mixed $newValue The new value of the property. 3222 * 3223 * @return void 3224 */ 3225 public function propertyChanged($sender, $propertyName, $oldValue, $newValue) 3226 { 3227 $oid = spl_object_hash($sender); 3228 $class = $this->em->getClassMetadata(get_class($sender)); 3229 3230 $isAssocField = isset($class->associationMappings[$propertyName]); 3231 3232 if ( ! $isAssocField && ! isset($class->fieldMappings[$propertyName])) { 3233 return; // ignore non-persistent fields 3234 } 3235 3236 // Update changeset and mark entity for synchronization 3237 $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue]; 3238 3239 if ( ! isset($this->scheduledForSynchronization[$class->rootEntityName][$oid])) { 3240 $this->scheduleForDirtyCheck($sender); 3241 } 3242 } 3243 3244 /** 3245 * Gets the currently scheduled entity insertions in this UnitOfWork. 3246 * 3247 * @return array 3248 */ 3249 public function getScheduledEntityInsertions() 3250 { 3251 return $this->entityInsertions; 3252 } 3253 3254 /** 3255 * Gets the currently scheduled entity updates in this UnitOfWork. 3256 * 3257 * @return array 3258 */ 3259 public function getScheduledEntityUpdates() 3260 { 3261 return $this->entityUpdates; 3262 } 3263 3264 /** 3265 * Gets the currently scheduled entity deletions in this UnitOfWork. 3266 * 3267 * @return array 3268 */ 3269 public function getScheduledEntityDeletions() 3270 { 3271 return $this->entityDeletions; 3272 } 3273 3274 /** 3275 * Gets the currently scheduled complete collection deletions 3276 * 3277 * @return array 3278 */ 3279 public function getScheduledCollectionDeletions() 3280 { 3281 return $this->collectionDeletions; 3282 } 3283 3284 /** 3285 * Gets the currently scheduled collection inserts, updates and deletes. 3286 * 3287 * @return array 3288 */ 3289 public function getScheduledCollectionUpdates() 3290 { 3291 return $this->collectionUpdates; 3292 } 3293 3294 /** 3295 * Helper method to initialize a lazy loading proxy or persistent collection. 3296 * 3297 * @param object $obj 3298 * 3299 * @return void 3300 */ 3301 public function initializeObject($obj) 3302 { 3303 if ($obj instanceof Proxy) { 3304 $obj->__load(); 3305 3306 return; 3307 } 3308 3309 if ($obj instanceof PersistentCollection) { 3310 $obj->initialize(); 3311 } 3312 } 3313 3314 /** 3315 * Helper method to show an object as string. 3316 * 3317 * @param object $obj 3318 * 3319 * @return string 3320 */ 3321 private static function objToStr($obj) 3322 { 3323 return method_exists($obj, '__toString') ? (string) $obj : get_class($obj).'@'.spl_object_hash($obj); 3324 } 3325 3326 /** 3327 * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit(). 3328 * 3329 * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information 3330 * on this object that might be necessary to perform a correct update. 3331 * 3332 * @param object $object 3333 * 3334 * @return void 3335 * 3336 * @throws ORMInvalidArgumentException 3337 */ 3338 public function markReadOnly($object) 3339 { 3340 if ( ! is_object($object) || ! $this->isInIdentityMap($object)) { 3341 throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object); 3342 } 3343 3344 $this->readOnlyObjects[spl_object_hash($object)] = true; 3345 } 3346 3347 /** 3348 * Is this entity read only? 3349 * 3350 * @param object $object 3351 * 3352 * @return bool 3353 * 3354 * @throws ORMInvalidArgumentException 3355 */ 3356 public function isReadOnly($object) 3357 { 3358 if ( ! is_object($object)) { 3359 throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object); 3360 } 3361 3362 return isset($this->readOnlyObjects[spl_object_hash($object)]); 3363 } 3364 3365 /** 3366 * Perform whatever processing is encapsulated here after completion of the transaction. 3367 */ 3368 private function afterTransactionComplete() 3369 { 3370 $this->performCallbackOnCachedPersister(function (CachedPersister $persister) { 3371 $persister->afterTransactionComplete(); 3372 }); 3373 } 3374 3375 /** 3376 * Perform whatever processing is encapsulated here after completion of the rolled-back. 3377 */ 3378 private function afterTransactionRolledBack() 3379 { 3380 $this->performCallbackOnCachedPersister(function (CachedPersister $persister) { 3381 $persister->afterTransactionRolledBack(); 3382 }); 3383 } 3384 3385 /** 3386 * Performs an action after the transaction. 3387 * 3388 * @param callable $callback 3389 */ 3390 private function performCallbackOnCachedPersister(callable $callback) 3391 { 3392 if ( ! $this->hasCache) { 3393 return; 3394 } 3395 3396 foreach (array_merge($this->persisters, $this->collectionPersisters) as $persister) { 3397 if ($persister instanceof CachedPersister) { 3398 $callback($persister); 3399 } 3400 } 3401 } 3402 3403 private function dispatchOnFlushEvent() 3404 { 3405 if ($this->evm->hasListeners(Events::onFlush)) { 3406 $this->evm->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em)); 3407 } 3408 } 3409 3410 private function dispatchPostFlushEvent() 3411 { 3412 if ($this->evm->hasListeners(Events::postFlush)) { 3413 $this->evm->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em)); 3414 } 3415 } 3416 3417 /** 3418 * Verifies if two given entities actually are the same based on identifier comparison 3419 * 3420 * @param object $entity1 3421 * @param object $entity2 3422 * 3423 * @return bool 3424 */ 3425 private function isIdentifierEquals($entity1, $entity2) 3426 { 3427 if ($entity1 === $entity2) { 3428 return true; 3429 } 3430 3431 $class = $this->em->getClassMetadata(get_class($entity1)); 3432 3433 if ($class !== $this->em->getClassMetadata(get_class($entity2))) { 3434 return false; 3435 } 3436 3437 $oid1 = spl_object_hash($entity1); 3438 $oid2 = spl_object_hash($entity2); 3439 3440 $id1 = isset($this->entityIdentifiers[$oid1]) 3441 ? $this->entityIdentifiers[$oid1] 3442 : $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity1)); 3443 $id2 = isset($this->entityIdentifiers[$oid2]) 3444 ? $this->entityIdentifiers[$oid2] 3445 : $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity2)); 3446 3447 return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2); 3448 } 3449 3450 /** 3451 * @throws ORMInvalidArgumentException 3452 */ 3453 private function assertThatThereAreNoUnintentionallyNonPersistedAssociations() : void 3454 { 3455 $entitiesNeedingCascadePersist = \array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions); 3456 3457 $this->nonCascadedNewDetectedEntities = []; 3458 3459 if ($entitiesNeedingCascadePersist) { 3460 throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships( 3461 \array_values($entitiesNeedingCascadePersist) 3462 ); 3463 } 3464 } 3465 3466 /** 3467 * @param object $entity 3468 * @param object $managedCopy 3469 * 3470 * @throws ORMException 3471 * @throws OptimisticLockException 3472 * @throws TransactionRequiredException 3473 */ 3474 private function mergeEntityStateIntoManagedCopy($entity, $managedCopy) 3475 { 3476 if (! $this->isLoaded($entity)) { 3477 return; 3478 } 3479 3480 if (! $this->isLoaded($managedCopy)) { 3481 $managedCopy->__load(); 3482 } 3483 3484 $class = $this->em->getClassMetadata(get_class($entity)); 3485 3486 foreach ($this->reflectionPropertiesGetter->getProperties($class->name) as $prop) { 3487 $name = $prop->name; 3488 3489 $prop->setAccessible(true); 3490 3491 if ( ! isset($class->associationMappings[$name])) { 3492 if ( ! $class->isIdentifier($name)) { 3493 $prop->setValue($managedCopy, $prop->getValue($entity)); 3494 } 3495 } else { 3496 $assoc2 = $class->associationMappings[$name]; 3497 3498 if ($assoc2['type'] & ClassMetadata::TO_ONE) { 3499 $other = $prop->getValue($entity); 3500 if ($other === null) { 3501 $prop->setValue($managedCopy, null); 3502 } else { 3503 if ($other instanceof Proxy && !$other->__isInitialized()) { 3504 // do not merge fields marked lazy that have not been fetched. 3505 continue; 3506 } 3507 3508 if ( ! $assoc2['isCascadeMerge']) { 3509 if ($this->getEntityState($other) === self::STATE_DETACHED) { 3510 $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']); 3511 $relatedId = $targetClass->getIdentifierValues($other); 3512 3513 if ($targetClass->subClasses) { 3514 $other = $this->em->find($targetClass->name, $relatedId); 3515 } else { 3516 $other = $this->em->getProxyFactory()->getProxy( 3517 $assoc2['targetEntity'], 3518 $relatedId 3519 ); 3520 $this->registerManaged($other, $relatedId, []); 3521 } 3522 } 3523 3524 $prop->setValue($managedCopy, $other); 3525 } 3526 } 3527 } else { 3528 $mergeCol = $prop->getValue($entity); 3529 3530 if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) { 3531 // do not merge fields marked lazy that have not been fetched. 3532 // keep the lazy persistent collection of the managed copy. 3533 continue; 3534 } 3535 3536 $managedCol = $prop->getValue($managedCopy); 3537 3538 if ( ! $managedCol) { 3539 $managedCol = new PersistentCollection( 3540 $this->em, 3541 $this->em->getClassMetadata($assoc2['targetEntity']), 3542 new ArrayCollection 3543 ); 3544 $managedCol->setOwner($managedCopy, $assoc2); 3545 $prop->setValue($managedCopy, $managedCol); 3546 } 3547 3548 if ($assoc2['isCascadeMerge']) { 3549 $managedCol->initialize(); 3550 3551 // clear and set dirty a managed collection if its not also the same collection to merge from. 3552 if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) { 3553 $managedCol->unwrap()->clear(); 3554 $managedCol->setDirty(true); 3555 3556 if ($assoc2['isOwningSide'] 3557 && $assoc2['type'] == ClassMetadata::MANY_TO_MANY 3558 && $class->isChangeTrackingNotify() 3559 ) { 3560 $this->scheduleForDirtyCheck($managedCopy); 3561 } 3562 } 3563 } 3564 } 3565 } 3566 3567 if ($class->isChangeTrackingNotify()) { 3568 // Just treat all properties as changed, there is no other choice. 3569 $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy)); 3570 } 3571 } 3572 } 3573 3574 /** 3575 * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle. 3576 * Unit of work able to fire deferred events, related to loading events here. 3577 * 3578 * @internal should be called internally from object hydrators 3579 */ 3580 public function hydrationComplete() 3581 { 3582 $this->hydrationCompleteHandler->hydrationComplete(); 3583 } 3584 3585 /** 3586 * @param string $entityName 3587 */ 3588 private function clearIdentityMapForEntityName($entityName) 3589 { 3590 if (! isset($this->identityMap[$entityName])) { 3591 return; 3592 } 3593 3594 $visited = []; 3595 3596 foreach ($this->identityMap[$entityName] as $entity) { 3597 $this->doDetach($entity, $visited, false); 3598 } 3599 } 3600 3601 /** 3602 * @param string $entityName 3603 */ 3604 private function clearEntityInsertionsForEntityName($entityName) 3605 { 3606 foreach ($this->entityInsertions as $hash => $entity) { 3607 // note: performance optimization - `instanceof` is much faster than a function call 3608 if ($entity instanceof $entityName && get_class($entity) === $entityName) { 3609 unset($this->entityInsertions[$hash]); 3610 } 3611 } 3612 } 3613 3614 /** 3615 * @param ClassMetadata $class 3616 * @param mixed $identifierValue 3617 * 3618 * @return mixed the identifier after type conversion 3619 * 3620 * @throws \Doctrine\ORM\Mapping\MappingException if the entity has more than a single identifier 3621 */ 3622 private function convertSingleFieldIdentifierToPHPValue(ClassMetadata $class, $identifierValue) 3623 { 3624 return $this->em->getConnection()->convertToPHPValue( 3625 $identifierValue, 3626 $class->getTypeOfField($class->getSingleIdentifierFieldName()) 3627 ); 3628 } 3629} 3630