Completed
Push — master ( 43c25e...6a12cd )
by Adrien
07:57
created

Mailer   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 153
Duplicated Lines 0 %

Test Coverage

Coverage 66.67%

Importance

Changes 0
Metric Value
eloc 58
dl 0
loc 153
ccs 40
cts 60
cp 0.6667
rs 10
c 0
b 0
f 0
wmc 13

6 Methods

Rating   Name   Duplication   Size   Complexity  
A sendMessageAsync() 0 16 2
A sendMessage() 0 21 4
A __construct() 0 7 1
A acquireLock() 0 18 3
A sendAllMessages() 0 6 2
A modelMessageToMailMessage() 0 15 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Service;
6
7
use Application\Model\Message;
8
use Application\Repository\LogRepository;
9
use Cake\Chronos\Chronos;
10
use Doctrine\ORM\EntityManager;
11
use Exception;
12
use Zend\Mail;
13
use Zend\Mail\Transport\TransportInterface;
14
use Zend\Mime\Message as MimeMessage;
15
use Zend\Mime\Mime;
16
use Zend\Mime\Part as MimePart;
17
18
/**
19
 * Service to send a message as an email
20
 */
21
class Mailer
22
{
23
    /**
24
     * @var resource
25
     */
26
    private $lock;
27
28
    /**
29
     * @var EntityManager
30
     */
31
    private $entityManager;
32
33
    /**
34
     * @var TransportInterface
35
     */
36
    private $transport;
37
38
    /**
39
     * @var null|string
40
     */
41
    private $emailOverride;
42
43
    /**
44
     * @var string
45
     */
46
    private $fromEmail;
47
48
    /**
49
     * @var string
50
     */
51
    private $phpPath;
52
53 2
    public function __construct(EntityManager $entityManager, TransportInterface $transport, ?string $emailOverride, string $fromEmail, string $phpPath)
54
    {
55 2
        $this->entityManager = $entityManager;
56 2
        $this->transport = $transport;
57 2
        $this->emailOverride = $emailOverride;
58 2
        $this->fromEmail = $fromEmail;
59 2
        $this->phpPath = $phpPath;
60 2
    }
61
62
    /**
63
     * Send a message asynchronously in a separate process.
64
     *
65
     * This should be the preferred way to send a message, unless if we are the cron.
66
     *
67
     * @param Message $message
68
     */
69 4
    public function sendMessageAsync(Message $message): void
70
    {
71
        // Be sure we have an ID before "forking" process
72 4
        if ($message->getId() === null) {
0 ignored issues
show
introduced by
The condition $message->getId() === null is always false.
Loading history...
73 4
            $this->entityManager->flush();
74
        }
75
76
        $args = [
77 4
            realpath('bin/send-message.php'),
78 4
            $message->getId(),
79
        ];
80
81 4
        $escapedArgs = array_map('escapeshellarg', $args);
82
83 4
        $cmd = escapeshellcmd($this->phpPath) . ' ' . implode(' ', $escapedArgs) . ' > /dev/null 2>&1 &';
84 4
        exec($cmd);
85 4
    }
86
87
    /**
88
     * Send a message
89
     *
90
     * @param Message $message
91
     */
92 1
    public function sendMessage(Message $message): void
93
    {
94 1
        $mailMessage = $this->modelMessageToMailMessage($message);
95
96 1
        $email = $message->getEmail();
97 1
        $overriddenBy = '';
98 1
        if ($this->emailOverride) {
99
            $email = $this->emailOverride;
100
            $overriddenBy = ' overridden by ' . $email;
101
        }
102
103 1
        $recipientName = $message->getRecipient() ? $message->getRecipient()->getName() : null;
104 1
        if ($email) {
105 1
            $mailMessage->addTo($email, $recipientName);
106 1
            $this->transport->send($mailMessage);
107
        }
108
109 1
        $message->setDateSent(new Chronos());
110 1
        $this->entityManager->flush();
111
112 1
        echo 'email sent to: ' . $message->getEmail() . "\t" . $overriddenBy . "\t" . $message->getSubject() . PHP_EOL;
113 1
    }
114
115
    /**
116
     * Convert our model message to a mail message
117
     *
118
     * @param Message $modelMessage
119
     *
120
     * @return Mail\Message
121
     */
122 1
    private function modelMessageToMailMessage(Message $modelMessage): Mail\Message
123
    {
124
        // set Mime type html
125 1
        $htmlPart = new MimePart($modelMessage->getBody());
126 1
        $htmlPart->type = Mime::TYPE_HTML;
127 1
        $body = new MimeMessage();
128 1
        $body->setParts([$htmlPart]);
129
130 1
        $mailMessage = new Mail\Message();
131 1
        $mailMessage->setEncoding('UTF-8');
132 1
        $mailMessage->setSubject($modelMessage->getSubject());
133 1
        $mailMessage->setBody($body);
134 1
        $mailMessage->setFrom($this->fromEmail, 'Ichtus');
135
136 1
        return $mailMessage;
137
    }
138
139
    /**
140
     * Send all messages that are not sent yet
141
     */
142
    public function sendAllMessages(): void
143
    {
144
        $this->acquireLock();
145
        $messages = $this->entityManager->getRepository(Message::class)->getAllMessageToSend();
146
        foreach ($messages as $message) {
147
            $this->sendMessage($message);
148
        }
149
    }
150
151
    /**
152
     * Acquire an exclusive lock
153
     *
154
     * This is to ensure only one mailer can run at any given time. This is to prevent sending the same email twice.
155
     */
156
    private function acquireLock(): void
157
    {
158
        $lockFile = 'data/tmp/mailer.lock';
159
        touch($lockFile);
160
        $this->lock = fopen($lockFile, 'r+');
0 ignored issues
show
Documentation Bug introduced by
It seems like fopen($lockFile, 'r+') can also be of type false. However, the property $lock is declared as type resource. 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...
161
        if ($this->lock === false) {
162
            throw new Exception('Could not read lock file. This is not normal and might be a permission issue');
163
        }
164
165
        if (!flock($this->lock, LOCK_EX | LOCK_NB)) {
166
            $message = LogRepository::MAILER_LOCKED;
167
            _log()->info($message);
168
169
            echo $message . PHP_EOL;
170
            echo 'If the problem persist and another mailing is not in progress, try deleting ' . $lockFile . PHP_EOL;
171
172
            // Not getting the lock is not considered as error to avoid being spammed
173
            die();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
174
        }
175
    }
176
}
177