PageRenderTime 84ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/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

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

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