Message   F
last analyzed

Complexity

Total Complexity 72

Size/Duplication

Total Lines 515
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 148
dl 0
loc 515
rs 2.64
c 3
b 1
f 0
wmc 72

37 Methods

Rating   Name   Duplication   Size   Complexity  
A toString() 0 3 1
A getUniqueArguments() 0 3 1
A setHtmlBody() 0 4 1
A getCharset() 0 3 1
A getSubject() 0 3 1
A getTemplateId() 0 3 1
A getTo() 0 3 1
A attachContent() 0 11 4
A getTemplateModel() 0 3 2
A getFromName() 0 8 3
A getTempDir() 0 14 4
A setBcc() 0 4 1
A setFrom() 0 7 2
A normalizeEmails() 0 17 6
A addUniqueArgument() 0 4 1
A getHeaders() 0 3 2
A setCharset() 0 3 1
B stringifyEmails() 0 21 7
A attach() 0 12 2
A embedContent() 0 11 4
A getCc() 0 3 1
A send() 0 7 2
A getReplyTo() 0 14 4
A getTextBody() 0 3 1
A getAttachments() 0 3 2
A getHtmlBody() 0 3 1
A getBcc() 0 3 1
A setReplyTo() 0 7 2
A setTo() 0 4 1
A embed() 0 13 2
A getFrom() 0 13 3
A setSubject() 0 4 1
A addHeader() 0 3 1
A setTemplateModel() 0 4 1
A setTemplateId() 0 4 1
A setTextBody() 0 4 1
A setCc() 0 4 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * Message.php
4
 *
5
 * PHP version 5.6+
6
 *
7
 * @author Manuel Avelar <[email protected]>
8
 * @copyright 2024 Manuel Avelar
9
 * @license http://www.pixelcreart.com/license license
10
 * @version 1.0.0
11
 * @link http://www.pixelcreart.com
12
 * @package pixelcreart\sendgrid
13
 */
14
15
namespace pixelcreart\sendgrid;
16
17
use Yii;
18
use yii\base\InvalidConfigException;
19
use yii\base\InvalidArgumentException;
20
use yii\mail\BaseMessage;
21
use yii\mail\MailerInterface;
22
23
/**
24
 * This component allow user to send an email
25
 *
26
 * @author Manuel Avelar <[email protected]>
27
 * @copyright 2024 Manuel Avelar
28
 * @license http://www.pixelcreart.com/license license
29
 * @version 1.0.0
30
 * @link http://www.pixelcreart.com
31
 * @package pixelcreart\sendgrid
32
 * @since 1.0.0
33
 */
34
class Message extends BaseMessage
35
{
36
    /**
37
     * @var string|array from
38
     */
39
    protected $from;
40
41
    /**
42
     * @var array
43
     */
44
    protected $to = [];
45
46
    /**
47
     * @var string|array reply to
48
     */
49
    protected $replyTo;
50
51
    /**
52
     * @var array
53
     */
54
    protected $cc = [];
55
56
    /**
57
     * @var array
58
     */
59
    protected $bcc = [];
60
61
    /**
62
     * @var string
63
     */
64
    protected $subject;
65
66
    /**
67
     * @var string
68
     */
69
    protected $textBody;
70
71
    /**
72
     * @var string
73
     */
74
    protected $htmlBody;
75
76
    /**
77
     * @var array
78
     */
79
    protected $attachments = [];
80
81
    /**
82
     * @var string temporary attachment directory
83
     */
84
    protected $attachmentsTmdDir;
85
86
    /**
87
     * @var array
88
     */
89
    protected $uniqueArguments = [];
90
91
    /**
92
     * @var array
93
     */
94
    protected $headers = [];
95
96
    /**
97
     * @var string
98
     */
99
    protected $templateId;
100
101
    /**
102
     * @var array
103
     */
104
    protected $templateModel;
105
106
    /**
107
     * @var array substitution pairs used to mark expandable vars in template mode https://github.com/sendgrid/sendgrid-php#setsubstitutions
108
     */
109
    public $substitutionsPairs = ['{', '}'];
110
111
    /**
112
     * @inheritdoc
113
     */
114
    public function getCharset()
115
    {
116
        return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the return type mandated by yii\mail\MessageInterface::getCharset() of string.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
117
    }
118
119
    /**
120
     * @inheritdoc
121
     */
122
    public function setCharset($charset)
123
    {
124
        return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the return type mandated by yii\mail\MessageInterface::setCharset() of yii\mail\MessageInterface.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
125
    }
126
127
    /**
128
     * @inheritdoc
129
     */
130
    public function getFrom()
131
    {
132
        $fromMail = null;
133
        reset($this->from);
0 ignored issues
show
Bug introduced by
It seems like $this->from can also be of type string; however, parameter $array of reset() does only seem to accept array|object, maybe add an additional type check? ( Ignorable by Annotation )

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

133
        reset(/** @scrutinizer ignore-type */ $this->from);
Loading history...
134
        foreach($this->from as $email => $name) {
135
            if (is_numeric($email) === true) {
136
                $fromMail = $name;
137
            } else {
138
                $fromMail = $email;
139
            }
140
        }
141
        
142
        return $fromMail;
143
    }
144
145
    /**
146
     * @return string|null extract and return the name associated with from
147
     * @since 1.0.0
148
     */
149
    public function getFromName()
150
    {
151
        reset($this->from);
0 ignored issues
show
Bug introduced by
It seems like $this->from can also be of type string; however, parameter $array of reset() does only seem to accept array|object, maybe add an additional type check? ( Ignorable by Annotation )

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

151
        reset(/** @scrutinizer ignore-type */ $this->from);
Loading history...
152
        foreach($this->from as $email => $name) {
153
            if (is_numeric($email) === false) {
154
                return $name;
155
            } else {
156
                return null;
157
            }
158
        }
159
    }
160
161
    /**
162
     * @inheritdoc
163
     */
164
    public function setFrom($from)
165
    {
166
        if (is_string($from) === true) {
167
            $from = [$from];
168
        }
169
        $this->from = $from;
170
        return $this;
171
    }
172
173
    /**
174
     * @inheritdoc
175
     */
176
    public function getTo()
177
    {
178
        return self::normalizeEmails($this->to);
179
    }
180
181
    /**
182
     * @inheritdoc
183
     */
184
    public function setTo($to)
185
    {
186
        $this->to = $to;
187
        return $this;
188
    }
189
190
    /**
191
     * @inheritdoc
192
     */
193
    public function getReplyTo()
194
    {
195
        $replyTo = null;
196
        if (is_array($this->replyTo) === true) {
197
            reset($this->replyTo);
198
            foreach($this->replyTo as $email => $name) {
199
                if (is_numeric($email) === true) {
200
                    $replyTo = $name;
201
                } else {
202
                    $replyTo = $email;
203
                }
204
            }
205
        }
206
        return $replyTo;
207
    }
208
209
    /**
210
     * @inheritdoc
211
     */
212
    public function setReplyTo($replyTo)
213
    {
214
        if (is_string($replyTo) === true) {
215
            $replyTo = [$replyTo];
216
        }
217
        $this->replyTo = $replyTo;
218
        return $this;
219
    }
220
221
    /**
222
     * @inheritdoc
223
     */
224
    public function getCc()
225
    {
226
        return $this->cc;
227
    }
228
229
    /**
230
     * @inheritdoc
231
     */
232
    public function setCc($cc)
233
    {
234
        $this->cc = self::normalizeEmails($cc);
235
        return $this;
236
    }
237
238
    /**
239
     * @inheritdoc
240
     */
241
    public function getBcc()
242
    {
243
        return $this->bcc;
244
    }
245
246
    /**
247
     * @inheritdoc
248
     */
249
    public function setBcc($bcc)
250
    {
251
        $this->bcc = self::normalizeEmails($bcc);
252
        return $this;
253
    }
254
255
    /**
256
     * @inheritdoc
257
     */
258
    public function getSubject()
259
    {
260
        return $this->subject;
261
    }
262
263
    /**
264
     * @inheritdoc
265
     */
266
    public function setSubject($subject)
267
    {
268
        $this->subject = $subject;
269
        return $this;
270
    }
271
272
    /**
273
     * @return string|null text body of the message
274
     * @since 1.0.0
275
     */
276
    public function getTextBody()
277
    {
278
        return $this->textBody;
279
    }
280
281
    /**
282
     * @inheritdoc
283
     */
284
    public function setTextBody($text)
285
    {
286
        $this->textBody = $text;
287
        return $this;
288
    }
289
290
    /**
291
     * @return string|null html body of the message
292
     * @since 1.0.0
293
     */
294
    public function getHtmlBody()
295
    {
296
        return $this->htmlBody;
297
    }
298
299
    /**
300
     * @inheritdoc
301
     */
302
    public function setHtmlBody($html)
303
    {
304
        $this->htmlBody = $html;
305
        return $this;
306
    }
307
308
    /**
309
     * @return array list of unique arguments attached to the email
310
     * @since 1.0.0
311
     */
312
    public function getUniqueArguments()
313
    {
314
        return $this->uniqueArguments;
315
    }
316
317
    /**
318
     * @param string $key key of the unique argument
319
     * @param string $value value of the unique argument which will be added to the mail
320
     * @return $this
321
     * @since 1.0.0
322
     */
323
    public function addUniqueArgument($key, $value)
324
    {
325
        $this->uniqueArguments[$key] = $value;
326
        return $this;
327
    }
328
329
    /**
330
     * @param string $templateId template Id used. in this case, Subject / HtmlBody / TextBody are discarded
331
     * @return $this
332
     * @since 1.0.0
333
     */
334
    public function setTemplateId($templateId)
335
    {
336
        $this->templateId = $templateId;
337
        return $this;
338
    }
339
340
    /**
341
     * @return string|null current templateId
342
     * @since 1.0.0
343
     */
344
    public function getTemplateId()
345
    {
346
        return $this->templateId;
347
    }
348
349
    /**
350
     * @param array $templateModel model associated with the template
351
     * @return $this
352
     * @since 1.0.0
353
     */
354
    public function setTemplateModel($templateModel)
355
    {
356
        $this->templateModel = $templateModel;
357
        return $this;
358
    }
359
360
    /**
361
     * @return array current template model
362
     * @since 1.0.0
363
     */
364
    public function getTemplateModel()
365
    {
366
        return !empty($this->templateModel) ? $this->templateModel : [];
367
    }
368
369
    /**
370
     * @param array $header add custom header to the mail
371
     * @since 1.0.0
372
     */
373
    public function addHeader($header)
374
    {
375
        $this->headers[] = $header;
376
    }
377
378
    /**
379
     * @return array|null headers which should be added to the mail
380
     * @since 1.0.0
381
     */
382
    public function getHeaders()
383
    {
384
        return empty($this->headers) ? [] : $this->headers;
385
    }
386
387
    /**
388
     * @return array|null list of attachments
389
     * @since 1.0.0
390
     */
391
    public function getAttachments()
392
    {
393
        return empty($this->attachments) ? [] : $this->attachments;
394
    }
395
396
    /**
397
     * @inheritdoc
398
     */
399
    public function attach($fileName, array $options = [])
400
    {
401
        $attachment = [
402
            'File' => $fileName
403
        ];
404
        if (!empty($options['fileName'])) {
405
            $attachment['Name'] = $options['fileName'];
406
        } else {
407
            $attachment['Name'] = pathinfo($fileName, PATHINFO_BASENAME);
408
        }
409
        $this->attachments[] = $attachment;
410
        return $this;
411
    }
412
413
    /**
414
     * @return string temporary directory to store contents
415
     * @since 1.0.0
416
     * @throws InvalidConfigException
417
     */
418
    protected function getTempDir()
419
    {
420
        if ($this->attachmentsTmdDir === null) {
421
            $uid = uniqid();
422
            $this->attachmentsTmdDir = Yii::getAlias('@app/runtime/'.$uid.'/');
0 ignored issues
show
Documentation Bug introduced by
It seems like Yii::getAlias('@app/runtime/' . $uid . '/') can also be of type boolean. However, the property $attachmentsTmdDir is declared as type string. Maybe add an additional type 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 mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
423
            $status = true;
424
            if (file_exists($this->attachmentsTmdDir) === false) {
0 ignored issues
show
Bug introduced by
It seems like $this->attachmentsTmdDir can also be of type boolean; however, parameter $filename of file_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

424
            if (file_exists(/** @scrutinizer ignore-type */ $this->attachmentsTmdDir) === false) {
Loading history...
425
                $status = mkdir($this->attachmentsTmdDir, 0755, true);
0 ignored issues
show
Bug introduced by
It seems like $this->attachmentsTmdDir can also be of type boolean; however, parameter $directory of mkdir() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

425
                $status = mkdir(/** @scrutinizer ignore-type */ $this->attachmentsTmdDir, 0755, true);
Loading history...
426
            }
427
            if ($status === false) {
428
                throw new InvalidConfigException('Directory \''.$this->attachmentsTmdDir.'\' cannot be created');
429
            }
430
        }
431
        return $this->attachmentsTmdDir;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->attachmentsTmdDir also could return the type boolean which is incompatible with the documented return type string.
Loading history...
432
    }
433
434
    /**
435
     * @inheritdoc
436
     */
437
    public function attachContent($content, array $options = [])
438
    {
439
        if (!isset($options['fileName']) || empty($options['fileName'])) {
440
            throw new InvalidArgumentException('Filename is missing');
441
        }
442
        $filePath = $this->getTempDir().'/'.$options['fileName'];
443
        if (file_put_contents($filePath, $content) === false) {
444
            throw new InvalidConfigException('Cannot write file \''.$filePath.'\'');
445
        }
446
        $this->attach($filePath, $options);
447
        return $this;
448
    }
449
450
    /**
451
     * @inheritdoc
452
     */
453
    public function embed($fileName, array $options = [])
454
    {
455
        $embed = [
456
            'File' => $fileName
457
        ];
458
        if (!empty($options['fileName'])) {
459
            $embed['Name'] = $options['fileName'];
460
        } else {
461
            $embed['Name'] = pathinfo($fileName, PATHINFO_BASENAME);
462
        }
463
        $embed['ContentID'] = 'cid:' . uniqid();
464
        $this->attachments[] = $embed;
465
        return $embed['ContentID'];
466
    }
467
468
    /**
469
     * @inheritdoc
470
     */
471
    public function embedContent($content, array $options = [])
472
    {
473
        if (isset($options['fileName']) === false || empty($options['fileName'])) {
474
            throw new InvalidArgumentException('fileName is missing');
475
        }
476
        $filePath = $this->getTempDir().'/'.$options['fileName'];
477
        if (file_put_contents($filePath, $content) === false) {
478
            throw new InvalidConfigException('Cannot write file \''.$filePath.'\'');
479
        }
480
        $cid = $this->embed($filePath, $options);
481
        return $cid;
482
    }
483
484
    /**
485
     * @inheritdoc
486
     * @todo make real serialization to make message compliant with PostmarkAPI
487
     */
488
    public function toString()
489
    {
490
        return serialize($this);
491
    }
492
493
494
    /**
495
     * @param array|string $emailsData email can be defined as string. In this case no transformation is done
496
     *                                 or as an array ['[email protected]', '[email protected]' => 'Email 2']
497
     * @return string|null
498
     * @since 1.0.0
499
     */
500
    public static function stringifyEmails($emailsData)
501
    {
502
        $emails = null;
503
        if (empty($emailsData) === false) {
504
            if (is_array($emailsData) === true) {
505
                foreach ($emailsData as $key => $email) {
506
                    if (is_int($key) === true) {
507
                        $emails[] = $email;
508
                    } else {
509
                        if (preg_match('/[.,:]/', $email) > 0) {
510
                            $email = '"'. $email .'"';
511
                        }
512
                        $emails[] = $email . ' ' . '<' . $key . '>';
513
                    }
514
                }
515
                $emails = implode(', ', $emails);
516
            } elseif (is_string($emailsData) === true) {
0 ignored issues
show
introduced by
The condition is_string($emailsData) === true is always true.
Loading history...
517
                $emails = $emailsData;
518
            }
519
        }
520
        return $emails;
521
    }
522
523
    public static function normalizeEmails($emailsData)
524
    {
525
        $emails = null;
526
        if (empty($emailsData) === false) {
527
            if (is_array($emailsData) === true) {
528
                foreach ($emailsData as $key => $email) {
529
                    if (is_int($key) === true) {
530
                        $emails[$email] = null;
531
                    } else {
532
                        $emails[$key] = $email;
533
                    }
534
                }
535
            } elseif (is_string($emailsData) === true) {
536
                $emails[$emailsData] = null;
537
            }
538
        }
539
        return $emails;
540
    }
541
542
    public function send(MailerInterface $mailer = null)
543
    {
544
        $result = parent::send($mailer);
545
        if ($this->attachmentsTmdDir !== null) {
546
            //TODO: clean up tmpdir after ourselves
547
        }
548
        return $result;
549
    }
550
551
552
}