PageRenderTime 70ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

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

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