E-Mails modern, typsicher und testbar
Wer E-Mails in PHP mit Swift Mailer oder rohem PHP mail() versendet, kämpft mit fehlendem Queuing, unkontrollierbaren SMTP-Verbindungen in Tests und unleserlichem Template-Code. Symfony Mailer löst das vollständig — typsichere E-Mail-Objekte, Twig-Templates, pluggable Transports, DKIM-Signierung und saubere Test-Isolation ohne SMTP-Server.
Inhaltsverzeichnis
- 1. Warum Symfony Mailer statt Swift Mailer oder PHPMailer
- 2. Installation und Transport-Konfiguration
- 3. Typsichere E-Mail-Klassen mit TemplatedEmail
- 4. Twig-Templates für HTML- und Text-E-Mails
- 5. Anhänge, Inline-Bilder und Multipart
- 6. DKIM-Signierung und E-Mail-Authentifizierung
- 7. Asynchrones Senden über Symfony Messenger
- 8. Tests ohne echten SMTP-Server
- 9. Transport-Optionen im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Symfony Mailer statt Swift Mailer oder PHPMailer
Symfony Mailer ist der offizielle Nachfolger von Swift Mailer und übernimmt dessen Platz in allen Symfony-Projekten ab Version 4.3. Der entscheidende Unterschied zu älteren Lösungen liegt in der Architektur: Symfony Mailer trennt sauber zwischen dem E-Mail-Objekt (was gesendet wird), dem Transport (wie es gesendet wird) und dem optionalen Queue-System (wann es gesendet wird). Diese Trennung macht es möglich, E-Mails in Entwicklung in eine Debug-Datei zu schreiben, in Tests im Speicher festzuhalten und in Produktion über Postmark oder Amazon SES zu senden — ohne eine einzige Zeile Applikationscode zu ändern.
PHPMailer und Swift Mailer haben dasselbe Grundproblem: Das E-Mail-Objekt ist direkt mit dem Transport verknüpft. Wer E-Mails in Tests abfangen will, muss entweder einen Mock-Server starten oder den E-Mail-Code mit Bedingungen umgeben. Symfony Mailer löst das elegant: Der Transport ist ein austauschbarer Service im Container, in Tests wird er durch null:// oder den InMemoryTransport ersetzt. Tests prüfen, was die Anwendung senden wollte — nicht ob ein SMTP-Server korrekt antwortet. Das ist testorientierte E-Mail-Entwicklung, wie sie sein sollte.
2. Installation und Transport-Konfiguration
Die Installation von Symfony Mailer erfolgt per composer require symfony/mailer. Für spezifische E-Mail-Dienstleister gibt es dedizierte Bridge-Pakete: symfony/postmark-mailer für Postmark, symfony/mailgun-mailer für Mailgun, symfony/amazon-mailer für Amazon SES. Diese Bridges bringen einen vorkonfigurierten Transport-Service mit und benötigen nur den API-Schlüssel als Umgebungsvariable. Für Twig-Templates — und das ist der empfohlene Weg — braucht man zusätzlich symfony/twig-bundle und twig/extra-bundle für E-Mail-spezifische Twig-Extensions.
Die Transport-Konfiguration in mailer.yaml ist ein einzelner DSN-String, der den Dienstleister, Credentials und Port enthält. Für verschiedene Umgebungen wird der DSN über Umgebungsvariablen gesteuert: In Entwicklung null://null (E-Mails werden verworfen) oder smtp://localhost:1025 (MailHog), in Produktion postmark+api://TOKEN@default. Das Symfony Debug-Toolbar zeigt alle versendeten E-Mails im Entwicklungsmodus an — Absender, Empfänger, Betreff und vollständigen Body — ohne dass eine E-Mail tatsächlich zugestellt wird.
<?php
declare(strict_types=1);
namespace App\Mail;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mime\Address;
/**
* Typed email class for order confirmation emails.
* Encapsulates all email parameters — no raw array manipulation in calling code.
*/
final class OrderConfirmationEmail extends TemplatedEmail
{
/**
* Configure the order confirmation email.
*
* @param string[] $ccAddresses
*/
public function __construct(
private readonly string $customerEmail,
private readonly string $customerName,
private readonly string $orderId,
private readonly float $totalAmount,
private readonly array $ccAddresses = [],
) {
parent::__construct();
$this
->from(new Address('orders@mironsoft.de', 'Mironsoft Shop'))
->to(new Address($customerEmail, $customerName))
->subject("Bestellbestätigung #{$orderId}")
->htmlTemplate('emails/order-confirmation.html.twig')
->textTemplate('emails/order-confirmation.txt.twig')
->context([
'order_id' => $orderId,
'customer' => $customerName,
'total_amount' => $totalAmount,
]);
foreach ($ccAddresses as $cc) {
$this->addCc(new Address($cc));
}
}
}
3. Typsichere E-Mail-Klassen mit TemplatedEmail
Der Schlüssel zu wartbarer E-Mail-Logik in Symfony Mailer ist die Erstellung eigener E-Mail-Klassen, die TemplatedEmail erweitern. Statt die E-Mail-Parameter direkt im Service zu konfigurieren — Empfänger, Betreff, Kontext-Variablen — kapselt eine dedizierte Klasse alle Details. Der aufrufende Code erstellt ein Objekt und sendet es: $this->mailer->send(new OrderConfirmationEmail($order)). Das ist typsicher, IDE-freundlich und testbar — eine Änderung am E-Mail-Layout, Betreff oder Kontext berührt nur die E-Mail-Klasse, nicht alle Stellen im Anwendungscode, die diese E-Mail senden.
Das Muster ist ähnlich wie Value Objects in der Domänenmodellierung: Eine E-Mail-Klasse ist ein unveränderlicher Werttyp, der alle Eigenschaften einer bestimmten E-Mail beschreibt. Sie kann in Tests direkt instanziiert und auf korrekte Konfiguration geprüft werden — ohne dass der Symfony Mailer-Transport aufgerufen werden muss. Unit-Tests für E-Mails prüfen Empfänger, Betreff und Kontext, Integrationstests prüfen das gerenderte HTML, und End-to-End-Tests prüfen die tatsächliche Zustellung. Jede Schicht testet genau das, was ihr Scope abdeckt.
4. Twig-Templates für HTML- und Text-E-Mails
Twig-Templates für E-Mails in Symfony Mailer haben einen besonderen Mechanismus: Eine einzelne Template-Datei kann sowohl den Subject-Block, den HTML-Block als auch den Text-Block enthalten. Das Template wird mit dem email-Objekt als Kontext-Variable gerendert, sodass Methoden wie email.subject(), email.from() und email.to() direkt im Template zugänglich sind. Für HTML-E-Mails ist CSS-Inlining wichtig — die meisten E-Mail-Clients ignorieren externe Stylesheets. Das twig/extra-bundle enthält CssInlinerExtension, die CSS-Regeln automatisch als Inline-Styles in die HTML-Elemente überträgt.
E-Mail-Templates erben typischerweise von einem Basis-Template, das Header, Footer, globale CSS-Styles und das Brand-Layout enthält. Einzelne E-Mail-Typen überschreiben nur den Inhalts-Block. Das ist dasselbe Vererbungsprinzip wie bei Web-Templates, funktioniert in E-Mail-Clients aber besonders gut, weil HTML-E-Mails vollständig gerendert werden — kein JavaScript, kein CSS-Linking, alles inline. Das InkyExtension aus twig/extra-bundle erlaubt die Verwendung des Foundation-for-Emails-Frameworks direkt in Twig — responsives E-Mail-Layout ohne manuelles CSS-Tabellen-Hacking.
<?php
// templates/emails/order-confirmation.html.twig
// (shown as PHP string for syntax highlighting)
//
// {% extends 'emails/base.html.twig' %}
//
// {% block subject %}Bestellbestätigung #{ { order_id } }{% endblock %}
//
// {% block body_html %}
// <h1>Vielen Dank, { { customer } }!</h1>
// <p>Ihre Bestellung #{ { order_id } } wurde erfolgreich aufgenommen.</p>
// <table>
// <tr>
// <th>Bestellnummer</th>
// <td>{ { order_id } }</td>
// </tr>
// <tr>
// <th>Gesamtbetrag</th>
// <td>{ { total_amount | number_format(2, ',', '.') } } €</td>
// </tr>
// </table>
// {% endblock %}
//
// templates/emails/base.html.twig (simplified):
// <!DOCTYPE html>
// <html>
// <head>
// <style>
// /* CSS here is inlined automatically by CssInlinerExtension */
// body { font-family: Arial, sans-serif; color: #333; }
// h1 { color: #0f172a; }
// </style>
// </head>
// <body>{% block body_html %}{% endblock %}</body>
// </html>
declare(strict_types=1);
namespace App\Service;
use App\Mail\OrderConfirmationEmail;
use Symfony\Component\Mailer\MailerInterface;
/**
* Sends transactional emails using typed email classes.
*/
final readonly class EmailNotificationService
{
public function __construct(
private MailerInterface $mailer,
) {}
/**
* Send an order confirmation email to the customer.
*/
public function sendOrderConfirmation(
string $customerEmail,
string $customerName,
string $orderId,
float $totalAmount,
): void {
$email = new OrderConfirmationEmail(
customerEmail: $customerEmail,
customerName: $customerName,
orderId: $orderId,
totalAmount: $totalAmount,
);
// If Messenger is configured, this sends asynchronously via the queue
$this->mailer->send($email);
}
}
5. Anhänge, Inline-Bilder und Multipart
Datei-Anhänge in Symfony Mailer werden per $email->attachFromPath() oder $email->attach() hinzugefügt. attachFromPath() akzeptiert einen absoluten Dateipfad und optionalen Content-Type — geeignet für statische Dateien wie AGBs oder Rechnungstemplates. attach() akzeptiert einen String oder eine Resource — geeignet für dynamisch generierte Inhalte wie PDF-Rechnungen, die im Arbeitsspeicher erzeugt werden. Der optionale dritte Parameter setzt den Dateinamen, der dem Empfänger angezeigt wird, unabhängig vom tatsächlichen Dateipfad.
Inline-Bilder — für Logos und Produktbilder im E-Mail-Body — werden mit $email->embedFromPath() eingebettet und im Template über email.image(path) als Base64-encodierte Data-URLs oder CID-Referenzen referenziert. Das stellt sicher, dass Bilder auch dann angezeigt werden, wenn der E-Mail-Client externe Bilder blockiert. Multipart-E-Mails mit HTML-Body und Text-Fallback sind Standard in Symfony Mailer: Wer htmlTemplate() und textTemplate() setzt, erhält automatisch eine MIME-Multipart-Nachricht mit beiden Teilen — E-Mail-Clients wählen den Teil, den sie rendern können.
6. DKIM-Signierung und E-Mail-Authentifizierung
DKIM (DomainKeys Identified Mail) ist eine Signaturmethode für E-Mails, die Empfänger-Mailserver verifizieren können. E-Mails ohne DKIM-Signatur landen häufiger im Spam-Ordner, besonders wenn sie von einer Domain gesendet werden, für die kein DKIM-DNS-Eintrag existiert. Symfony Mailer unterstützt DKIM-Signierung über das DkimSigner-Middleware-System: Ein privater RSA-Schlüssel signiert jede ausgehende E-Mail, der zugehörige öffentliche Schlüssel wird als DNS-TXT-Record bei der Domain hinterlegt. Empfänger-Server verifizieren die Signatur und erhöhen so die Zustellbarkeit.
Die DKIM-Integration in Symfony Mailer ist ein Event-Subscriber-Pattern: DkimSigner implementiert MessageSignerInterface und wird über das Mailer-Event-System aufgerufen, bevor die E-Mail an den Transport übergeben wird. Der private Schlüssel wird sicher als Umgebungsvariable oder Vault-Secret injiziert — niemals im Code oder in Konfigurationsdateien. Die DKIM-Domain und der Selector müssen mit dem DNS-TXT-Record übereinstimmen. Für Projekte mit mehreren Absender-Domains kann man mehrere DkimSigner-Instanzen für verschiedene Domains konfigurieren und per E-Mail-Klasse selektiv anwenden.
<?php
declare(strict_types=1);
namespace App\Tests\Mail;
use App\Mail\OrderConfirmationEmail;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mailer\Test\Constraint\EmailCount;
use Symfony\Component\Mime\Email;
/**
* Tests the OrderConfirmationEmail class directly — no transport needed.
*/
final class OrderConfirmationEmailTest extends TestCase
{
public function testEmailConfiguration(): void
{
$email = new OrderConfirmationEmail(
customerEmail: 'max@example.de',
customerName: 'Max Mustermann',
orderId: 'ORD-2026-001',
totalAmount: 129.99,
);
// Verify recipient
self::assertCount(1, $email->getTo());
self::assertSame('max@example.de', $email->getTo()[0]->getAddress());
self::assertSame('Max Mustermann', $email->getTo()[0]->getName());
// Verify subject
self::assertSame('Bestellbestätigung #ORD-2026-001', $email->getSubject());
// Verify sender
self::assertSame('orders@mironsoft.de', $email->getFrom()[0]->getAddress());
// Verify template context
self::assertSame('ORD-2026-001', $email->getContext()['order_id']);
self::assertSame(129.99, $email->getContext()['total_amount']);
}
public function testCcAddresses(): void
{
$email = new OrderConfirmationEmail(
customerEmail: 'max@example.de',
customerName: 'Max Mustermann',
orderId: 'ORD-2026-002',
totalAmount: 49.00,
ccAddresses: ['buchhaltung@mironsoft.de'],
);
self::assertCount(1, $email->getCc());
self::assertSame('buchhaltung@mironsoft.de', $email->getCc()[0]->getAddress());
}
}
7. Asynchrones Senden über Symfony Messenger
Synchrones E-Mail-Senden im HTTP-Request verlängert die Antwortzeit — jeder SMTP-Verbindungsaufbau kostet Zeit. Symfony Mailer integriert sich nahtlos mit Symfony Messenger für asynchrones E-Mail-Queuing. Die Integration erfolgt über einen einzigen Konfigurationseintrag in messenger.yaml: Symfony\Component\Mailer\Messenger\SendEmailMessage: async. Danach werden alle E-Mails, die über $mailer->send() abgeschickt werden, in die konfigurierte Message-Queue eingereiht — statt sofort an den Transport übergeben zu werden.
Der Message-Worker-Prozess verarbeitet die Queue asynchron und übergibt die E-Mails dann an den SMTP-Transport oder API-Provider. Das Ergebnis: HTTP-Requests antworten sofort, E-Mail-Zustellung geschieht im Hintergrund ohne Benutzer-Blockierung. Bei Netzwerkfehlern oder temporären SMTP-Ausfällen kann Messenger automatisch Retries mit exponentiellem Backoff durchführen — deutlich robuster als synchrones Senden im Request. Für Transaktions-E-Mails, die nach einer erfolgreichen Datenbankoperation gesendet werden sollen, ist das Dispatch-After-Current-Bus-Pattern ideal: Die E-Mail wird erst in die Queue eingereiht, wenn die Transaktion erfolgreich committed wurde.
8. Tests ohne echten SMTP-Server
Unit-Tests für E-Mail-Klassen sind einfach: Die Symfony Mailer-E-Mail-Klasse wird direkt instanziiert und ihre Properties werden geprüft — kein Transport, kein Netzwerk. Für Integrationstests, die prüfen, ob ein Service eine E-Mail mit den richtigen Parametern abschickt, nutzt man MailerInterface-Mocks oder den Symfony Mailer Test-Transport. Das Test-Bundle stellt assertEmailCount(), assertEmailIsQueued() und assertEmailHasHeader()-Assertions bereit. Diese Assertions greifen auf den InMemoryTransport zu, der alle gesendeten E-Mails im Speicher hält.
Der InMemoryTransport ist der richtige Test-Transport für Kernel-Tests und Functional Tests. Er wird automatisch aktiviert, wenn der Mailer-DSN in der Test-Umgebung auf null://null gesetzt ist. Gesendete E-Mails sind über den InMemoryTransport-Service abrufbar. Für Tests, die prüfen, ob das gerenderte Twig-Template den richtigen HTML-Output produziert, rendert man das Template direkt mit der Symfony Test-Kernel-Infrastruktur und vergleicht den Output — ohne den Mailer-Transport aufzurufen. Diese Schichtentrennung macht E-Mail-Tests schnell, deterministisch und unabhängig von externer Infrastruktur.
9. Transport-Optionen im Vergleich
Die Wahl des richtigen Transports für Symfony Mailer hängt von den Anforderungen an Zustellbarkeit, Kosten, Logging und Testbarkeit ab.
| Transport | DSN-Schema | Einsatz | Besonderheit |
|---|---|---|---|
| SMTP | smtp://user:pass@host:587 |
Eigener Mail-Server | Volle Kontrolle, TLS-Support |
| Postmark | postmark+api://TOKEN@default |
Transaktions-E-Mails | Hohes Deliverability-Level |
| Mailgun | mailgun+api://KEY:DOMAIN@default |
Bulk + Transaktions | Detailliertes Tracking |
| Amazon SES | ses+api://KEY:SECRET@default |
Hohes Volumen | Günstig, AWS-Integration |
| Null (Test) | null://null |
Tests, Dev | E-Mails werden verworfen |
Für die meisten Symfony-Projekte empfiehlt sich ein Cloud-basierter E-Mail-Dienstleister statt eines eigenen SMTP-Servers. Postmark und Mailgun bieten detaillierte Zustellberichte, Bounce-Handling und DKIM-Konfiguration über ihr Dashboard — das vereinfacht den E-Mail-Betrieb erheblich. Die Symfony Mailer-Bridge-Pakete abstrahieren die API-Unterschiede vollständig: Wechsel zwischen Postmark und Mailgun bedeutet nur eine Änderung im DSN-String.
Mironsoft
Symfony E-Mail-Architektur, Mailer-Integration und Deliverability-Optimierung
Symfony-E-Mail-System professionell aufbauen?
Wir entwickeln vollständige E-Mail-Systeme mit Symfony Mailer — typsichere E-Mail-Klassen, Twig-Templates, DKIM-Signierung, Messenger-Queue und vollständig testbare Transports für euren Stack.
E-Mail-Architektur
Typsichere E-Mail-Klassen, Twig-Templates und Transport-Konfiguration für alle Umgebungen
Queue & Async
Messenger-Integration für asynchrones Senden mit Retry-Logik und Fehlerbehandlung
Deliverability
DKIM-Signierung, SPF-Konfiguration und Postmark/Mailgun-Integration für maximale Zustellbarkeit
10. Zusammenfassung
Symfony Mailer modernisiert E-Mail-Versand in PHP-Projekten auf mehreren Ebenen gleichzeitig. Typsichere E-Mail-Klassen kapseln alle E-Mail-Parameter und machen Refactoring sicher. Twig-Templates mit automatischem CSS-Inlining erzeugen HTML-E-Mails, die in allen gängigen Clients korrekt angezeigt werden. Austauschbare Transports erlauben unterschiedliche Konfigurationen für Entwicklung, Staging und Produktion — ohne Codeänderungen. Die Messenger-Integration macht asynchrones E-Mail-Senden zu einer Ein-Zeilen-Konfiguration. Und der null://-Transport in Tests stellt sicher, dass keine E-Mail je versehentlich aus einer Test-Umgebung heraus versendet wird.
Der größte praktische Gewinn liegt in der Test-Isolation: E-Mail-Klassen sind direkt testbar, Transport-Verhalten ist mockbar, und die gerenderten Twig-Templates lassen sich in Integrationstests verifizieren. Teams, die von Swift Mailer oder PHPMailer migrieren, profitieren sofort — weniger Boilerplate, mehr Typsicherheit, bessere Testbarkeit. Die DKIM-Integration und die Cloud-Transports für Postmark, Mailgun und Amazon SES sind Production-Ready und machen Symfony Mailer zur vollständigen E-Mail-Infrastruktur für skalierbare Symfony-Projekte.
Symfony Mailer — Das Wichtigste auf einen Blick
Typsichere E-Mail-Klassen
TemplatedEmail erweitern, alle Parameter im Konstruktor kapseln. Aufrufender Code sendet ein Objekt — keine rohen Arrays, volle IDE-Unterstützung.
Transport & Umgebungen
null://null in Dev und Test, Postmark/Mailgun in Produktion — nur der DSN-String ändert sich, kein Applikationscode.
Async via Messenger
SendEmailMessage: async in messenger.yaml — E-Mails werden gequeuet, HTTP-Requests antworten sofort, Worker senden im Hintergrund.
Testing
E-Mail-Klassen direkt instanziieren und Properties prüfen. null://-Transport verhindert echtes Senden. InMemoryTransport hält E-Mails für Assertions.