hautzi/system-mail-bundle
Symfony bundle for sending system-generated emails with configurable templates and mail transport integration. Use a simple service API to dispatch transactional notifications, confirmations, and other automated messages from your application.
The first thing to internalise is that this is a Symfony bundle, not a Laravel package — there is no service provider, no facade, no config/system-mail.php. A Laravel developer's job here is usually one of two things: (a) you're being asked to work in a Symfony codebase that uses it, or (b) you're evaluating the pattern to port into a Laravel app. Both paths start in the same place.
Install and register (Symfony 3.x/4.x era):
composer require hautzi/system-mail-bundle
// app/AppKernel.php (SF3) or config/bundles.php (SF4)
$bundles[] = new Hautzi\SystemMailBundle\HautziSystemMailBundle();
Check the bundle's composer.json constraints before you run anything — the last tagged release is 2018-12-12, so expect PHP 7.x and Symfony 3/4 pins. On a modern Symfony 5/6/7 app composer require will fail resolution outright; that's the signal to stop and vendor the pattern instead of the package.
Where to look first, in order:
README.md — the only real documentation; read the usage snippet line by line, it's short.DependencyInjection/ — the service ID you'll inject (something like hautzi.system_mail) and the config tree keys.Resources/config/ — the service definitions; this tells you what the mailer actually depends on (SwiftMailer mailer, translator, templating).Your first use case is almost always a transactional mail you already have in a Laravel app: password reset, welcome, order confirmation. Pick one, define a translation key pair for it, and send it to yourself. The mental model is:
a mail type (a string key) + a recipient + parameters + a locale → a fully rendered, localised email.
If that framing sounds familiar, that's because it's Laravel's Mail::to($user)->locale($user->locale)->send(new WelcomeMail($user)) with the blade view replaced by translation strings. Keep that mapping in your head throughout; it makes the Symfony API legible.
1. Mail types as translation keys, not classes. The bundle's design pushes you away from one Mailable per email and toward a flat registry of named mails. In Symfony you end up with something like:
// translations/system_mail.en.yml
welcome:
subject: "Welcome, %name%"
body: "Hi %name%, your account is ready. Confirm at %url%"
password_reset:
subject: "Reset your password"
body: "Use this link within 24h: %url%"
$systemMail->send('welcome', $user->getEmail(), [
'%name%' => $user->getFirstName(),
'%url%' => $this->generateUrl('confirm', [], UrlGeneratorInterface::ABSOLUTE_URL),
], $user->getLocale());
This is a good pattern for non-developer-editable mail: copy changes go to a translator or PM editing YAML, not to a developer editing Twig. That's the trade-off versus Laravel Mailable classes — you lose per-mail logic and gain editorial control and uniform localisation.
2. Locale resolution is the interesting part. Decide deliberately between $request->getLocale() (sender's locale — almost always wrong for system mail) and the recipient's stored locale column. For a Laravel port this is exactly Mailable::locale() or Notification::locale(); in Symfony you pass it as an argument. Make the recipient locale the default in your own wrapper service so callers can't get it wrong:
final class AppMailer
{
public function __construct(private SystemMail $inner) {}
public function toUser(string $type, User $user, array $params = []): void
{
$this->inner->send($type, $user->getEmail(), $params, $user->getLocale());
}
}
This thin wrapper is also where you'll later swap SwiftMailer for Symfony Mailer, add logging, or add a "do not send in dev to real customers" guard — all without touching call sites.
3. Keep HTML and plaintext as separate keys. The bundle renders whatever the catalogue gives it; if the body is HTML, the parameters are interpolated into HTML. Always provide a text variant and prefer %name%-style placeholders over raw concatenation so you have one place to escape.
4. Queueing via transport config, not the bundle. Because the bundle ultimately hands a message to the Symfony mailer, swiftmailer: { spool: { type: memory } } + a kernel.terminate flush, or a Messenger transport, gives you async delivery for free. Don't try to build queueing into the mail-type layer.
5. Testing. Point swiftmailer at MailHog/Mailcatcher in config_test.yml and assert on the profiler's mailer panel, or grab the spool directly in a kernel test. For the Laravel equivalent you get Mail::fake() + Mail::assertSent(WelcomeMail::class, fn ($m) => $m->locale === 'de') — strictly better ergonomics, and worth noting when you argue for porting rather than adopting.
6. Integration tips for a hybrid team. If Symfony and Laravel services coexist, don't share YAML/JSON catalogues by symlinking; export system_mail.*.yml as flat JSON and load it in Laravel with a custom FileLoader registered on the translator. Shared placeholder conventions (%name% vs :name) are the usual source of silent breakage — normalise at the boundary.
%name% in an HTML body is stored XSS in an email — and worse, header injection if the bundle also interpolates the subject. Escape parameters yourself before passing them in; don't rely on the bundle.bin/console cache:clear (and check translations/ is actually where the bundle's loader looks — a wrong directory fails silently, rendering the raw key or an empty body).framework.default_locale + translator.fallbacks interact with the recipient locale in ways the README doesn't spell out; test a de_AT recipient explicitly rather than assuming de fallback.Resources/views/ templates via app/Resources/HautziSystemMailBundle/ (SF3) or templates/bundles/... (SF4+), and inject a custom translator. There's no event system for "before send", so add your own via the wrapper service above.SystemMail value object, a system_mail.php/JSON catalogue per locale, a SystemMailer service that composes a Mailable with ->locale($locale) and a generic Blade view doing __('system_mail.'.$type.'.body', $params). You get the same editorial workflow with maintained, first-party components — and Mail::fake() in tests. That's the recommendation unless you're specifically maintaining an existing Symfony 3/4 app.How can I help you explore Laravel packages today?