Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Messenger Mailer Bundle Laravel Package

fusonic/messenger-mailer-bundle

Symfony bundle that bridges Mailer and Messenger, letting you dispatch emails as messages and send them asynchronously through your Messenger transport. Configure transports, queue mail instead of blocking requests, and keep email delivery reliable and decoupled from your application flow.

View on GitHub
Deep Wiki
Context7

Getting Started

The bundle exists to solve one problem: Symfony's SendEmailMessage is serialized onto the Messenger transport, and Symfony\Component\Mime\Part\File (the thing Email::attachFromPath() uses) is not serializable. Queue an email with an attachment and you get a serialization exception at dispatch time, or a worker that fails because the file path no longer exists on the consuming machine. This bundle makes attachment-bearing emails survive the round trip.

Minimal path:

  1. composer require fusonic/messenger-mailer-bundle (Flex registers it; otherwise add it to config/bundles.php).
  2. Confirm a Messenger transport exists and that the mailer is actually routed through the bus:
# config/packages/messenger.yaml
framework:
    messenger:
        transports:
            async_mail:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
        routing:
            'Symfony\Component\Mailer\Messenger\SendEmailMessage': async_mail
# config/packages/mailer.yaml
framework:
    mailer:
        dsn: '%env(MAILER_DSN)%'
  1. Send mail exactly as you always have — the bundle takes over attachment handling:
$email = (new Email())
    ->from('[email protected]')
    ->to($customer->getEmail())
    ->subject('Your invoice')
    ->html($this->twig->render('email/invoice.html.twig', ['invoice' => $invoice]))
    ->attachFromPath('/var/app/invoices/'.$invoice->getId().'.pdf', 'invoice.pdf', 'application/pdf');

$this->mailer->send($email); // returns immediately; nothing sent yet
  1. Run a worker: bin/console messenger:consume async_mail -vv.

Where to look first: the README for the supported Symfony/Messenger version matrix, then vendor/fusonic/messenger-mailer-bundle/src/ — specifically the DI extension and whatever handler/middleware it registers for SendEmailMessage. With 6 stars and zero dependents, the source is the documentation; budget 20 minutes to read it before designing around it. Also read tests/ — the fixtures usually reveal the intended attachment semantics (path vs. string vs. stream).

First use case: transactional email with a generated PDF (invoice, ticket, report). This is the sweet spot — the file exists on the web node, the worker is a separate process, and you don't want the HTTP request blocked on SMTP.

Implementation Patterns

Build the email in the controller, send through the mailer. Don't hand-dispatch SendEmailMessage yourself. The bundle's guarantees apply to the mailer pipeline; bypassing it is how you reintroduce the original bug.

Separate "what to send" from "build the email" for anything with mutable state. Two viable shapes:

  • Queue the email (what this bundle enables): simplest, but everything — recipient, body, attachment bytes — is frozen at dispatch time.
  • Queue a small command (SendInvoiceEmail($invoiceId)) and build the Email inside the handler. Attachments are read from disk at consume time, which is better for large files and avoids stale serialized entities.

Use the first for user-triggered one-offs (password resets, receipts); use the second for bulk/scheduled mail.

Dedicate a transport. Mail queues have different failure characteristics than job queues. Give them their own transport with a tuned retry strategy (max_retries: 3, exponential delay) and a failure transport, so a dead SMTP relay doesn't block unrelated async work:

framework:
    messenger:
        failure_transport: failed
        routing:
            'Symfony\Component\Mailer\Messenger\SendEmailMessage': async_mail

Tests stay synchronous. In config/packages/test/messenger.yaml override async_mail with sync:// so feature tests assert against mailer.transport / a profiler mailer assertion as usual. For unit-level tests asserting enqueueing, keep the transport in-memory and inspect it:

$transport = self::getContainer()->get('messenger.transport.async_mail');
$this->assertCount(1, $transport->getSent());

Integration tips that pay off immediately:

  • Send through Twig with ->html($this->twig->render(...)) and keep templates in templates/email/.
  • Attach from an absolute, deterministic path or from a string; never from $_FILES/UploadedFile (it's gone by the time the worker runs — move it to durable storage first).
  • Add a Monolog channel/handler on the messenger worker so send failures are greppable.
  • Deploy the worker with Supervisor or messenger:consume --time-limit=3600 --memory-limit=128M to recycle processes after long-running PHP memory creep.

Gotchas and Tips

Attachment bytes live inside the queued message. Once inlined, a 10 MB PDF becomes ~13 MB of base64 in your Doctrine/Redis transport. Check your transport ceiling before you ship: SQS caps at 256 KB, Redis/AMQP have practical limits, and the Doctrine transport will happily bloat your messenger_messages table. For anything large, store the file (S3, local disk on a shared volume) and attach by path at consume time — or send a short-lived signed URL in the body instead of a file.

Path existence is not guaranteed at consume time. The whole reason this bundle exists is that the worker may be on another host with a different filesystem view. If you keep path-based attachments (queue-a-command pattern), make sure the storage is shared and the file isn't cleaned up by a temp-file reaper between dispatch and consume.

Don't serialize entities. A queued Email built from a Doctrine entity is fine (the email is a value object), but a queued custom message holding an entity will be stale or trigger lazy-loading inside __serialize. Pass scalars/IDs.

Retries re-send. A partially successful send that throws afterwards will be retried and the recipient gets two emails. Keep handlers idempotent, or accept at-least-once semantics explicitly.

Time-sensitive content is frozen at dispatch. "This link expires in 24 hours" signed URLs, OTP codes, and "your order ships today" copy are all computed in the web request. If the queue backs up, recipients get expired links. Generate those inside the handler when it matters.

Debugging checklist when nothing sends:

  1. Is framework.mailer actually routing through the bus? If message_bus is unset/null, the bundle is inert and mail is synchronous — a common silent misconfiguration.
  2. bin/console debug:messenger — confirm a handler exists for SendEmailMessage.
  3. bin/console messenger:failed:show and messenger:failed:retry for anything that blew up in the worker.
  4. Dump a raw row (SELECT body FROM messenger_messages) to verify the attachment was inlined as a string and not left as a path reference.
  5. Flip the transport to sync:// temporarily — if it works synchronously, the problem is serialization/worker-side, not the mailer.

Extension points. If the bundle's behavior doesn't fit (custom storage, size limits, S3 offloading), don't fork it — decorate the SendEmailMessage handler, or add a Messenger middleware / SendMessageToTransportsEvent listener that rewrites attachments before the envelope is serialized. That's the same seam the bundle uses, and it keeps your logic portable.

Adoption risk is real. Six stars, zero dependents, one maintainer org. Pin the exact version in composer.json, read the source before upgrading Symfony major versions, and keep your mailer calls behind a thin wrapper service so you can drop the bundle in an afternoon by inlining attachments yourself:

new DataPart(file_get_contents($path), 'invoice.pdf', 'application/pdf')

That one-liner is the entire fallback strategy — worth knowing before you depend on the package in a critical path.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource