PageRenderTime 39ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/Doctrine/ODM/MongoDB/UnitOfWork.php

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