Passed
Push — master ( 484a31...46a6f1 )
by Chris
01:45
created

SparkPostCourier   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 404
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 47
eloc 156
dl 0
loc 404
ccs 134
cts 134
cp 1
rs 8.64
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A supportedContent() 0 6 1
A deliver() 0 12 2
A prepareContent() 0 24 4
A __construct() 0 5 3
A supportsContent() 0 9 3
A prepareEmail() 0 20 4
A send() 0 16 2
A buildSimpleContent() 0 32 5
C buildTemplateData() 0 45 10
A buildCcHeader() 0 5 1
A getContentHeaders() 0 17 3
A createAddress() 0 6 1
A buildInlineAttachments() 0 13 2
A buildHeaderTo() 0 5 1
A buildTemplateContent() 0 17 3
A buildAttachments() 0 13 2

How to fix   Complexity   

Complex Class

Complex classes like SparkPostCourier 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 SparkPostCourier, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Courier\Sparkpost;
6
7
use Courier\ConfirmingCourier;
8
use Courier\Exceptions\TransmissionException;
9
use Courier\Exceptions\UnsupportedContentException;
10
use Courier\SavesReceipts;
11
use PhpEmail\Address;
12
use PhpEmail\Content;
13
use PhpEmail\Email;
14
use Psr\Log\LoggerInterface;
15
use Psr\Log\NullLogger;
16
use SparkPost\SparkPost;
17
use SparkPost\SparkPostException;
18
use SparkPost\SparkPostResponse;
19
20
/**
21
 * A courier implementation using SparkPost as the third-party provider. This library uses the web API and the
22
 * php-sparkpost library to send transmissions.
23
 *
24
 * An important note is that while the SparkPost API does not support sending attachments on templated transmissions,
25
 * this API simulates the feature by creating an inline template based on the defined template using the API. In this
26
 * case, all template variables will be sent as expected, but tracking/reporting may not work as expected within
27
 * SparkPost.
28
 */
29
class SparkPostCourier implements ConfirmingCourier
30
{
31
    use SavesReceipts;
32
33
    const RECIPIENTS        = 'recipients';
34
    const CC                = 'cc';
35
    const BCC               = 'bcc';
36
    const REPLY_TO          = 'reply_to';
37
    const SUBSTITUTION_DATA = 'substitution_data';
38
39
    const CONTENT       = 'content';
40
    const FROM          = 'from';
41
    const SUBJECT       = 'subject';
42
    const HTML          = 'html';
43
    const TEXT          = 'text';
44
    const INLINE_IMAGES = 'inline_images';
45
    const ATTACHMENTS   = 'attachments';
46
    const TEMPLATE_ID   = 'template_id';
47
48
    const HEADERS   = 'headers';
49
    const CC_HEADER = 'CC';
50
51
    const ADDRESS       = 'address';
52
    const CONTACT_NAME  = 'name';
53
    const CONTACT_EMAIL = 'email';
54
    const HEADER_TO     = 'header_to';
55
56
    const ATTACHMENT_NAME = 'name';
57
    const ATTACHMENT_TYPE = 'type';
58
    const ATTACHMENT_DATA = 'data';
59
60
    /**
61
     * @var SparkPost
62
     */
63
    private $sparkPost;
64
65
    /**
66
     * @var SparkPostTemplates
67
     */
68
    private $templates;
69
70
    /**
71
     * @var LoggerInterface
72
     */
73
    private $logger;
74
75
    /**
76
     * @param SparkPost          $sparkPost
77
     * @param SparkPostTemplates $templates
78
     * @param LoggerInterface    $logger
79
     */
80 14
    public function __construct(SparkPost $sparkPost, SparkPostTemplates $templates = null, LoggerInterface $logger = null)
81
    {
82 14
        $this->sparkPost = $sparkPost;
83 14
        $this->templates = $templates ?: new SparkPostTemplates($sparkPost, $logger);
84 14
        $this->logger    = $logger ?: new NullLogger();
85
    }
86
87 14
    public function deliver(Email $email): void
88
    {
89 14
        if (!$this->supportsContent($email->getContent())) {
90 1
            throw new UnsupportedContentException($email->getContent());
91
        }
92
93 13
        $mail = $this->prepareEmail($email);
94 13
        $mail = $this->prepareContent($email, $mail);
95
96 11
        $response = $this->send($mail);
97
98 10
        $this->saveReceipt($email, $response->getBody()['results']['id']);
99
    }
100
101
    /**
102
     * @param array $mail
103
     *
104
     * @return SparkPostResponse
105
     */
106 11
    protected function send(array $mail): SparkPostResponse
107
    {
108 11
        $promise = $this->sparkPost->transmissions->post($mail);
109
110
        try {
111 11
            return $promise->wait();
112 1
        } catch (SparkPostException $e) {
113 1
            $this->logger->error(
114 1
                'Received status {code} from SparkPost with body: {body}',
115
                [
116 1
                    'code' => $e->getCode(),
117 1
                    'body' => $e->getBody(),
118
                ]
119
            );
120
121 1
            throw new TransmissionException($e->getCode(), $e);
122
        }
123
    }
124
125
    /**
126
     * @return array
127
     */
128 14
    protected function supportedContent(): array
129
    {
130
        return [
131 14
            Content\EmptyContent::class,
132
            Content\Contracts\SimpleContent::class,
133
            Content\Contracts\TemplatedContent::class,
134
        ];
135
    }
136
137
    /**
138
     * Determine if the content is supported by this courier.
139
     *
140
     * @param Content $content
141
     *
142
     * @return bool
143
     */
144 14
    protected function supportsContent(Content $content): bool
145
    {
146 14
        foreach ($this->supportedContent() as $contentType) {
147 14
            if ($content instanceof $contentType) {
148 14
                return true;
149
            }
150
        }
151
152 1
        return false;
153
    }
154
155
    /**
156
     * @param Email $email
157
     *
158
     * @return array
159
     */
160 13
    protected function prepareEmail(Email $email): array
161
    {
162 13
        $message  = [];
163 13
        $headerTo = $this->buildHeaderTo($email);
164
165 13
        $message[self::RECIPIENTS] = [];
166
167 13
        foreach ($email->getToRecipients() as $recipient) {
168 13
            $message[self::RECIPIENTS][] = $this->createAddress($recipient, $headerTo);
169
        }
170
171 13
        foreach ($email->getCcRecipients() as $recipient) {
172 5
            $message[self::RECIPIENTS][] = $this->createAddress($recipient, $headerTo);
173
        }
174
175 13
        foreach ($email->getBccRecipients() as $recipient) {
176 3
            $message[self::RECIPIENTS][] = $this->createAddress($recipient, $headerTo);
177
        }
178
179 13
        return $message;
180
    }
181
182
    /**
183
     * @param Email $email
184
     * @param array $message
185
     *
186
     * @return array
187
     */
188 13
    protected function prepareContent(Email $email, array $message): array
189
    {
190
        switch (true) {
191 13
            case $email->getContent() instanceof Content\Contracts\TemplatedContent:
192 8
                $message[self::CONTENT]           = $this->buildTemplateContent($email);
193 6
                $message[self::SUBSTITUTION_DATA] = $this->buildTemplateData($email);
194
195 6
                break;
196
197 5
            case $email->getContent() instanceof Content\EmptyContent:
198 2
                $email->setContent(new Content\SimpleContent(
199 2
                    new Content\SimpleContent\Message(''),
200 2
                    new Content\SimpleContent\Message('')
201
                ));
202
203 2
                $message[self::CONTENT] = $this->buildSimpleContent($email);
204
205 2
                break;
206
207 3
            case $email->getContent() instanceof Content\SimpleContent:
208 3
                $message[self::CONTENT] = $this->buildSimpleContent($email);
209
        }
210
211 11
        return $message;
212
    }
213
214
    /**
215
     * Attempt to create template data using the from, subject and reply to, which SparkPost considers to be
216
     * part of the templates substitutable content.
217
     *
218
     * @param Email $email
219
     *
220
     * @return array
221
     */
222 6
    protected function buildTemplateData(Email $email): array
223
    {
224
        /** @var Content\TemplatedContent $emailContent */
225 6
        $emailContent = $email->getContent();
226 6
        $templateData = $emailContent->getTemplateData();
227
228 6
        if ($email->getReplyTos()) {
229 4
            $replyTos = $email->getReplyTos();
230 4
            $first    = reset($replyTos);
231
232 4
            if (!array_key_exists('replyTo', $templateData)) {
233 4
                $templateData['replyTo'] = $first->toRfc2822();
234
            }
235
        }
236
237 6
        if (!array_key_exists('fromName', $templateData)) {
238 6
            $templateData['fromName'] = $email->getFrom()->getName();
239
        }
240
241 6
        if (!array_key_exists('fromAddress', $templateData)) {
242 6
            $templateData['fromAddress'] = $email->getFrom()->getEmail();
243
        }
244
245
        // Deprecated: fromEmail will be removed in later releases of the SparkPostCourier in favor of fromAddress
246 6
        if (!array_key_exists('fromEmail', $templateData)) {
247 6
            $templateData['fromEmail'] = explode('@', $email->getFrom()->getEmail())[0];
248
        }
249
250
        // Deprecated: fromDomain will be removed in later releases of the SparkPostCourier in favor of fromAddress
251 6
        if (!array_key_exists('fromDomain', $templateData)) {
252 6
            $templateData['fromDomain'] = explode('@', $email->getFrom()->getEmail())[1];
253
        }
254
255 6
        if (!array_key_exists('subject', $templateData)) {
256 6
            $templateData['subject'] = $email->getSubject();
257
        }
258
259
        // @TODO Remove this variable once SparkPost CC headers work properly for templates
260 6
        if (!array_key_exists('ccHeader', $templateData)) {
261 6
            if ($header = $this->buildCcHeader($email)) {
262 3
                $templateData['ccHeader'] = $header;
263
            }
264
        }
265
266 6
        return $templateData;
267
    }
268
269
    /**
270
     * @param Email $email
271
     *
272
     * @return array
273
     */
274 8
    protected function buildTemplateContent(Email $email): array
275
    {
276
        // SparkPost does not currently support templated emails with attachments, so it must be converted to a
277
        // SimpleContent message instead.
278 8
        if ($email->getAttachments()) {
279 6
            return $this->buildSimpleContent($this->templates->convertTemplatedEmail($email));
280
        }
281
282
        $content = [
283 2
            self::TEMPLATE_ID => $email->getContent()->getTemplateId(),
0 ignored issues
show
Bug introduced by
The method getTemplateId() does not exist on PhpEmail\Content. It seems like you code against a sub-type of PhpEmail\Content such as PhpEmail\Content\Contracts\TemplatedContent. ( Ignorable by Annotation )

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

283
            self::TEMPLATE_ID => $email->getContent()->/** @scrutinizer ignore-call */ getTemplateId(),
Loading history...
284
        ];
285
286 2
        if ($headers = $this->getContentHeaders($email)) {
287 1
            $content[self::HEADERS] = $headers;
288
        }
289
290 2
        return $content;
291
    }
292
293
    /**
294
     * @param Email $email
295
     *
296
     * @return array
297
     */
298 9
    protected function buildSimpleContent(Email $email): array
299
    {
300 9
        $replyTo = null;
301 9
        if (!empty($email->getReplyTos())) {
302
            // SparkPost only supports a single reply-to
303 5
            $replyTos = $email->getReplyTos();
304 5
            $first    = reset($replyTos);
305
306 5
            $replyTo = $first->toRfc2822();
307
        }
308
309
        /** @var Content\Contracts\SimpleContent $emailContent */
310 9
        $emailContent = $email->getContent();
311
312
        $content = [
313 9
            self::FROM          => [
314 9
                self::CONTACT_NAME  => $email->getFrom()->getName(),
315 9
                self::CONTACT_EMAIL => $email->getFrom()->getEmail(),
316
            ],
317 9
            self::SUBJECT       => $email->getSubject(),
318 9
            self::HTML          => $emailContent->getHtml() !== null ? $emailContent->getHtml()->getBody() : null,
319 9
            self::TEXT          => $emailContent->getText() !== null ? $emailContent->getText()->getBody() : null,
320 9
            self::ATTACHMENTS   => $this->buildAttachments($email),
321 9
            self::INLINE_IMAGES => $this->buildInlineAttachments($email),
322 9
            self::REPLY_TO      => $replyTo,
323
        ];
324
325 9
        if ($headers = $this->getContentHeaders($email)) {
326 6
            $content[self::HEADERS] = $headers;
327
        }
328
329 9
        return $content;
330
    }
331
332 11
    protected function getContentHeaders(Email $email): array
333
    {
334 11
        $headers = [];
335
336 11
        foreach ($email->getHeaders() as $header) {
337 6
            $headers[$header->getField()] = $header->getValue();
338
        }
339
340 11
        if ($ccHeader = $this->buildCcHeader($email)) {
341 5
            $headers[self::CC_HEADER] = $ccHeader;
342
        } else {
343
            // If this was set on a template in SparkPost, we will remove it, because there are no
344
            // CCs defined on the email itself.
345 6
            unset($headers[self::CC_HEADER]);
346
        }
347
348 11
        return $headers;
349
    }
350
351
    /**
352
     * @param Email $email
353
     *
354
     * @return array
355
     */
356 9
    private function buildAttachments(Email $email): array
357
    {
358 9
        $attachments = [];
359
360 9
        foreach ($email->getAttachments() as $attachment) {
361 6
            $attachments[] = [
362 6
                self::ATTACHMENT_NAME => $attachment->getName(),
363 6
                self::ATTACHMENT_TYPE => $attachment->getRfc2822ContentType(),
364 6
                self::ATTACHMENT_DATA => $attachment->getBase64Content(),
365
            ];
366
        }
367
368 9
        return $attachments;
369
    }
370
371
    /**
372
     * @param Email $email
373
     *
374
     * @return array
375
     */
376 9
    private function buildInlineAttachments(Email $email): array
377
    {
378 9
        $inlineAttachments = [];
379
380 9
        foreach ($email->getEmbedded() as $embedded) {
381 4
            $inlineAttachments[] = [
382 4
                self::ATTACHMENT_NAME => $embedded->getContentId(),
383 4
                self::ATTACHMENT_TYPE => $embedded->getRfc2822ContentType(),
384 4
                self::ATTACHMENT_DATA => $embedded->getBase64Content(),
385
            ];
386
        }
387
388 9
        return $inlineAttachments;
389
    }
390
391
    /**
392
     * @param Address $address
393
     * @param string  $headerTo
394
     *
395
     * @return array
396
     */
397 13
    private function createAddress(Address $address, string $headerTo): array
398
    {
399
        return [
400 13
            self::ADDRESS => [
401 13
                self::CONTACT_EMAIL => $address->getEmail(),
402 13
                self::HEADER_TO     => $headerTo,
403
            ],
404
        ];
405
    }
406
407
    /**
408
     * Build a string representing the header_to field of this email.
409
     *
410
     * @param Email $email
411
     *
412
     * @return string
413
     */
414
    private function buildHeaderTo(Email $email): string
415
    {
416 13
        return implode(',', array_map(function (Address $address) {
417 13
            return $address->toRfc2822();
418 13
        }, $email->getToRecipients()));
419
    }
420
421
    /**
422
     * Build a string representing the CC header for this email.
423
     *
424
     * @param Email $email
425
     *
426
     * @return string
427
     */
428
    private function buildCcHeader(Email $email): string
429
    {
430 11
        return implode(',', array_map(function (Address $address) {
431 5
            return $address->toRfc2822();
432 11
        }, $email->getCcRecipients()));
433
    }
434
}
435