Search Documentation

Search for pages and headings in the documentation

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.

FeldBedeutung
idEindeutige, namensraumierte ID. Wird in crispy_notifications.type gespeichert.
translationKeyAnzeigename in den Benutzereinstellungen.
descriptionTranslationKeyOptionaler Erklärtext unter dem Namen.
groupGruppenüberschrift in den Einstellungen (Übersetzungsschlüssel oder Klartext).
defaultChannelKanal, solange der Benutzer nichts eingestellt hat.
mandatoryIgnoriert Benutzereinstellungen vollständig. Sparsam einsetzen — z. B. Lizenzablauf.

Zustellungsregeln

  1. Hat die Benachrichtigung einen registrierten Typ, wird sie durch die Einstellungen des jeweiligen Benutzers gefiltert (crispy_notification_preferences), ersatzweise durch den defaultChannel des Typs. Ein mandatory-Typ überspringt das.

  2. Benachrichtigungen ohne Typ werden nie gefiltert.

  3. Der E-Mail-Kanal setzt zusätzlich Helper::mailSendingIsEnabled() voraus. Ist der Mailversand aus, wird die interne Benachrichtigung trotzdem erzeugt.

  4. Vorrangig vor allem anderen greifen die administratorseitigen Schalter unter Einstellungen → Benachrichtigungen. Jede benachrichtigende Funktion hat davon zwei, einen pro Kanal:

    KonfigurationsschlüsselKanal
    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 als configKey an der NotificationTypeDefinition deklariert; Typen ohne configKey (z. B. verpflichtende) kennen keine globalen Schalter.

    NotificationService::globalChannel($type) liefert den global erlaubten Kanal, NotificationService::isEnabledGlobally($type) ist genau dann false, wenn beide Kanäle aus sind. Aufrufende Tasks/Services benutzen Letzteres anstelle des früheren einzelnen Config::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/me nicht als wirkungsloser Schalter, sondern als Hinweis „Deaktiviert” dargestellt.


Datenmodell

TabelleInhalt
crispy_notificationsEine Zeile pro Empfänger, nicht pro Ereignis. Dadurch sind Gelesen-/Markiert-/Archiviert-Zustand ohne Zwischentabelle benutzerbezogen.
crispy_notification_preferencesOpt-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:

KonfigurationStandardWirkung
CMSControl_Notification_RetentionDays90Löscht bereits archivierte Benachrichtigungen.
CMSControl_Notification_StaleRetentionDays365Auffangnetz 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

OrtDatei
Glocke in der TopbarComponents/Navbar/Notifications.twig + js/CMSControl/Notifications.js
Vollständige ListeViews/Notifications.twig (/admin/notifications)
Detail-ModalComponents/Modals/NotificationDetail.twig + js/CMSControl/NotificationDetail.js
BenutzereinstellungenKarte „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.