Install the Bundle
composer require draw/post-office-bundle
Add to config/bundles.php:
return [
// ...
Draw\Bundle\PostOfficeBundle\PostOfficeBundle::class => ['all' => true],
];
Configure Default from Address
In config/packages/draw_post_office.yaml:
draw_post_office:
default_from: 'support@example.com'
Create Your First Email Class
Place in src/Email/ForgotPasswordEmail.php:
<?php
namespace App\Email;
use Symfony\Component\Mime\Email;
class ForgotPasswordEmail extends Email
{
public function __construct(string $to, string $resetLink)
{
$this->to($to)
->subject('Reset Your Password')
->html('<p>Click <a href="' . $resetLink . '">here</a> to reset your password.</p>');
}
}
Create a Writer Service
Place in src/Email/ForgotPasswordEmailWriter.php:
<?php
namespace App\Email;
use Draw\Bundle\PostOfficeBundle\Email\EmailWriterInterface;
class ForgotPasswordEmailWriter implements EmailWriterInterface
{
public function getForEmails(): array
{
return [
'sendForgotPasswordEmail' => 10, // Priority (higher = called first)
];
}
public function sendForgotPasswordEmail(ForgotPasswordEmail $email)
{
// Custom logic (e.g., logging, analytics) before sending
return $email;
}
}
Register the Writer as a Service
In config/services.yaml:
services:
App\Email\ForgotPasswordEmailWriter:
tags:
- { name: 'draw.post_office.email_writer' }
Trigger the Email in a Controller
use Symfony\Component\Mailer\MailerInterface;
class ForgotPasswordController extends AbstractController
{
public function sendResetLink(User $user, MailerInterface $mailer): Response
{
$email = new ForgotPasswordEmail($user->getEmail(), $this->generateResetLink($user));
$mailer->send($email);
return $this->redirectToRoute('home');
}
}
Symfony\Component\Mime\Email for all custom emails.to, resetLink) to the email class.src/Email/ for clarity.public function getForEmails(): array
{
return [
'handlePriorityEmail' => 20, // Higher priority
'handleDefaultEmail' => 0, // Default priority
];
}
// Only called if the first argument is `ForgotPasswordEmail`
public function sendForgotPasswordEmail(ForgotPasswordEmail $email) { ... }
public function sendForgotPasswordEmail(ForgotPasswordEmail $email)
{
$email->text('Plaintext fallback');
return $email;
}
Symfony\Component\Mailer\Event\MessageEvent.from Address: Override globally in config or per-email:
$email->from('custom@example.com');
MailerInterface with async transport (e.g., symfony/mailer + doctrine/doctrine-bundle for queues).$this->container->set('App\Email\ForgotPasswordEmailWriter', $mockWriter);
Symfony\Component\Mime\Email assertions:
$this->assertEquals('support@example.com', $email->getFrom());
class EmailFactory
{
public function createForgotPasswordEmail(string $to, string $resetLink): ForgotPasswordEmail
{
return new ForgotPasswordEmail($to, $resetLink);
}
}
Priority Collisions
Missing EmailWriterInterface Tag
draw.post_office.email_writer or they won’t register.config/services.yaml or use autoconfiguration.Symfony Mailer Compatibility
symfony/mailer to a stable version in composer.json.Circular Dependencies
MailerInterface into writers (can cause loops).Email object directly to writers.Default from Overrides
from addresses override the global config, but invalid addresses may fail silently.from addresses in writers:
if (!$email->getFrom()) {
$email->from('fallback@example.com');
}
Log Writer Calls Add a debug writer to trace execution:
class DebugEmailWriter implements EmailWriterInterface
{
public function getForEmails(): array { return ['debug' => -1000]; }
public function debug(Email $email)
{
\Log::debug('Email sent:', [
'to' => $email->getTo(),
'subject' => $email->getSubject(),
]);
return $email;
}
}
Check Registered Writers Dump the registered writers in a controller:
$writers = $container->get('draw.post_office.email_writer.collection');
\dump($writers->getWriters());
Validate Email Events
Use Symfony’s event dispatcher to inspect the MessageEvent:
$event = new MessageEvent($mailer, $email);
\dump($event->getMessage());
Custom Email Validation Add a writer to validate emails before sending:
public function validateEmail(Email $email)
{
if (empty($email->getTo())) {
throw new \RuntimeException('Recipient email is missing!');
}
return $email;
}
Template-Based Emails
Combine with twig/mailer for dynamic templates:
$email->html($this->renderView('emails/forgot_password.html.twig', ['link' => $resetLink]));
Bulk Email Handling Use a writer to batch emails (e.g., for newsletters):
public function sendBulkEmail(BulkEmail $email)
{
foreach ($email->getRecipients() as $recipient) {
$mailer->send(clone $email->withTo($recipient));
}
}
Analytics Integration Track email opens/clicks by modifying the email HTML:
public function addTracking(ForgotPasswordEmail $email)
{
$email->html(
$email->getHtml() .
'<img src="https://tracker.example.com/pixel?email=' . urlencode($email->getTo()) . '" width="1" height="1" />'
);
}
How can I help you explore Laravel packages today?