Completed
Push — master ( 3b415d...9c7552 )
by Alejandro
02:33
created

MailService::setBody()   D

Complexity

Conditions 9
Paths 14

Size

Total Lines 36
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 9

Importance

Changes 10
Bugs 3 Features 2
Metric Value
c 10
b 3
f 2
dl 0
loc 36
ccs 25
cts 25
cp 1
rs 4.9091
cc 9
eloc 22
nc 14
nop 2
crap 9
1
<?php
2
namespace AcMailer\Service;
3
4
use AcMailer\Event\MailEvent;
5
use AcMailer\Event\MailListenerInterface;
6
use AcMailer\Event\MailListenerAwareInterface;
7
use AcMailer\Exception\MailException;
8
use AcMailer\View\DefaultLayout;
9
use AcMailer\View\DefaultLayoutInterface;
10
use Zend\EventManager\EventManager;
11
use Zend\EventManager\EventManagerAwareInterface;
12
use Zend\EventManager\EventManagerInterface;
13
use Zend\Mail\Transport\TransportInterface;
14
use Zend\Mail\Message;
15
use Zend\Mime;
16
use Zend\Mail\Exception\ExceptionInterface as ZendMailException;
17
use AcMailer\Result\ResultInterface;
18
use AcMailer\Result\MailResult;
19
use Zend\View\Model\ViewModel;
20
use Zend\View\Renderer\RendererInterface;
21
use AcMailer\Exception\InvalidArgumentException;
22
23
/**
24
 * Wraps Zend\Mail functionality
25
 * @author Alejandro Celaya Alastrué
26
 * @link http://www.alejandrocelaya.com
27
 */
28
class MailService implements MailServiceInterface, EventManagerAwareInterface, MailListenerAwareInterface
29
{
30
    /**
31
     * @var \Zend\Mail\Message
32
     */
33
    private $message;
34
    /**
35
     * @var \Zend\Mail\Transport\TransportInterface
36
     */
37
    private $transport;
38
    /**
39
     * @var RendererInterface
40
     */
41
    private $renderer;
42
    /**
43
     * @var EventManagerInterface
44
     */
45
    private $events;
46
    /**
47
     * @var array
48
     */
49
    private $attachments = [];
50
    /**
51
     * @var DefaultLayoutInterface
52
     */
53
    private $defaultLayout;
54
55
    /**
56
     * Creates a new MailService
57
     * @param Message $message
58
     * @param TransportInterface $transport
59
     * @param RendererInterface $renderer Renderer used to render templates, typically a PhpRenderer
60
     */
61 33
    public function __construct(Message $message, TransportInterface $transport, RendererInterface $renderer)
62
    {
63 33
        $this->message      = $message;
64 33
        $this->transport    = $transport;
65 33
        $this->renderer     = $renderer;
66 33
        $this->setDefaultLayout();
67 33
    }
68
69
    /**
70
     * Returns this service's message
71
     * @return \Zend\Mail\Message
72
     * @see \AcMailer\Service\MailServiceInterface::getMessage()
73
     */
74 13
    public function getMessage()
75
    {
76 13
        return $this->message;
77
    }
78
79
    /**
80
     * Sends the mail
81
     * @return ResultInterface
82
     * @throws MailException
83
     */
84 9
    public function send()
85
    {
86 9
        $result = new MailResult();
87
        try {
88
            // Trigger pre send event
89 9
            $this->getEventManager()->trigger($this->createMailEvent());
90
91
            // Attach files before sending the email
92 9
            $this->attachFiles();
93
94
            // Try to send the message
95 9
            $this->transport->send($this->message);
96
97
            // Trigger post send event
98 5
            $this->getEventManager()->trigger($this->createMailEvent(MailEvent::EVENT_MAIL_POST_SEND, $result));
99 9
        } catch (\Exception $e) {
100 4
            $result = $this->createMailResultFromException($e);
101
            // Trigger send error event
102 4
            $this->getEventManager()->trigger($this->createMailEvent(MailEvent::EVENT_MAIL_SEND_ERROR, $result));
103
104
            // If the exception produced is not a Zend\Mail exception, rethrow it as a MailException
105 4
            if (! $e instanceof ZendMailException) {
106 1
                throw new MailException('An non Zend\Mail exception occurred', $e->getCode(), $e);
107
            }
108
        }
109
110 8
        return $result;
111
    }
112
113
    /**
114
     * Creates a new MailEvent object
115
     * @param ResultInterface $result
116
     * @param string $name
117
     * @return MailEvent
118
     */
119 9
    protected function createMailEvent($name = MailEvent::EVENT_MAIL_PRE_SEND, ResultInterface $result = null)
120
    {
121 9
        $event = new MailEvent($this, $name);
122 9
        if (isset($result)) {
123 9
            $event->setResult($result);
124 9
        }
125 9
        return $event;
126
    }
127
128
    /**
129
     * Creates a error MailResult from an exception
130
     * @param \Exception $e
131
     * @return MailResult
132
     */
133 4
    protected function createMailResultFromException(\Exception $e)
134
    {
135 4
        return new MailResult(false, $e->getMessage(), $e);
136
    }
137
138
    /**
139
     * Sets the message body
140
     * @param \Zend\Mime\Part|\Zend\Mime\Message|string $body Email body
141
     * @param string $charset
142
     * @return $this Returns this MailService for chaining purposes
143
     * @throws InvalidArgumentException
144
     * @see \AcMailer\Service\MailServiceInterface::setBody()
145
     */
146 21
    public function setBody($body, $charset = null)
147
    {
148 21
        if (is_string($body)) {
149
            // Create a Mime\Part and wrap it into a Mime\Message
150 17
            $mimePart = new Mime\Part($body);
151 17
            $mimePart->type     = $body != strip_tags($body) ? Mime\Mime::TYPE_HTML : Mime\Mime::TYPE_TEXT;
152 17
            $mimePart->charset  = $charset ?: self::DEFAULT_CHARSET;
153 17
            $body = new Mime\Message();
154 17
            $body->setParts([$mimePart]);
155 21
        } elseif ($body instanceof Mime\Part) {
156
            // Overwrite the charset if the Part object if provided
157 2
            if (isset($charset)) {
158 1
                $body->charset = $charset;
159 1
            }
160
            // The body is a Mime\Part. Wrap it into a Mime\Message
161 2
            $mimeMessage = new Mime\Message();
162 2
            $mimeMessage->setParts([$body]);
163 2
            $body = $mimeMessage;
164 2
        }
165
166
        // If the body is not a string or a Mime\Message at this point, it is not a valid argument
167 21
        if (! is_string($body) && ! $body instanceof Mime\Message) {
168 1
            throw new InvalidArgumentException(sprintf(
169 1
                'Provided body is not valid. It should be one of "%s". %s provided',
170 1
                implode('", "', ['string', 'Zend\Mime\Part', 'Zend\Mime\Message']),
171 1
                is_object($body) ? get_class($body) : gettype($body)
172 1
            ));
173
        }
174
175
        // The headers Content-type and Content-transfer-encoding are duplicated every time the body is set.
176
        // Removing them before setting the body prevents this error
177 20
        $this->message->getHeaders()->removeHeader('contenttype');
178 20
        $this->message->getHeaders()->removeHeader('contenttransferencoding');
179 20
        $this->message->setBody($body);
180 20
        return $this;
181
    }
182
183
    /**
184
     * Sets the body of this message from a template
185
     * @param string|\Zend\View\Model\ViewModel $template
186
     * @param array $params
187
     * @see \AcMailer\Service\MailServiceInterface::setTemplate()
188
     */
189 5
    public function setTemplate($template, array $params = [])
190
    {
191 5
        if ($template instanceof ViewModel) {
192 2
            $view = $template;
193 2
        } else {
194 3
            $view = new ViewModel();
195 3
            $view->setTemplate($template)
196 3
                 ->setVariables($params);
197
        }
198
199
        // Check if a common layout has to be used
200 5
        if ($this->defaultLayout->hasModel()) {
201 2
            $layoutModel = $this->defaultLayout->getModel();
202 2
            $layoutModel->addChild($view, $this->defaultLayout->getTemplateCaptureTo());
203 2
            $view = $layoutModel;
204 2
        }
205
        // Render the template and all of its children
206 5
        $this->renderChildren($view);
207
208 5
        $charset = isset($params['charset']) ? $params['charset'] : null;
209 5
        $this->setBody($this->renderer->render($view), $charset);
210 4
    }
211
212
    /**
213
     * Sets the default layout to be used with all the templates set when calling setTemplate.
214
     *
215
     * @param DefaultLayoutInterface $layout
216
     * @return mixed
217
     */
218 33
    public function setDefaultLayout(DefaultLayoutInterface $layout = null)
219
    {
220 33
        $this->defaultLayout = isset($layout) ? $layout : new DefaultLayout();
221 33
    }
222
223
    /**
224
     * Renders template childrens.
225
     * Inspired on Zend\View\View implementation to recursively render child models
226
     * @param ViewModel $model
227
     * @see Zend\View\View::renderChildren
228
     */
229 5
    protected function renderChildren(ViewModel $model)
230
    {
231 5
        if (! $model->hasChildren()) {
232 5
            return;
233
        }
234
235
        /* @var ViewModel $child */
236 3
        foreach ($model as $child) {
237 3
            $capture = $child->captureTo();
238 3
            if (! empty($capture)) {
239
                // Recursively render children
240 3
                $this->renderChildren($child);
241 3
                $result = $this->renderer->render($child);
242
243 3
                if ($child->isAppend()) {
244
                    $oldResult = $model->{$capture};
245
                    $model->setVariable($capture, $oldResult . $result);
246
                } else {
247 3
                    $model->setVariable($capture, $result);
248
                }
249 3
            }
250 3
        }
251 3
    }
252
253
    /**
254
     * Attaches files to the message if any
255
     */
256 9
    protected function attachFiles()
257
    {
258 9
        if (count($this->attachments) === 0) {
259 7
            return;
260
        }
261
262
        // Get old message parts
263 2
        $mimeMessage = $this->message->getBody();
264 2
        if (is_string($mimeMessage)) {
265 1
            $originalBodyPart = new Mime\Part($mimeMessage);
266 1
            $originalBodyPart->type = $mimeMessage != strip_tags($mimeMessage)
267 1
                ? Mime\Mime::TYPE_HTML
268 1
                : Mime\Mime::TYPE_TEXT;
269
270
            // A Mime\Part body will be wraped into a Mime\Message, ensuring we handle a Mime\Message after this point
271 1
            $this->setBody($originalBodyPart);
272 1
            $mimeMessage = $this->message->getBody();
273 1
        }
274 2
        $oldParts = $mimeMessage->getParts();
275
276
        // Generate a new Mime\Part for each attachment
277 2
        $attachmentParts    = [];
278 2
        $info               = new \finfo(FILEINFO_MIME_TYPE);
279 2
        foreach ($this->attachments as $key => $attachment) {
280 2
            if (! is_file($attachment)) {
281 1
                continue; // If checked file is not valid, continue to the next
282
            }
283
284
            // If the key is a string, use it as the attachment name
285 2
            $basename = is_string($key) ? $key : basename($attachment);
286
287 2
            $part               = new Mime\Part(fopen($attachment, 'r'));
288 2
            $part->id           = $basename;
289 2
            $part->filename     = $basename;
290 2
            $part->type         = $info->file($attachment);
291 2
            $part->encoding     = Mime\Mime::ENCODING_BASE64;
292 2
            $part->disposition  = Mime\Mime::DISPOSITION_ATTACHMENT;
293 2
            $attachmentParts[]  = $part;
294 2
        }
295
296 2
        $body = new Mime\Message();
297 2
        $body->setParts(array_merge($oldParts, $attachmentParts));
298 2
        $this->message->setBody($body);
299 2
    }
300
301
    /**
302
     * Sets the message subject
303
     * @param string $subject The subject of the message
304
     * @return $this Returns this MailService for chaining purposes
305
     * @deprecated Use $mailService->getMessage()->setSubject() instead
306
     */
307
    public function setSubject($subject)
308
    {
309
        $this->message->setSubject($subject);
310
        return $this;
311
    }
312
313
    /**
314
     * @param string $path
315
     * @param string|null $filename
316
     * @return $this
317
     */
318 2 View Code Duplication
    public function addAttachment($path, $filename = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
319
    {
320 2
        if (isset($filename)) {
321 1
            $this->attachments[$filename] = $path;
322 1
        } else {
323 2
            $this->attachments[] = $path;
324
        }
325 2
        return $this;
326
    }
327
328
    /**
329
     * @param array $paths
330
     * @return $this
331
     */
332 12
    public function addAttachments(array $paths)
333
    {
334 12
        return $this->setAttachments(array_merge($this->attachments, $paths));
335
    }
336
337
    /**
338
     * @param array $paths
339
     * @return $this
340
     */
341 14
    public function setAttachments(array $paths)
342
    {
343 14
        $this->attachments = $paths;
344 14
        return $this;
345
    }
346
347
    /**
348
     * Returns the list of attachments
349
     * @return array
350
     */
351 2
    public function getAttachments()
352
    {
353 2
        return $this->attachments;
354
    }
355
356
    /**
357
     * Inject an EventManager instance
358
     * @param EventManagerInterface $events
359
     * @return $this|void
360
     */
361 10
    public function setEventManager(EventManagerInterface $events)
362
    {
363 10
        $events->setIdentifiers([
364 10
            __CLASS__,
365 10
            get_called_class(),
366 10
        ]);
367 10
        $this->events = $events;
368 10
        return $this;
369
    }
370
    /**
371
     * Retrieve the event manager
372
     * Lazy-loads an EventManager instance if none registered.
373
     * @return EventManagerInterface
374
     */
375 10
    public function getEventManager()
376
    {
377 10
        if (! isset($this->events)) {
378 10
            $this->setEventManager(new EventManager());
379 10
        }
380
381 10
        return $this->events;
382
    }
383
384
    /**
385
     * Attaches a new MailListenerInterface
386
     * @param MailListenerInterface $mailListener
387
     * @param int $priority
388
     * @return mixed|void
389
     */
390 4
    public function attachMailListener(MailListenerInterface $mailListener, $priority = 1)
391
    {
392 4
        $this->getEventManager()->attach($mailListener, $priority);
0 ignored issues
show
Documentation introduced by
$mailListener is of type object<AcMailer\Event\MailListenerInterface>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
$priority is of type integer, but the function expects a callable|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
393 4
        return $this;
394
    }
395
396
    /**
397
     * Detaches provided MailListener
398
     * @param MailListenerInterface $mailListener
399
     * @return $this
400
     */
401 1
    public function detachMailListener(MailListenerInterface $mailListener)
402
    {
403 1
        $mailListener->detach($this->getEventManager());
0 ignored issues
show
Bug introduced by
It seems like $this->getEventManager() can be null; however, detach() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
404 1
        return $this;
405
    }
406
407
    /**
408
     * @param TransportInterface $transport
409
     * @return $this
410
     */
411 1
    public function setTransport(TransportInterface $transport)
412
    {
413 1
        $this->transport = $transport;
414 1
        return $this;
415
    }
416
417
    /**
418
     * Returns the transport object that will be used to send the wrapped message
419
     * @return TransportInterface
420
     */
421 5
    public function getTransport()
422
    {
423 5
        return $this->transport;
424
    }
425
426
    /**
427
     * @param RendererInterface $renderer
428
     *
429
     * @return $this
430
     */
431 1
    public function setRenderer(RendererInterface $renderer)
432
    {
433 1
        $this->renderer = $renderer;
434 1
        return $this;
435
    }
436
437
    /**
438
     * Returns the renderer object that will be used to render templates
439
     * @return RendererInterface
440
     */
441 4
    public function getRenderer()
442
    {
443 4
        return $this->renderer;
444
    }
445
}
446