Completed
Push — 2x-delete-validate-for-v2 ( ac7109 )
by Shingo
01:26
created

SendgridTransport   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 297
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
wmc 51
lcom 1
cbo 9
dl 0
loc 297
rs 7.92
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
B send() 0 48 8
A getPersonalizations() 0 27 5
A getFrom() 0 9 3
A getReplyTo() 0 9 3
B getContents() 0 42 7
A __construct() 0 6 2
A getAttachments() 0 19 5
C setParameters() 0 51 13
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 Illuminate\Support\Arr;
9
use Sichikawa\LaravelSendgridDriver\SendGrid;
10
use Swift_Attachment;
11
use Swift_Image;
12
use Swift_Mime_SimpleMessage;
13
use Swift_MimePart;
14
15
class SendgridTransport extends Transport
16
{
17
    use SendGrid {
18
        sgDecode as decode;
19
    }
20
21
    const SMTP_API_NAME = 'sendgrid/x-smtpapi';
22
    const BASE_URL = 'https://api.sendgrid.com/v3/mail/send';
23
24
    /**
25
     * @var Client
26
     */
27
    private $client;
28
    private $attachments;
29
    private $numberOfRecipients;
30
    private $apiKey;
31
    private $endpoint;
32
33
    public function __construct(ClientInterface $client, $api_key, $endpoint = null)
34
    {
35
        $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...
36
        $this->apiKey = $api_key;
37
        $this->endpoint = isset($endpoint) ? $endpoint : self::BASE_URL;
38
    }
39
40
    /**
41
     * {@inheritdoc}
42
     */
43
    public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
44
    {
45
        $this->beforeSendPerformed($message);
46
47
        $data = [
48
            'personalizations' => $this->getPersonalizations($message),
49
            'from'             => $this->getFrom($message),
50
            'subject'          => $message->getSubject(),
51
        ];
52
53
        if ($contents = $this->getContents($message)) {
54
            $data['content'] = $contents;
55
        }
56
57
        if ($reply_to = $this->getReplyTo($message)) {
58
            $data['reply_to'] = $reply_to;
59
        }
60
61
        $attachments = $this->getAttachments($message);
62
        if (count($attachments) > 0) {
63
            $data['attachments'] = $attachments;
64
        }
65
66
        $data = $this->setParameters($message, $data);
67
68
        $payload = [
69
            'headers' => [
70
                'Authorization' => 'Bearer ' . $this->apiKey,
71
                'Content-Type' => 'application/json',
72
            ],
73
            'json' => $data,
74
        ];
75
76
        $response = $this->post($payload);
77
78
        if (method_exists($response, 'getHeaderLine')) {
79
            $message->getHeaders()->addTextHeader('X-Message-Id', $response->getHeaderLine('X-Message-Id'));
80
        }
81
82
        if (is_callable([$this, "sendPerformed"])) {
83
            $this->sendPerformed($message);
84
        }
85
86
        if (is_callable([$this, "numberOfRecipients"])) {
87
            return $this->numberOfRecipients ?: $this->numberOfRecipients($message);
88
        }
89
        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...
90
    }
91
92
    /**
93
     * @param Swift_Mime_SimpleMessage $message
94
     * @return array
95
     */
96
    private function getPersonalizations(Swift_Mime_SimpleMessage $message)
97
    {
98
        $setter = function (array $addresses) {
99
            $recipients = [];
100
            foreach ($addresses as $email => $name) {
101
                $address = [];
102
                $address['email'] = $email;
103
                if ($name) {
104
                    $address['name'] = $name;
105
                }
106
                $recipients[] = $address;
107
            }
108
            return $recipients;
109
        };
110
111
        $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...
112
113
        if ($cc = $message->getCc()) {
114
            $personalization['cc'] = $setter($cc);
115
        }
116
117
        if ($bcc = $message->getBcc()) {
118
            $personalization['bcc'] = $setter($bcc);
119
        }
120
121
        return [$personalization];
122
    }
123
124
    /**
125
     * Get From Addresses.
126
     *
127
     * @param Swift_Mime_SimpleMessage $message
128
     * @return array
129
     */
130
    private function getFrom(Swift_Mime_SimpleMessage $message)
131
    {
132
        if ($message->getFrom()) {
133
            foreach ($message->getFrom() as $email => $name) {
134
                return ['email' => $email, 'name' => $name];
135
            }
136
        }
137
        return [];
138
    }
139
140
    /**
141
     * Get ReplyTo Addresses.
142
     *
143
     * @param Swift_Mime_SimpleMessage $message
144
     * @return array
145
     */
146
    private function getReplyTo(Swift_Mime_SimpleMessage $message)
147
    {
148
        if ($message->getReplyTo()) {
149
            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...
150
                return ['email' => $email, 'name' => $name];
151
            }
152
        }
153
        return null;
154
    }
155
156
    /**
157
     * Get contents.
158
     *
159
     * @param Swift_Mime_SimpleMessage $message
160
     * @return array
161
     */
162
    private function getContents(Swift_Mime_SimpleMessage $message)
163
    {
164
        $contentType = $message->getContentType();
165
        switch ($contentType) {
166
            case 'text/plain':
167
                return [
168
                    [
169
                        'type'  => 'text/plain',
170
                        'value' => $message->getBody(),
171
172
                    ],
173
                ];
174
            case 'text/html':
175
                return [
176
                    [
177
                        'type'  => 'text/html',
178
                        'value' => $message->getBody(),
179
                    ],
180
                ];
181
        }
182
183
        // Following RFC 1341, text/html after text/plain in multipart
184
        $content = [];
185
        foreach ($message->getChildren() as $child) {
186
            if ($child instanceof Swift_MimePart && $child->getContentType() === 'text/plain') {
187
                $content[] = [
188
                    'type'  => 'text/plain',
189
                    'value' => $child->getBody(),
190
                ];
191
            }
192
        }
193
194
        if (is_null($message->getBody())) {
195
            return null;
196
        }
197
198
        $content[] = [
199
            'type'  => 'text/html',
200
            'value' => $message->getBody(),
201
        ];
202
        return $content;
203
    }
204
205
    /**
206
     * @param Swift_Mime_SimpleMessage $message
207
     * @return array
208
     */
209
    private function getAttachments(Swift_Mime_SimpleMessage $message)
210
    {
211
        $attachments = [];
212
        foreach ($message->getChildren() as $attachment) {
213
            if ((!$attachment instanceof Swift_Attachment && !$attachment instanceof Swift_Image)
214
                || $attachment->getFilename() === self::SMTP_API_NAME
215
            ) {
216
                continue;
217
            }
218
            $attachments[] = [
219
                'content'     => base64_encode($attachment->getBody()),
220
                'filename'    => $attachment->getFilename(),
221
                'type'        => $attachment->getContentType(),
222
                'disposition' => $attachment->getDisposition(),
223
                'content_id'  => $attachment->getId(),
224
            ];
225
        }
226
        return $this->attachments = $attachments;
227
    }
228
229
    /**
230
     * Set Request Body Parameters
231
     *
232
     * @param Swift_Mime_SimpleMessage $message
233
     * @param array $data
234
     * @return array
235
     * @throws \Exception
236
     */
237
    protected function setParameters(Swift_Mime_SimpleMessage $message, $data)
238
    {
239
        $this->numberOfRecipients = 0;
240
241
        $smtp_api = [];
242
        foreach ($message->getChildren() as $attachment) {
243
            if (!$attachment instanceof Swift_Image
244
                || !in_array(self::SMTP_API_NAME, [$attachment->getFilename(), $attachment->getContentType()])
245
            ) {
246
                continue;
247
            }
248
            $smtp_api = self::decode($attachment->getBody());
249
        }
250
251
        if (!is_array($smtp_api)) {
252
            return $data;
253
        }
254
255
        foreach ($smtp_api as $key => $val) {
256
257
            switch ($key) {
258
259
                case 'api_key':
260
                    $this->apiKey = $val;
261
                    continue 2;
262
263
                case 'personalizations':
264
                    $this->setPersonalizations($data, $val);
265
                    continue 2;
266
267
                case 'attachments':
268
                    $val = array_merge($this->attachments, $val);
269
                    break;
270
271
                case 'unique_args':
272
                    throw new \Exception('Sendgrid v3 now uses custom_args instead of unique_args');
273
274
                case 'custom_args':
275
                    foreach ($val as $name => $value) {
276
                        if (!is_string($value)) {
277
                            throw new \Exception('Sendgrid v3 custom arguments have to be a string.');
278
                        }
279
                    }
280
                    break;
281
282
            }
283
284
            Arr::set($data, $key, $val);
285
        }
286
        return $data;
287
    }
288
289
    private function setPersonalizations(&$data, $personalizations)
290
    {
291
        foreach ($personalizations as $index => $params) {
292
            foreach ($params as $key => $val) {
293
                if (in_array($key, ['to', 'cc', 'bcc'])) {
294
                    Arr::set($data, 'personalizations.' . $index . '.' . $key, [$val]);
295
                    ++$this->numberOfRecipients;
296
                } else {
297
                    Arr::set($data, 'personalizations.' . $index . '.' . $key, $val);
298
                }
299
            }
300
        }
301
    }
302
303
    /**
304
     * @param $payload
305
     * @return Response
306
     */
307
    private function post($payload)
308
    {
309
        return $this->client->post($this->endpoint, $payload);
310
    }
311
}
312