Completed
Pull Request — master (#66)
by Shingo
01:49 queued 28s
created

SendgridTransport   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 282
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 48
c 3
b 0
f 0
lcom 1
cbo 7
dl 0
loc 282
rs 8.4864

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
C send() 0 41 7
B getPersonalizations() 0 27 5
A getFrom() 0 9 3
A getReplyTo() 0 9 3
B getContents() 0 37 6
B getAttachments() 0 20 6
C setParameters() 0 47 12
A setPersonalizations() 0 13 4
A post() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like SendgridTransport 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 SendgridTransport, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Sichikawa\LaravelSendgridDriver\Transport;
3
4
use GuzzleHttp\Client;
5
use GuzzleHttp\ClientInterface;
6
use GuzzleHttp\Psr7\Response;
7
use Illuminate\Mail\Transport\Transport;
8
use Swift_Attachment;
9
use Swift_Image;
10
use Swift_Mime_SimpleMessage;
11
use Swift_MimePart;
12
13
class SendgridTransport extends Transport
14
{
15
    const MAXIMUM_FILE_SIZE = 7340032;
16
    const SMTP_API_NAME = 'sendgrid/x-smtpapi';
17
    const BASE_URL = 'https://api.sendgrid.com/v3/mail/send';
18
19
    /**
20
     * @var Client
21
     */
22
    private $client;
23
    private $options;
24
    private $attachments;
25
    private $numberOfRecipients;
26
27
    public function __construct(ClientInterface $client, $api_key)
28
    {
29
        $this->client = $client;
0 ignored issues
show
Documentation Bug introduced by
$client is of type object<GuzzleHttp\ClientInterface>, but the property $client was declared to be of type object<GuzzleHttp\Client>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
30
        $this->options = [
31
            'headers' => [
32
                'Authorization' => 'Bearer ' . $api_key,
33
                'Content-Type'  => 'application/json',
34
            ],
35
        ];
36
    }
37
38
    /**
39
     * {@inheritdoc}
40
     */
41
    public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
42
    {
43
        $this->beforeSendPerformed($message);
44
45
        $payload = $this->options;
46
47
        $data = [
48
            'personalizations' => $this->getPersonalizations($message),
49
            'from'             => $this->getFrom($message),
50
            'subject'          => $message->getSubject(),
51
            'content'          => $this->getContents($message),
52
        ];
53
54
        if ($reply_to = $this->getReplyTo($message)) {
55
            $data['reply_to'] = $reply_to;
56
        }
57
58
        $attachments = $this->getAttachments($message);
59
        if (count($attachments) > 0) {
60
            $data['attachments'] = $attachments;
61
        }
62
63
        $data = $this->setParameters($message, $data);
64
65
        $payload['json'] = $data;
66
67
        $response = $this->post($payload);
68
69
        if (method_exists($response, 'getHeaderLine')) {
70
            $message->getHeaders()->addTextHeader('X-Message-Id', $response->getHeaderLine('X-Message-Id'));
71
        }
72
73
        if (is_callable([$this, "sendPerformed"])) {
74
            $this->sendPerformed($message);
75
        }
76
77
        if (is_callable([$this, "numberOfRecipients"])) {
78
            return $this->numberOfRecipients ?: $this->numberOfRecipients($message);
79
        }
80
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $response; (GuzzleHttp\Psr7\Response) is incompatible with the return type declared by the interface Swift_Transport::send of type integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
81
    }
82
83
    /**
84
     * @param Swift_Mime_SimpleMessage $message
85
     * @return array
86
     */
87
    private function getPersonalizations(Swift_Mime_SimpleMessage $message)
88
    {
89
        $setter = function (array $addresses) {
90
            $recipients = [];
91
            foreach ($addresses as $email => $name) {
92
                $address = [];
93
                $address['email'] = $email;
94
                if ($name) {
95
                    $address['name'] = $name;
96
                }
97
                $recipients[] = $address;
98
            }
99
            return $recipients;
100
        };
101
102
        $personalization['to'] = $setter($message->getTo());
0 ignored issues
show
Coding Style Comprehensibility introduced by
$personalization was never initialized. Although not strictly required by PHP, it is generally a good practice to add $personalization = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
103
104
        if ($cc = $message->getCc()) {
105
            $personalization['cc'] = $setter($cc);
106
        }
107
108
        if ($bcc = $message->getBcc()) {
109
            $personalization['bcc'] = $setter($bcc);
110
        }
111
112
        return [$personalization];
113
    }
114
115
    /**
116
     * Get From Addresses.
117
     *
118
     * @param Swift_Mime_SimpleMessage $message
119
     * @return array
120
     */
121
    private function getFrom(Swift_Mime_SimpleMessage $message)
122
    {
123
        if ($message->getFrom()) {
124
            foreach ($message->getFrom() as $email => $name) {
125
                return ['email' => $email, 'name' => $name];
126
            }
127
        }
128
        return [];
129
    }
130
131
    /**
132
     * Get ReplyTo Addresses.
133
     *
134
     * @param Swift_Mime_SimpleMessage $message
135
     * @return array
136
     */
137
    private function getReplyTo(Swift_Mime_SimpleMessage $message)
138
    {
139
        if ($message->getReplyTo()) {
140
            foreach ($message->getReplyTo() as $email => $name) {
0 ignored issues
show
Bug introduced by
The expression $message->getReplyTo() of type string is not traversable.
Loading history...
141
                return ['email' => $email, 'name' => $name];
142
            }
143
        }
144
        return null;
145
    }
146
147
    /**
148
     * Get contents.
149
     *
150
     * @param Swift_Mime_SimpleMessage $message
151
     * @return array
152
     */
153
    private function getContents(Swift_Mime_SimpleMessage $message)
154
    {
155
        $contentType = $message->getContentType();
156
        switch ($contentType) {
157
            case 'text/plain':
158
                return [
159
                    [
160
                        'type'  => 'text/plain',
161
                        'value' => $message->getBody(),
162
163
                    ],
164
                ];
165
            case 'text/html':
166
                return [
167
                    [
168
                        'type'  => 'text/html',
169
                        'value' => $message->getBody(),
170
                    ],
171
                ];
172
        }
173
174
        // Following RFC 1341, text/html after text/plain in multipart
175
        $content = [];
176
        foreach ($message->getChildren() as $child) {
177
            if ($child instanceof Swift_MimePart && $child->getContentType() === 'text/plain') {
178
                $content[] = [
179
                    'type'  => 'text/plain',
180
                    'value' => $child->getBody(),
181
                ];
182
            }
183
        }
184
        $content[] = [
185
            'type'  => 'text/html',
186
            'value' => $message->getBody(),
187
        ];
188
        return $content;
189
    }
190
191
    /**
192
     * @param Swift_Mime_SimpleMessage $message
193
     * @return array
194
     */
195
    private function getAttachments(Swift_Mime_SimpleMessage $message)
196
    {
197
        $attachments = [];
198
        foreach ($message->getChildren() as $attachment) {
199
            if ((!$attachment instanceof Swift_Attachment && !$attachment instanceof Swift_Image)
200
                || $attachment->getFilename() === self::SMTP_API_NAME
201
                || !strlen($attachment->getBody()) > self::MAXIMUM_FILE_SIZE
202
            ) {
203
                continue;
204
            }
205
            $attachments[] = [
206
                'content'     => base64_encode($attachment->getBody()),
207
                'filename'    => $attachment->getFilename(),
208
                'type'        => $attachment->getContentType(),
209
                'disposition' => $attachment->getDisposition(),
210
                'content_id'  => $attachment->getId(),
211
            ];
212
        }
213
        return $this->attachments = $attachments;
214
    }
215
216
    /**
217
     * Set Request Body Parameters
218
     *
219
     * @param Swift_Mime_SimpleMessage $message
220
     * @param array $data
221
     * @return array
222
     * @throws \Exception
223
     */
224
    protected function setParameters(Swift_Mime_SimpleMessage $message, $data)
225
    {
226
        $this->numberOfRecipients = 0;
227
228
        $smtp_api = [];
229
        foreach ($message->getChildren() as $attachment) {
230
            if (!$attachment instanceof Swift_Image
231
                || !in_array(self::SMTP_API_NAME, [$attachment->getFilename(), $attachment->getContentType()])
232
            ) {
233
                continue;
234
            }
235
            $smtp_api = $attachment->getBody();
236
        }
237
238
        if (!is_array($smtp_api)) {
239
            return $data;
240
        }
241
242
        foreach ($smtp_api as $key => $val) {
243
244
            switch ($key) {
245
246
                case 'personalizations':
247
                    $this->setPersonalizations($data, $val);
248
                    continue 2;
249
250
                case 'attachments':
251
                    $val = array_merge($this->attachments, $val);
252
                    break;
253
254
                case 'unique_args':
255
                    throw new \Exception('Sendgrid v3 now uses custom_args instead of unique_args');
256
257
                case 'custom_args':
258
                    foreach ($val as $name => $value) {
259
                        if (!is_string($value)) {
260
                            throw new \Exception('Sendgrid v3 custom arguments have to be a string.');
261
                        }
262
                    }
263
                    break;
264
265
            }
266
267
            array_set($data, $key, $val);
268
        }
269
        return $data;
270
    }
271
272
    private function setPersonalizations(&$data, $personalizations)
273
    {
274
        foreach ($personalizations as $index => $params) {
275
            foreach ($params as $key => $val) {
276
                if (in_array($key, ['to', 'cc', 'bcc'])) {
277
                    array_set($data, 'personalizations.' . $index . '.' . $key, [$val]);
278
                    ++$this->numberOfRecipients;
279
                } else {
280
                    array_set($data, 'personalizations.' . $index . '.' . $key, $val);
281
                }
282
            }
283
        }
284
    }
285
286
    /**
287
     * @param $payload
288
     * @return Response
289
     */
290
    private function post($payload)
291
    {
292
        return $this->client->post('https://api.sendgrid.com/v3/mail/send', $payload);
293
    }
294
}
295