PageRenderTime 37ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/Doctrine/ORM/UnitOfWork.php

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