custom/plugins/FourtwosixThemeExtension/src/Decorator/Core/Content/Flow/Dispatching/Action/SendMailActionDecorator.php line 118

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace FourtwosixThemeExtension\Decorator\Core\Content\Flow\Dispatching\Action;
  4. use Shopware\Core\Content\Flow\Dispatching\Action\SendMailAction as Decorated;
  5. use Doctrine\DBAL\Connection;
  6. use Psr\Log\LoggerInterface;
  7. use Shopware\Core\Checkout\Document\DocumentCollection;
  8. use Shopware\Core\Checkout\Document\DocumentService;
  9. use Shopware\Core\Checkout\Document\Service\DocumentGenerator;
  10. use Shopware\Core\Content\ContactForm\Event\ContactFormEvent;
  11. use Shopware\Core\Content\Flow\Dispatching\StorableFlow;
  12. use Shopware\Core\Content\Flow\Events\FlowSendMailActionEvent;
  13. use Shopware\Core\Content\Mail\Service\AbstractMailService;
  14. use Shopware\Core\Content\MailTemplate\Exception\MailEventConfigurationException;
  15. use Shopware\Core\Content\MailTemplate\Exception\SalesChannelNotFoundException;
  16. use Shopware\Core\Content\MailTemplate\MailTemplateEntity;
  17. use Shopware\Core\Content\MailTemplate\Subscriber\MailSendSubscriberConfig;
  18. use Shopware\Core\Content\Media\MediaCollection;
  19. use Shopware\Core\Content\Media\MediaEntity;
  20. use Shopware\Core\Content\Media\MediaService;
  21. use Shopware\Core\Defaults;
  22. use Shopware\Core\Framework\Adapter\Translation\Translator;
  23. use Shopware\Core\Framework\Context;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
  25. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  26. use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException;
  27. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  28. use Shopware\Core\Framework\Event\FlowEvent;
  29. use Shopware\Core\Framework\Event\MailAware;
  30. use Shopware\Core\Framework\Event\OrderAware;
  31. use Shopware\Core\Framework\Feature;
  32. use Shopware\Core\Framework\Uuid\Uuid;
  33. use Shopware\Core\Framework\Validation\DataBag\DataBag;
  34. use Shopware\Core\System\Locale\LanguageLocaleCodeProvider;
  35. use Symfony\Contracts\EventDispatcher\Event;
  36. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  37. class SendMailActionDecorator extends Decorated
  38. {
  39.     private const RECIPIENT_CONFIG_ADMIN 'admin';
  40.     private const RECIPIENT_CONFIG_CUSTOM 'custom';
  41.     private const RECIPIENT_CONFIG_CONTACT_FORM_MAIL 'contactFormMail';
  42.     private EntityRepositoryInterface $mailTemplateRepository;
  43.     private MediaService $mediaService;
  44.     private EntityRepositoryInterface $mediaRepository;
  45.     private EntityRepositoryInterface $documentRepository;
  46.     private LoggerInterface $logger;
  47.     private AbstractMailService $emailService;
  48.     private EventDispatcherInterface $eventDispatcher;
  49.     private EntityRepositoryInterface $mailTemplateTypeRepository;
  50.     private Translator $translator;
  51.     private Connection $connection;
  52.     private LanguageLocaleCodeProvider $languageLocaleProvider;
  53.     private bool $updateMailTemplate;
  54.     private DocumentGenerator $documentGenerator;
  55.     private DocumentService $documentService;
  56.     public function __construct(
  57.         AbstractMailService $emailService,
  58.         EntityRepositoryInterface $mailTemplateRepository,
  59.         MediaService $mediaService,
  60.         EntityRepositoryInterface $mediaRepository,
  61.         EntityRepositoryInterface $documentRepository,
  62.         DocumentService $documentService,
  63.         DocumentGenerator $documentGenerator,
  64.         LoggerInterface $logger,
  65.         EventDispatcherInterface $eventDispatcher,
  66.         EntityRepositoryInterface $mailTemplateTypeRepository,
  67.         Translator $translator,
  68.         Connection $connection,
  69.         LanguageLocaleCodeProvider $languageLocaleProvider,
  70.         bool $updateMailTemplate
  71.     ) {
  72.         $this->mailTemplateRepository $mailTemplateRepository;
  73.         $this->mediaService $mediaService;
  74.         $this->mediaRepository $mediaRepository;
  75.         $this->documentRepository $documentRepository;
  76.         $this->logger $logger;
  77.         $this->emailService $emailService;
  78.         $this->eventDispatcher $eventDispatcher;
  79.         $this->mailTemplateTypeRepository $mailTemplateTypeRepository;
  80.         $this->translator $translator;
  81.         $this->connection $connection;
  82.         $this->languageLocaleProvider $languageLocaleProvider;
  83.         $this->updateMailTemplate $updateMailTemplate;
  84.         $this->documentGenerator $documentGenerator;
  85.         $this->documentService $documentService;
  86.         parent::__construct(
  87.             $emailService,
  88.             $mailTemplateRepository,
  89.             $mediaService,
  90.             $mediaRepository,
  91.             $documentRepository,
  92.             $documentService,
  93.             $documentGenerator,
  94.             $logger,
  95.             $eventDispatcher,
  96.             $mailTemplateTypeRepository,
  97.             $translator,
  98.             $connection,
  99.             $languageLocaleProvider,
  100.             $updateMailTemplate
  101.         );
  102.     }
  103.     /**
  104.      * @deprecated tag:v6.5.0 Will be removed, implement handleFlow instead
  105.      *
  106.      * @throws MailEventConfigurationException
  107.      * @throws SalesChannelNotFoundException
  108.      * @throws InconsistentCriteriaIdsException
  109.      */
  110.     public function handle(Event $event): void
  111.     {
  112.         Feature::triggerDeprecationOrThrow(
  113.             'v6.5.0.0',
  114.             Feature::deprecatedMethodMessage(__CLASS____METHOD__'v6.5.0.0')
  115.         );
  116.         if (!$event instanceof FlowEvent) {
  117.             return;
  118.         }
  119.         $mailEvent $event->getEvent();
  120.         $extension $event->getContext()->getExtension(self::MAIL_CONFIG_EXTENSION);
  121.         if (!$extension instanceof MailSendSubscriberConfig) {
  122.             $extension = new MailSendSubscriberConfig(false, [], []);
  123.         }
  124.         if ($extension->skip()) {
  125.             return;
  126.         }
  127.         if (!$mailEvent instanceof MailAware) {
  128.             throw new MailEventConfigurationException('Not an instance of MailAware'\get_class($mailEvent));
  129.         }
  130.         $eventConfig $event->getConfig();
  131.         if (empty($eventConfig['recipient'])) {
  132.             throw new MailEventConfigurationException('The recipient value in the flow action configuration is missing.'\get_class($mailEvent));
  133.         }
  134.         if (!isset($eventConfig['mailTemplateId'])) {
  135.             return;
  136.         }
  137.         $mailTemplate $this->getMailTemplate($eventConfig['mailTemplateId'], $event->getContext());
  138.         if ($mailTemplate === null) {
  139.             return;
  140.         }
  141.         $injectedTranslator $this->injectTranslator($mailEvent->getContext(), $mailEvent->getSalesChannelId());
  142.         $data = new DataBag();
  143.         $contactFormData = [];
  144.         if ($mailEvent instanceof ContactFormEvent) {
  145.             $contactFormData $mailEvent->getContactFormData();
  146.             $data->set("replyTo"$contactFormData["email"]);
  147.         }
  148.         $recipients $this->getRecipients($eventConfig['recipient'], $mailEvent->getMailStruct()->getRecipients(), $contactFormData);
  149.         if (empty($recipients)) {
  150.             return;
  151.         }
  152.         $data->set('recipients'$recipients);
  153.         $data->set('senderName'$mailTemplate->getTranslation('senderName'));
  154.         $data->set('salesChannelId'$mailEvent->getSalesChannelId());
  155.         $data->set('templateId'$mailTemplate->getId());
  156.         $data->set('customFields'$mailTemplate->getCustomFields());
  157.         $data->set('contentHtml'$mailTemplate->getTranslation('contentHtml'));
  158.         $data->set('contentPlain'$mailTemplate->getTranslation('contentPlain'));
  159.         $data->set('subject'$mailTemplate->getTranslation('subject'));
  160.         $data->set('mediaIds', []);
  161.         $attachments array_unique($this->buildAttachments(
  162.             $event->getContext(),
  163.             $mailTemplate,
  164.             $extension,
  165.             $eventConfig,
  166.             $mailEvent instanceof OrderAware $mailEvent->getOrderId() : null
  167.         ), \SORT_REGULAR);
  168.         if (!empty($attachments)) {
  169.             $data->set('binAttachments'$attachments);
  170.         }
  171.         $this->eventDispatcher->dispatch(new FlowSendMailActionEvent($data$mailTemplate$event));
  172.         if ($data->has('templateId')) {
  173.             $this->updateMailTemplateType(
  174.                 $event->getContext(),
  175.                 $event,
  176.                 $this->getTemplateData($mailEvent),
  177.                 $mailTemplate
  178.             );
  179.         }
  180.         $templateData array_merge([
  181.             'eventName' => $mailEvent->getName(),
  182.         ], $this->getTemplateData($mailEvent));
  183.         $this->send($data$event->getContext(), $templateData$attachments$extension$injectedTranslator);
  184.     }
  185.     /**
  186.      * @param array<string, mixed> $templateData
  187.      * @param array<mixed, mixed> $attachments
  188.      */
  189.     private function send(DataBag $dataContext $context, array $templateData, array $attachmentsMailSendSubscriberConfig $extensionbool $injectedTranslator): void
  190.     {
  191.         try {
  192.             $this->emailService->send(
  193.                 $data->all(),
  194.                 $context,
  195.                 $templateData
  196.             );
  197.             $documentAttachments array_filter($attachments, function (array $attachment) use ($extension) {
  198.                 return \array_key_exists('id'$attachment) && \in_array($attachment['id'], $extension->getDocumentIds(), true);
  199.             });
  200.             $documentAttachments array_column($documentAttachments'id');
  201.             if (!empty($documentAttachments)) {
  202.                 $this->connection->executeStatement(
  203.                     'UPDATE `document` SET `updated_at` = :now, `sent` = 1 WHERE `id` IN (:ids)',
  204.                     ['ids' => Uuid::fromHexToBytesList($documentAttachments), 'now' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT)],
  205.                     ['ids' => Connection::PARAM_STR_ARRAY]
  206.                 );
  207.             }
  208.         } catch (\Exception $e) {
  209.             $this->logger->error(
  210.                 "Could not send mail:\n"
  211.                     $e->getMessage() . "\n"
  212.                     'Error Code:' $e->getCode() . "\n"
  213.                     "Template data: \n"
  214.                     json_encode($data->all()) . "\n"
  215.             );
  216.         }
  217.         if ($injectedTranslator) {
  218.             $this->translator->resetInjection();
  219.         }
  220.     }
  221.     /**
  222.      * @param FlowEvent|StorableFlow $event
  223.      * @param array<string, mixed> $templateData
  224.      */
  225.     private function updateMailTemplateType(
  226.         Context $context,
  227.         $event,
  228.         array $templateData,
  229.         MailTemplateEntity $mailTemplate
  230.     ): void {
  231.         if (!$mailTemplate->getMailTemplateTypeId()) {
  232.             return;
  233.         }
  234.         if (!$this->updateMailTemplate) {
  235.             return;
  236.         }
  237.         $mailTemplateTypeTranslation $this->connection->fetchOne(
  238.             'SELECT 1 FROM mail_template_type_translation WHERE language_id = :languageId AND mail_template_type_id =:mailTemplateTypeId',
  239.             [
  240.                 'languageId' => Uuid::fromHexToBytes($context->getLanguageId()),
  241.                 'mailTemplateTypeId' => Uuid::fromHexToBytes($mailTemplate->getMailTemplateTypeId()),
  242.             ]
  243.         );
  244.         if (!$mailTemplateTypeTranslation) {
  245.             // Don't throw errors if this fails // Fix with NEXT-15475
  246.             $this->logger->error(
  247.                 "Could not update mail template type, because translation for this language does not exits:\n"
  248.                     'Flow id: ' $event->getFlowState()->flowId "\n"
  249.                     'Sequence id: ' $event->getFlowState()->getSequenceId()
  250.             );
  251.             return;
  252.         }
  253.         $this->mailTemplateTypeRepository->update([[
  254.             'id' => $mailTemplate->getMailTemplateTypeId(),
  255.             'templateData' => $templateData,
  256.         ]], $context);
  257.     }
  258.     private function getMailTemplate(string $idContext $context): ?MailTemplateEntity
  259.     {
  260.         $criteria = new Criteria([$id]);
  261.         $criteria->setTitle('send-mail::load-mail-template');
  262.         $criteria->addAssociation('media.media');
  263.         $criteria->setLimit(1);
  264.         return $this->mailTemplateRepository
  265.             ->search($criteria$context)
  266.             ->first();
  267.     }
  268.     /**
  269.      * @throws MailEventConfigurationException
  270.      *
  271.      * @return array<string, mixed>
  272.      */
  273.     private function getTemplateData(MailAware $event): array
  274.     {
  275.         $data = [];
  276.         foreach (array_keys($event::getAvailableData()->toArray()) as $key) {
  277.             $getter 'get' ucfirst($key);
  278.             if (!method_exists($event$getter)) {
  279.                 throw new MailEventConfigurationException('Data for ' $key ' not available.'\get_class($event));
  280.             }
  281.             $data[$key] = $event->$getter();
  282.         }
  283.         return $data;
  284.     }
  285.     /**
  286.      * @param array<string, mixed> $eventConfig
  287.      *
  288.      * @return array<mixed, mixed>
  289.      */
  290.     private function buildAttachments(
  291.         Context $context,
  292.         MailTemplateEntity $mailTemplate,
  293.         MailSendSubscriberConfig $extensions,
  294.         array $eventConfig,
  295.         ?string $orderId
  296.     ): array {
  297.         $attachments = [];
  298.         if ($mailTemplate->getMedia() !== null) {
  299.             foreach ($mailTemplate->getMedia() as $mailTemplateMedia) {
  300.                 if ($mailTemplateMedia->getMedia() === null) {
  301.                     continue;
  302.                 }
  303.                 if ($mailTemplateMedia->getLanguageId() !== null && $mailTemplateMedia->getLanguageId() !== $context->getLanguageId()) {
  304.                     continue;
  305.                 }
  306.                 $attachments[] = $this->mediaService->getAttachment(
  307.                     $mailTemplateMedia->getMedia(),
  308.                     $context
  309.                 );
  310.             }
  311.         }
  312.         if (!empty($extensions->getMediaIds())) {
  313.             $criteria = new Criteria($extensions->getMediaIds());
  314.             $criteria->setTitle('send-mail::load-media');
  315.             /** @var MediaCollection<MediaEntity> $entities */
  316.             $entities $this->mediaRepository->search($criteria$context);
  317.             foreach ($entities as $media) {
  318.                 $attachments[] = $this->mediaService->getAttachment($media$context);
  319.             }
  320.         }
  321.         $documentIds $extensions->getDocumentIds();
  322.         if (!empty($eventConfig['documentTypeIds']) && \is_array($eventConfig['documentTypeIds']) && $orderId) {
  323.             $latestDocuments $this->getLatestDocumentsOfTypes($orderId$eventConfig['documentTypeIds']);
  324.             $documentIds array_unique(array_merge($documentIds$latestDocuments));
  325.         }
  326.         if (!empty($documentIds)) {
  327.             $extensions->setDocumentIds($documentIds);
  328.             if (Feature::isActive('v6.5.0.0')) {
  329.                 $attachments $this->mappingAttachments($documentIds$attachments$context);
  330.             } else {
  331.                 $attachments $this->buildOrderAttachments($documentIds$attachments$context);
  332.             }
  333.         }
  334.         return $attachments;
  335.     }
  336.     private function injectTranslator(Context $context, ?string $salesChannelId): bool
  337.     {
  338.         if ($salesChannelId === null) {
  339.             return false;
  340.         }
  341.         if ($this->translator->getSnippetSetId() !== null) {
  342.             return false;
  343.         }
  344.         $this->translator->injectSettings(
  345.             $salesChannelId,
  346.             $context->getLanguageId(),
  347.             $this->languageLocaleProvider->getLocaleForLanguageId($context->getLanguageId()),
  348.             $context
  349.         );
  350.         return true;
  351.     }
  352.     /**
  353.      * @param array<string, mixed> $recipients
  354.      * @param array<string, mixed> $mailStructRecipients
  355.      * @param array<int|string, mixed> $contactFormData
  356.      *
  357.      * @return array<int|string, string>
  358.      */
  359.     private function getRecipients(array $recipients, array $mailStructRecipients, array $contactFormData): array
  360.     {
  361.         switch ($recipients['type']) {
  362.             case self::RECIPIENT_CONFIG_CUSTOM:
  363.                 return $recipients['data'];
  364.             case self::RECIPIENT_CONFIG_ADMIN:
  365.                 $admins $this->connection->fetchAllAssociative(
  366.                     'SELECT first_name, last_name, email FROM user WHERE admin = true'
  367.                 );
  368.                 $emails = [];
  369.                 foreach ($admins as $admin) {
  370.                     $emails[$admin['email']] = $admin['first_name'] . ' ' $admin['last_name'];
  371.                 }
  372.                 return $emails;
  373.             case self::RECIPIENT_CONFIG_CONTACT_FORM_MAIL:
  374.                 if (empty($contactFormData)) {
  375.                     return [];
  376.                 }
  377.                 if (!\array_key_exists('email'$contactFormData)) {
  378.                     return [];
  379.                 }
  380.                 return [$contactFormData['email'] => ($contactFormData['firstName'] ?? '') . ' ' . ($contactFormData['lastName'] ?? '')];
  381.             default:
  382.                 return $mailStructRecipients;
  383.         }
  384.     }
  385.     /**
  386.      * @param array<string> $documentIds
  387.      * @param array<mixed, mixed> $attachments
  388.      *
  389.      * @return array<mixed, mixed>
  390.      */
  391.     private function buildOrderAttachments(array $documentIds, array $attachmentsContext $context): array
  392.     {
  393.         $criteria = new Criteria($documentIds);
  394.         $criteria->setTitle('send-mail::load-attachments');
  395.         $criteria->addAssociation('documentMediaFile');
  396.         $criteria->addAssociation('documentType');
  397.         /** @var DocumentCollection $documents */
  398.         $documents $this->documentRepository->search($criteria$context)->getEntities();
  399.         return $this->mappingAttachmentsInfo($documents$attachments$context);
  400.     }
  401.     /**
  402.      * @param array<string> $documentTypeIds
  403.      *
  404.      * @return array<string>
  405.      */
  406.     private function getLatestDocumentsOfTypes(string $orderId, array $documentTypeIds): array
  407.     {
  408.         $documents $this->connection->fetchAllAssociative(
  409.             'SELECT
  410.                 LOWER(hex(`document`.`document_type_id`)) as doc_type,
  411.                 LOWER(hex(`document`.`id`)) as doc_id,
  412.                 `document`.`created_at` as newest_date
  413.             FROM
  414.                 `document`
  415.             WHERE
  416.                 HEX(`document`.`order_id`) = :orderId
  417.                 AND HEX(`document`.`document_type_id`) IN (:documentTypeIds)
  418.             ORDER BY `document`.`created_at` DESC',
  419.             [
  420.                 'orderId' => $orderId,
  421.                 'documentTypeIds' => $documentTypeIds,
  422.             ],
  423.             [
  424.                 'documentTypeIds' => Connection::PARAM_STR_ARRAY,
  425.             ]
  426.         );
  427.         $documentsGroupByType FetchModeHelper::group($documents);
  428.         $documentIds = [];
  429.         foreach ($documentsGroupByType as $document) {
  430.             $documentIds[] = array_shift($document)['doc_id'];
  431.         }
  432.         return $documentIds;
  433.     }
  434.     /**
  435.      * @param array<mixed, mixed> $attachments
  436.      *
  437.      * @return array<mixed, mixed>
  438.      */
  439.     private function mappingAttachmentsInfo(DocumentCollection $documents, array $attachmentsContext $context): array
  440.     {
  441.         foreach ($documents as $document) {
  442.             $documentId $document->getId();
  443.             $document $this->documentService->getDocument($document$context);
  444.             $attachments[] = [
  445.                 'id' => $documentId,
  446.                 'content' => $document->getFileBlob(),
  447.                 'fileName' => $document->getFilename(),
  448.                 'mimeType' => $document->getContentType(),
  449.             ];
  450.         }
  451.         return $attachments;
  452.     }
  453.     /**
  454.      * @param array<string> $documentIds
  455.      * @param array<mixed, mixed> $attachments
  456.      *
  457.      * @return array<mixed, mixed>
  458.      */
  459.     private function mappingAttachments(array $documentIds, array $attachmentsContext $context): array
  460.     {
  461.         foreach ($documentIds as $documentId) {
  462.             $document $this->documentGenerator->readDocument($documentId$context);
  463.             if ($document === null) {
  464.                 continue;
  465.             }
  466.             $attachments[] = [
  467.                 'id' => $documentId,
  468.                 'content' => $document->getContent(),
  469.                 'fileName' => $document->getName(),
  470.                 'mimeType' => $document->getContentType(),
  471.             ];
  472.         }
  473.         return $attachments;
  474.     }
  475. }