grizzlylab/mailer-bundle
GrizzlyLab MailerBundle is a Symfony bundle that integrates mailer services into PHP applications, offering straightforward configuration for sending transactional email and managing mail delivery from your Symfony project. It helps simplify mailer setup and keep email configuration centralized.
Step 0 — confirm the platform before writing code. This is a Symfony 2/3 bundle ("type": "symfony-bundle"). It boots through AppKernel::registerBundles() and defines its services against Symfony's DI container. There is no Laravel service provider, facade, or publishable config, so composer require grizzlylab/mailer-bundle in a Laravel 9+ app will at best resolve nothing useful and at worst fight symfony/mailer constraints in the lockfile. Do that check first; it takes ten minutes and saves a day.
Where to look first (in order):
composer.json — the require block is the real documentation. It tells you the pinned symfony/* and mailer (Swiftmailer-era) versions, which determines whether the bundle can coexist with anything current.DependencyInjection/Configuration.php — the authoritative list of config keys. With 3 stars and a thin README, this file is more trustworthy than the docs. It defines the tree for transports, default sender, and template paths.Resources/config/services.yml — the service IDs and aliases you'll inject. This is what you actually call day-to-day.Registering it (native Symfony 2/3 only):
// app/AppKernel.php
$bundles = [
// ...
new GrizzlyLab\MailerBundle\GrizzlyLabMailerBundle(),
];
Then configure under the bundle's root key in app/config/config.yml (verify the exact key name in Configuration.php — it is not guaranteed to match the vendor/package name).
First use case to target: the one thing this bundle exists to remove — boilerplate for templated transactional mail with a single configured sender. Welcome email, password reset, receipt. The win is: transport credentials and from address live in YAML, the body lives in a Twig template with a shared layout, and the controller is one call instead of twenty lines of message assembly.
If you are a Laravel developer, the correct first step is the port, not the install. Reproduce the bundle's intent natively in about an hour:
| Bundle concept | Laravel equivalent |
|---|---|
Transport + credentials in config.yml |
MAIL_MAILER, MAIL_HOST, MAIL_FROM_ADDRESS in .env + config/mail.php |
| Default sender configured once | 'from' => ['address' => ..., 'name' => ...] in config/mail.php |
| Twig template + shared layout | Mailable + Blade view in resources/views/emails with <x-mail::layout> or a base Blade layout |
| Mailer service injected | Mail facade, or inject Illuminate\Contracts\Mail\Mailer |
| Bundle "testing" API | Mail::fake() + Mail::assertSent() |
Pattern 1 — config-first transport, service-injected send. The intended day-to-day shape. Transports and the default sender are declared once in YAML; application code injects the bundle's mailer service and never touches Swift_Message construction or setFrom().
# app/config/config.yml (verify keys against Configuration.php)
grizzly_lab_mailer:
from: { address: [email protected], name: "Example" }
transport: smtp
# host/port/username/password, template paths...
The payoff is that moving from SMTP to a provider is a config diff, not a code sweep. Preserve that property in a Laravel port: keep everything configurable in config/mail.php, and never hardcode a from inside a Mailable.
Pattern 2 — template-driven composition with a shared layout. A single base Twig template holds the HTML skeleton (header, footer, wrapper table, inline styles); each message extends it and overrides content blocks. Day-to-day this means adding a new transactional email is "add one Twig file + one send call," and brand changes are one file. Keep the block names stable — they're your internal API. Mirror this with a single resources/views/emails/layout.blade.php on Laravel.
Pattern 3 — queue via the spool, not via the bundle. The bundle advertises queueing, but on Symfony 2/3 that almost certainly means it cooperates with Swiftmailer's spool, not a real job queue. Typical wiring:
swiftmailer:
spool: { type: memory } # flushed on kernel.terminate
The practical workflow: use the memory spool so the HTTP response returns before SMTP completes; switch to a file spool plus a cron consumer when volume grows. Treat this as "defer the socket write," not "durable delivery" — there are no retries or backoff. The Laravel equivalent is ->queue() on a Mailable with a real queue driver, which is strictly better; if you port, take the Laravel path.
Pattern 4 — test double at the mailer boundary. In tests, the goal is to assert that mail was sent with the right template and params, never to hit SMTP. In Symfony, override the transport in config_test.yml to a null/log transport and assert on the collected messages (or read the profiler's mailer panel). In Laravel: Mail::fake() and Mail::assertSent(WelcomeMail::class, fn ($m) => ...). Keep assertions on the template name + view data, not on rendered HTML — the layout will change and the test shouldn't.
Pattern 5 — if you must bridge legacy to Laravel, isolate at the seam. Do not attempt to load Symfony services into Laravel's container. Either (a) put the legacy sender behind HTTP/queue and call it from Laravel, or (b) extract only the transport class — Symfony transports implement Symfony\Component\Mailer\Transport\TransportInterface, which Laravel 9+ consumes — and register it with Mail::extend(). Drop the bundle/DI scaffolding entirely.
Migration mapping when porting a call site:
| Bundle call shape | Mailable shape |
|---|---|
send('Emails/welcome', $params, $to) |
Mail::to($to)->send(new WelcomeMail($user)) |
| Template path in config | WelcomeMail::content()->view('emails.welcome') |
| Shared Twig layout | Base Blade layout / Markdown mailable |
YAML from |
WelcomeMail::envelope()->from(...) or global config |
symfony/framework-bundle pins will conflict with PHP 8.1+/Symfony 6+ requirements. If a legacy app must have it, give that app its own composer.lock and never let it share a monorepo lockfile with modern services.kernel.terminate and is lost if the process dies mid-request; a file spool needs an external consumer you have to provision and monitor. No retries, no backoff, no dead-letter. Do not let a reliability roadmap item land on this bundle.swiftmailer.delivery_addresses, disable_delivery) that can make mail look like it worked when it didn't. Check both the bundle config and the app-level swiftmailer config, and enable the mailer logger plugin in dev so every send is visible in dev.log.from typing. Mailer configs commonly want { address: ..., name: ... } rather than a bare string, and %parameter% resolution happens at container compile time. A malformed from usually surfaces as a container compile error, not a send-time error — read the full exception, not just the first line.|raw for pre-rendered fragments; overusing it to silence escaping bugs is how you ship injection in a mail template. Prefer passing already-safe values in.services.yml) and not a bare \Swift_Mailer.app/config/services.yml works until the bundle changes its alias. Prefer decorating the service, or better, don't customize — keep the bundle as a thin config wrapper and put logic in your own service.How can I help you explore Laravel packages today?