Install the Package
composer require draw/mailer
Ensure draw/core is also installed (dependency).
Configure Default from Address
Add to config/packages/draw_post_office.yaml:
draw_post_office:
default_from: 'noreply@example.com'
Create Your First Email Class
Place in src/Email/ForgotPasswordEmail.php:
namespace App\Email;
use Symfony\Component\Mime\Email;
class ForgotPasswordEmail extends Email
{
public function __construct(private string $email) {}
public function getEmail(): string { return $this->email; }
}
Create a Writer
Place in src/Email/ForgotPasswordEmailWriter.php:
namespace App\Email;
use Draw\Component\Mailer\EmailWriter\EmailWriterInterface;
class ForgotPasswordEmailWriter implements EmailWriterInterface
{
public static function getForEmails(): array { return ['compose']; }
public function compose(ForgotPasswordEmail $email)
{
$email->to($email->getEmail())
->subject('Reset Password')
->html('<p>Click <a href="#">here</a> to reset.</p>');
}
}
Send the Email In a controller:
use App\Email\ForgotPasswordEmail;
use Symfony\Component\Mailer\MailerInterface;
public function sendForgotPassword(MailerInterface $mailer)
{
$mailer->send(new ForgotPasswordEmail('user@example.com'));
}
src/Email/: Store all email classes here.config/packages/draw_post_office.yaml: Configure defaults.EmailWriterInterface: Reference for writer contracts.DefaultFromEmailWriter: Example of a global writer.Symfony\Component\Mime\Email or TemplatedEmail for Twig templates.getEmail()).getForEmails() to register methods with optional priority:
// Default priority (0)
public static function getForEmails(): array { return ['compose']; }
// Custom priority (e.g., 100 for higher precedence)
public static function getForEmails(): array { return ['compose' => 100]; }
htmlTemplate() with relative paths (e.g., 'emails/forgot_password.html.twig').context():
$email->htmlTemplate('emails/welcome.html.twig')
->context(['name' => $user->getName()]);
from: Configured via draw_post_office.default_from.DefaultFromEmailWriter to modify all emails:
class GlobalEmailWriter implements EmailWriterInterface
{
public static function getForEmails(): array { return ['*' => 100]; } // Catch-all
public function __invoke(Email $email)
{
$email->replyTo('support@example.com');
}
}
Mailer mock:
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects($this->once())->method('send');
MessageEvent to trigger writers.Envelope as the second argument if needed:
public function compose(ForgotPasswordEmail $email, Envelope $envelope)
{
// Customize envelope (e.g., headers, priority)
}
Writer Registration Order
from) have higher priority.getForEmails():
return ['compose' => 100]; // Higher than default (0)
Circular Dependencies
services.yaml:
services:
App\Email\ForgotPasswordEmailWriter:
arguments:
$lostPasswordTokenProvider: '@app.lost_password_token_provider'
Twig Template Not Found
TemplateNotFoundException.twig.paths in config/packages/twig.yaml:
twig:
paths: ['%kernel.project_dir%/templates']
Email Class Not Matched
getForEmails() returns the correct method name and the email class matches the method’s first argument type.Symfony Mailer Version Mismatch
symfony/mailer to a stable version in composer.json:
"symfony/mailer": "6.4.*"
Log Writer Calls Add debug logs in writers to verify execution:
public function compose(ForgotPasswordEmail $email)
{
\Log::debug('ForgotPasswordEmailWriter called', ['email' => $email->getEmail()]);
// ...
}
Check Registered Writers Dump the registered writers in a command or controller:
use Draw\Component\Mailer\EmailWriter\EmailWriterRegistry;
public function debugWriters(EmailWriterRegistry $registry)
{
\dd($registry->getWriters());
}
Validate Email Content
Use Symfony’s Email dumping for debugging:
$email = new ForgotPasswordEmail('test@example.com');
$writer->compose($email);
\dd($email->getHeaders()->get('To')->getFieldBody());
Custom Email Types
Extend Email or TemplatedEmail for domain-specific emails (e.g., NewsletterEmail).
Dynamic Writers
Implement EmailWriterInterface dynamically (e.g., based on runtime conditions):
class ConditionalEmailWriter implements EmailWriterInterface
{
public static function getForEmails(): array { return ['compose']; }
public function compose(Email $email)
{
if ($someCondition) {
// Custom logic
}
}
}
Event Listeners
Hook into MessageEvent to modify emails before sending:
use Symfony\Component\Mailer\Event\MessageEvent;
public function onMessage(MessageEvent $event)
{
$email = $event->getMessage();
$email->getHeaders()->addTextHeader('X-Custom', 'value');
}
Testing Helpers Create a test trait to assert email content:
trait AssertsEmail
{
public function assertEmailSent(MailerInterface $mailer, Email $expectedEmail)
{
$sent = $mailer->getSent();
$this->assertCount(1, $sent);
$this->assertEquals($expectedEmail->getSubject(), $sent[0]->getSubject());
}
}
Performance
How can I help you explore Laravel packages today?