Benachrichtigungssystem
CrispyCMS besitzt ein zentrales Benachrichtigungssystem. Es gibt genau einen Einstiegspunkt, um einen Benutzer über irgendetwas zu informieren: Crispy\Services\NotificationService.
Eine Benachrichtigung kann dabei
- intern zugestellt werden (Eintrag in der Glocke in der Topbar und unter
/admin/notifications), - per E-Mail zugestellt werden,
- oder beides — mit einem einzigen Aufruf.
Der einfachste Fall
use Crispy\Services\NotificationService;
NotificationService::send($user, 'Seite veröffentlicht', 'Das Impressum ist jetzt online.', '/admin/pages/12');
Das erzeugt einen Eintrag in der Glocke des Benutzers. Mehr ist nicht nötig.
Soll dieselbe Benachrichtigung zusätzlich als E-Mail rausgehen, ist das genau ein Argument:
use Crispy\Enums\NotificationChannel;
NotificationService::send(
$user,
'Seite veröffentlicht',
'Das Impressum ist jetzt online.',
'/admin/pages/12',
channel: NotificationChannel::BOTH,
);
Es wird dann die generische Benachrichtigungs-E-Mail (templates/mail/notifications/generic_notification.mjml.twig) gerendert — Sie brauchen kein eigenes MJML-Template.
Nur E-Mail, ohne Glocken-Eintrag
Transaktionale E-Mails haben in der Benachrichtigungszentrale nichts verloren: Einladungen, E-Mail-Verifizierung, Passwort-Zurücksetzen, Kontaktformular-Weiterleitungen. Dafür gibt es sendMail(), das niemals einen Eintrag in crispy_notifications schreibt:
NotificationService::sendMail(
recipients: $user, // UserModel, rohe E-Mail-Adresse oder ein Array aus beidem
subject: Translation::fetch('MeinPlugin.Mail.Titel'),
mailTemplate: 'mail/notifications/mein_template.mjml.twig',
mailVariables: ['Foo' => $foo],
);
sendMail() akzeptiert bewusst auch rohe E-Mail-Adressen — Empfänger, die gar keine CrispyCMS-Benutzer sind.
Empfänger über Berechtigungen auflösen
Für „alle, die X dürfen” gibt es eine eigene Kurzform. Empfänger werden dabei automatisch nach Benutzer-ID dedupliziert (ein Benutzer, der über zwei Rollen passt, wird nur einmal benachrichtigt):
NotificationService::sendToPermission(
permissions: [Permissions::WRITE_BROKEN_MEDIA, Permissions::SUPERUSER],
title: $titel,
content: $text,
link: '/admin/broken-media-finder',
severity: NotificationSeverity::ERROR,
channel: NotificationChannel::BOTH,
type: NotificationTypes::BROKEN_MEDIA,
);
Eigenes MJML-Template verwenden
Wenn die E-Mail reicher sein soll als Titel + Text + Button (Tabellen, Abschnitte, Diagramme), übergeben Sie Ihr eigenes Template. Die Variablen werden unmittelbar vor dem Rendern pro Empfänger als ThemeVariables gesetzt:
NotificationService::send(
recipients: $empfaenger,
title: $titel,
content: $text,
link: '/admin/meine-seite',
channel: NotificationChannel::BOTH,
type: 'meinplugin.mein_typ',
mailTemplate: 'mail/notifications/mein_template.mjml.twig',
mailVariables: ['MeineTabelle' => $zeilen],
);
Benachrichtigungstypen registrieren
Ein Typ ist das, was ein Benutzer unter /admin/me abschalten kann. Er ist optional: send() funktioniert auch ohne Typ (dann greifen keine Benutzereinstellungen). Registrieren Sie einen Typ, sobald die Benachrichtigung wiederkehrend ist.
Typen werden — genau wie Berechtigungen — beim Boot registriert und die Registry anschließend gesperrt. Plugins hängen sich dafür an NotificationTypeRegisterEvent:
use Crispy\Enums\NotificationChannel;
use Crispy\Events\NotificationTypeRegisterEvent;
use Crispy\Notifications\NotificationTypeDefinition;
use Crispy\Notifications\NotificationTypeRegistry;
class PluginEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [NotificationTypeRegisterEvent::class => 'onNotificationTypeRegister'];
}
public function onNotificationTypeRegister(): void
{
NotificationTypeRegistry::register(
new NotificationTypeDefinition(
id: 'kiosk.device_offline',
translationKey: 'Kiosk.Notifications.DeviceOffline',
descriptionTranslationKey: 'Kiosk.Notifications.DeviceOffline.Hint',
group: 'Kiosk',
defaultChannel: NotificationChannel::BOTH,
),
);
}
}
IDs müssen mit einem Punkt namensraumiert sein (meinplugin.mein_typ), analog zu PermissionDefinition.
| Feld | Bedeutung |
|---|---|
id | Eindeutige, namensraumierte ID. Wird in crispy_notifications.type gespeichert. |
translationKey | Anzeigename in den Benutzereinstellungen. |
descriptionTranslationKey | Optionaler Erklärtext unter dem Namen. |
group | Gruppenüberschrift in den Einstellungen (Übersetzungsschlüssel oder Klartext). |
defaultChannel | Kanal, solange der Benutzer nichts eingestellt hat. |
mandatory | Ignoriert Benutzereinstellungen vollständig. Sparsam einsetzen — z. B. Lizenzablauf. |
Zustellungsregeln
-
Hat die Benachrichtigung einen registrierten Typ, wird sie durch die Einstellungen des jeweiligen Benutzers gefiltert (
crispy_notification_preferences), ersatzweise durch dendefaultChanneldes Typs. Einmandatory-Typ überspringt das. -
Benachrichtigungen ohne Typ werden nie gefiltert.
-
Der E-Mail-Kanal setzt zusätzlich
Helper::mailSendingIsEnabled()voraus. Ist der Mailversand aus, wird die interne Benachrichtigung trotzdem erzeugt. -
Vorrangig vor allem anderen greifen die administratorseitigen Schalter unter Einstellungen → Benachrichtigungen. Jede benachrichtigende Funktion hat davon zwei, einen pro Kanal:
Konfigurationsschlüssel Kanal CMSControl_Notification_{Feature}_Internal_EnabledGlocke / Benachrichtigungszentrale CMSControl_Notification_{Feature}_EnabledE-Mail Sie sind unabhängig voneinander: Wer den Mailversand einer Funktion abschaltet, entfernt sie damit nicht mehr aus der Glocke aller Benutzer. Der
{Feature}-Teil wird alsconfigKeyan derNotificationTypeDefinitiondeklariert; Typen ohneconfigKey(z. B. verpflichtende) kennen keine globalen Schalter.NotificationService::globalChannel($type)liefert den global erlaubten Kanal,NotificationService::isEnabledGlobally($type)ist genau dannfalse, wenn beide Kanäle aus sind. Aufrufende Tasks/Services benutzen Letzteres anstelle des früheren einzelnenConfig::get(...)-Guards, um teure Arbeit (Empfänger auflösen, De-Dup-Sync) zu überspringen, wenn ohnehin nichts zugestellt würde.Ein global abgeschalteter Kanal wird in den persönlichen Einstellungen unter
/admin/menicht als wirkungsloser Schalter, sondern als Hinweis „Deaktiviert” dargestellt.
mailSendingIsEnabled()-Guard mehr im AufruferTasks, die früher früh abgebrochen haben, wenn kein Mailversand konfiguriert war, dürfen das nicht mehr tun: Die Benachrichtigung landet ja weiterhin in der Glocke. NotificationService lässt den E-Mail-Teil selbstständig weg.
Datenmodell
| Tabelle | Inhalt |
|---|---|
crispy_notifications | Eine Zeile pro Empfänger, nicht pro Ereignis. Dadurch sind Gelesen-/Markiert-/Archiviert-Zustand ohne Zwischentabelle benutzerbezogen. |
crispy_notification_preferences | Opt-out pro Benutzer und Typ. Zeilen entstehen erst, wenn ein Benutzer etwas ändert — ein neuer Typ braucht also keine Backfill-Migration. |
type ist bewusst ein einfacher String und kein Fremdschlüssel: Typen leben im Code, nicht in der Datenbank, und eine Benachrichtigung muss die Deaktivierung ihres Plugins überleben.
Aufbewahrung
Crispy\Tasks\NotificationPurgeTask läuft nachts und räumt auf:
| Konfiguration | Standard | Wirkung |
|---|---|---|
CMSControl_Notification_RetentionDays | 90 | Löscht bereits archivierte Benachrichtigungen. |
CMSControl_Notification_StaleRetentionDays | 365 | Auffangnetz für Benachrichtigungen, die nie angefasst wurden. |
Angeheftete (gepinnte) Benachrichtigungen werden von keinem der beiden Durchläufe gelöscht — und auch „Alle archivieren” lässt sie stehen. Das Anheften ist damit die einzige Möglichkeit, eine Benachrichtigung dauerhaft zu behalten.
Oberfläche
| Ort | Datei |
|---|---|
| Glocke in der Topbar | Components/Navbar/Notifications.twig + js/CMSControl/Notifications.js |
| Vollständige Liste | Views/Notifications.twig (/admin/notifications) |
| Detail-Modal | Components/Modals/NotificationDetail.twig + js/CMSControl/NotificationDetail.js |
| Benutzereinstellungen | Karte „Benachrichtigungen” auf /admin/me |
Beide Listen zeigen pro Eintrag nur eine gekürzte Zeile. Ein Klick auf den Eintrag öffnet das gemeinsame Detail-Modal mit dem vollständigen Text, Typ, Schweregrad und Zeitstempel — und markiert die Benachrichtigung als gelesen. Der hinterlegte Link ist dort ein eigener Button: „lesen” und „dorthin springen” sind bewusst getrennte Aktionen, weil ein Klick sonst wegnavigieren würde, bevor man den Text gelesen hat.
Mehrzeilige Inhalte
Der Benachrichtigungstext (content) darf mehrzeilig sein. Listen zeigen davon nur die erste Zeile, das Detail-Modal rendert ihn vollständig mit white-space: pre-wrap. Die Website-Zusammenfassung nutzt das: SiteDigestMailerService::buildNotificationSummary() baut denselben Bericht, der auch in der E-Mail steht (meistbesuchte Seiten, Speicher inkl. Delta, Audit-Log, Barrierefreiheit, defekte Medien, News), als Klartext in den Benachrichtigungstext — man muss die E-Mail also nicht öffnen, um den Digest zu lesen. Die Rohwerte liegen zusätzlich strukturiert in data.
Der Text ist bewusst Klartext, kein HTML: Benachrichtigungsinhalte werden überall als Text gerendert, damit ein aus einem Plugin stammender Inhalt kein Markup einschleusen kann.
Das Anheften (Pin) ist von beiden Listen und aus dem Modal heraus erreichbar. Es ist die einzige benutzerseitige Möglichkeit, eine Benachrichtigung dauerhaft zu behalten — der Tooltip am Pin-Button sagt das auch explizit, damit die Wirkung nicht geraten werden muss.
Die Glocke pollt ausschließlich den günstigen Endpunkt /admin/api/notifications/unread-count (standardmäßig alle 60 Sekunden) und lädt die Liste selbst erst, wenn das Dropdown geöffnet wird. Der Poll pausiert, solange der Tab im Hintergrund ist.