Closed samuelgfeller closed 3 years ago
With all due respect, but PHPMailer is a pretty outdated package. It was written at a time when TDD was not as popular as it is today. If you still want to use PHPMailer, then you may wrap it behind a custom MailerInterface with a public send
method. Then you could replace that mailer with a NullMailer or so.
Better would be to use the Symfony Mailer component, because you can replace the Transport class with a NullTransport adapter for testing purposes.
The Laminas Mailer offers the same concept for testing.
Interesting, I thought that it was still modern as there are many recent commits.
Between Symfony mailer and Laminas mailer, is there one you recommend over the other or are they very similar and similarly well maintained?
P.S. The answer to this question would be a valuable addition in your blog article in my opinion as well as maybe a note on PHPMailer
Symfony mailer and Laminas mailer are very good and well maintained, but the Symfony mailer has a bigger community. The Symfony mailer provides Twig integration and some more Transport / Auth possibilities, while the Laminas mailer feels more lightweight to me.
Alright, thank you.
I have now installed symfony mailer and added the null transport to env.testing.php
. How do I test the emails from here?
Awesome would be to have these assertions. I grabbed the MailerAssertionsTrait.php from the Symfony repo which I added to my test traits.
Important there though is the function getMessageMailerEvents() which returns the event with which RawMessage
can be extracted and later asserted.
The function seems to take it from some kind of logger as it attempts to get 'mailer.message_logger_listener'
from the Symfony container $container->get('mailer.message_logger_listener')
.
I don't have a Symfony container though and and if I try to take those strings there's an error saying that there is no entry in my container.
Do I have to make my own NullTransport
which extends AbstractTransport
and writes the content I want to assert in a logfile in the function doSend
? Because the original NullTransport
does not do anything in its doSend
function. That would be pretty inpractical as I would have to manually write the content I want to assert in a way I can extract them easily for the assertion later.
Better would be if there is a way I can access the RawMessage
object and use the Symfony assert functions with it.
I never tested it so far, because for me, it doesn't matter how many emails has been sent, because I assume that the prod environment is properly configured for SMTP. So the NullTransport was always good enough for me.
For testing you could configure null
as adapter, for example: null://user:pass@smtp.example.com:25
The NullTransport extends from AbstractTransport. So the EventDispatcherInterface should also work because the send method triggers the MessageEvent to the dispatcher.
Symfony specific container keys like mailer.message_logger_listener
will not work. In your case, you have to add a DI container definition for Symfony\Component\Mailer\Transport\TransportInterface::class
instead and use that interface within your MailerInterface::class
DI container definition, e.g. $container->get(TransportInterface::class);
I never tested it so far, because for me, it doesn't matter how many emails has been sent, because I assume that the prod environment is properly configured for SMTP. So the NullTransport was always good enough for me.
I'm not specifically interested in the amount of emails being sent but more their content, subject and headers. All assertions in MailerAssertionsTrait work with a RawMessage
object and the way to grab that object is to use the $this->getMailerMessage(0)
function which takes it from the event.
you have to add a DI container definition for Symfony\Component\Mailer\Transport\TransportInterface::class instead and use that interface within your MailerInterface::class DI container definition, e.g. $container->get(TransportInterface::class);
I have to admit, I don't think that I fully understood that part. My container looks like this now
// SMTP transport
MailerInterface::class => function (ContainerInterface $container) {
return new Mailer($container->get(TransportInterface::class));
},
// Mailer transport interface
TransportInterface::class => function (ContainerInterface $container) {
$settings = $container->get('settings')['smtp'];
// smtp://user:pass@smtp.example.com:25
$dsn =
sprintf(
'%s://%s:%s@%s:%s',
$settings['type'],
$settings['username'],
$settings['password'],
$settings['host'],
$settings['port']
);
return Transport::fromDsn($dsn);
},
Is that correct?
Now when trying to access that RawMessage
object it fails
/** @var ?RawMessage $email */
$email = $this->container->get(TransportInterface::class)->getEvents()->getMessages(null);
// Error : Call to undefined method Symfony\Component\Mailer\Transport\NullTransport::getEvents()
For context, here are the Symfony functions:
public static function getMailerMessages(string $transport = null): array
{
return self::getMessageMailerEvents()->getMessages($transport);
}
public static function getMailerMessage(int $index = 0, string $transport = null): ?RawMessage
{
return self::getMailerMessages($transport)[$index] ?? null;
}
private static function getMessageMailerEvents(): MessageEvents
{
$container = static::getContainer();
if ($container->has('mailer.message_logger_listener')) {
return $container->get('mailer.message_logger_listener')->getEvents();
}
if ($container->has('mailer.logger_message_listener')) {
return $container->get('mailer.logger_message_listener')->getEvents();
}
static::fail('A client must have Mailer enabled to make email assertions. Did you forget to require symfony/mailer?');
}
The DI container definition looks quite correct. Then also add/change and this definition/s:
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Mailer\EventListener\EnvelopeListener;
use Symfony\Component\Mailer\EventListener\MessageListener;
use Symfony\Component\Mailer\EventListener\MessageLoggerListener;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Transport\TransportInterface;
// ...
// Mailer
MailerInterface::class => function (ContainerInterface $container) {
return new Mailer($container->get(TransportInterface::class));
},
// Mailer transport
TransportInterface::class => function (ContainerInterface $container) {
$settings = $container->get('settings')['smtp'];
// smtp://user:pass@smtp.example.com:25
$dsn = sprintf(
'%s://%s:%s@%s:%s',
$settings['type'],
$settings['username'],
$settings['password'],
$settings['host'],
$settings['port']
);
$eventDispatcher = $container->get(EventDispatcherInterface::class);
return Transport::fromDsn($dsn, $eventDispatcher);
},
EventDispatcherInterface::class => function () {
$eventDispatcher = new EventDispatcher();
$eventDispatcher->addSubscriber(new MessageListener());
$eventDispatcher->addSubscriber(new EnvelopeListener());
$eventDispatcher->addSubscriber(new MessageLoggerListener());
return $eventDispatcher;
},
// Error : Call to undefined method Symfony\Component\Mailer\Transport\NullTransport::getEvents()
Makes sense because there is no getEvents method.
Now when trying to access that RawMessage object it fails
I would try to refactor the getMessageMailerEvents
method like this to get the events:
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Mailer\EventListener\MessageLoggerListener;
use Symfony\Component\Mailer\Event\MessageEvents;
use RuntimeException;
// ...
private function getMessageMailerEvents(): MessageEvents
{
$dispatcher = $this->container->get(EventDispatcherInterface::class)
/** @var EventSubscriberInterface[] $listeners */
foreach ($dispatcher->getListeners() as $listeners) {
foreach ($listeners as $listener) {
$listenerInstance = $listener[0];
if (!$listenerInstance instanceof MessageLoggerListener) {
continue;
}
return $listenerInstance->getEvents();
}
}
throw new RuntimeException('The Mailer event dispatcher must be enabled to make email assertions.');
}
The DI container definition looks quite correct.
Yay!
I would try to refactor the getMessageMailerEvents method like this to get the events:
This is amazing! A thousand thanks for this implementation.
I would definitely have struggled a lot especially with those event subscribers I don't think that I would have been able to find that out, even with hours of trying.
I admire your ability a lot and hope to get to this point once.
I'm using PHPMailer and the best (and only free / open source) way I found on how to test emails was this article from codeception written in 2013 which uses MailCatcher.
Although I like the functionality, I don't feel too well having this additional dependency on the testing environment which also needs ruby and gem installed.
I guess the best way I can imagine would be, if I could tell PHPMailer in the testing env configuration to write emails to a specific file, so I could assert its content later instead of really sending them out. The only thing I found does not work for me, I think because I'm using
smtp
and notsendmail
right?Curiously, I hardly found anything on this topic on the internet, and I'm unsure why. Probably I'm just searching wrongly I guess.