| Total Complexity | 40 |
| Total Lines | 331 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like MailService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use MailService, and based on these observations, apply Extract Interface, too.
| 1 | <?php declare(strict_types=1); |
||
| 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'] === '') { |
||
|
|
|||
| 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); |
||
| 164 | $template = $data['senderName']; |
||
| 165 | $data['senderName'] = $this->templateRenderer->render($template, $templateData, $context, false); |
||
| 166 | foreach ($contents as $index => $template) { |
||
| 167 | $contents[$index] = $this->templateRenderer->render($template, $templateData, $context, $index !== 'text/plain'); |
||
| 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 |
||
| 346 | } |
||
| 347 | |||
| 348 | /** |
||
| 349 | * @param mixed[] $data |
||
| 350 | */ |
||
| 351 | private function isTestMode(array $data = []): bool |
||
| 354 | } |
||
| 355 | |||
| 356 | /** |
||
| 357 | * @param mixed[] $templateData |
||
| 358 | */ |
||
| 359 | private function templateDataContainsSalesChannel(array $templateData): bool |
||
| 362 | } |
||
| 363 | } |
||
| 364 |