Symfony Mailer: E-Mails modern, typsicher und testbar
AI generated
SF
{ }
Symfony · Mailer · E-Mail · Twig · Messenger · PHP 8.4
Symfony Mailer:
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.

16 Min. Lesezeit TemplatedEmail · Transports · DKIM · Messenger · Testing Symfony 7.x · PHP 8.4

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.

11. FAQ: Symfony Mailer

1Was ist Symfony Mailer?
Offizielle E-Mail-Komponente für Symfony, Nachfolger von Swift Mailer. Typsichere E-Mail-Objekte, austauschbare Transports, Twig-Integration, DKIM und Messenger-Queue.
2Email vs. TemplatedEmail?
Email ist das Basis-Objekt. TemplatedEmail erweitert es um Twig-Template-Rendering mit Kontext-Variablen und CSS-Inlining — der empfohlene Weg für HTML-E-Mails.
3Transport per Umgebung?
MAILER_DSN-Umgebungsvariable: null://null in Dev, postmark+api://TOKEN in Prod. Nur der DSN ändert sich — kein Applikationscode anpassen.
4Asynchrones Senden?
messenger.yaml: SendEmailMessage → async. Alle $mailer->send()-Aufrufe werden gequeuet. Worker sendet im Hintergrund mit Retry-Logik.
5Tests ohne SMTP-Server?
E-Mail-Klassen direkt instanziieren und Properties prüfen. null:// verhindert echtes Senden. InMemoryTransport hält E-Mails für assertEmailCount().
6Dateianhänge hinzufügen?
attachFromPath('/pfad/datei.pdf') für statische Dateien. attach($body, 'name.pdf', 'application/pdf') für dynamisch generierte Inhalte. embedFromPath() für Inline-Bilder.
7Was ist DKIM?
Kryptografische E-Mail-Signatur. DkimSigner signiert alle ausgehenden E-Mails mit einem privaten RSA-Schlüssel. Empfänger-Server verifizieren gegen den DNS-TXT-Record.
8Unterstützte E-Mail-Anbieter?
Postmark, Mailgun, Amazon SES, Sendgrid, Brevo, Mandrill — alle per DSN-String konfigurierbar. Alle Bridges implementieren dasselbe MailerInterface.
9Migration von Swift Mailer?
composer require symfony/mailer, dann Service-by-Service migrieren: Swift_Message → Email/TemplatedEmail, Inject-Typen anpassen und E-Mail-Aufbau-Methoden aktualisieren.
10CSS-Inlining für HTML-E-Mails?
CssInlinerExtension aus twig/extra-bundle wandelt <style>-CSS automatisch in Inline-Styles um beim Twig-Rendering — kompatibel mit allen E-Mail-Clients.