vendor/bestit/commercetools-odm/src/UnitOfWork.php line 573

Open in your IDE?
  1. <?php
  2. namespace BestIt\CommercetoolsODM;
  3. use BestIt\CommercetoolsODM\ActionBuilder\ActionBuilderProcessorInterface;
  4. use BestIt\CommercetoolsODM\ActionBuilder\Product\ResolveAttributeValueTrait;
  5. use BestIt\CommercetoolsODM\Event\LifecycleEventArgs;
  6. use BestIt\CommercetoolsODM\Event\ListenersInvoker;
  7. use BestIt\CommercetoolsODM\Event\OnFlushEventArgs;
  8. use BestIt\CommercetoolsODM\Exception\APIException;
  9. use BestIt\CommercetoolsODM\Exception\ConnectException;
  10. use BestIt\CommercetoolsODM\Helper\DocumentManagerAwareTrait;
  11. use BestIt\CommercetoolsODM\Helper\EventManagerAwareTrait;
  12. use BestIt\CommercetoolsODM\Helper\ListenerInvokerAwareTrait;
  13. use BestIt\CommercetoolsODM\Mapping\ClassMetadataInterface;
  14. use BestIt\CommercetoolsODM\Repository\ObjectRepository;
  15. use BestIt\CommercetoolsODM\UnitOfWork\ChangeManager;
  16. use BestIt\CommercetoolsODM\UnitOfWork\ChangeManagerInterface;
  17. use BestIt\CommercetoolsODM\UnitOfWork\ResponseHandlers\ResponseHandlerComposite;
  18. use BestIt\CommercetoolsODM\UnitOfWork\ResponseHandlers\ResponseHandlerInterface;
  19. use Commercetools\Core\Error\ApiException as CtApiException;
  20. use Commercetools\Core\Model\Cart\CartDraft;
  21. use Commercetools\Core\Model\Common\AbstractJsonDeserializeObject;
  22. use Commercetools\Core\Model\Common\AssetDraft;
  23. use Commercetools\Core\Model\Common\AssetDraftCollection;
  24. use Commercetools\Core\Model\Common\AttributeCollection;
  25. use Commercetools\Core\Model\Common\DateTimeDecorator;
  26. use Commercetools\Core\Model\Common\JsonObject;
  27. use Commercetools\Core\Model\Common\PriceDraft;
  28. use Commercetools\Core\Model\Common\PriceDraftCollection;
  29. use Commercetools\Core\Model\CustomField\CustomFieldObject;
  30. use Commercetools\Core\Model\CustomField\FieldContainer;
  31. use Commercetools\Core\Model\Product\Product;
  32. use Commercetools\Core\Model\Product\ProductDraft;
  33. use Commercetools\Core\Model\Product\ProductVariant;
  34. use Commercetools\Core\Model\Product\ProductVariantDraft;
  35. use Commercetools\Core\Model\Type\TypeReference;
  36. use Commercetools\Core\Request\AbstractDeleteRequest;
  37. use Commercetools\Core\Request\ClientRequestInterface;
  38. use Commercetools\Core\Response\ApiResponseInterface;
  39. use DateTime;
  40. use Doctrine\Common\EventManager;
  41. use Exception;
  42. use InvalidArgumentException;
  43. use Psr\Log\LoggerAwareTrait;
  44. use Psr\Log\LoggerInterface;
  45. use Psr\Log\NullLogger;
  46. use RuntimeException;
  47. use SplObjectStorage;
  48. use Traversable;
  49. use function array_filter;
  50. use function array_keys;
  51. use function array_search;
  52. use function array_walk;
  53. use function count;
  54. use function Funct\Strings\upperCaseFirst;
  55. use function get_class;
  56. use function is_array;
  57. use function is_string;
  58. use function memory_get_usage;
  59. use function method_exists;
  60. use function spl_object_hash;
  61. use function stripos;
  62. use function ucfirst;
  63. use function var_dump;
  64. /**
  65. * The unit of work inspired by the couch db odm structure.
  66. *
  67. * @author blange <lange@bestit-online.de>
  68. * @internal
  69. * @package BestIt\CommercetoolsODM
  70. */
  71. class UnitOfWork implements UnitOfWorkInterface
  72. {
  73. use ActionBuilderProcessorAwareTrait;
  74. use ClientAwareTrait;
  75. use DocumentManagerAwareTrait;
  76. use EventManagerAwareTrait;
  77. use ListenerInvokerAwareTrait;
  78. use LoggerAwareTrait;
  79. use ResolveAttributeValueTrait;
  80. /**
  81. * Handles the change state of given models.
  82. *
  83. * @var ChangeManagerInterface|null
  84. */
  85. private $changeManager;
  86. /**
  87. * Maps containers and keys to ids.
  88. *
  89. * @var array
  90. */
  91. protected $containerKeyMap = [];
  92. /**
  93. * Maps customer ids.
  94. *
  95. * @var array
  96. */
  97. protected $customerIdMap = [];
  98. /**
  99. * Which objects should be detached after flush.
  100. *
  101. * @var SplObjectStorage
  102. */
  103. private $detachQueue = null;
  104. /**
  105. * Matches object ids to commercetools ids.
  106. *
  107. * @var array
  108. */
  109. protected $documentIdentifiers = [];
  110. /**
  111. * The states for given object ids.
  112. *
  113. * @todo Rename var.
  114. * @var array
  115. */
  116. protected $documentState = [];
  117. /**
  118. * The number of the consecutive flushs.
  119. *
  120. * @var int
  121. */
  122. private $flushRuns = 0;
  123. /**
  124. * Maps documents to ids.
  125. *
  126. * @var array
  127. */
  128. protected $identityMap = [];
  129. /**
  130. * Maps keys to ids.
  131. *
  132. * @var array
  133. */
  134. protected $keyMap = [];
  135. /**
  136. * Saving of the callbacks for objects
  137. *
  138. * @var SplObjectStorage
  139. */
  140. private $modifiers;
  141. /**
  142. * Saves the completely new documents.
  143. *
  144. * @var array
  145. */
  146. protected $newDocuments = [];
  147. /**
  148. * Helper to handle responses.
  149. *
  150. * @var ResponseHandlerInterface
  151. */
  152. private $responseHandler;
  153. /**
  154. * How many times should we retry the flush?
  155. *
  156. * @var int
  157. */
  158. private $retryCount = self::RETRY_STATUS_DEFAULT;
  159. /**
  160. * UnitOfWork constructor.
  161. *
  162. * @param ActionBuilderProcessorInterface $actionBuilderProcessor
  163. * @param DocumentManagerInterface $documentManager
  164. * @param EventManager $eventManager
  165. * @param ListenersInvoker $listenersInvoker
  166. */
  167. public function __construct(
  168. ActionBuilderProcessorInterface $actionBuilderProcessor,
  169. DocumentManagerInterface $documentManager,
  170. EventManager $eventManager,
  171. ListenersInvoker $listenersInvoker
  172. ) {
  173. $this
  174. ->setActionBuilderProcessor($actionBuilderProcessor)
  175. ->setClient($documentManager->getClient())
  176. ->setDocumentManager($documentManager)
  177. ->setEventManager($eventManager)
  178. ->setListenerInvoker($listenersInvoker);
  179. $this->detachQueue = new SplObjectStorage();
  180. $this->logger = new NullLogger();
  181. $this->modifiers = new SplObjectStorage();
  182. }
  183. /**
  184. * Adds the inserts to the request batch.
  185. *
  186. * @return $this
  187. */
  188. private function addInsertsToRequestBatch(): self
  189. {
  190. $client = $this->getClient();
  191. $this->logger->debug(
  192. 'Iterates thru the new documents for inserts.',
  193. [
  194. 'newObjectCount' => $this->countNewObjects(),
  195. ]
  196. );
  197. foreach ($this->newDocuments as $id => $object) {
  198. $this->logger->debug(
  199. 'Iterates to an object for an insert.',
  200. ['class' => $object::class]
  201. );
  202. $request = $this->createNewRequest($this->getClassMetadata($object), $object);
  203. // The responses are marked for the given identifier.
  204. $client->addBatchRequest($request->setIdentifier($id));
  205. }
  206. return $this;
  207. }
  208. /**
  209. * Adds a modifier for the given object.
  210. *
  211. * @param callable $change
  212. * @return void
  213. */
  214. private function addModifier(mixed $object, callable $change)
  215. {
  216. if (!$this->modifiers->contains($object)) {
  217. $this->modifiers->attach($object, []);
  218. }
  219. $callbacks = $this->modifiers[$object];
  220. $callbacks[] = $change;
  221. $this->modifiers[$object] = $callbacks;
  222. }
  223. /**
  224. * Adds the removal requests to the request batch.
  225. *
  226. * @todo Check for needed usage of version.
  227. *
  228. * @return UnitOfWork
  229. */
  230. private function addRemovalsToRequestBatch(): self
  231. {
  232. $client = $this->getClient();
  233. $this->logger->debug(
  234. 'Iterates thru the identity map for removals.',
  235. [
  236. 'allIds' => array_keys($this->identityMap),
  237. ]
  238. );
  239. foreach ($this->identityMap as $id => $model) {
  240. $isRemoved = $this->isObjectRemoved($model);
  241. $this->logger->debug(
  242. 'Iterates to an object for a possible remove.',
  243. [
  244. 'class' => $model::class,
  245. 'id' => $id,
  246. 'isRemoved' => $isRemoved,
  247. ]
  248. );
  249. if ($isRemoved) {
  250. $request = $this->createRemovalRequest($model);
  251. // The responses are marked for the given identifier.
  252. $client->addBatchRequest($request->setIdentifier($id));
  253. }
  254. }
  255. return $this;
  256. }
  257. /**
  258. * Iterates through the entities and creates their update / creation actions if needed.
  259. *
  260. * @return void
  261. */
  262. private function addRequestsToBatch()
  263. {
  264. $this
  265. ->addUpdatesToRequestBatch()
  266. ->addInsertsToRequestBatch()
  267. ->addRemovalsToRequestBatch();
  268. }
  269. /**
  270. * Adds the updates to the request batch.
  271. *
  272. * @return $this
  273. */
  274. private function addUpdatesToRequestBatch(): self
  275. {
  276. $client = $this->getClient();
  277. $this->logger->debug(
  278. 'Iterates thru the identity map for updates.',
  279. [
  280. 'allIds' => array_keys($this->identityMap),
  281. ]
  282. );
  283. foreach ($this->identityMap as $id => $object) {
  284. $isManaged = $this->isObjectManaged($object);
  285. $this->logger->debug(
  286. 'Iterates to an object for a possible update.',
  287. [
  288. 'class' => $object::class,
  289. 'id' => $id,
  290. 'isManaged' => $isManaged,
  291. ]
  292. );
  293. if ($isManaged) {
  294. $this->invokeLifecycleEvents($object, Events::PRE_PERSIST);
  295. $isChanged = $this->getChangeManager()->isChanged($object);
  296. if ($isChanged) {
  297. $this->logger->debug(
  298. 'Adds the possible update of the object to the batch or detaches it if required.',
  299. [
  300. 'class' => $object::class,
  301. 'id' => $id,
  302. 'isChanged' => $isChanged,
  303. 'isManaged' => $isManaged,
  304. ]
  305. );
  306. // The responses are marked for the given identifier.
  307. $updateRequest = $this->computeChangedObject($object);
  308. if ($updateRequest instanceof ClientRequestInterface) {
  309. $client->addBatchRequest($updateRequest->setIdentifier($id));
  310. } else {
  311. $this->invokeLifecycleEvents($object, Events::POST_PERSIST);
  312. }
  313. } else {
  314. $this->invokeLifecycleEvents($object, Events::POST_PERSIST);
  315. //We can remove it now, if there are no changed but a deferred detach.
  316. $this->processDeferredDetach($object);
  317. }
  318. }
  319. }
  320. return $this;
  321. }
  322. /**
  323. * Is a flush retry allowed?
  324. *
  325. * @param bool $increase Should the retry count be increased after the check?
  326. *
  327. * @return bool
  328. */
  329. #[\Override]
  330. public function canRetry(bool $increase = false): bool
  331. {
  332. $canRetry = ($this->retryCount < 0) || ($this->retryCount && $this->flushRuns < $this->retryCount);
  333. if ($increase) {
  334. ++$this->flushRuns;
  335. }
  336. return $canRetry;
  337. }
  338. /**
  339. * Cascades a detach operation to associated documents.
  340. *
  341. * @param array $visited
  342. * @return void
  343. */
  344. private function cascadeDetach(mixed $document, array &$visited)
  345. {
  346. }
  347. /**
  348. * Cascades the save into the documents childs.
  349. *
  350. * @param ClassMetadataInterface $class
  351. * @param array $visited
  352. * @return UnitOfWork
  353. */
  354. private function cascadeScheduleInsert(ClassMetadataInterface $class, mixed $document, array &$visited): self
  355. {
  356. // TODO
  357. return $this;
  358. }
  359. /**
  360. * Creates the update action for the given object if there is a change in the data.
  361. *
  362. * @todo Topmost array should be used as a whole.
  363. * @return ClientRequestInterface|null
  364. */
  365. private function computeChangedObject(mixed $object)
  366. {
  367. return $this->createUpdateRequest(
  368. $this->getChangeManager()->getChanges($object),
  369. $this->getChangeManager()->getOriginalStatus($object),
  370. $object
  371. );
  372. }
  373. /**
  374. * Returns true if the unit of work contains the given document.
  375. *
  376. * @param mixed $document
  377. *
  378. * @return bool
  379. */
  380. #[\Override]
  381. public function contains($document): bool
  382. {
  383. $objectKey = $this->getKeyForObject($document);
  384. return isset($this->documentIdentifiers[$objectKey]) || isset($this->newDocuments[$objectKey]);
  385. }
  386. /**
  387. * Returns the count of managed entities.
  388. *
  389. * @return int
  390. */
  391. #[\Override]
  392. public function count(): int
  393. {
  394. return count($this->identityMap) + $this->countNewObjects();
  395. }
  396. /**
  397. * Returns the count of managed objects.
  398. *
  399. * @return int
  400. */
  401. #[\Override]
  402. public function countManagedObjects(): int
  403. {
  404. return count(array_filter($this->identityMap, [$this, 'isObjectManaged']));
  405. }
  406. /**
  407. * Returns the count for new objects.
  408. *
  409. * @return int
  410. */
  411. #[\Override]
  412. public function countNewObjects(): int
  413. {
  414. return count($this->newDocuments);
  415. }
  416. /**
  417. * Returns the count of scheduled removals.
  418. *
  419. * @return int
  420. */
  421. #[\Override]
  422. public function countRemovals(): int
  423. {
  424. return count(array_filter($this->identityMap, [$this, 'isObjectRemoved']));
  425. }
  426. /**
  427. * Creates and executes the request batch after checking every relevant object in the uow.
  428. *
  429. * @throws APIException
  430. * @throws ConnectException
  431. *
  432. * @return void
  433. */
  434. private function createAndExecuteBatch()
  435. {
  436. $this->addRequestsToBatch();
  437. try {
  438. if ($batchResponses = $this->getClient()->executeBatch()) {
  439. $this->processResponsesFromBatch($batchResponses);
  440. }
  441. } catch (CtApiException $exception) {
  442. if (stripos($exception->getMessage(), 'Error completing request') !== false) {
  443. throw new ConnectException($exception->getMessage(), $exception->getCode(), $exception);
  444. }
  445. }
  446. }
  447. /**
  448. * Creates a document and registers it as managed.
  449. *
  450. * @param string $className
  451. * @param mixed $responseObject The mapped Response from commercetools.
  452. * @param array $hints
  453. * @param bool $withRegistration Should we register the object in the unit of work.
  454. *
  455. * @return mixed The document matching to $className.
  456. */
  457. #[\Override]
  458. public function createDocument(
  459. string $className,
  460. $responseObject,
  461. array $hints = [],
  462. bool $withRegistration = true
  463. ) {
  464. unset($hints);
  465. /** @var ClassMetadataInterface $metadata */
  466. $document = null;
  467. $id = null;
  468. $metadata = $this->getClassMetadata($className);
  469. $version = null;
  470. if ($responseObject instanceof $className) {
  471. $targetDocument = clone $responseObject;
  472. $id = $responseObject->getId();
  473. $version = $responseObject->getVersion();
  474. } else {
  475. /** @var CustomFieldObject $customObject */
  476. $targetDocument = $metadata->getNewInstance();
  477. $customObject = $metadata->getCustomTypeFields() ? $responseObject->getCustom() : new CustomFieldObject();
  478. if ($metadata->getIdentifier()) {
  479. $id = $responseObject->getId();
  480. }
  481. if ($metadata->getVersion()) {
  482. $version = $responseObject->getVersion();
  483. }
  484. // TODO Make it more nice.
  485. foreach ($metadata->getFieldNames() as $fieldName) {
  486. if ($metadata->isCustomTypeField($fieldName)) {
  487. $foundValue = $customObject->getFields()->get($fieldName);
  488. } else {
  489. $foundValue = method_exists($responseObject, $getter = 'get' . ucfirst($fieldName))
  490. ? $responseObject->$getter()
  491. : $responseObject->$fieldName;
  492. }
  493. if (!empty($foundValue) || !$metadata->ignoreFieldOnEmpty($fieldName)) {
  494. $parsedValue = $this->parseFoundFieldValue($fieldName, $metadata, $foundValue);
  495. $targetDocument->{'set' . ucfirst($fieldName)}($parsedValue);
  496. }
  497. }
  498. }
  499. // TODO Find in new objects.
  500. $this->invokeLifecycleEvents($targetDocument, Events::POST_LOAD, $metadata);
  501. if (@$id && $withRegistration) {
  502. $this->registerAsManaged($targetDocument, $id, @$version);
  503. }
  504. return $targetDocument;
  505. }
  506. /**
  507. * Creates the draft for a new request.
  508. *
  509. * @todo Move to factory.
  510. *
  511. * @param ClassMetadataInterface $metadata
  512. * @param mixed $object The source object.
  513. * @param array $fields
  514. *
  515. * @return JsonObject
  516. */
  517. private function createDraftObjectForNewRequest(
  518. ClassMetadataInterface $metadata,
  519. mixed $object,
  520. array $fields
  521. ): JsonObject {
  522. $draftClass = $metadata->getDraft();
  523. if ($draftClass === ProductDraft::class) {
  524. $values = $this->parseValuesForProductDraft($object);
  525. } elseif ($draftClass === CartDraft::class) {
  526. $values = $this->parseValuesForCartDraft($metadata, $object, $fields);
  527. } else {
  528. $values = $this->parseValuesForSimpleDraft($metadata, $object, $fields);
  529. }
  530. return new $draftClass($values);
  531. }
  532. /**
  533. * Returns the create query for the given document.
  534. *
  535. * @param ClassMetadataInterface $metadata
  536. *
  537. * @return ClientRequestInterface
  538. */
  539. private function createNewRequest(ClassMetadataInterface $metadata, mixed $object): ClientRequestInterface
  540. {
  541. $fields = array_filter($metadata->getFieldNames(), fn(string $field) => !$metadata->isVersion($field) && !$metadata->isIdentifier($field) &&
  542. !$metadata->isFieldReadOnly($field));
  543. if ($metadata->isCTStandardModel()) {
  544. unset(
  545. $fields[array_search('createdAt', $fields)],
  546. $fields[array_search('id', $fields)],
  547. $fields[array_search('lastModifiedAt', $fields)],
  548. $fields[array_search('version', $fields)]
  549. );
  550. }
  551. $draftObject = $this->createDraftObjectForNewRequest($metadata, $object, $fields);
  552. return $this->getDocumentManager()->createRequest(
  553. $metadata->getName(),
  554. DocumentManager::REQUEST_TYPE_CREATE,
  555. $draftObject
  556. );
  557. }
  558. /**
  559. * Creates the removal request for the given model.
  560. *
  561. *
  562. * @return AbstractDeleteRequest
  563. */
  564. private function createRemovalRequest(mixed $model): AbstractDeleteRequest
  565. {
  566. return $this->getDocumentManager()->createRequest(
  567. $this->getClassMetadata($model)->getName(),
  568. DocumentManager::REQUEST_TYPE_DELETE_BY_ID,
  569. $model->getId(),
  570. $model->getVersion()
  571. );
  572. }
  573. /**
  574. * Creates the update request for the given changed data.
  575. *
  576. * @param array $changedData
  577. * @param array $oldData
  578. * @param ClassMetadataInterface|null $metadata
  579. * @return ClientRequestInterface|null
  580. */
  581. private function createUpdateRequest(
  582. array $changedData,
  583. array $oldData,
  584. mixed $document,
  585. ClassMetadataInterface $metadata = null
  586. ) {
  587. $documentClass = $document::class;
  588. if (!$metadata) {
  589. /** @var ClassMetadataInterface $metadata */
  590. $metadata = $this->getClassMetadata($document);
  591. }
  592. $actions = $this->getActionBuilderProcessor()->createUpdateActions(
  593. $metadata,
  594. $changedData,
  595. $oldData,
  596. $document
  597. );
  598. // There are possible differences between the raw view on changes and the real usable changes. If there are no
  599. // real usable changes then skip the request creation!
  600. if (!$actions) {
  601. $this->logger->debug(
  602. 'Skips the creation of the update request because there are no actions.',
  603. [
  604. 'actions' => $actions,
  605. 'class' => $document::class,
  606. 'memory' => memory_get_usage(true) / 1024 / 1024,
  607. 'objectId' => $document->getId(),
  608. 'objectKey' => $this->getKeyForObject($document),
  609. 'objectVersion' => $document->getVersion(),
  610. ]
  611. );
  612. return null;
  613. }
  614. $requestClass = $this->getDocumentManager()->getRequestClass(
  615. $documentClass,
  616. DocumentManager::REQUEST_TYPE_UPDATE_BY_ID
  617. );
  618. if (method_exists($requestClass, 'ofObject')) {
  619. $request = $requestClass::ofObject($document);
  620. } else {
  621. $request = $this->getDocumentManager()->createRequest(
  622. $documentClass,
  623. DocumentManager::REQUEST_TYPE_UPDATE_BY_ID,
  624. $document->getId(),
  625. $document->getVersion()
  626. );
  627. /** @var ObjectRepository $repository */
  628. $repository = $this->getDocumentManager()->getRepository($documentClass);
  629. // TODO Try to refactor to an explicit api, even if the doctrine base api does not support it.
  630. if (($repository instanceof ObjectRepository) && ($expands = $repository->getExpands())) {
  631. array_walk($expands, [$request, 'expand']);
  632. }
  633. $this->logger->debug(
  634. 'Created the update request.',
  635. [
  636. 'actions' => $actions,
  637. 'class' => $document::class,
  638. 'memory' => memory_get_usage(true) / 1024 / 1024,
  639. 'objectId' => $document->getId(),
  640. 'objectKey' => $this->getKeyForObject($document),
  641. 'objectVersion' => $document->getVersion(),
  642. 'request' => $request::class,
  643. ]
  644. );
  645. $request->setActions($actions);
  646. }
  647. return $request;
  648. }
  649. /**
  650. * Detaches a document from the persistence management.
  651. * It's persistence will no longer be managed by Doctrine.
  652. *
  653. * @param mixed $object The document to detach.
  654. *
  655. * @return void
  656. */
  657. #[\Override]
  658. public function detach($object)
  659. {
  660. $visited = [];
  661. $this->doDetach($object, $visited);
  662. }
  663. /**
  664. * Detaches the given object after flush.
  665. *
  666. * @param mixed $object
  667. *
  668. * @return void
  669. */
  670. #[\Override]
  671. public function detachDeferred($object)
  672. {
  673. $this->detachQueue->attach($object);
  674. }
  675. /**
  676. * Executes a detach operation on the given entity.
  677. *
  678. * @param array $visited
  679. * @return void
  680. */
  681. private function doDetach(mixed $model, array &$visited)
  682. {
  683. $oid = $this->getKeyForObject($model);
  684. if (!isset($visited[$oid])) {
  685. $this->logger->info(
  686. 'Model was detached from unit of work.',
  687. [
  688. 'class' => $model::class,
  689. 'id' => $model->getId(),
  690. ]
  691. );
  692. $visited[$oid] = $model; // mark visited
  693. $this->detachQueue->detach($model);
  694. $this->modifiers->detach($model); // TODO Check
  695. $this->removeFromIdentityMap($model);
  696. $this->cascadeDetach($model, $visited);
  697. $this->getChangeManager()->detach($model);
  698. unset($this->newDocuments[$oid]);
  699. $this->invokeLifecycleEvents($model, Events::POST_DETACH);
  700. }
  701. }
  702. /**
  703. * Schedules the removal of the given object.
  704. *
  705. * @param array $visited
  706. * @return void
  707. */
  708. private function doScheduleRemove(mixed $object, array &$visited)
  709. {
  710. $oid = $this->getKeyForObject($object);
  711. if (!isset($visited[$oid])) {
  712. $this->registerAsRemoved($object);
  713. $this->invokeLifecycleEvents($object, Events::PRE_REMOVE);
  714. }
  715. }
  716. /**
  717. * Queues the entity for saving or throws an exception if there is something wrong.
  718. *
  719. * @param array $visited
  720. * @return void
  721. */
  722. private function doScheduleSave(mixed $entity, array &$visited)
  723. {
  724. $oid = $this->getKeyForObject($entity);
  725. if (!isset($visited[$oid])) {
  726. $visited[$oid] = true;
  727. $class = $this->getClassMetadata($entity);
  728. $state = $this->getDocumentState($entity);
  729. switch ($state) {
  730. case self::STATE_NEW:
  731. $this->persistNew($entity);
  732. break;
  733. case self::STATE_MANAGED:
  734. // TODO: Change Tracking Deferred Explicit
  735. break;
  736. case self::STATE_REMOVED:
  737. // document becomes managed again
  738. $this->documentState[$oid] = self::STATE_MANAGED;
  739. break;
  740. case self::STATE_DETACHED:
  741. throw new InvalidArgumentException('Detached document passed to persist().');
  742. break;
  743. }
  744. $this->cascadeScheduleInsert($class, $entity, $visited);
  745. }
  746. }
  747. /**
  748. * Commits every change to commercetools.
  749. *
  750. * @todo Add the detach queue for ignored objects
  751. *
  752. * @return void
  753. */
  754. #[\Override]
  755. public function flush()
  756. {
  757. $this->getEventManager()->dispatchEvent(Events::ON_FLUSH, new OnFlushEventArgs($this));
  758. while ($this->needsToFlush() && ($this->canRetry(true))) {
  759. $this->logger->debug(
  760. 'Flushes the batch.',
  761. [
  762. 'memory' => memory_get_usage(true) / 1024 / 1024,
  763. 'retryCount' => $this->retryCount,
  764. 'run' => $this->flushRuns,
  765. ]
  766. );
  767. $this->createAndExecuteBatch();
  768. $this->logger->info(
  769. 'Flushed the batch.',
  770. [
  771. 'memory' => memory_get_usage(true) / 1024 / 1024,
  772. 'retryCount' => $this->retryCount,
  773. 'run' => $this->flushRuns,
  774. ]
  775. );
  776. }
  777. $this->flushRuns = 0;
  778. }
  779. /**
  780. * Returns the change manager.
  781. *
  782. * @return ChangeManagerInterface
  783. */
  784. private function getChangeManager(): ChangeManagerInterface
  785. {
  786. if (!$this->changeManager) {
  787. $this->changeManager = $this->loadChangeManager();
  788. }
  789. return $this->changeManager;
  790. }
  791. /**
  792. * Returns the metadata for the given class.
  793. *
  794. * @param string|object $class
  795. *
  796. * @return ClassMetadataInterface
  797. */
  798. protected function getClassMetadata($class): ClassMetadataInterface
  799. {
  800. return $this->getDocumentManager()->getClassMetadata(is_string($class) ? $class : $class::class);
  801. }
  802. /**
  803. * Get the state of a document.
  804. *
  805. * @todo Split for Key and ID. Catch the exception of the commercetools process?
  806. * @return int
  807. */
  808. protected function getDocumentState(mixed $document): int
  809. {
  810. /** @var ClassMetadataInterface $class */
  811. $class = $this->getClassMetadata($className = $document::class);
  812. $isStandard = $document instanceof JsonObject;
  813. $oid = $this->getKeyForObject($document);
  814. $state = $this->documentState[$oid] ?? null;
  815. $id = $isStandard ? $document->getId() : $document->{'get' . ucfirst($class->getIdentifier())}();
  816. // Check with the id.
  817. if (!$state && $id) {
  818. if ($this->tryGetById($id)) {
  819. $state = self::STATE_DETACHED;
  820. } else {
  821. $request = $this->getDocumentManager()->createRequest(
  822. $className,
  823. DocumentManager::REQUEST_TYPE_FIND_BY_ID,
  824. $id
  825. );
  826. $response = $this->getDocumentManager()->getClient()->execute($request);
  827. $state = $response->getStatusCode() === 404 ? self::STATE_NEW : self::STATE_DETACHED;
  828. }
  829. }
  830. // Check with the key.
  831. if (!$state && ($keyName = $class->getKey()) && ($keyValue = $document->{'get' . ucfirst($keyName)}())) {
  832. $request = $this->getDocumentManager()->createRequest(
  833. $className,
  834. DocumentManager::REQUEST_TYPE_FIND_BY_KEY,
  835. $keyValue
  836. );
  837. $response = $this->getDocumentManager()->getClient()->execute($request);
  838. $state = $response->getStatusCode() === 404 ? self::STATE_NEW : self::STATE_DETACHED;
  839. }
  840. return $state ?? self::STATE_NEW;
  841. }
  842. /**
  843. * Returns a key for the given object.
  844. *
  845. *
  846. * @return string
  847. */
  848. private function getKeyForObject(mixed $object): string
  849. {
  850. return spl_object_hash($object);
  851. }
  852. /**
  853. * Returns the used response handler.
  854. *
  855. * @return ResponseHandlerInterface
  856. */
  857. private function getResponseHandler(): ResponseHandlerInterface
  858. {
  859. if (!$this->responseHandler) {
  860. $this->setResponseHandler($this->loadResponseHandler());
  861. }
  862. return $this->responseHandler;
  863. }
  864. /**
  865. * Are there any modify callbacks for the given object?
  866. *
  867. * @param mixed $object
  868. *
  869. * @return bool
  870. */
  871. #[\Override]
  872. public function hasModifyCallbacks($object): bool
  873. {
  874. return $this->modifiers->contains($object);
  875. }
  876. /**
  877. * Invokes the lifecycle events for the given model.
  878. *
  879. * @param mixed $model
  880. * @param string $eventName
  881. * @param ClassMetadataInterface|null $metadata
  882. *
  883. * @return void
  884. */
  885. #[\Override]
  886. public function invokeLifecycleEvents($model, string $eventName, ClassMetadataInterface $metadata = null)
  887. {
  888. $this->getListenerInvoker()->invoke(
  889. new LifecycleEventArgs($model, $this->getDocumentManager()),
  890. $eventName,
  891. $model,
  892. $metadata ?: $this->getClassMetadata($model)
  893. );
  894. }
  895. /**
  896. * Returns true if the given object is managed by this class.
  897. *
  898. *
  899. * @return bool
  900. */
  901. private function isObjectManaged(mixed $object): bool
  902. {
  903. return $this->getDocumentState($object) === self::STATE_MANAGED;
  904. }
  905. /**
  906. * Returns true if the given object is managed by this class but marked as removed.
  907. *
  908. *
  909. * @return bool
  910. */
  911. private function isObjectRemoved(mixed $object): bool
  912. {
  913. return $this->getDocumentState($object) === self::STATE_REMOVED;
  914. }
  915. /**
  916. * Loads a fresh change manager.
  917. *
  918. * @todo Refactor.
  919. *
  920. * @return ChangeManagerInterface
  921. */
  922. private function loadChangeManager(): ChangeManagerInterface
  923. {
  924. $changeManager = new ChangeManager($this->getDocumentManager());
  925. $changeManager->setLogger($this->logger);
  926. return $changeManager;
  927. }
  928. /**
  929. * Returns a fresh response handler instance.
  930. *
  931. * @return ResponseHandlerInterface
  932. */
  933. protected function loadResponseHandler(): ResponseHandlerInterface
  934. {
  935. $handler = new ResponseHandlerComposite($this->getDocumentManager());
  936. $handler->setLogger($this->logger);
  937. return $handler;
  938. }
  939. /**
  940. * This method uses a callback to modify the given object to get conflict resolution in case of a 409 error.
  941. *
  942. * @param mixed $object
  943. * @param callable $change The callback is called with the given object.
  944. *
  945. * @return mixed Returns the changed object.
  946. */
  947. #[\Override]
  948. public function modify($object, callable $change)
  949. {
  950. $change($object);
  951. $this->addModifier($object, $change);
  952. return $object;
  953. }
  954. /**
  955. * Are there any objects which need to be flushed to the database.
  956. *
  957. * @todo CheckCleanQueue
  958. *
  959. * @return bool
  960. */
  961. private function needsToFlush()
  962. {
  963. if ($this->newDocuments || count($this->detachQueue)) {
  964. return true;
  965. }
  966. foreach ($this->identityMap as $id => $object) {
  967. if ($this->isObjectManaged($object) && $this->changeManager->isChanged($object)) {
  968. return true;
  969. }
  970. if ($this->isObjectRemoved($object)) {
  971. return true;
  972. }
  973. }
  974. return false;
  975. }
  976. /**
  977. * Parses the found value with the data from the field declaration.
  978. *
  979. * @param string $field
  980. * @param ClassMetadataInterface $metadata
  981. *
  982. * @return bool|DateTime|int|string
  983. */
  984. private function parseFoundFieldValue(string $field, ClassMetadataInterface $metadata, mixed $value)
  985. {
  986. switch ($metadata->getTypeOfField($field)) {
  987. case 'array':
  988. case 'set':
  989. // Force parse to array.
  990. if (!$value) {
  991. $value = [];
  992. }
  993. if (!is_array($returnValue = $value)) {
  994. $returnValue = $value instanceof Traversable ? iterator_to_array($value) : (array) $value;
  995. }
  996. // clean up.
  997. array_walk($returnValue, function ($value) {
  998. if ($value instanceof AbstractJsonDeserializeObject) {
  999. $value->parentSet(null)->rootSet(null);
  1000. }
  1001. });
  1002. break;
  1003. case 'boolean':
  1004. $returnValue = (bool) $value;
  1005. break;
  1006. case 'dateTime':
  1007. $returnValue = (new DateTimeDecorator($value))->getDateTime();
  1008. break;
  1009. case 'int':
  1010. $returnValue = (int) $value;
  1011. break;
  1012. case 'string':
  1013. $returnValue = (string) $value;
  1014. break;
  1015. default:
  1016. $returnValue = $value;
  1017. }
  1018. return $returnValue;
  1019. }
  1020. /**
  1021. * Parses the data of the given object to create a cart draft
  1022. *
  1023. * @param ClassMetadataInterface $metadata
  1024. * @param mixed $object The source object.
  1025. * @param array $fields
  1026. *
  1027. * @return array
  1028. */
  1029. private function parseValuesForCartDraft(ClassMetadataInterface $metadata, mixed $object, array $fields): array
  1030. {
  1031. $values = $this->parseValuesForSimpleDraft($metadata, $object, $fields);
  1032. $objectArray = $object->toArray();
  1033. $values['currency'] = $objectArray['currency'];
  1034. if (isset($objectArray['shippingInfo']['shippingMethod'])) {
  1035. $values['shippingMethod'] = $objectArray['shippingInfo']['shippingMethod'];
  1036. }
  1037. return $values;
  1038. }
  1039. /**
  1040. * Parses the data of the given object to create a value array for the draft of the object.
  1041. *
  1042. * @todo To hard coupled with the standard object.
  1043. * @todo Not completely tested.
  1044. *
  1045. * @param Product $product The source object.
  1046. *
  1047. * @return array
  1048. */
  1049. private function parseValuesForProductDraft(Product $product): array
  1050. {
  1051. $values = [
  1052. 'key' => (string) $product->getKey(),
  1053. 'productType' => $product->getProductType(),
  1054. 'state' => $product->getState(),
  1055. 'taxCategory' => $product->getTaxCategory(),
  1056. 'variants' => null,
  1057. ];
  1058. if ($productData = $product->getMasterData()) {
  1059. $values += [
  1060. 'publish' => (bool) $productData->getPublished(),
  1061. ];
  1062. $projection = $productData->getStaged();
  1063. $valueNames = [
  1064. 'categoryOrderHints',
  1065. 'categories',
  1066. 'description',
  1067. 'name',
  1068. 'metaKeywords',
  1069. 'metaDescription',
  1070. 'metaTitle',
  1071. 'slug',
  1072. ];
  1073. $searchKeywords = $projection->getSearchKeywords();
  1074. if ($searchKeywords && count($searchKeywords)) {
  1075. $valueNames[] = 'searchKeywords';
  1076. }
  1077. foreach ($valueNames as $name) {
  1078. $values[$name] = @$projection->get($name);
  1079. }
  1080. // getAllVariants() did not work as expected and Collection::toArray() changes to mush.
  1081. $variants = [$projection->getMasterVariant()];
  1082. foreach ($projection->getVariants() ?? [] as $variant) {
  1083. $variants[] = $variant;
  1084. }
  1085. /** @var ProductVariant $variant */
  1086. foreach ($variants as $index => $variant) {
  1087. $attributes = [];
  1088. if ($variant->getAttributes() instanceof AttributeCollection) {
  1089. $attributes = array_map(function (array $attribute) {
  1090. $attribute['value'] = $this->resolveAttributeValue($attribute['value']);
  1091. return $attribute;
  1092. }, $variant->getAttributes()->toArray());
  1093. }
  1094. $variantDraft = ProductVariantDraft::fromArray(
  1095. array_filter(
  1096. [
  1097. 'attributes' => $attributes,
  1098. 'images' => $variant->getImages(),
  1099. 'key' => $variant->getKey(),
  1100. 'sku' => (string) $variant->getSku(),
  1101. ]
  1102. )
  1103. );
  1104. if (($prices = $variant->getPrices()) && (count($prices))) {
  1105. $variantDraft->setPrices(new PriceDraftCollection());
  1106. foreach ($prices as $price) {
  1107. $variantDraft->getPrices()->add(PriceDraft::fromArray($price->toArray()));
  1108. }
  1109. }
  1110. if (($assets = $variant->getAssets()) && (count($assets))) {
  1111. $variantDraft->setAssets(new AssetDraftCollection());
  1112. foreach ($assets as $asset) {
  1113. $variantDraft->getAssets()->add(AssetDraft::fromArray($asset->toArray()));
  1114. }
  1115. }
  1116. if (!$index) {
  1117. $values['masterVariant'] = $variantDraft;
  1118. } else {
  1119. $values['variants'][] = $variantDraft;
  1120. }
  1121. }
  1122. }
  1123. if (!@$values['searchKeywords']) {
  1124. unset($values['searchKeywords']);
  1125. }
  1126. array_walk($values, function (&$value, $key) {
  1127. if ((is_object($value)) && (method_exists($value, 'toArray'))) {
  1128. $value = $value->toArray();
  1129. }
  1130. });
  1131. return array_filter($values);
  1132. }
  1133. /**
  1134. * Parses the data of the given object to create a value array for the draft of the object.
  1135. *
  1136. * @param ClassMetadataInterface $metadata
  1137. * @param mixed $object The source object.
  1138. * @param array $fields
  1139. *
  1140. * @return array
  1141. */
  1142. private function parseValuesForSimpleDraft(ClassMetadataInterface $metadata, mixed $object, array $fields): array
  1143. {
  1144. $customValues = [];
  1145. $values = [];
  1146. foreach ($fields as $field) {
  1147. $usedValue = $object->{'get' . ucfirst((string) $field)}();
  1148. if ($metadata->isCustomTypeField($field)) {
  1149. if (!@$values['custom']) {
  1150. $values['custom'] = (new CustomFieldObject())
  1151. ->setType(TypeReference::ofKey($metadata->getCustomType($field)));
  1152. }
  1153. $customValues[$field] = $usedValue;
  1154. } else {
  1155. $values[$field] = $usedValue;
  1156. }
  1157. }
  1158. if ($customValues) {
  1159. $values['custom']->setFields(FieldContainer::fromArray($customValues));
  1160. }
  1161. return $values;
  1162. }
  1163. /**
  1164. * Persist new document, marking it managed and generating the id.
  1165. *
  1166. * This method is either called through `DocumentManager#persist()` or during `DocumentManager#flush()`,
  1167. * when persistence by reachability is applied.
  1168. *
  1169. *
  1170. * @return UnitOfWork
  1171. */
  1172. protected function persistNew(mixed $document): UnitOfWork
  1173. {
  1174. $this->invokeLifecycleEvents($document, Events::PRE_PERSIST);
  1175. $this->registerAsManaged($document);
  1176. return $this;
  1177. }
  1178. /**
  1179. * Processed the deferred detach for the given object.
  1180. *
  1181. * @param mixed $model
  1182. *
  1183. * @return void
  1184. */
  1185. #[\Override]
  1186. public function processDeferredDetach($model)
  1187. {
  1188. $needsToDetach = $this->detachQueue->contains($model);
  1189. $this->logger->debug(
  1190. 'Processes the detach queue for the given model.',
  1191. [
  1192. 'class' => $model::class,
  1193. 'id' => $model->getId(),
  1194. 'needsDetach' => $needsToDetach,
  1195. ]
  1196. );
  1197. if ($needsToDetach) {
  1198. $this->detach($model);
  1199. }
  1200. }
  1201. /**
  1202. * Processes the responses from the batch.
  1203. *
  1204. * @param ApiResponseInterface[] $batchResponses
  1205. * @throws APIException
  1206. *
  1207. * @return void
  1208. */
  1209. private function processResponsesFromBatch(array $batchResponses)
  1210. {
  1211. $responseHandler = $this->getResponseHandler();
  1212. $this->logger->debug(
  1213. 'Handling batch responses.',
  1214. [
  1215. 'memory' => memory_get_usage(true) / 1024 / 1024,
  1216. 'responseCount' => count($batchResponses),
  1217. ]
  1218. );
  1219. /** @var ApiResponseInterface $response */
  1220. foreach ($batchResponses as $key => $response) {
  1221. $this->logger->debug(
  1222. 'Got a batch response.',
  1223. [
  1224. 'memory' => memory_get_usage(true) / 1024 / 1024,
  1225. 'objectId' => $key,
  1226. 'response' => $response->getResponse(),
  1227. 'request' => $response->getRequest(),
  1228. ]
  1229. );
  1230. try {
  1231. $responseHandler->handleResponse($response);
  1232. } catch (Exception $exception) {
  1233. // Just debug level. You can make it to an error on higher layers.
  1234. $this->logger->debug(
  1235. 'Received an error and throws it as an exception.',
  1236. [
  1237. 'exception' => $exception,
  1238. 'memory' => memory_get_usage(true) / 1024 / 1024,
  1239. ]
  1240. );
  1241. throw $exception;
  1242. }
  1243. }
  1244. }
  1245. /**
  1246. * Refreshes the persistent state of an object from the database,
  1247. * overriding any local changes that have not yet been persisted.
  1248. *
  1249. * @param mixed $object The object to refresh.
  1250. * @param mixed $overwrite Commercetools returns a representation of the object for many update actions, so use
  1251. * this responds directly.
  1252. * @return void
  1253. */
  1254. #[\Override]
  1255. public function refresh($object, $overwrite = null)
  1256. {
  1257. $metadata = $this->getClassMetadata($object);
  1258. if (!$overwrite) {
  1259. throw new RuntimeException('Not yet implemented');
  1260. }
  1261. foreach ($metadata->getFieldNames() as $fieldName) {
  1262. $value = $overwrite->{'get' . upperCaseFirst($fieldName)}();
  1263. if ($value instanceof DateTimeDecorator) {
  1264. $value = $value->getDateTime();
  1265. }
  1266. if ($value !== null) {
  1267. $object->{'set' . upperCaseFirst($fieldName)}($value);
  1268. }
  1269. }
  1270. }
  1271. /**
  1272. * Registers the given document as managed.
  1273. *
  1274. * @param mixed $document
  1275. * @param string|int $identifier
  1276. * @param mixed|null $revision
  1277. *
  1278. * @return UnitOfWorkInterface
  1279. */
  1280. #[\Override]
  1281. public function registerAsManaged($document, string $identifier = '', $revision = null): UnitOfWorkInterface
  1282. {
  1283. $oid = $this->getKeyForObject($document);
  1284. $this->documentState[$oid] = self::STATE_MANAGED;
  1285. if ($identifier) {
  1286. $oldIdentifier = @$this->documentIdentifiers[$oid];
  1287. if ($oldIdentifier !== $identifier) {
  1288. unset($this->identityMap[$oldIdentifier]);
  1289. }
  1290. $this->documentIdentifiers[$oid] = (string) $identifier;
  1291. $this->identityMap[$identifier] = $document;
  1292. $this->getChangeManager()->registerStatus($document);
  1293. unset($this->newDocuments[$oid]);
  1294. } else {
  1295. $this->newDocuments[$oid] = $document;
  1296. }
  1297. $this->invokeLifecycleEvents($document, Events::POST_REGISTER);
  1298. return $this;
  1299. }
  1300. /**
  1301. * Registers the given document as removed.
  1302. *
  1303. * @todo Handle id and version even for custom objects.
  1304. * @return UnitOfWorkInterface
  1305. */
  1306. public function registerAsRemoved(mixed $document): UnitOfWorkInterface
  1307. {
  1308. $identifier = $document->getId();
  1309. $oid = $this->getKeyForObject($document);
  1310. $this->documentState[$oid] = self::STATE_REMOVED;
  1311. $this->documentIdentifiers[$oid] = (string) $identifier;
  1312. $this->identityMap[$identifier] = $document;
  1313. return $this;
  1314. }
  1315. /**
  1316. * INTERNAL:
  1317. * Removes an document from the identity map. This effectively detaches the
  1318. * document from the persistence management of Doctrine.
  1319. *
  1320. * @ignore
  1321. * @todo Add key/container clear.
  1322. * @return void
  1323. */
  1324. private function removeFromIdentityMap(mixed $document)
  1325. {
  1326. $oid = $this->getKeyForObject($document);
  1327. if (isset($this->documentIdentifiers[$oid])) {
  1328. unset($this->identityMap[$this->documentIdentifiers[$oid]]);
  1329. }
  1330. unset(
  1331. $this->documentIdentifiers[$oid],
  1332. $this->documentState[$oid]
  1333. );
  1334. }
  1335. /**
  1336. * Changes the object with the registered modify callbacks.
  1337. *
  1338. * @param mixed $object
  1339. *
  1340. * return mixed the modified object.
  1341. */
  1342. #[\Override]
  1343. public function runModifyCallbacks($object)
  1344. {
  1345. foreach ($this->modifiers[$object] as $callback) {
  1346. $callback($object);
  1347. }
  1348. return $object;
  1349. }
  1350. /**
  1351. * Removes the object from the commercetools database.
  1352. *
  1353. * @param mixed $object
  1354. *
  1355. * @return UnitOfWorkInterface
  1356. */
  1357. #[\Override]
  1358. public function scheduleRemove($object): UnitOfWorkInterface
  1359. {
  1360. $visited = [];
  1361. $this->doScheduleRemove($object, $visited);
  1362. return $this;
  1363. }
  1364. /**
  1365. * Puts the given object in the save queue.
  1366. *
  1367. * @param mixed $entity
  1368. *
  1369. * @return UnitOfWorkInterface
  1370. */
  1371. #[\Override]
  1372. public function scheduleSave($entity): UnitOfWorkInterface
  1373. {
  1374. $visited = [];
  1375. $this->doScheduleSave($entity, $visited);
  1376. return $this;
  1377. }
  1378. /**
  1379. * Sets the change manager.
  1380. *
  1381. * @param ChangeManagerInterface $changeManager
  1382. *
  1383. * @return $this
  1384. */
  1385. public function setChangeManager(ChangeManagerInterface $changeManager): self
  1386. {
  1387. $this->changeManager = $changeManager;
  1388. return $this;
  1389. }
  1390. /**
  1391. * Sets the response handler for this class.
  1392. *
  1393. * @param ResponseHandlerInterface $responseHandler
  1394. *
  1395. * @return $this
  1396. */
  1397. public function setResponseHandler(ResponseHandlerInterface $responseHandler): self
  1398. {
  1399. $this->responseHandler = $responseHandler;
  1400. return $this;
  1401. }
  1402. /**
  1403. * How often should the flush be retried?
  1404. *
  1405. * You can use the constants for disabling or an infinite loop.
  1406. *
  1407. * @param int $retryCount
  1408. *
  1409. * @return $this
  1410. */
  1411. public function setRetryCount(int $retryCount): self
  1412. {
  1413. $this->retryCount = $retryCount;
  1414. return $this;
  1415. }
  1416. /**
  1417. * Tries to find a managed object by its key and container.
  1418. *
  1419. * @param string $container
  1420. * @param string $key
  1421. *
  1422. * @return mixed|void
  1423. */
  1424. #[\Override]
  1425. public function tryGetByContainerAndKey(string $container, string $key)
  1426. {
  1427. $key = $container . '|' . $key;
  1428. $return = null;
  1429. if (array_key_exists($key, $this->containerKeyMap)) {
  1430. $return = $this->tryGetById($this->containerKeyMap[$key]);
  1431. }
  1432. return $return;
  1433. }
  1434. /**
  1435. * Tries to find an document with the given customer identifier in the identity map of this UnitOfWork.
  1436. *
  1437. * @param string $id The document customer id to look for.
  1438. *
  1439. * @return mixed Returns the document with the specified identifier if it exists in
  1440. * this UnitOfWork, void otherwise.
  1441. */
  1442. #[\Override]
  1443. public function tryGetByCustomerId(string $id)
  1444. {
  1445. $return = null;
  1446. if (array_key_exists($id, $this->customerIdMap)) {
  1447. $return = $this->tryGetById($this->customerIdMap[$id]);
  1448. }
  1449. return $return;
  1450. }
  1451. /**
  1452. * Tries to find an document with the given identifier in the identity map of this UnitOfWork.
  1453. *
  1454. * @param mixed $id The document identifier to look for.
  1455. *
  1456. * @return mixed Returns the document with the specified identifier if it exists in
  1457. * this UnitOfWork, void otherwise.
  1458. */
  1459. #[\Override]
  1460. public function tryGetById($id)
  1461. {
  1462. $model = @$this->identityMap[$id];
  1463. if (!$model) {
  1464. $model = @$this->newDocuments[$id];
  1465. }
  1466. return $model;
  1467. }
  1468. /**
  1469. * Tries to find an document with the given identifier in the identity map of this UnitOfWork.
  1470. *
  1471. * @param string $key The document key to look for.
  1472. *
  1473. * @return mixed Returns the document with the specified identifier if it exists in
  1474. * this UnitOfWork, void otherwise.
  1475. */
  1476. #[\Override]
  1477. public function tryGetByKey(string $key)
  1478. {
  1479. $return = null;
  1480. if (array_key_exists($key, $this->keyMap)) {
  1481. $return = $this->tryGetById($this->keyMap[$key]);
  1482. }
  1483. return $return;
  1484. }
  1485. }