Skip to content

Commit fabbdfc

Browse files
authored
feat(SendIt): Add pre and post render events (#1233)
1 parent 261523e commit fabbdfc

File tree

4 files changed

+149
-8
lines changed

4 files changed

+149
-8
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\SendIt\Event;
6+
7+
use Spiral\Mailer\MessageInterface;
8+
use Symfony\Component\Mime\Email;
9+
use Symfony\Contracts\EventDispatcher\Event;
10+
11+
/**
12+
* Event triggered after the email content is rendered but before it is sent.
13+
*
14+
* Unlike {@see PreRender}, this event is dispatched when the email has already been transformed
15+
* into its final form (e.g., templates applied, HTML generated). Listeners can access the rendered
16+
* content but should avoid modifying it unless necessary (e.g., adding debug headers).
17+
*
18+
* Typical use cases:
19+
* - Logging the final email content
20+
* - Adding transport-specific headers
21+
* - Conditional last-minute modifications
22+
*
23+
* Example:
24+
* ```php
25+
* $dispatcher->addListener(PostRender::class, function (PostRender $event) {
26+
* $event->email->getHeaders()->addTextHeader('X-Debug', '1');
27+
* });
28+
* ```
29+
*/
30+
final class PostRender extends Event
31+
{
32+
public function __construct(
33+
public readonly MessageInterface $message,
34+
public readonly Email $email,
35+
) {}
36+
}

src/SendIt/src/Event/PreRender.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\SendIt\Event;
6+
7+
use Spiral\Mailer\MessageInterface;
8+
use Symfony\Component\Mime\Email;
9+
use Symfony\Contracts\EventDispatcher\Event;
10+
11+
/**
12+
* Event triggered before the email content is rendered.
13+
*
14+
* This event allows you to modify the {@see Email} object (e.g., headers, body, or attachments)
15+
* before it is passed to the mailer for sending. The original {@see MessageInterface} is provided
16+
* for context but should not be modified.
17+
*
18+
* Example usage (listener):
19+
* ```
20+
* $dispatcher->addListener(PreRender::class, function (PreRender $event) {
21+
* $event->email->addHeader('X-Custom', 'value');
22+
* });
23+
* ```
24+
*/
25+
final class PreRender extends Event
26+
{
27+
public function __construct(
28+
public readonly MessageInterface $message,
29+
public readonly Email $email,
30+
) {}
31+
}

src/SendIt/src/Renderer/ViewRenderer.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
namespace Spiral\SendIt\Renderer;
66

7+
use Psr\EventDispatcher\EventDispatcherInterface;
78
use Spiral\Mailer\Exception\MailerException;
89
use Spiral\Mailer\MessageInterface;
10+
use Spiral\SendIt\Event\PostRender;
11+
use Spiral\SendIt\Event\PreRender;
912
use Spiral\SendIt\RendererInterface;
1013
use Spiral\Views\Exception\ViewException;
1114
use Spiral\Views\ViewsInterface;
@@ -15,6 +18,7 @@ final class ViewRenderer implements RendererInterface
1518
{
1619
public function __construct(
1720
private readonly ViewsInterface $views,
21+
private readonly ?EventDispatcherInterface $eventDispatcher = null,
1822
) {}
1923

2024
/**
@@ -51,23 +55,26 @@ public function render(MessageInterface $message): Email
5155
);
5256
}
5357

54-
$msg = new Email();
58+
$email = new Email();
5559

5660
if ($message->getFrom() !== null) {
57-
$msg->from($message->getFrom());
61+
$email->from($message->getFrom());
5862
}
5963

6064
if ($message->getReplyTo() !== null) {
61-
$msg->replyTo($message->getReplyTo());
65+
$email->replyTo($message->getReplyTo());
6266
}
6367

64-
$msg->to(...$message->getTo());
65-
$msg->cc(...$message->getCC());
66-
$msg->bcc(...$message->getBCC());
68+
$email->to(...$message->getTo());
69+
$email->cc(...$message->getCC());
70+
$email->bcc(...$message->getBCC());
6771

6872
try {
73+
$cloneMessage = clone $message;
74+
$this->eventDispatcher?->dispatch(new PreRender(message: $cloneMessage, email: $email));
6975
// render message partials
70-
$view->render(\array_merge(['_msg_' => $msg], $message->getData()));
76+
$view->render(\array_merge(['_msg_' => $email], $cloneMessage->getData()));
77+
$this->eventDispatcher?->dispatch(new PostRender(message: $cloneMessage, email: $email));
7178
} catch (ViewException $e) {
7279
throw new MailerException(
7380
\sprintf('Unable to render email `%s`: %s', $message->getSubject(), $e->getMessage()),
@@ -76,6 +83,6 @@ public function render(MessageInterface $message): Email
7683
);
7784
}
7885

79-
return $msg;
86+
return $email;
8087
}
8188
}

src/SendIt/tests/ViewRendererTest.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,85 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace Spiral\Tests\SendIt;
46

57
use PHPUnit\Framework\TestCase;
8+
use Spiral\Mailer\Message;
69
use Spiral\Mailer\MessageInterface;
10+
use Spiral\SendIt\Event\PostRender;
11+
use Spiral\SendIt\Event\PreRender;
712
use Spiral\SendIt\Renderer\ViewRenderer;
813
use Spiral\Views\ViewInterface;
914
use Spiral\Views\ViewsInterface;
15+
use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher;
16+
use Symfony\Component\EventDispatcher\EventDispatcher;
17+
use Symfony\Component\Stopwatch\Stopwatch;
1018

1119
/**
1220
* @coversDefaultClass \Spiral\SendIt\Renderer\ViewRenderer
1321
*/
1422
final class ViewRendererTest extends TestCase
1523
{
24+
/**
25+
* Checking for event hits and processing by listeners without changing the original $message
26+
*
27+
* @covers ::render
28+
*/
29+
public function testRender(): void
30+
{
31+
$view = $this->createMock(ViewInterface::class);
32+
$view->expects(self::once())->method('render');
33+
34+
$views = $this->createMock(ViewsInterface::class);
35+
$views->expects(self::once())->method('get')->willReturn($view);
36+
37+
$message = new Message('subject has not been changed', 'to@mail.test');
38+
$beforeHash = \spl_object_hash($message);
39+
40+
$eventDispatcher = new TraceableEventDispatcher(
41+
new EventDispatcher(),
42+
new Stopwatch(),
43+
);
44+
45+
$eventDispatcher->addListener(PreRender::class, static function (PreRender $event): void {
46+
$message = $event->message;
47+
$message->setSubject('subject1');
48+
});
49+
$eventDispatcher->addListener(PostRender::class, static function (PostRender $event): void {
50+
$event->message->setSubject('subject2');
51+
});
52+
53+
$target = new ViewRenderer($views, $eventDispatcher);
54+
$msg = $target->render($message);
55+
56+
$afterHash = \spl_object_hash($message);
57+
58+
self::assertSame($beforeHash, $afterHash);
59+
self::assertSame(['To: to@mail.test'], $msg->getHeaders()->toArray());
60+
self::assertCount(2, $eventDispatcher->getCalledListeners());
61+
self::assertSame('subject has not been changed', $message->getSubject());
62+
}
63+
64+
/**
65+
* Health check when EventDispatcher = null.
66+
*
67+
* @covers ::render
68+
*/
69+
public function testRenderWithoutDispatcher(): void
70+
{
71+
$view = $this->createMock(ViewInterface::class);
72+
$view->expects(self::once())->method('render');
73+
74+
$views = $this->createMock(ViewsInterface::class);
75+
$views->expects(self::once())->method('get')->willReturn($view);
76+
77+
$message = new Message('subject has not been changed', 'to@mail.test');
78+
79+
$target = new ViewRenderer($views);
80+
$target->render($message);
81+
}
82+
1683
/**
1784
* Check that all added headers are added to the final object.
1885
*

0 commit comments

Comments
 (0)