Passed
Push — 6.4 ( 232ba6...48563d )
by Christian
18:34 queued 27s
created

MailService::send()   F

Complexity

Conditions 18
Paths 398

Size

Total Lines 139
Code Lines 93

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
eloc 93
nc 398
nop 3
dl 0
loc 139
rs 1.3993
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\Content\Mail\Service;
4
5
use Monolog\Logger;
6
use Psr\Log\LoggerInterface;
7
use Shopware\Core\Content\MailTemplate\Exception\SalesChannelNotFoundException;
8
use Shopware\Core\Content\MailTemplate\Service\Event\MailBeforeSentEvent;
9
use Shopware\Core\Content\MailTemplate\Service\Event\MailBeforeValidateEvent;
10
use Shopware\Core\Content\MailTemplate\Service\Event\MailErrorEvent;
11
use Shopware\Core\Content\MailTemplate\Service\Event\MailSentEvent;
12
use Shopware\Core\Content\Media\MediaCollection;
13
use Shopware\Core\Content\Media\Pathname\UrlGeneratorInterface;
14
use Shopware\Core\Framework\Adapter\Twig\StringTemplateRenderer;
15
use Shopware\Core\Framework\Context;
16
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
17
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
18
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
19
use Shopware\Core\Framework\DataAbstractionLayer\Validation\EntityExists;
20
use Shopware\Core\Framework\Log\Package;
21
use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
22
use Shopware\Core\Framework\Validation\DataValidationDefinition;
23
use Shopware\Core\Framework\Validation\DataValidator;
24
use Shopware\Core\System\SalesChannel\SalesChannelDefinition;
25
use Shopware\Core\System\SalesChannel\SalesChannelEntity;
26
use Shopware\Core\System\SystemConfig\SystemConfigService;
27
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
28
use Symfony\Component\Mime\Email;
29
use Symfony\Component\Validator\Constraints\NotBlank;
30
31
#[Package('system-settings')]
32
class MailService extends AbstractMailService
33
{
34
    private DataValidator $dataValidator;
35
36
    private StringTemplateRenderer $templateRenderer;
37
38
    private AbstractMailFactory $mailFactory;
39
40
    private EntityRepositoryInterface $mediaRepository;
41
42
    private SalesChannelDefinition $salesChannelDefinition;
43
44
    /**
45
     * @var EntityRepositoryInterface
46
     */
47
    private $salesChannelRepository;
48
49
    /**
50
     * @var SystemConfigService
51
     */
52
    private $systemConfigService;
53
54
    private EventDispatcherInterface $eventDispatcher;
55
56
    private UrlGeneratorInterface $urlGenerator;
57
58
    private AbstractMailSender $mailSender;
59
60
    private LoggerInterface $logger;
61
62
    /**
63
     * @internal
64
     */
65
    public function __construct(
66
        DataValidator $dataValidator,
67
        StringTemplateRenderer $templateRenderer,
68
        AbstractMailFactory $mailFactory,
69
        AbstractMailSender $emailSender,
70
        EntityRepositoryInterface $mediaRepository,
71
        SalesChannelDefinition $salesChannelDefinition,
72
        EntityRepositoryInterface $salesChannelRepository,
73
        SystemConfigService $systemConfigService,
74
        EventDispatcherInterface $eventDispatcher,
75
        UrlGeneratorInterface $urlGenerator,
76
        LoggerInterface $logger
77
    ) {
78
        $this->dataValidator = $dataValidator;
79
        $this->templateRenderer = $templateRenderer;
80
        $this->mailFactory = $mailFactory;
81
        $this->mailSender = $emailSender;
82
        $this->mediaRepository = $mediaRepository;
83
        $this->salesChannelDefinition = $salesChannelDefinition;
84
        $this->salesChannelRepository = $salesChannelRepository;
85
        $this->systemConfigService = $systemConfigService;
86
        $this->eventDispatcher = $eventDispatcher;
87
        $this->urlGenerator = $urlGenerator;
88
        $this->logger = $logger;
89
    }
90
91
    public function getDecorated(): AbstractMailService
92
    {
93
        throw new DecorationPatternException(self::class);
94
    }
95
96
    /**
97
     * @param mixed[] $data
98
     * @param mixed[] $templateData
99
     */
100
    public function send(array $data, Context $context, array $templateData = []): ?Email
101
    {
102
        $event = new MailBeforeValidateEvent($data, $context, $templateData);
103
        $this->eventDispatcher->dispatch($event);
104
        $data = $event->getData();
105
        $templateData = $event->getTemplateData();
106
107
        if ($event->isPropagationStopped()) {
108
            return null;
109
        }
110
111
        $definition = $this->getValidationDefinition($context);
112
        $this->dataValidator->validate($data, $definition);
113
114
        $recipients = $data['recipients'];
115
        $salesChannelId = $data['salesChannelId'];
116
        $salesChannel = null;
117
118
        if (($salesChannelId !== null && !isset($templateData['salesChannel'])) || $this->isTestMode($data)) {
119
            $criteria = $this->getSalesChannelDomainCriteria($salesChannelId, $context);
120
121
            /** @var SalesChannelEntity|null $salesChannel */
122
            $salesChannel = $this->salesChannelRepository->search($criteria, $context)->get($salesChannelId);
123
124
            if ($salesChannel === null) {
125
                throw new SalesChannelNotFoundException($salesChannelId);
126
            }
127
128
            $templateData['salesChannel'] = $salesChannel;
129
        } elseif ($this->templateDataContainsSalesChannel($templateData)) {
130
            $salesChannel = $templateData['salesChannel'];
131
        }
132
133
        $senderEmail = $data['senderMail'] ?? $this->getSender($data, $salesChannelId, $context);
134
135
        if ($senderEmail === null) {
136
            $event = new MailErrorEvent(
137
                $context,
138
                Logger::ERROR,
139
                null,
140
                'senderMail not configured for salesChannel: ' . $salesChannelId . '. Please check system_config \'core.basicInformation.email\'',
141
                null,
142
                $templateData
143
            );
144
145
            $this->eventDispatcher->dispatch($event);
146
            $this->logger->error(
147
                'senderMail not configured for salesChannel: ' . $salesChannelId . '. Please check system_config \'core.basicInformation.email\'',
148
                $templateData
149
            );
150
        }
151
152
        $contents = $this->buildContents($data, $salesChannel);
153
        if ($this->isTestMode($data)) {
154
            $this->templateRenderer->enableTestMode();
155
            if (!isset($templateData['order']) && !isset($templateData['order']['deepLinkCode']) || $templateData['order']['deepLinkCode'] === '') {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (! IssetNode && ! IssetN...['deepLinkCode'] === '', Probably Intended Meaning: ! IssetNode && (! IssetN...'deepLinkCode'] === '')
Loading history...
156
                $templateData['order']['deepLinkCode'] = 'home';
157
            }
158
        }
159
160
        $template = $data['subject'];
161
162
        try {
163
            $data['subject'] = $this->templateRenderer->render($template, $templateData, $context, false);
0 ignored issues
show
Deprecated Code introduced by
The function Shopware\Core\Framework\...plateRenderer::render() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

163
            $data['subject'] = /** @scrutinizer ignore-deprecated */ $this->templateRenderer->render($template, $templateData, $context, false);
Loading history...
164
            $template = $data['senderName'];
165
            $data['senderName'] = $this->templateRenderer->render($template, $templateData, $context, false);
0 ignored issues
show
Deprecated Code introduced by
The function Shopware\Core\Framework\...plateRenderer::render() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

165
            $data['senderName'] = /** @scrutinizer ignore-deprecated */ $this->templateRenderer->render($template, $templateData, $context, false);
Loading history...
166
            foreach ($contents as $index => $template) {
167
                $contents[$index] = $this->templateRenderer->render($template, $templateData, $context, $index !== 'text/plain');
0 ignored issues
show
Deprecated Code introduced by
The function Shopware\Core\Framework\...plateRenderer::render() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

167
                $contents[$index] = /** @scrutinizer ignore-deprecated */ $this->templateRenderer->render($template, $templateData, $context, $index !== 'text/plain');
Loading history...
168
            }
169
        } catch (\Throwable $e) {
170
            $event = new MailErrorEvent(
171
                $context,
172
                Logger::ERROR,
173
                $e,
174
                'Could not render Mail-Template with error message: ' . $e->getMessage(),
175
                $template,
176
                $templateData
177
            );
178
            $this->eventDispatcher->dispatch($event);
179
            $this->logger->error(
180
                'Could not render Mail-Template with error message: ' . $e->getMessage(),
181
                array_merge([
182
                    'template' => $template,
183
                    'exception' => (string) $e,
184
                ], $templateData)
185
            );
186
187
            return null;
188
        }
189
        if (isset($data['testMode']) && (bool) $data['testMode'] === true) {
190
            $this->templateRenderer->disableTestMode();
191
        }
192
193
        $mediaUrls = $this->getMediaUrls($data, $context);
194
195
        $binAttachments = $data['binAttachments'] ?? null;
196
197
        $mail = $this->mailFactory->create(
198
            $data['subject'],
199
            [$senderEmail => $data['senderName']],
200
            $recipients,
201
            $contents,
202
            $mediaUrls,
203
            $data,
204
            $binAttachments
205
        );
206
207
        if ($mail->getBody()->toString() === '') {
208
            $event = new MailErrorEvent(
209
                $context,
210
                Logger::ERROR,
211
                null,
212
                'mail body is null',
213
                null,
214
                $templateData
215
            );
216
217
            $this->eventDispatcher->dispatch($event);
218
            $this->logger->error(
219
                'mail body is null',
220
                $templateData
221
            );
222
223
            return null;
224
        }
225
226
        $event = new MailBeforeSentEvent($data, $mail, $context, $templateData['eventName'] ?? null);
227
        $this->eventDispatcher->dispatch($event);
228
229
        if ($event->isPropagationStopped()) {
230
            return null;
231
        }
232
233
        $this->mailSender->send($mail);
234
235
        $event = new MailSentEvent($data['subject'], $recipients, $contents, $context, $templateData['eventName'] ?? null);
236
        $this->eventDispatcher->dispatch($event);
237
238
        return $mail;
239
    }
240
241
    /**
242
     * @param mixed[] $data
243
     */
244
    private function getSender(array $data, ?string $salesChannelId, Context $context): ?string
245
    {
246
        $senderEmail = $data['senderEmail'] ?? null;
247
248
        if ($senderEmail === null || trim($senderEmail) === '') {
249
            $senderEmail = $this->systemConfigService->get('core.basicInformation.email', $salesChannelId);
250
        }
251
252
        if ($senderEmail === null || trim($senderEmail) === '') {
253
            $senderEmail = $this->systemConfigService->get('core.mailerSettings.senderAddress', $salesChannelId);
254
        }
255
256
        if ($senderEmail === null || trim($senderEmail) === '') {
257
            return null;
258
        }
259
260
        return $senderEmail;
261
    }
262
263
    /**
264
     * Attaches header and footer to given email bodies
265
     *
266
     * @param mixed[] $data
267
     * e.g. ['contentHtml' => 'foobar', 'contentPlain' => '<h1>foobar</h1>']
268
     *
269
     * @return mixed[]
270
     * e.g. ['text/plain' => '{{foobar}}', 'text/html' => '<h1>{{foobar}}</h1>']
271
     *
272
     * @internal
273
     */
274
    private function buildContents(array $data, ?SalesChannelEntity $salesChannel): array
275
    {
276
        if ($salesChannel && $mailHeaderFooter = $salesChannel->getMailHeaderFooter()) {
277
            $headerPlain = $mailHeaderFooter->getTranslation('headerPlain') ?? '';
278
            $footerPlain = $mailHeaderFooter->getTranslation('footerPlain') ?? '';
279
            $headerHtml = $mailHeaderFooter->getTranslation('headerHtml') ?? '';
280
            $footerHtml = $mailHeaderFooter->getTranslation('footerHtml') ?? '';
281
282
            return [
283
                'text/plain' => sprintf('%s%s%s', $headerPlain, $data['contentPlain'], $footerPlain),
284
                'text/html' => sprintf('%s%s%s', $headerHtml, $data['contentHtml'], $footerHtml),
285
            ];
286
        }
287
288
        return [
289
            'text/html' => $data['contentHtml'],
290
            'text/plain' => $data['contentPlain'],
291
        ];
292
    }
293
294
    private function getValidationDefinition(Context $context): DataValidationDefinition
295
    {
296
        $definition = new DataValidationDefinition('mail_service.send');
297
298
        $definition->add('recipients', new NotBlank());
299
        $definition->add('salesChannelId', new EntityExists(['entity' => $this->salesChannelDefinition->getEntityName(), 'context' => $context]));
300
        $definition->add('contentHtml', new NotBlank());
301
        $definition->add('contentPlain', new NotBlank());
302
        $definition->add('subject', new NotBlank());
303
        $definition->add('senderName', new NotBlank());
304
305
        return $definition;
306
    }
307
308
    /**
309
     * @param mixed[] $data
310
     *
311
     * @return string[]
312
     */
313
    private function getMediaUrls(array $data, Context $context): array
314
    {
315
        if (!isset($data['mediaIds']) || empty($data['mediaIds'])) {
316
            return [];
317
        }
318
        $criteria = new Criteria($data['mediaIds']);
319
        $criteria->setTitle('mail-service::resolve-media-ids');
320
        $media = null;
321
        $mediaRepository = $this->mediaRepository;
322
        $context->scope(Context::SYSTEM_SCOPE, static function (Context $context) use ($criteria, $mediaRepository, &$media): void {
323
            /** @var MediaCollection $media */
324
            $media = $mediaRepository->search($criteria, $context)->getElements();
325
        });
326
327
        $urls = [];
328
        foreach ($media ?? [] as $mediaItem) {
329
            $urls[] = $this->urlGenerator->getRelativeMediaUrl($mediaItem);
330
        }
331
332
        return $urls;
333
    }
334
335
    private function getSalesChannelDomainCriteria(string $salesChannelId, Context $context): Criteria
336
    {
337
        $criteria = new Criteria([$salesChannelId]);
338
        $criteria->setTitle('mail-service::resolve-sales-channel-domain');
339
        $criteria->addAssociation('mailHeaderFooter');
340
        $criteria->getAssociation('domains')
341
            ->addFilter(
342
                new EqualsFilter('languageId', $context->getLanguageId())
343
            );
344
345
        return $criteria;
346
    }
347
348
    /**
349
     * @param mixed[] $data
350
     */
351
    private function isTestMode(array $data = []): bool
352
    {
353
        return isset($data['testMode']) && (bool) $data['testMode'] === true;
354
    }
355
356
    /**
357
     * @param mixed[] $templateData
358
     */
359
    private function templateDataContainsSalesChannel(array $templateData): bool
360
    {
361
        return isset($templateData['salesChannel']) && $templateData['salesChannel'] instanceof SalesChannelEntity;
362
    }
363
}
364