Newsletter Management: Technische Details
Architektur, Plugin-Struktur, Tabellen und Entities, Migrationen, Entwurfsentscheidungen, Tests und Entwicklungs-Workflow des Sylius-Newsletter-Management-Plugins
Newsletter Management: Technische Details
Diese Dokumentation bezieht sich immer auf die aktuellste veröffentlichte Version.
Diese Seite beschreibt Aufbau und Interna des Plugins für Integratoren und Entwickler: den Ablauf von Anmeldung und Versand, die Struktur, die Tabellen und die wichtigsten Entwurfsentscheidungen.
Architektur
Anmeldungen laufen über genau einen Einstiegspunkt, den NewsletterSubscriptionManager. Egal ob Storefront-Formular, Registrierungs-Checkbox oder Shop API: Alle rufen dieselben Methoden zum Anmelden, Bestätigen, Ändern und Abmelden auf. Der Footer-Teaser ruft den Manager nicht selbst auf, er übergibt per GET an das Formular auf /newsletter. Es gibt keine zweite Geschäftslogik für die API.
graph TD
A["Storefront: Formular, Registrierung"] --> M["NewsletterSubscriptionManager"]
B["Shop API: Processors und Provider"] --> M
M --> E{"Event PRE_SUBSCRIBE"}
E -->|"abgelehnt"| R["Abgelehnt: Flash oder 422"]
E -->|"frei"| P["Abonnement speichern"]
P --> ML["NewsletterMailer: Bestätigung, Willkommen, Bereits angemeldet"]
P --> S["Synchronisation des Customer-Newsletter-Flags"]
P --> T["Tracking: Einwilligung und Löschung"]
Der Versand läuft getrennt davon über Cron:
graph LR
C["Cron: mmd:newsletter:send"] --> SV["NewsletterSendingService"]
SV --> D["Zustellungen reservieren"]
D --> MS["NewsletterMailSender"]
MS --> RE["NewsletterRenderer"]
RE --> TH["Theme des Channels"]
RE --> LT["NewsletterLinkTracker"]
MS --> TR["Symfony Mailer"]
ADM["Admin: Senden, Planen, Abbrechen"] --> SV
Plugin-Struktur
sylius-newsletter-management/
├── assets/ Admin- und Shop-Assets
├── config/
│ ├── config.yaml
│ ├── mailer.yaml E-Mail-Codes der drei Abonnenten-Mails
│ ├── services.php alle Services in einer Datei
│ ├── grids/ Admin-Grids
│ ├── routes/ admin.yaml, shop.yaml
│ └── twig_hooks/ admin.yaml, shop.yaml
├── src/
│ ├── Api/ Shop-API: Processors, Provider, Query-Extension
│ ├── Command/ setup, send, subscription:purge, tracking:purge
│ ├── Controller/ Storefront, Dashboard, Vorschau, Versand, Tracking, Export
│ ├── DependencyInjection/
│ ├── Entity/ Entities, Interfaces und Enums
│ ├── Event/ PRE_SUBSCRIBE
│ ├── EventListener/ Customer-Checkbox, Löschschutz
│ ├── Factory/
│ ├── Form/ Form-Typen und Modelle
│ ├── Mailer/ Abonnenten-Mails
│ ├── Manager/ NewsletterSubscriptionManager
│ ├── Menu/ Admin-Menü
│ ├── Migrations/ 11 Doctrine-Migrationen
│ ├── Newsletter/ Platzhalter, Renderer, Textgenerator, Theme, URLs
│ ├── Repository/
│ ├── Sender/ Newsletter-Versand pro Empfänger
│ ├── Sending/ Versand-Orchestrierung
│ ├── Settings/ Einstellungen pro Channel, Datenschutz-Link, Einwilligungstext
│ ├── Statistics/ Zahlen für Dashboard und Newsletter-Detail
│ ├── Sync/ Abgleich mit dem Customer-Newsletter-Flag
│ ├── Tracking/ Link-Tracker, Recorder, Signatur
│ └── Twig/ Twig-Extension
├── templates/ admin, emails, shop
├── tests/ Unit, Integration, Functional, TestApplication
└── translations/ messages, flashes, validators (de, en)
Entities und Tabellen
Alle Tabellen tragen das Präfix mmd_sylius_newsletter_management_.
| Entity | Tabelle | Inhalt |
|---|---|---|
NewsletterSubscription |
..._subscription |
Ein Abonnement pro Adresse und Channel (eindeutig). Status, Quelle, Tokens, Zeitstempel, Einwilligung zum Tracking, Channel und Customer (optional). |
NewsletterCategory |
..._category |
Kategorie mit Code, Position und Aktiv-Schalter |
NewsletterCategoryTranslation |
..._category_translation |
Übersetzbarer Name und Beschreibung |
NewsletterChannelConfiguration |
..._channel_configuration |
Einstellungen pro Channel (genau eine Zeile pro Channel) |
Newsletter |
..._newsletter |
Betreff, Preheader, HTML, Text, Status, Zeitplan, Empfängerzahl |
NewsletterDelivery |
..._delivery |
Versandprotokoll: eine Zeile pro Newsletter und Adresse (eindeutig) |
NewsletterLink |
..._link |
Verfolgte Links eines Newsletters: Code und URL |
NewsletterTrackingHit |
..._tracking_hit |
Anonymer Zähler pro Newsletter, Tag und Ziel |
NewsletterTrackingEvent |
..._tracking_event |
Einzelnes Ereignis (Öffnung oder Klick) eines zustimmenden Abonnenten |
Dazu kommen drei Verknüpfungstabellen: ..._subscription_category (Kategorien eines Abonnements), ..._newsletter_category (Zielgruppe eines Newsletters) und ..._category_channel (Channels, in denen eine Kategorie angeboten wird; leer = alle).
erDiagram
SUBSCRIPTION }o--o{ CATEGORY : "waehlt"
NEWSLETTER }o--o{ CATEGORY : "Zielgruppe"
CATEGORY ||--o{ CATEGORY_TRANSLATION : "uebersetzt"
NEWSLETTER ||--o{ DELIVERY : "Versandprotokoll"
SUBSCRIPTION |o--o{ DELIVERY : "empfaengt"
NEWSLETTER ||--o{ LINK : "verfolgt"
NEWSLETTER ||--o{ TRACKING_HIT : "zaehlt"
NEWSLETTER ||--o{ TRACKING_EVENT : "protokolliert"
SUBSCRIPTION ||--o{ TRACKING_EVENT : "loest aus"
Enums (als PHP-Backed-Enums, keine eigenen Tabellen):
| Enum | Werte |
|---|---|
NewsletterStatus |
draft, ready, scheduled, sending, sent, cancelled |
NewsletterSubscriptionStatus |
pending, confirmed, unsubscribed |
NewsletterSubscriptionSource |
storefront, api, registration, admin |
NewsletterDeliveryStatus |
pending, sending, sent, failed, skipped |
NewsletterTrackingMode |
off, anonymous, personal |
NewsletterTrackingEventType |
open, click |
Migrationen
Das Plugin bringt 11 Migrationen mit. Eingespielt werden sie mit bin/console mmd:newsletter-management:setup (siehe Installation).
| Nr. | Zweck |
|---|---|
| 1 | Tabellen für Abonnements, Kategorien (mit Übersetzung), Channel-Einstellungen und die Kategorie-Zuordnung |
| 2 | Vor- und Nachname am Abonnement |
| 3 | Datenschutzseite pro Channel |
| 4 | last_notified_at am Abonnement (Mail-Drosselung) |
| 5 | Newsletter-Tabelle samt Kategorie-Zuordnung |
| 6 | Channel-Beschränkung der Kategorien |
| 7 | Versand: Zeitplan- und Fortschrittsspalten, Versandprotokoll, Mailer-Transport pro Channel |
| 8 | Anonymes Tracking: Modus und Aufbewahrung, Link-Tabelle, Tageszähler |
| 9 | Personenbezogenes Tracking: Einwilligung, Ereignistabelle, Einwilligungstext und Aufbewahrung pro Channel |
| 10 | Aufbewahrung abgemeldeter Abonnements pro Channel |
| 11 | Aufbewahrung unbestätigter Anmeldungen pro Channel |
Entwurfsentscheidungen
- Ein Einstiegspunkt für Abonnements. Der
NewsletterSubscriptionManagerist die einzige Stelle, die anmeldet, bestätigt, ändert und abmeldet. Storefront und Shop API unterscheiden sich nur darin, wie sie Eingaben entgegennehmen und Antworten ausgeben. Bevor er etwas speichert oder verschickt, löst er das EventPRE_SUBSCRIBEaus (siehe Erweiterbarkeit). - Cron statt Messenger. Der Versand ist ein Konsolenbefehl. Ein Worker für eine asynchrone Queue ist Infrastruktur, die das Plugin in einem fremden Shop nicht voraussetzen kann, und eine nicht geroutete Messenger-Nachricht würde im Admin-Request laufen. Ein Lauf darf sich gefahrlos mit einem anderen überlappen.
- Empfänger werden beim Start festgelegt. Pro Newsletter und Adresse gibt es genau eine Zustellung (eindeutiger Schlüssel). So bekommt niemand denselben Newsletter doppelt, egal was parallel läuft.
- Atomares Reservieren. Jede Zustellung wird vor dem Senden reserviert. Eine nie abgeschlossene Reservierung geht nach 15 Minuten zurück auf wartend, ein fehlgeschlagener Versand wird bis zu dreimal versucht.
- Zähler ohne Datenbank-Sonderfunktionen. Der Recorder erhöht Zähler portabel: erst
UPDATE, bei null TreffernINSERT, und bei einer Kollision mit einer parallelen Anfrage noch einmalUPDATE. Es gibt keinON DUPLICATE KEY. - Signierte Tracking-Adressen.
TrackingSignerbildet einen HMAC-SHA256 überkernel.secretund nutzt die ersten 32 Zeichen. Er soll Abtasten verhindern, nicht authentifizieren. Links tragen nur einen Code (die ersten 16 Zeichen des SHA-256 der URL), das Ziel liegt in der Datenbank. - Ein Foreign Key, der das Protokoll schützt. Der Bezug eines Versandprotokolls zum Abonnement steht auf
SET NULL, nicht aufCASCADE. Wird ein Abonnement gelöscht, bleibt das Protokoll samt Adresse bestehen. Tracking-Ereignisse hängen dagegen perCASCADEam Abonnement und verschwinden mit ihm. Der Bezug des Abonnements zu Channel und Customer steht ebenfalls aufSET NULL. - Platzhalter statt Twig. Platzhalter sind eine feste Liste und werden durch einfaches Ersetzen befüllt. Das gibt Autoren nicht die ganze Twig-Laufzeit.
- Theme des Channels. Ein Decorator der Sylius-Theme-Context schaltet für die Dauer eines Renders auf das Theme des Newsletter-Channels um und stellt es danach wieder her. Sonst würde der Cronjob ohne Request das nackte Sylius-Layout rendern.
Tests
| Verzeichnis | Inhalt |
|---|---|
tests/Unit/ |
Unit-Tests: DependencyInjection, Entity, EventListener, Manager, Newsletter, Settings, Sync, Twig |
tests/Integration/ |
Integrationstest des Setup-Befehls (Command) |
tests/Functional/ |
Funktionale Tests, z. B. Anmeldefluss, Shop API, Versand, Tracking, Statistik, Theme, Aufräumbefehle |
tests/TestApplication/ |
Sylius-Testanwendung (config, src, templates) |
Entwicklung
Entwickelt wird ausschließlich im Container (PHP 8.3, MySQL 8.4, nginx). Ein lokales PHP ist nicht vorgesehen.
| Befehl | Zweck |
|---|---|
make init |
Compose-Override anlegen, composer install, Frontend der Testanwendung bauen, Container starten |
make database-init |
Datenbank anlegen und Migrationen ausführen |
make load-fixtures |
Sylius-Fixtures laden (die funktionalen Tests brauchen sie) |
make phpunit |
Unit- und funktionale Tests |
make phpstan |
Statische Analyse |
make ecs |
Code-Style |
make up, make down, make clean |
Container starten, stoppen, samt Volumes entfernen |
make php-shell |
Shell im PHP-Container |
Diese Befehle gehören zur Entwicklung des Plugins selbst. Für den Betrieb in deinem Shop brauchst du sie nicht.