PageRenderTime 55ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Doctrine/ODM/CouchDB/UnitOfWork.php

https://github.com/wraheem/couchdb-odm
PHP | 1263 lines | 851 code | 138 blank | 274 comment | 244 complexity | 89ae78295698aa3b56e35da0bed8f83a MD5 | raw file

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

  1. <?php
  2. /*
  3. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  4. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  5. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  6. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  7. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  8. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  9. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  10. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  11. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  12. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  13. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  14. *
  15. * This software consists of voluntary contributions made by many individuals
  16. * and is licensed under the MIT license. For more information, see
  17. * <http://www.doctrine-project.org>.
  18. */
  19. namespace Doctrine\ODM\CouchDB;
  20. use Doctrine\CouchDB\Attachment;
  21. use Doctrine\ODM\CouchDB\Mapping\ClassMetadata;
  22. use Doctrine\ODM\CouchDB\Types\Type;
  23. use Doctrine\Common\Collections\Collection;
  24. use Doctrine\Common\Collections\ArrayCollection;
  25. use Doctrine\CouchDB\HTTP\HTTPException;
  26. /**
  27. * Unit of work class
  28. *
  29. * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
  30. * @link www.doctrine-project.com
  31. * @since 1.0
  32. * @author Benjamin Eberlei <kontakt@beberlei.de>
  33. * @author Lukas Kahwe Smith <smith@pooteeweet.org>
  34. */
  35. class UnitOfWork
  36. {
  37. const STATE_NEW = 1;
  38. const STATE_MANAGED = 2;
  39. const STATE_REMOVED = 3;
  40. const STATE_DETACHED = 4;
  41. /**
  42. * @var DocumentManager
  43. */
  44. private $dm = null;
  45. /**
  46. * @var array
  47. */
  48. private $identityMap = array();
  49. /**
  50. * @var array
  51. */
  52. private $documentIdentifiers = array();
  53. /**
  54. * @var array
  55. */
  56. private $documentRevisions = array();
  57. /**
  58. * @var array
  59. */
  60. private $documentState = array();
  61. /**
  62. * CouchDB always returns and updates the whole data of a document. If on update data is "missing"
  63. * this means the data is deleted. This also applies to attachments. This is why we need to ensure
  64. * that data that is not mapped is not lost. This map here saves all the "left-over" data and keeps
  65. * track of it if necessary.
  66. *
  67. * @var array
  68. */
  69. private $nonMappedData = array();
  70. /**
  71. * There is no need for a differentiation between original and changeset data in CouchDB, since
  72. * updates have to be complete updates of the document (unless you are using an update handler, which
  73. * is not yet a feature of CouchDB ODM).
  74. *
  75. * @var array
  76. */
  77. private $originalData = array();
  78. /**
  79. * The original data of embedded document handled separetly from simple property mapping data.
  80. *
  81. * @var array
  82. */
  83. private $originalEmbeddedData = array();
  84. /**
  85. * Contrary to the ORM, CouchDB only knows "updates". The question is wheater a revion exists (Real update vs insert).
  86. *
  87. * @var array
  88. */
  89. private $scheduledUpdates = array();
  90. /**
  91. * @var array
  92. */
  93. private $scheduledRemovals = array();
  94. /**
  95. * @var array
  96. */
  97. private $visitedCollections = array();
  98. /**
  99. * @var array
  100. */
  101. private $idGenerators = array();
  102. /**
  103. * @var EventManager
  104. */
  105. private $evm;
  106. /**
  107. * @var MetadataResolver
  108. */
  109. private $metadataResolver;
  110. /**
  111. * @param DocumentManager $dm
  112. */
  113. public function __construct(DocumentManager $dm)
  114. {
  115. $this->dm = $dm;
  116. $this->evm = $dm->getEventManager();
  117. $this->metadataResolver = $dm->getConfiguration()->getMetadataResolverImpl();
  118. $this->embeddedSerializer = new Mapping\EmbeddedDocumentSerializer($this->dm->getMetadataFactory(),
  119. $this->metadataResolver);
  120. }
  121. /**
  122. * Create a document given class, data and the doc-id and revision
  123. *
  124. * @param string $documentName
  125. * @param array $documentState
  126. * @param array $hints
  127. * @return object
  128. */
  129. public function createDocument($documentName, $data, array &$hints = array())
  130. {
  131. if (!$this->metadataResolver->canMapDocument($data)) {
  132. throw new \InvalidArgumentException("Missing or mismatching metadata description in the Document, cannot hydrate!");
  133. }
  134. $type = $this->metadataResolver->getDocumentType($data);
  135. $class = $this->dm->getClassMetadata($type);
  136. $documentState = array();
  137. $nonMappedData = array();
  138. $embeddedDocumentState = array();
  139. $id = $data['_id'];
  140. $rev = $data['_rev'];
  141. $conflict = false;
  142. foreach ($data as $jsonName => $jsonValue) {
  143. if (isset($class->jsonNames[$jsonName])) {
  144. $fieldName = $class->jsonNames[$jsonName];
  145. if (isset($class->fieldMappings[$fieldName])) {
  146. if ($jsonValue === null) {
  147. $documentState[$class->fieldMappings[$fieldName]['fieldName']] = null;
  148. } else if (isset($class->fieldMappings[$fieldName]['embedded'])) {
  149. $embeddedInstance =
  150. $this->embeddedSerializer->createEmbeddedDocument($jsonValue, $class->fieldMappings[$fieldName]);
  151. $documentState[$jsonName] = $embeddedInstance;
  152. // storing the jsonValue for embedded docs for now
  153. $embeddedDocumentState[$jsonName] = $jsonValue;
  154. } else {
  155. $documentState[$class->fieldMappings[$fieldName]['fieldName']] =
  156. Type::getType($class->fieldMappings[$fieldName]['type'])
  157. ->convertToPHPValue($jsonValue);
  158. }
  159. }
  160. } else if ($jsonName == '_rev' || $jsonName == "type") {
  161. continue;
  162. } else if ($jsonName == '_conflicts') {
  163. $conflict = true;
  164. } else if ($class->hasAttachments && $jsonName == '_attachments') {
  165. $documentState[$class->attachmentField] = $this->createDocumentAttachments($id, $jsonValue);
  166. } else if ($this->metadataResolver->canResolveJsonField($jsonName)) {
  167. $documentState = $this->metadataResolver->resolveJsonField($class, $this->dm, $documentState, $jsonName, $data);
  168. } else {
  169. $nonMappedData[$jsonName] = $jsonValue;
  170. }
  171. }
  172. if ($conflict && $this->evm->hasListeners(Event::onConflict)) {
  173. // there is a conflict and we have an event handler that might resolve it
  174. $this->evm->dispatchEvent(Event::onConflict, new Event\ConflictEventArgs($data, $this->dm, $type));
  175. // the event might be resolved in the couch now, load it again:
  176. return $this->dm->find($type, $id);
  177. }
  178. // initialize inverse side collections
  179. foreach ($class->associationsMappings AS $assocName => $assocOptions) {
  180. if (!$assocOptions['isOwning'] && $assocOptions['type'] & ClassMetadata::TO_MANY) {
  181. $documentState[$class->associationsMappings[$assocName]['fieldName']] = new PersistentViewCollection(
  182. new \Doctrine\Common\Collections\ArrayCollection(),
  183. $this->dm,
  184. $id,
  185. $class->associationsMappings[$assocName]
  186. );
  187. }
  188. }
  189. if (isset($this->identityMap[$id])) {
  190. $document = $this->identityMap[$id];
  191. $overrideLocalValues = false;
  192. if ( ($document instanceof Proxy && !$document->__isInitialized__) || isset($hints['refresh'])) {
  193. $overrideLocalValues = true;
  194. $oid = spl_object_hash($document);
  195. }
  196. } else {
  197. $document = $class->newInstance();
  198. $this->identityMap[$id] = $document;
  199. $oid = spl_object_hash($document);
  200. $this->documentState[$oid] = self::STATE_MANAGED;
  201. $this->documentIdentifiers[$oid] = $id;
  202. $this->documentRevisions[$oid] = $rev;
  203. $overrideLocalValues = true;
  204. }
  205. if ($documentName && !($document instanceof $documentName)) {
  206. throw new InvalidDocumentTypeException($type, $documentName);
  207. }
  208. if ($overrideLocalValues) {
  209. $this->nonMappedData[$oid] = $nonMappedData;
  210. foreach ($class->reflFields as $prop => $reflFields) {
  211. $value = isset($documentState[$prop]) ? $documentState[$prop] : null;
  212. if (isset($embeddedDocumentState[$prop])) {
  213. $this->originalEmbeddedData[$oid][$prop] = $embeddedDocumentState[$prop];
  214. } else {
  215. $this->originalData[$oid][$prop] = $value;
  216. }
  217. $reflFields->setValue($document, $value);
  218. }
  219. }
  220. if ($this->evm->hasListeners(Event::postLoad)) {
  221. $this->evm->dispatchEvent(Event::postLoad, new Event\LifecycleEventArgs($document, $this->dm));
  222. }
  223. return $document;
  224. }
  225. /**
  226. * @param string $documentId
  227. * @param array $data
  228. * @return array
  229. */
  230. private function createDocumentAttachments($documentId, $data)
  231. {
  232. $attachments = array();
  233. $client = $this->dm->getHttpClient();
  234. $basePath = '/' . $this->dm->getCouchDBClient()->getDatabase() . '/' . $documentId . '/';
  235. foreach ($data AS $filename => $attachment) {
  236. if (isset($attachment['stub']) && $attachment['stub']) {
  237. $instance = Attachment::createStub($attachment['content_type'], $attachment['length'], $attachment['revpos'], $client, $basePath . $filename);
  238. } else if (isset($attachment['data'])) {
  239. $instance = Attachment::createFromBase64Data($attachment['data'], $attachment['content_type'], $attachment['revpos']);
  240. }
  241. $attachments[$filename] = $instance;
  242. }
  243. return $attachments;
  244. }
  245. /**
  246. * @param object $document
  247. * @return array
  248. */
  249. public function getOriginalData($document)
  250. {
  251. return $this->originalData[\spl_object_hash($document)];
  252. }
  253. /**
  254. * Schedule insertion of this document and cascade if neccessary.
  255. *
  256. * @param object $document
  257. */
  258. public function scheduleInsert($document)
  259. {
  260. $visited = array();
  261. $this->doScheduleInsert($document, $visited);
  262. }
  263. private function doScheduleInsert($document, &$visited)
  264. {
  265. $oid = \spl_object_hash($document);
  266. if (isset($visited[$oid])) {
  267. return;
  268. }
  269. $visited[$oid] = true;
  270. $class = $this->dm->getClassMetadata(get_class($document));
  271. $state = $this->getDocumentState($document);
  272. switch ($state) {
  273. case self::STATE_NEW:
  274. $this->persistNew($class, $document);
  275. break;
  276. case self::STATE_MANAGED:
  277. // TODO: Change Tracking Deferred Explicit
  278. break;
  279. case self::STATE_REMOVED:
  280. // document becomes managed again
  281. unset($this->scheduledRemovals[$oid]);
  282. $this->documentState[$oid] = self::STATE_MANAGED;
  283. break;
  284. case self::STATE_DETACHED:
  285. throw new \InvalidArgumentException("Detached document passed to persist().");
  286. break;
  287. }
  288. $this->cascadeScheduleInsert($class, $document, $visited);
  289. }
  290. /**
  291. *
  292. * @param ClassMetadata $class
  293. * @param object $document
  294. * @param array $visited
  295. */
  296. private function cascadeScheduleInsert($class, $document, &$visited)
  297. {
  298. foreach ($class->associationsMappings AS $assocName => $assoc) {
  299. if ( ($assoc['cascade'] & ClassMetadata::CASCADE_PERSIST) ) {
  300. $related = $class->reflFields[$assocName]->getValue($document);
  301. if (!$related) {
  302. continue;
  303. }
  304. if ($class->associationsMappings[$assocName]['type'] & ClassMetadata::TO_ONE) {
  305. if ($this->getDocumentState($related) == self::STATE_NEW) {
  306. $this->doScheduleInsert($related, $visited);
  307. }
  308. } else {
  309. // $related can never be a persistent collection in case of a new entity.
  310. foreach ($related AS $relatedDocument) {
  311. if ($this->getDocumentState($relatedDocument) == self::STATE_NEW) {
  312. $this->doScheduleInsert($relatedDocument, $visited);
  313. }
  314. }
  315. }
  316. }
  317. }
  318. }
  319. private function getIdGenerator($type)
  320. {
  321. if (!isset($this->idGenerators[$type])) {
  322. $this->idGenerators[$type] = Id\IdGenerator::create($type);
  323. }
  324. return $this->idGenerators[$type];
  325. }
  326. public function scheduleRemove($document)
  327. {
  328. $visited = array();
  329. $this->doRemove($document, $visited);
  330. }
  331. private function doRemove($document, &$visited)
  332. {
  333. $oid = \spl_object_hash($document);
  334. if (isset($visited[$oid])) {
  335. return;
  336. }
  337. $visited[$oid] = true;
  338. $this->scheduledRemovals[$oid] = $document;
  339. $this->documentState[$oid] = self::STATE_REMOVED;
  340. if ($this->evm->hasListeners(Event::preRemove)) {
  341. $this->evm->dispatchEvent(Event::preRemove, new Event\LifecycleEventArgs($document, $this->dm));
  342. }
  343. $this->cascadeRemove($document, $visited);
  344. }
  345. private function cascadeRemove($document, &$visited)
  346. {
  347. $class = $this->dm->getClassMetadata(get_class($document));
  348. foreach ($class->associationsMappings AS $name => $assoc) {
  349. if ($assoc['cascade'] & ClassMetadata::CASCADE_REMOVE) {
  350. $related = $class->reflFields[$assoc['fieldName']]->getValue($document);
  351. if ($related instanceof Collection || is_array($related)) {
  352. // If its a PersistentCollection initialization is intended! No unwrap!
  353. foreach ($related as $relatedDocument) {
  354. $this->doRemove($relatedDocument, $visited);
  355. }
  356. } else if ($related !== null) {
  357. $this->doRemove($related, $visited);
  358. }
  359. }
  360. }
  361. }
  362. public function refresh($document)
  363. {
  364. $visited = array();
  365. $this->doRefresh($document, $visited);
  366. }
  367. private function doRefresh($document, &$visited)
  368. {
  369. $oid = \spl_object_hash($document);
  370. if (isset($visited[$oid])) {
  371. return;
  372. }
  373. $visited[$oid] = true;
  374. $response = $this->dm->getCouchDBClient()->findDocument($this->getDocumentIdentifier($document));
  375. if ($response->status == 404) {
  376. throw new \Doctrine\ODM\CouchDB\DocumentNotFoundException();
  377. }
  378. $hints = array('refresh' => true);
  379. $this->createDocument($this->dm->getClassMetadata(get_class($document))->name, $response->body, $hints);
  380. $this->cascadeRefresh($document, $visited);
  381. }
  382. public function merge($document)
  383. {
  384. $visited = array();
  385. return $this->doMerge($document, $visited);
  386. }
  387. private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
  388. {
  389. if (!is_object($document)) {
  390. throw CouchDBException::unexpectedDocumentType($document);
  391. }
  392. $oid = spl_object_hash($document);
  393. if (isset($visited[$oid])) {
  394. return; // Prevent infinite recursion
  395. }
  396. $visited[$oid] = $document; // mark visited
  397. $class = $this->dm->getClassMetadata(get_class($document));
  398. // First we assume DETACHED, although it can still be NEW but we can avoid
  399. // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
  400. // we need to fetch it from the db anyway in order to merge.
  401. // MANAGED entities are ignored by the merge operation.
  402. if ($this->getDocumentState($document) == self::STATE_MANAGED) {
  403. $managedCopy = $document;
  404. } else {
  405. $id = $class->getIdentifierValue($document);
  406. if (!$id) {
  407. // document is new
  408. // TODO: prePersist will be fired on the empty object?!
  409. $managedCopy = $class->newInstance();
  410. $this->persistNew($class, $managedCopy);
  411. } else {
  412. $managedCopy = $this->tryGetById($id);
  413. if ($managedCopy) {
  414. // We have the document in-memory already, just make sure its not removed.
  415. if ($this->getDocumentState($managedCopy) == self::STATE_REMOVED) {
  416. throw new \InvalidArgumentException('Removed document detected during merge.'
  417. . ' Can not merge with a removed document.');
  418. }
  419. } else {
  420. // We need to fetch the managed copy in order to merge.
  421. $managedCopy = $this->dm->find($class->name, $id);
  422. }
  423. if ($managedCopy === null) {
  424. // If the identifier is ASSIGNED, it is NEW, otherwise an error
  425. // since the managed document was not found.
  426. if ($class->idGenerator == ClassMetadata::IDGENERATOR_ASSIGNED) {
  427. $managedCopy = $class->newInstance();
  428. $class->setIdentifierValue($managedCopy, $id);
  429. $this->persistNew($class, $managedCopy);
  430. } else {
  431. throw new DocumentNotFoundException();
  432. }
  433. }
  434. }
  435. if ($class->isVersioned) {
  436. $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
  437. $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
  438. // Throw exception if versions dont match.
  439. if ($managedCopyVersion != $documentVersion) {
  440. throw OptimisticLockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
  441. }
  442. }
  443. $managedOid = spl_object_hash($managedCopy);
  444. // Merge state of $entity into existing (managed) entity
  445. foreach ($class->reflFields as $name => $prop) {
  446. if ( ! isset($class->associationsMappings[$name])) {
  447. if ( ! $class->isIdentifier($name)) {
  448. $prop->setValue($managedCopy, $prop->getValue($document));
  449. }
  450. } else {
  451. $assoc2 = $class->associationsMappings[$name];
  452. if ($assoc2['type'] & ClassMetadata::TO_ONE) {
  453. $other = $prop->getValue($document);
  454. if ($other === null) {
  455. $prop->setValue($managedCopy, null);
  456. } else if ($other instanceof Proxy && !$other->__isInitialized__) {
  457. // do not merge fields marked lazy that have not been fetched.
  458. continue;
  459. } else if ( $assoc2['cascade'] & ClassMetadata::CASCADE_MERGE == 0) {
  460. if ($this->getDocumentState($other) == self::STATE_MANAGED) {
  461. $prop->setValue($managedCopy, $other);
  462. } else {
  463. $targetClass = $this->dm->getClassMetadata($assoc2['targetDocument']);
  464. $id = $targetClass->getIdentifierValues($other);
  465. $proxy = $this->dm->getProxyFactory()->getProxy($assoc2['targetDocument'], $id);
  466. $prop->setValue($managedCopy, $proxy);
  467. $this->registerManaged($proxy, $id, null);
  468. }
  469. }
  470. } else {
  471. $mergeCol = $prop->getValue($document);
  472. if ($mergeCol instanceof PersistentCollection && !$mergeCol->isInitialized) {
  473. // do not merge fields marked lazy that have not been fetched.
  474. // keep the lazy persistent collection of the managed copy.
  475. continue;
  476. }
  477. $managedCol = $prop->getValue($managedCopy);
  478. if (!$managedCol) {
  479. if ($assoc2['isOwning']) {
  480. $managedCol = new PersistentIdsCollection(
  481. new ArrayCollection,
  482. $assoc2['targetDocument'],
  483. $this->dm,
  484. array()
  485. );
  486. } else {
  487. $managedCol = new PersistentViewCollection(
  488. new ArrayCollection,
  489. $this->dm,
  490. $this->documentIdentifiers[$managedOid],
  491. $assoc2
  492. );
  493. }
  494. $prop->setValue($managedCopy, $managedCol);
  495. $this->originalData[$managedOid][$name] = $managedCol;
  496. }
  497. if ($assoc2['cascade'] & ClassMetadata::CASCADE_MERGE > 0) {
  498. $managedCol->initialize();
  499. if (!$managedCol->isEmpty()) {
  500. // clear managed collection, in casacadeMerge() the collection is filled again.
  501. $managedCol->unwrap()->clear();
  502. $managedCol->setDirty(true);
  503. }
  504. }
  505. }
  506. }
  507. }
  508. }
  509. if ($prevManagedCopy !== null) {
  510. $assocField = $assoc['fieldName'];
  511. $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
  512. if ($assoc['type'] & ClassMetadata::TO_ONE) {
  513. $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
  514. } else {
  515. $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
  516. if ($assoc['type'] == ClassMetadata::ONE_TO_MANY) {
  517. $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
  518. }
  519. }
  520. }
  521. // Mark the managed copy visited as well
  522. $visited[spl_object_hash($managedCopy)] = true;
  523. $this->cascadeMerge($document, $managedCopy, $visited);
  524. return $managedCopy;
  525. }
  526. /**
  527. * Cascades a merge operation to associated entities.
  528. *
  529. * @param object $document
  530. * @param object $managedCopy
  531. * @param array $visited
  532. */
  533. private function cascadeMerge($document, $managedCopy, array &$visited)
  534. {
  535. $class = $this->dm->getClassMetadata(get_class($document));
  536. foreach ($class->associationsMappings as $assoc) {
  537. if ( $assoc['cascade'] & ClassMetadata::CASCADE_MERGE == 0) {
  538. continue;
  539. }
  540. $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
  541. if ($relatedDocuments instanceof Collection) {
  542. if ($relatedDocuments instanceof PersistentCollection) {
  543. // Unwrap so that foreach() does not initialize
  544. $relatedDocuments = $relatedDocuments->unwrap();
  545. }
  546. foreach ($relatedDocuments as $relatedDocument) {
  547. $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
  548. }
  549. } else if ($relatedDocuments !== null) {
  550. $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
  551. }
  552. }
  553. }
  554. /**
  555. * Detaches a document from the persistence management. It's persistence will
  556. * no longer be managed by Doctrine.
  557. *
  558. * @param object $document The document to detach.
  559. */
  560. public function detach($document)
  561. {
  562. $visited = array();
  563. $this->doDetach($document, $visited);
  564. }
  565. /**
  566. * Executes a detach operation on the given entity.
  567. *
  568. * @param object $document
  569. * @param array $visited
  570. */
  571. private function doDetach($document, array &$visited)
  572. {
  573. $oid = spl_object_hash($document);
  574. if (isset($visited[$oid])) {
  575. return; // Prevent infinite recursion
  576. }
  577. $visited[$oid] = $document; // mark visited
  578. switch ($this->getDocumentState($document)) {
  579. case self::STATE_MANAGED:
  580. if (isset($this->identityMap[$this->documentIdentifiers[$oid]])) {
  581. $this->removeFromIdentityMap($document);
  582. }
  583. unset($this->scheduledRemovals[$oid], $this->scheduledUpdates[$oid],
  584. $this->originalData[$oid], $this->documentRevisions[$oid],
  585. $this->documentIdentifiers[$oid], $this->documentState[$oid]);
  586. break;
  587. case self::STATE_NEW:
  588. case self::STATE_DETACHED:
  589. return;
  590. }
  591. $this->cascadeDetach($document, $visited);
  592. }
  593. /**
  594. * Cascades a detach operation to associated documents.
  595. *
  596. * @param object $document
  597. * @param array $visited
  598. */
  599. private function cascadeDetach($document, array &$visited)
  600. {
  601. $class = $this->dm->getClassMetadata(get_class($document));
  602. foreach ($class->associationsMappings as $assoc) {
  603. if ( $assoc['cascade'] & ClassMetadata::CASCADE_DETACH == 0) {
  604. continue;
  605. }
  606. $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
  607. if ($relatedDocuments instanceof Collection) {
  608. if ($relatedDocuments instanceof PersistentCollection) {
  609. // Unwrap so that foreach() does not initialize
  610. $relatedDocuments = $relatedDocuments->unwrap();
  611. }
  612. foreach ($relatedDocuments as $relatedDocument) {
  613. $this->doDetach($relatedDocument, $visited);
  614. }
  615. } else if ($relatedDocuments !== null) {
  616. $this->doDetach($relatedDocuments, $visited);
  617. }
  618. }
  619. }
  620. private function cascadeRefresh($document, &$visited)
  621. {
  622. $class = $this->dm->getClassMetadata(get_class($document));
  623. foreach ($class->associationsMappings as $assoc) {
  624. if ($assoc['cascade'] & ClassMetadata::CASCADE_REFRESH) {
  625. $related = $class->reflFields[$assoc['fieldName']]->getValue($document);
  626. if ($related instanceof Collection) {
  627. if ($related instanceof PersistentCollection) {
  628. // Unwrap so that foreach() does not initialize
  629. $related = $related->unwrap();
  630. }
  631. foreach ($related as $relatedDocument) {
  632. $this->doRefresh($relatedDocument, $visited);
  633. }
  634. } else if ($related !== null) {
  635. $this->doRefresh($related, $visited);
  636. }
  637. }
  638. }
  639. }
  640. /**
  641. * Get the state of a document.
  642. *
  643. * @param object $document
  644. * @return int
  645. */
  646. public function getDocumentState($document)
  647. {
  648. $oid = \spl_object_hash($document);
  649. if (!isset($this->documentState[$oid])) {
  650. $class = $this->dm->getClassMetadata(get_class($document));
  651. $id = $class->getIdentifierValue($document);
  652. if (!$id) {
  653. return self::STATE_NEW;
  654. } else if ($class->idGenerator == ClassMetadata::IDGENERATOR_ASSIGNED) {
  655. if ($class->isVersioned) {
  656. if ($class->getFieldValue($document, $class->versionField)) {
  657. return self::STATE_DETACHED;
  658. } else {
  659. return self::STATE_NEW;
  660. }
  661. } else {
  662. if ($this->tryGetById($id)) {
  663. return self::STATE_DETACHED;
  664. } else {
  665. $response = $this->dm->getCouchDBClient()->findDocument($id);
  666. if ($response->status == 404) {
  667. return self::STATE_NEW;
  668. } else {
  669. return self::STATE_DETACHED;
  670. }
  671. }
  672. }
  673. } else {
  674. return self::STATE_DETACHED;
  675. }
  676. }
  677. return $this->documentState[$oid];
  678. }
  679. private function detectChangedDocuments()
  680. {
  681. foreach ($this->identityMap AS $id => $document) {
  682. $state = $this->getDocumentState($document);
  683. if ($state == self::STATE_MANAGED) {
  684. $class = $this->dm->getClassMetadata(get_class($document));
  685. $this->computeChangeSet($class, $document);
  686. }
  687. }
  688. }
  689. /**
  690. * @param ClassMetadata $class
  691. * @param object $document
  692. * @return void
  693. */
  694. public function computeChangeSet(ClassMetadata $class, $document)
  695. {
  696. if ($document instanceof Proxy\Proxy && !$document->__isInitialized__) {
  697. return;
  698. }
  699. $oid = \spl_object_hash($document);
  700. $actualData = array();
  701. $embeddedActualData = array();
  702. // 1. compute the actual values of the current document
  703. foreach ($class->reflFields AS $fieldName => $reflProperty) {
  704. $value = $reflProperty->getValue($document);
  705. if ($class->isCollectionValuedAssociation($fieldName) && $value !== null
  706. && !($value instanceof PersistentCollection)) {
  707. if (!$value instanceof Collection) {
  708. $value = new ArrayCollection($value);
  709. }
  710. if ($class->associationsMappings[$fieldName]['isOwning']) {
  711. $coll = new PersistentIdsCollection(
  712. $value,
  713. $class->associationsMappings[$fieldName]['targetDocument'],
  714. $this->dm,
  715. array()
  716. );
  717. } else {
  718. $coll = new PersistentViewCollection(
  719. $value,
  720. $this->dm,
  721. $this->documentIdentifiers[$oid],
  722. $class->associationsMappings[$fieldName]
  723. );
  724. }
  725. $class->reflFields[$fieldName]->setValue($document, $coll);
  726. $actualData[$fieldName] = $coll;
  727. } else {
  728. $actualData[$fieldName] = $value;
  729. if (isset($class->fieldMappings[$fieldName]['embedded']) && $value !== null) {
  730. // serializing embedded value right here, to be able to detect changes for later invocations
  731. $embeddedActualData[$fieldName] =
  732. $this->embeddedSerializer->serializeEmbeddedDocument($value, $class->fieldMappings[$fieldName]);
  733. }
  734. }
  735. // TODO: ORM transforms arrays and collections into persistent collections
  736. }
  737. // unset the revision field if necessary, it is not to be managed by the user in write scenarios.
  738. if ($class->isVersioned) {
  739. unset($actualData[$class->versionField]);
  740. }
  741. // 2. Compare to the original, or find out that this document is new.
  742. if (!isset($this->originalData[$oid])) {
  743. // document is New and should be inserted
  744. $this->originalData[$oid] = $actualData;
  745. $this->scheduledUpdates[$oid] = $document;
  746. $this->originalEmbeddedData[$oid] = $embeddedActualData;
  747. } else {
  748. // document is "fully" MANAGED: it was already fully persisted before
  749. // and we have a copy of the original data
  750. $changed = false;
  751. foreach ($actualData AS $fieldName => $fieldValue) {
  752. // Important to not check embeded values here, because those are objects, equality check isn't enough
  753. //
  754. if (isset($class->fieldMappings[$fieldName])
  755. && !isset($class->fieldMappings[$fieldName]['embedded'])
  756. && $this->originalData[$oid][$fieldName] !== $fieldValue) {
  757. $changed = true;
  758. break;
  759. } else if(isset($class->associationsMappings[$fieldName])) {
  760. if (!$class->associationsMappings[$fieldName]['isOwning']) {
  761. continue;
  762. }
  763. if ( ($class->associationsMappings[$fieldName]['type'] & ClassMetadata::TO_ONE) && $this->originalData[$oid][$fieldName] !== $fieldValue) {
  764. $changed = true;
  765. break;
  766. } else if ( ($class->associationsMappings[$fieldName]['type'] & ClassMetadata::TO_MANY)) {
  767. if ( !($fieldValue instanceof PersistentCollection)) {
  768. // if its not a persistent collection and the original value changed. otherwise it could just be null
  769. $changed = true;
  770. break;
  771. } else if ($fieldValue->changed()) {
  772. $this->visitedCollections[] = $fieldValue;
  773. $changed = true;
  774. break;
  775. }
  776. }
  777. } else if ($class->hasAttachments && $fieldName == $class->attachmentField) {
  778. // array of value objects, can compare that stricly
  779. if ($this->originalData[$oid][$fieldName] !== $fieldValue) {
  780. $changed = true;
  781. break;
  782. }
  783. }
  784. }
  785. // Check embedded documents here, only if there is no change yet
  786. if (!$changed) {
  787. foreach ($embeddedActualData as $fieldName => $fieldValue) {
  788. if (!isset($this->originalEmbeddedData[$oid][$fieldName])
  789. || $this->embeddedSerializer->isChanged(
  790. $actualData[$fieldName], /* actual value */
  791. $this->originalEmbeddedData[$oid][$fieldName], /* original state */
  792. $class->fieldMappings[$fieldName]
  793. )) {
  794. $changed = true;
  795. break;
  796. }
  797. }
  798. }
  799. if ($changed) {
  800. $this->originalData[$oid] = $actualData;
  801. $this->scheduledUpdates[$oid] = $document;
  802. $this->originalEmbeddedData[$oid] = $embeddedActualData;
  803. }
  804. }
  805. // 3. check if any cascading needs to happen
  806. foreach ($class->associationsMappings AS $name => $assoc) {
  807. if ($this->originalData[$oid][$name]) {
  808. $this->computeAssociationChanges($assoc, $this->originalData[$oid][$name]);
  809. }
  810. }
  811. }
  812. /**
  813. * Computes the changes of an association.
  814. *
  815. * @param AssociationMapping $assoc
  816. * @param mixed $value The value of the association.
  817. */
  818. private function computeAssociationChanges($assoc, $value)
  819. {
  820. // Look through the entities, and in any of their associations, for transient (new)
  821. // enities, recursively. ("Persistence by reachability")
  822. if ($assoc['type'] & ClassMetadata::TO_ONE) {
  823. if ($value instanceof Proxy && ! $value->__isInitialized__) {
  824. return; // Ignore uninitialized proxy objects
  825. }
  826. $value = array($value);
  827. } else if ($value instanceof PersistentCollection) {
  828. // Unwrap. Uninitialized collections will simply be empty.
  829. $value = $value->unwrap();
  830. }
  831. foreach ($value as $entry) {
  832. $targetClass = $this->dm->getClassMetadata($assoc['targetDocument'] ?: get_class($entry));
  833. $state = $this->getDocumentState($entry);
  834. $oid = spl_object_hash($entry);
  835. if ($state == self::STATE_NEW) {
  836. if ( !($assoc['cascade'] & ClassMetadata::CASCADE_PERSIST) ) {
  837. throw new \InvalidArgumentException("A new document was found through a relationship that was not"
  838. . " configured to cascade persist operations: " . self::objToStr($entry) . "."
  839. . " Explicitly persist the new document or configure cascading persist operations"
  840. . " on the relationship.");
  841. }
  842. $this->persistNew($targetClass, $entry);
  843. $this->computeChangeSet($targetClass, $entry);
  844. } else if ($state == self::STATE_REMOVED) {
  845. return new \InvalidArgumentException("Removed document detected during flush: "
  846. . self::objToStr($entry).". Remove deleted documents from associations.");
  847. } else if ($state == self::STATE_DETACHED) {
  848. // Can actually not happen right now as we assume STATE_NEW,
  849. // so the exception will be raised from the DBAL layer (constraint violation).
  850. throw new \InvalidArgumentException("A detached document was found through a "
  851. . "relationship during cascading a persist operation.");
  852. }
  853. // MANAGED associated entities are already taken into account
  854. // during changeset calculation anyway, since they are in the identity map.
  855. }
  856. }
  857. /**
  858. * Persist new document, marking it managed and generating the id.
  859. *
  860. * This method is either called through `DocumentManager#persist()` or during `DocumentManager#flush()`,
  861. * when persistence by reachability is applied.
  862. *
  863. * @param ClassMetadata $class
  864. * @param object $document
  865. * @return void
  866. */
  867. public function persistNew($class, $document)
  868. {
  869. $id = $this->getIdGenerator($class->idGenerator)->generate($document, $class, $this->dm);
  870. $this->registerManaged($document, $id, null);
  871. if ($this->evm->hasListeners(Event::prePersist)) {
  872. $this->evm->dispatchEvent(Event::prePersist, new Event\LifecycleEventArgs($document, $this->dm));
  873. }
  874. }
  875. /**
  876. * Flush Operation - Write all dirty entries to the CouchDB.
  877. *
  878. * @return void
  879. */
  880. public function flush()
  881. {
  882. $this->detectChangedDocuments();
  883. if ($this->evm->hasListeners(Event::onFlush)) {
  884. $this->evm->dispatchEvent(Event::onFlush, new Event\OnFlushEventArgs($this));
  885. }
  886. $config = $this->dm->getConfiguration();
  887. $bulkUpdater = $this->dm->getCouchDBClient()->createBulkUpdater();
  888. $bulkUpdater->setAllOrNothing($config->getAllOrNothingFlush());
  889. foreach ($this->scheduledRemovals AS $oid => $document) {
  890. $bulkUpdater->deleteDocument($this->documentIdentifiers[$oid], $this->documentRevisions[$oid]);
  891. $this->removeFromIdentityMap($document);
  892. if ($this->evm->hasListeners(Event::postRemove)) {
  893. $this->evm->dispatchEvent(Event::postRemove, new Event\LifecycleEventArgs($document, $this->dm));
  894. }
  895. }
  896. foreach ($this->scheduledUpdates AS $oid => $document) {
  897. $class = $this->dm->getClassMetadata(get_class($document));
  898. if ($this->evm->hasListeners(Event::preUpdate)) {
  899. $this->evm->dispatchEvent(Event::preUpdate, new Event\LifecycleEventArgs($document, $this->dm));
  900. $this->computeChangeSet($class, $document); // TODO: prevent association computations in this case?
  901. }
  902. $data = $this->metadataResolver->createDefaultDocumentStruct($class);
  903. // Convert field values to json values.
  904. foreach ($this->originalData[$oid] AS $fieldName => $fieldValue) {
  905. if (isset($class->fieldMappings[$fieldName])) {
  906. if ($fieldValue !== null && isset($class->fieldMappings[$fieldName]['embedded'])) {
  907. // As we store the serialized value in originalEmbeddedData, we can simply copy here.
  908. $fieldValue = $this->originalEmbeddedData[$oid][$class->fieldMappings[$fieldName]['jsonName']];
  909. } else if ($fieldValue !== null) {
  910. $fieldValue = Type::getType($class->fieldMappings[$fieldName]['type'])
  911. ->convertToCouchDBValue($fieldValue);
  912. }
  913. $data[$class->fieldMappings[$fieldName]['jsonName']] = $fieldValue;
  914. } else if (isset($class->associationsMappings[$fieldName])) {
  915. if ($class->associationsMappings[$fieldName]['type'] & ClassMetadata::TO_ONE) {
  916. if (\is_object($fieldValue)) {
  917. $fieldValue = $this->getDocumentIdentifier($fieldValue);
  918. } else {
  919. $fieldValue = null;
  920. }
  921. $data = $this->metadataResolver->storeAssociationField($data, $class, $this->dm, $fieldName, $fieldValue);
  922. } else if ($class->associationsMappings[$fieldName]['type'] & ClassMetadata::TO_MANY) {
  923. if ($class->associationsMappings[$fieldName]['isOwning']) {
  924. // TODO: Optimize when not initialized yet! In ManyToMany case we can keep track of ALL ids
  925. $ids = array();
  926. if (is_array($fieldValue) || $fieldValue instanceof \Doctrine\Common\Collections\Collection) {
  927. foreach ($fieldValue AS $key => $relatedObject) {
  928. $ids[$key] = $this->getDocumentIdentifier($relatedObject);
  929. }
  930. }
  931. $data = $this->metadataResolver->storeAssociationField($data, $class, $this->dm, $fieldName, $ids);
  932. }
  933. }
  934. } else if ($class->hasAttachments && $fieldName == $class->attachmentField) {
  935. if (is_array($fieldValue) && $fieldValue) {
  936. $data['_attachments'] = array();
  937. foreach ($fieldValue AS $filename => $attachment) {
  938. if (!($attachment instanceof \Doctrine\CouchDB\Attachment)) {
  939. throw CouchDBException::invalidAttachment($class->name, $this->documentIdentifiers[$oid], $filename);
  940. }
  941. $data['_attachments'][$filename] = $attachment->toArray();
  942. }
  943. }
  944. }
  945. }
  946. // respect the non mapped data, otherwise they will be deleted.
  947. if (isset($this->nonMappedData[$oid]) && $this->nonMappedData[$oid]) {
  948. $data = array_merge($data, $this->nonMappedData[$oid]);
  949. }
  950. $rev = $this->getDocumentRevision($document);
  951. if ($rev) {
  952. $data['_rev'] = $rev;
  953. }
  954. $bulkUpdater->updateDocument($data);
  955. }
  956. $response = $bulkUpdater->execute();
  957. $updateConflictDocuments = array();
  958. if ($response->status == 201) {
  959. foreach ($response->body AS $docResponse) {
  960. if (!isset($this->identityMap[$docResponse['id']])) {
  961. // deletions
  962. continue;
  963. }
  964. $document = $this->identityMap[$docResponse['id']];
  965. if (isset($docResponse['error'])) {
  966. $updateConflictDocuments[] = $document;
  967. } else {
  968. $this->documentRevisions[spl_object_hash($document)] = $docResponse['rev'];
  969. $class = $this->dm->getClassMetadata(get_class($document));
  970. if ($class->isVersioned) {
  971. $class->reflFields[$class->versionField]->setValue($document, $docResponse['rev']);
  972. }
  973. }
  974. if ($this->evm->hasListeners(Event::postUpdate)) {
  975. $this->evm->dispatchEvent(Event::postUpdate, new Event\LifecycleEventArgs($document, $this->dm));
  976. }
  977. }
  978. } else if ($response->status >= 400) {
  979. throw HTTPException::fromResponse($bulkUpdater->getPath(), $response);
  980. }
  981. foreach ($this->visitedCollections AS $col) {
  982. $col->takeSnapshot();
  983. }
  984. $this->scheduledUpdates =
  985. $this->scheduledRemovals =
  986. $this->visitedCollections = array();
  987. if (count($updateConflictDocuments)) {
  988. throw new UpdateConflictException($updateConflictDocuments);
  989. }
  990. }
  991. /**
  992. * INTERNAL:
  993. * Removes an document from the identity map. This effectively detaches the
  994. * document from the persistence management of Doctrine.
  995. *
  996. * @ignore
  997. * @param object $document
  998. * @return boolean
  999. */
  1000. public function removeFromIdentityMap($document)
  1001. {
  1002. $oid = spl_object_hash($document);
  1003. if (isset($this->identityMap[$this->documentIdentifiers[$oid]])) {
  1004. unset($this->identityMap[$this->documentIdentifiers[$oid]],
  1005. $this->documentIdentifiers[$oid],
  1006. $this->documentRevisions[$oid],
  1007. $this->documentState[$oid]);
  1008. return true;
  1009. }
  1010. return false;
  1011. }
  1012. /**
  1013. * @param object $document
  1014. * @return bool
  1015. */
  1016. public function contains($document)
  1017. {
  1018. return isset($this->documentIdentifiers[\spl_object_hash($document)]);
  1019. }
  1020. public function registerManaged($document, $identifier, $revision)
  1021. {
  1022. $oid = spl_object_hash($document);
  1023. $this->documentState[$oid] = self::STATE_MANAGED;
  1024. $this->documentIdentifiers[$oid] = $identifier;
  1025. $this->documentRevisions[$oid] = $revision;
  1026. $this->identityMap[$identifier] = $document;
  1027. }
  1028. /**
  1029. * Tries to find an document with the given identifier in the identity map of
  1030. * this UnitOfWork.
  1031. *
  1032. * @param mixed $id The document identifier to look for.
  1033. * @param string $rootClassName The name of the root class of the mapped document hierarchy.
  1034. * @return mixed Returns the document with the specified identifier if it exists in
  1035. * this UnitOfWork, FALSE otherwise.
  1036. */
  1037. public function tryGetById($id)
  1038. {
  1039. if (isset($this->identityMap[$id])) {
  1040. return $this->identityMap[$id];
  1041. }
  1042. return false;
  1043. }
  1044. /**
  1045. * Checks whether a document is registered in the identity map of this UnitOfWork.
  1046. *
  1047. * @param object $document
  1048. * @return boolean
  1049. */
  1050. public function isInIdentityMap($document)
  1051. {
  1052. $oid = spl_object_hash($document);
  1053. if ( ! isset($this->documentIdentifiers[$oid])) {
  1054. return false;
  1055. }
  1056. $classMetadata = $this->dm->getClassMetadata(get_class($document));
  1057. if ($this->documentIdentifiers[$oid] === '') {
  1058. return false;
  1059. }
  1060. return isset($this->identityMap[$this->documentIdentifiers[$oid]]);
  1061. }
  1062. /**
  1063. * Get the CouchDB revision of the document that was current upon retrieval.
  1064. *
  1065. * @throws CouchDBException
  1066. * @param object $document
  1067. * @return string
  1068. */
  1069. public function getDocumentRevision($document)
  1070. {
  1071. $oid = \spl_object_hash($document);
  1072. if (isset($this->documentRevisions[$oid])) {
  1073. return $this->documentRevisions[$oid];
  1074. }
  1075. return null;
  1076. }
  1077. public function getDocumentIdentifier($document)
  1078. {
  1079. $oid = \spl_object_hash($document);
  1080. if (isset($this->documentIdentifiers[$oid])) {
  1081. return $this->documentIdentifiers[$oid];
  1082. } else {
  1083. throw new CouchDBException("Document is not managed and has no identifier.");
  1084. }
  1085. }
  1086. private static function objToStr($obj)
  1087. {
  1088. return method_exists($obj, '__toString') ? (string)$obj : get_class($obj).'@'.spl_object_hash($obj);
  1089. }
  1090. /**
  1091. * Find many documents by id.
  1092. *
  1093. * Important: Each document is returned with the key it has in the $ids array!
  1094. *
  1095. * @param array $ids
  1096. * @param string $documentName
  1097. * @param int $limit
  1098. * @param int $offset
  1099. * @return array
  1100. */
  1101. public function findMany(array $ids, $documentName = null, $limit = null, $offset = null)
  1102. {
  1103. $response = $this->dm->getCouchDBClient()->findDocuments($ids, $limit, $offset);
  1104. $keys = array_flip($

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