PageRenderTime 52ms CodeModel.GetById 17ms 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

Large files files are truncated, but you can click here to view the full 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($ent

Large files files are truncated, but you can click here to view the full file