vendor/pimcore/translations-provider-interfaces/src/TranslationsProvider/TranslationsCom.php line 94

Open in your IDE?
  1. <?php
  2. /**
  3.  * Pimcore
  4.  *
  5.  * This source file is available under following license:
  6.  * - Pimcore Commercial License (PCL)
  7.  *
  8.  *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  9.  *  @license    http://www.pimcore.org/license     PCL
  10.  */
  11. namespace Pimcore\TranslationsProviderInterfaceBundle\TranslationsProvider;
  12. use Pimcore\Log\ApplicationLogger;
  13. use Pimcore\Logger;
  14. use Pimcore\Model\DataObject\Concrete;
  15. use Pimcore\Model\Notification\Service\NotificationService;
  16. use Pimcore\Translation\ExportDataExtractorService\ExportDataExtractorServiceInterface;
  17. use Pimcore\Translation\ExportService\Exporter\ExporterInterface;
  18. use Pimcore\Translation\ImporterService\ImporterServiceInterface;
  19. use Pimcore\Translation\Translator;
  20. use Pimcore\TranslationsProviderInterfaceBundle\Client\TranslationsCom\ProjectDirectorClient;
  21. use Pimcore\TranslationsProviderInterfaceBundle\Constant\JobState;
  22. use Pimcore\TranslationsProviderInterfaceBundle\Constant\LogContext;
  23. use Pimcore\TranslationsProviderInterfaceBundle\Entity\Job;
  24. use Pimcore\TranslationsProviderInterfaceBundle\Event\JobEvents;
  25. use Pimcore\TranslationsProviderInterfaceBundle\PimcoreTranslationsProviderInterfaceBundle;
  26. use Pimcore\TranslationsProviderInterfaceBundle\Service\ConfigurationService;
  27. use Pimcore\TranslationsProviderInterfaceBundle\Translation\ImportDataExtractor\TranslationsComDataExtractor;
  28. use Pimcore\TranslationsProviderInterfaceBundle\Workflow\Enum\JobWorkflow;
  29. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  30. use Symfony\Component\EventDispatcher\GenericEvent;
  31. use Symfony\Component\HttpFoundation\Response;
  32. use Symfony\Component\Workflow\StateMachine;
  33. class TranslationsCom extends AbstractTranslationsProvider implements SupportsDataCompareInterface
  34. {
  35.     const MAX_COMPLETED_TARGETS_LIMIT 100000;
  36.     /**
  37.      * @var string
  38.      */
  39.     private $projectShortCode '';
  40.     /**
  41.      * @var ProjectDirectorClient
  42.      */
  43.     private $projectDirectorClient;
  44.     /**
  45.      * @var ExportDataExtractorServiceInterface
  46.      */
  47.     private $translationDataExtractorService;
  48.     /**
  49.      * @var TranslationsComDataExtractor
  50.      */
  51.     private $importDataExtractor;
  52.     /**
  53.      * @var ImporterServiceInterface
  54.      */
  55.     private $importerService;
  56.     /**
  57.      * @var ExporterInterface
  58.      */
  59.     private $translationExporter;
  60.     /**
  61.      * @var StateMachine
  62.      */
  63.     private $workflow;
  64.     /**
  65.      * @var EventDispatcherInterface
  66.      */
  67.     private $eventDispatcher;
  68.     /**
  69.      * @var NotificationService
  70.      */
  71.     protected $notificationService;
  72.     /**
  73.      * @var Translator
  74.      */
  75.     protected $translator;
  76.     /** @var array */
  77.     protected $notificationRecipients;
  78.     public function __construct(string $shortcutEventDispatcherInterface $eventDispatcherNotificationService $notificationServiceTranslator $translatorConfigurationService $configurationServiceApplicationLogger $loggerstring $workflow 'translation_job_default')
  79.     {
  80.         parent::__construct($shortcut$notificationService$translator$configurationService$logger);
  81.         if (\Pimcore::getContainer()->hasParameter('translations_com.project-shortcode')) {
  82.             trigger_deprecation('pimcore/translations-provider-interfaces''2.4''Using parameter `translations_com.project-shortcode` is deprecated, use config instead. Will be removed in version 3.0');
  83.             $this->projectShortCode \Pimcore::getContainer()->getParameter('translations_com.project-shortcode');
  84.         } else {
  85.             $this->projectShortCode $configurationService->getProjectShortcode();
  86.         }
  87.         $this->workflow \Pimcore::getContainer()->get('state_machine.' $workflow);
  88.         $this->eventDispatcher $eventDispatcher;
  89.     }
  90.     /**
  91.      * @inheritdoc
  92.      */
  93.     public function submitJob(Job $job): bool
  94.     {
  95.         try {
  96.             $this->resetError($this->translationsJobService$job);
  97.             if (\Pimcore::inDebugMode()) {
  98.                 if ($simulatedError PimcoreTranslationsProviderInterfaceBundle::getSimulatedError()) {
  99.                     throw new \Exception($simulatedError ' (submit)');
  100.                 }
  101.             }
  102.             $ignoredItems = [];
  103.             $projectDirectorClient $this->getProjectDirectorClient();
  104.             if (!$job->getSubmissionName()) {
  105.                 // set default name
  106.                 $job->setSubmissionName($this->getSubmissionName($job));
  107.             }
  108.             $this->applicationLogger->debug('Submit Job ' $job->getSubmissionName(), ['component' => LogContext::TRANSLATION_PROVIDER_TRANSLATIONS_COM]);
  109.             $projectDirectorClient->initSubmission(
  110.                 $this->projectShortCode,
  111.                 $this->getSubmissionName($job),
  112.                 '',
  113.                 $this->getDueDate($job),
  114.                 false,
  115.                 $job->getCustomAttributes()
  116.             );
  117.             $exportData = [];
  118.             $items $this->translationsJobService->getTranslationItems($job)->getItems();
  119.             foreach ($items as $counter => $translationItem) {
  120.                 try {
  121.                     $result $this->getTranslationDataExtractorService()->extract($translationItem$job->getSourceLanguage(), $job->getTargetLanguages());
  122.                 } catch (\Exception $e) {
  123.                     $ignoredItems[$translationItem->getType()][$translationItem->getId()] = $job->getTargetLanguages();
  124.                     continue;
  125.                 }
  126.                 if ($result->isEmpty()) {
  127.                     $ignoredItems[$translationItem->getType()][$translationItem->getId()] = $job->getTargetLanguages();
  128.                     continue;
  129.                 }
  130.                 $id uniqid();
  131.                 $this->getTranslationExporter()->export($result$id);
  132.                 $exportFile $this->getTranslationExporter()->getExportFilePath($id);
  133.                 $exportFile file_get_contents($exportFile);
  134.                 $ticketId $projectDirectorClient->uploadTranslateableFile(
  135.                     $this->getSubmissionFilename($job$translationItem->getType() . '_' $translationItem->getId()),
  136.                     $exportFile,
  137.                     $this->convertLanguage($job->getSourceLanguage()),
  138.                     $this->convertLanguage($job->getTargetLanguages())
  139.                 );
  140.                 $exportData[] = [
  141.                     'ticketId' => $ticketId,
  142.                     'xml' => $exportFile,
  143.                     'ctype' => $translationItem->getType(),
  144.                     'cid' => $translationItem->getId()
  145.                 ];
  146.             }
  147.             if ($exportData || $ignoredItems) {
  148.                 if ($exportData) {
  149.                     $job->setExportData(json_encode($exportData));
  150.                 }
  151.                 if ($ignoredItems) {
  152.                     $job->setIgnoredTranslationItems($ignoredItems);
  153.                 }
  154.                 $this->translationsJobService->persist($job);
  155.                 if ($ticketId $projectDirectorClient->startSubmission()) {
  156.                     $this->workflow->apply($jobJobWorkflow::TRANSITION_SUBMIT);
  157.                     $job->setRemoteId($ticketId);
  158.                     $this->translationsJobService->persist($job);
  159.                     return true;
  160.                 }
  161.                 return false;
  162.             } else {
  163.                 Logger::debug('there is nothing to export');
  164.                 $this->translationsJobService->delete($job);
  165.                 return false;
  166.             }
  167.         } catch (\Exception $e) {
  168.             $this->sendErrorNotification($job$e);
  169.             $this->recordError($this->translationsJobService$job$eLogContext::TRANSLATION_PROVIDER_TRANSLATIONS_COM);
  170.             throw $e;
  171.         }
  172.     }
  173.     /**
  174.      * @inheritdoc
  175.      */
  176.     public function receiveJob(Job $job$isRedeliveryCheck false): bool
  177.     {
  178.         try {
  179.             $this->resetError($this->translationsJobService$job);
  180.             if (\Pimcore::inDebugMode()) {
  181.                 if ($simulatedError PimcoreTranslationsProviderInterfaceBundle::getSimulatedError()) {
  182.                     throw new \Exception($simulatedError ' (receive)');
  183.                 }
  184.             }
  185.             $this->applicationLogger->debug('Receive Job ' $job->getSubmissionName(), ['component' => LogContext::TRANSLATION_PROVIDER_TRANSLATIONS_COM]);
  186.             // here we receive not only the completed but also the cancelled and stuff
  187.             $completedTargets = @$this->projectDirectorClient->getGLExchange()->getCompletedTargetsBySubmission($job->getRemoteId(), self::MAX_COMPLETED_TARGETS_LIMIT);
  188.             $cancelledTargets = @$this->projectDirectorClient->getGLExchange()->getCancelledTargetsBySubmissions($job->getRemoteId(), self::MAX_COMPLETED_TARGETS_LIMIT);
  189.             if (!sizeof($completedTargets) && !sizeof($cancelledTargets)) {
  190.                 // nothing to do
  191.                 return false;
  192.             }
  193.             $exportData json_decode($job->getExportData(), true);
  194.             $totalCompletedCount 0;
  195.             $totalCancelledCount 0;
  196.             $totalNumberOfExpectedTargetDocuments sizeof($exportData) * sizeof($job->getTargetLanguages());
  197.             $resultData $job->getResultDataAsArray();
  198.             if ($completedTargets) {
  199.                 /** @var \PDTarget $target */
  200.                 foreach ($completedTargets as $target) {
  201.                     if (!$translatedXml $this->projectDirectorClient->getGLExchange()->downloadTarget($target->ticket)) {
  202.                         continue;
  203.                     }
  204.                     // send download confirmation
  205.                     $this->projectDirectorClient->getGLExchange()->sendDownloadConfirmation($target->ticket);
  206.                     $resultData[$target->targetLocale] = $resultData[$target->targetLocale] ?? [];
  207.                     $resultData[$target->targetLocale][$target->ticket] = [
  208.                         'xml' => $translatedXml,
  209.                         'state' => JobWorkflow::STATE_RECEIVED,  // job item received, meaning that it can be processed now.
  210.                         'documentTicket' => $target->documentTicket
  211.                     ];
  212.                 }
  213.             }
  214.             if ($cancelledTargets) {
  215.                 /** @var \PDTarget $target */
  216.                 foreach ($cancelledTargets as $target) {
  217.                     $resultData[$target->targetLocale][$target->ticket] = [
  218.                         'xml' => null,
  219.                         'state' => JobWorkflow::STATE_CANCELED,
  220.                         'ticket' => $target->ticket,
  221.                         'documentName' => $target->documentName,
  222.                         'documentTicket' => $target->documentTicket
  223.                         // job item cancelled
  224.                     ];
  225.                 }
  226.             }
  227.             $job->setResultData(json_encode($resultData));
  228.             // count canceled and completed based on aggregated result information
  229.             // not only based on last receive request, since completed are not received anymore
  230.             foreach ($resultData as $language => $items) {
  231.                 foreach ($items as $item) {
  232.                     if ($item['state'] == JobWorkflow::STATE_CANCELED) {
  233.                         $totalCancelledCount++;
  234.                     }
  235.                     if ($item['state'] == JobWorkflow::STATE_RECEIVED) {
  236.                         $totalCompletedCount++;
  237.                     }
  238.                 }
  239.             }
  240.             if ($totalCancelledCount === $totalNumberOfExpectedTargetDocuments) {
  241.                 // everything has been cancelled, so switch to cancelled state
  242.                 $this->workflow->apply($jobJobWorkflow::TRANSITION_CANCEL);
  243.                 $this->translationsJobService->persist($job);
  244.                 return true;
  245.             }
  246.             if ($isRedeliveryCheck) {
  247.                 if ($job->getState() == JobState::PROCESSED) {
  248.                     $this->workflow->apply($jobJobWorkflow::TRANSITION_RECEIVE);
  249.                 }
  250.                 $job->setProcessedTranslationItems([]);
  251.                 $this->translationsJobService->persist($job);
  252.                 return true;
  253.             }
  254.             //  if cancelled + partially received equal to total count then we are done
  255.             if ($totalCompletedCount $totalCancelledCount === $totalNumberOfExpectedTargetDocuments) {
  256.                 // if everything is there then we are complete
  257.                 $this->workflow->apply($jobJobWorkflow::TRANSITION_RECEIVE);
  258.                 $this->translationsJobService->persist($job);
  259.                 return true;
  260.             }
  261.             // otherwise there is still something missing
  262.             $this->workflow->apply($jobJobWorkflow::TRANSITION_RECEIVE_PARTIALLY);
  263.             // here the job is saved
  264.             $this->translationsJobService->persist($job);
  265.             return false;
  266.         } catch (\Exception $e) {
  267.             $this->sendErrorNotification($job$e);
  268.             $this->recordError($this->translationsJobService$job$eLogContext::TRANSLATION_PROVIDER_TRANSLATIONS_COM);
  269.             throw $e;
  270.         }
  271.     }
  272.     /**
  273.      *
  274.      * @throws \Exception
  275.      */
  276.     public function processReceived(Job $job): bool
  277.     {
  278.         try {
  279.             $this->resetError($this->translationsJobService$job);
  280.             if (!in_array($job->getState(), [JobWorkflow::STATE_RECEIVEDJobWorkflow::STATE_RECEIVED_PARTIALLY])) {
  281.                 return false;
  282.             }
  283.             if (\Pimcore::inDebugMode()) {
  284.                 if ($simulatedError PimcoreTranslationsProviderInterfaceBundle::getSimulatedError()) {
  285.                     throw new \Exception($simulatedError ' (process)');
  286.                 }
  287.             }
  288.             $this->applicationLogger->debug('Process Job ' $job->getSubmissionName(), ['component' => LogContext::TRANSLATION_PROVIDER_TRANSLATIONS_COM]);
  289.             $resultData json_decode($job->getResultData(), true);
  290.             $processedItems $job->getProcessedTranslationItems();
  291.             $changedTranslationItems = [];
  292.             foreach ($resultData as $targetLanguage => $items) {
  293.                 foreach ($items as $item) {
  294.                     if ($item['state'] === JobWorkflow::STATE_CANCELED) {
  295.                         continue;
  296.                     }
  297.                     $importDataExtractor $this->getImportDataExtractor();
  298.                     $id uniqid();
  299.                     $importFile $importDataExtractor->getImportFilePath($id);
  300.                     file_put_contents($importFile$item['xml']);
  301.                     $importDataExtractor->setSourceLanguage($job->getSourceLanguage());
  302.                     $importDataExtractor->setTargetLanguage($targetLanguage);
  303.                     /**
  304.                      * currently a separate file for each translation item will be created as this is the prefered way for translations.com
  305.                      */
  306.                     $attributeSet $importDataExtractor->extractElement($id0);
  307.                     $type $attributeSet->getTranslationItem()->getType();
  308.                     $id $attributeSet->getTranslationItem()->getId();
  309.                     if (!$this->isAlreadyProcessed($type$id$targetLanguage$processedItems)) {
  310.                         $this->getImporterService()->import($attributeSetfalse);
  311.                         $changedTranslationItems[] = $attributeSet->getTranslationItem();
  312.                         $this->markAsProcessed($type$id$targetLanguage$processedItems);
  313.                         $event = new GenericEvent($attributeSet->getTranslationItem()->getElement(), ['attributeSet' => $attributeSet]);
  314.                         $this->eventDispatcher->dispatch($eventJobEvents::TRANSLATION_ITEM_PROCESSED);
  315.                     }
  316.                 }
  317.             }
  318.             $this->saveTranslationItems($changedTranslationItems);
  319.             if (json_encode($job->getProcessedTranslationItems()) != json_encode($processedItems)
  320.                 || $job->getState() === JobWorkflow::STATE_RECEIVED) {
  321.                 $job->setProcessedTranslationItems($processedItems);
  322.                 if ($job->getState() === JobWorkflow::STATE_RECEIVED) {
  323.                     $this->workflow->apply($jobJobWorkflow::TRANSITION_PROCESS);
  324.                 }
  325.                 $this->translationsJobService->persist($job);
  326.                 return true;
  327.             }
  328.             return false;
  329.         } catch (\Exception $e) {
  330.             $this->sendErrorNotification($job$e);
  331.             $this->recordError($this->translationsJobService$job$eLogContext::TRANSLATION_PROVIDER_TRANSLATIONS_COM);
  332.             throw $e;
  333.         }
  334.     }
  335.     /**
  336.      * @param array $processedItems
  337.      *
  338.      */
  339.     private function isAlreadyProcessed(string $typestring $idstring $targetLanguage$processedItems): bool
  340.     {
  341.         return isset($processedItems[$type]) && isset($processedItems[$type][$id]) && in_array($targetLanguage$processedItems[$type][$id]);
  342.     }
  343.     /**
  344.      * @param array $processedItems
  345.      *
  346.      * @return void
  347.      */
  348.     private function markAsProcessed(string $typestring $idstring $targetLanguage, &$processedItems)
  349.     {
  350.         $processedItems[$type] = $processedItems[$type] ?? [];
  351.         $processedItems[$type][$id] = $processedItems[$type][$id] ?? [];
  352.         $processedItems[$type][$id][] = $targetLanguage;
  353.     }
  354.     private function saveTranslationItems(array $translationItems)
  355.     {
  356.         $savedItems = [];
  357.         foreach ($translationItems as $item) {
  358.             $itemKey $item->getType() . '_' $item->getId();
  359.             if (!in_array($itemKey$savedItems)) {
  360.                 if ($item->getElement() instanceof Concrete) {
  361.                     $item->getElement()->setOmitMandatoryCheck(true);
  362.                 }
  363.                 $item->getElement()->save();
  364.                 $savedItems[] = $itemKey;
  365.             }
  366.         }
  367.     }
  368.     public function getExportDataResponse(string $exportData): Response
  369.     {
  370.         $response = new Response($exportData);
  371.         $response->headers->set('Content-Type''text/json');
  372.         return $response;
  373.     }
  374.     public function getResultDataResponse(string $resultData): Response
  375.     {
  376.         $response = new Response($resultData);
  377.         $response->headers->set('Content-Type''text/json');
  378.         return $response;
  379.     }
  380.     public function getProjectDirectorClient(): ProjectDirectorClient
  381.     {
  382.         return $this->projectDirectorClient;
  383.     }
  384.     /**
  385.      *
  386.      * @required
  387.      */
  388.     public function setProjectDirectorClient(ProjectDirectorClient $projectDirectorClient): self
  389.     {
  390.         $this->projectDirectorClient $projectDirectorClient;
  391.         return $this;
  392.     }
  393.     public function getTranslationDataExtractorService(): ExportDataExtractorServiceInterface
  394.     {
  395.         return $this->translationDataExtractorService;
  396.     }
  397.     /**
  398.      *
  399.      * @required
  400.      */
  401.     public function setTranslationDataExtractorService(
  402.         ExportDataExtractorServiceInterface $translationDataExtractorService
  403.     ): self {
  404.         $this->translationDataExtractorService $translationDataExtractorService;
  405.         return $this;
  406.     }
  407.     public function getImportDataExtractor(): TranslationsComDataExtractor
  408.     {
  409.         return $this->importDataExtractor;
  410.     }
  411.     public function getTranslationExporter(): ExporterInterface
  412.     {
  413.         return $this->translationExporter;
  414.     }
  415.     /**
  416.      *
  417.      * @required
  418.      */
  419.     public function setTranslationExporter(ExporterInterface $translationExporter): self
  420.     {
  421.         $this->translationExporter $translationExporter;
  422.         return $this;
  423.     }
  424.     /**
  425.      *
  426.      * @required
  427.      */
  428.     public function setImportDataExtractor(TranslationsComDataExtractor $importDataExtractor): void
  429.     {
  430.         $this->importDataExtractor $importDataExtractor;
  431.     }
  432.     public function getImporterService(): ImporterServiceInterface
  433.     {
  434.         return $this->importerService;
  435.     }
  436.     /**
  437.      * @required
  438.      */
  439.     public function setImporterService(ImporterServiceInterface $importerService): self
  440.     {
  441.         $this->importerService $importerService;
  442.         return $this;
  443.     }
  444.     public function getProjectShortCode(): string
  445.     {
  446.         return $this->projectShortCode;
  447.     }
  448.     public function setProjectShortCode(string $projectShortCode): self
  449.     {
  450.         $this->projectShortCode $projectShortCode;
  451.         return $this;
  452.     }
  453.     /**
  454.      * @param string|array $language
  455.      *
  456.      * @return mixed
  457.      */
  458.     protected function convertLanguage($language)
  459.     {
  460.         if (is_array($language)) {
  461.             $result = [];
  462.             foreach ($language as $lang) {
  463.                 $result[] = str_replace('_''-'$lang);
  464.             }
  465.             return $result;
  466.         }
  467.         return str_replace('_''-'$language);
  468.     }
  469.     /**
  470.      * @return int
  471.      */
  472.     protected function getDueDate(Job $job)
  473.     {
  474.         return $job->getDueDate() ? $job->getDueDate() : -1;
  475.     }
  476.     /**
  477.      * @return string
  478.      */
  479.     protected function getSubmissionName(Job $job)
  480.     {
  481.         if ($job->getSubmissionName()) {
  482.             $submissionName $job->getSubmissionName();
  483.         } else {
  484.             $submissionName 'Job ID ' $job->getId();
  485.         }
  486.         return $submissionName;
  487.     }
  488.     /**
  489.      * @return string
  490.      */
  491.     protected function getSubmissionFilename(Job $jobstring $suffix)
  492.     {
  493.         return 'submission_' $job->getId() . '_' $suffix '.xml';
  494.     }
  495.     /**
  496.      * @return bool whether the job was successfully cancelled.
  497.      *
  498.      * @throws \Exception
  499.      */
  500.     public function cancelJob(Job $job): bool
  501.     {
  502.         try {
  503.             $this->resetError($this->translationsJobService$job);
  504.             if (\Pimcore::inDebugMode()) {
  505.                 if ($simulatedError PimcoreTranslationsProviderInterfaceBundle::getSimulatedError()) {
  506.                     throw new \Exception($simulatedError ' (cancel)');
  507.                 }
  508.             }
  509.             $this->applicationLogger->debug('Cancel Job ' $job->getSubmissionName(), ['component' => LogContext::TRANSLATION_PROVIDER_TRANSLATIONS_COM]);
  510.             if ($job->getRemoteId()) {
  511.                 $result $this->projectDirectorClient->getGLExchange()->cancelSubmission($job->getRemoteId());
  512.             } else {
  513.                 $this->applicationLogger->info('Not remotely cancelling job ' $job->getSubmissionName() . ' since there is no remote ID.', ['component' => LogContext::TRANSLATION_PROVIDER_TRANSLATIONS_COM]);
  514.             }
  515.             $this->workflow->apply($jobJobWorkflow::TRANSITION_CANCEL);
  516.             $this->translationsJobService->persist($job);
  517.             return true;
  518.         } catch (\Exception $e) {
  519.             $this->sendErrorNotification($job$e);
  520.             $this->recordError($this->translationsJobService$job$eLogContext::TRANSLATION_PROVIDER_TRANSLATIONS_COM);
  521.             throw $e;
  522.         }
  523.     }
  524.     public function getCancelledLanguages(Job $jobstring $elementType): array
  525.     {
  526.         $result = [];
  527.         $exportData $job->getExportData() ? json_decode($job->getExportData(), true) : [];
  528.         $structuredExportData $this->structureExportData($exportData);
  529.         $resultData $job->getResultDataAsArray();
  530.         foreach ($resultData as $language => $items) {
  531.             foreach ($items as $item) {
  532.                 if ($item['state'] == JobState::CANCELLED) {
  533.                     $documentTicketId $item['documentTicket'] ?? null;
  534.                     $exportItem $structuredExportData[$elementType][$documentTicketId];
  535.                     if ($exportItem['cid']) {
  536.                         $result[$exportItem['cid']][] = $language;
  537.                     }
  538.                 }
  539.             }
  540.         }
  541.         return $result;
  542.     }
  543.     public function getReceivedLanguages(Job $jobstring $elementType): array
  544.     {
  545.         $result = [];
  546.         $exportData $job->getExportData() ? json_decode($job->getExportData(), true) : [];
  547.         $structuredExportData $this->structureExportData($exportData);
  548.         $resultData $job->getResultDataAsArray();
  549.         foreach ($resultData as $language => $items) {
  550.             foreach ($items as $item) {
  551.                 if ($item['state'] == JobState::RECEIVED) {
  552.                     $documentTicketId $item['documentTicket'] ?? null;
  553.                     if (isset($structuredExportData[$elementType][$documentTicketId])) {
  554.                         $exportItem $structuredExportData[$elementType][$documentTicketId];
  555.                         if ($exportItem['cid']) {
  556.                             $result[$exportItem['cid']][] = $language;
  557.                         }
  558.                     }
  559.                 }
  560.             }
  561.         }
  562.         return $result;
  563.     }
  564.     protected function structureExportData(array $exportData): array
  565.     {
  566.         $structuredExportData = [];
  567.         foreach ($exportData as $exportItem) {
  568.             $structuredExportData[$exportItem['ctype']][$exportItem['ticketId']] = $exportItem;
  569.         }
  570.         return $structuredExportData;
  571.     }
  572.     protected function parseAttributes(string $xml, array &$compareArraystring $language)
  573.     {
  574.         $p xml_parser_create();
  575.         $values = [];
  576.         $index = [];
  577.         xml_parse_into_struct($p$xml$values$index);
  578.         xml_parser_free($p);
  579.         $id $values[$index['ITEM'][0]]['attributes']['PK'];
  580.         $type $values[$index['ITEM'][0]]['attributes']['TYPE'];
  581.         foreach ($index['ATTRIBUTE'] as $attribute) {
  582.             $compareArray[$type][$id][$values[$attribute]['attributes']['VARIABLE']][$language] = $values[$attribute]['value'];
  583.         }
  584.     }
  585.     public function createCompareArray(?array $exportedData, ?array $importedData): array
  586.     {
  587.         $compareArray = [];
  588.         if ($exportedData) {
  589.             foreach ($exportedData as $exportItem) {
  590.                 $this->parseAttributes($exportItem['xml'], $compareArray'source');
  591.             }
  592.         }
  593.         if ($importedData) {
  594.             foreach ($importedData as $language => $importedItems) {
  595.                 foreach ($importedItems as $importedItem) {
  596.                     $this->parseAttributes($importedItem['xml'], $compareArray$language);
  597.                 }
  598.             }
  599.         }
  600.         return $compareArray;
  601.     }
  602. }