Laravel-style Notification System for TYPO3 

Extension key

lex_notifications

Package name

agencelex/notifications

Version

1.4

Language

en

Author

Agence Lex

License

This document is published under the Creative Commons BY 4.0 license.

Rendered

Mon, 14 Sep 2026 21:25:06 +0000


lex_notifications brings the power, extensibility, and flexibility of Laravel's notification system to the TYPO3 ecosystem. Any PHP code — a controller, a plugin, a service, a domain model or a php file — can send a notification to any object that uses the Notifiable trait, through any combination of channels (email, database, Slack, …). The recipient can be a frontend user, a backend user, a domain model, a POPO, or even an anonymous inline class: if it uses Notifiable, it can receive notifications.

A ready-to-use backend module is included. It lets editors send messages to frontend users and serves as a practical code sample for dispatching batch notifications.


Table of Contents

Introduction 

What Does It Do? 

If you are familiar with the Laravel framework, you already know how flexible, robust, and modular its notification system is. With dozens of community and official channels available (Slack, Teams, Push, SMS, VoIP, and more), Laravel empowers applications to send notifications across a vast network.

lex_notifications brings this exact power, extensibility, and flexibility directly into the TYPO3 ecosystem.

The core idea is simple: any PHP object that uses the Notifiable trait can receive a notification. Any PHP code — a controller, an Extbase plugin, a domain service, a Scheduler task, a middleware — can send one. The two sides are decoupled through a Notification class that describes what to send and how to format it for each delivery channel.

There is no constraint on the direction of communication:

  • A frontend user can receive a notification when they place an order.
  • A backend user can be alerted when a content workflow step requires their approval.
  • An email address (represented by a class or even a lightweight inline class) can receive a transactional email without any database record.
  • Any custom domain model becomes a notification recipient with a single use Notifiable; declaration.

Built-in delivery channels:

  • Email channel — sends styled HTML/plain-text emails using TYPO3's mail system and Fluid templates.
  • Database channel — persists notifications in a database table so they can be retrieved and displayed as an in-app notification centre.

Both channels are extensible. Implement ChannelInterface to add Slack, push notifications, SMS, webhooks, or any other transport — the channel is automatically registered with no configuration required.

The included backend module (Web > Notifications) is a ready-to-use tool for editors who need to compose and send messages to frontend users. It also acts as a reference implementation showing how to dispatch batch notifications to a resolved list of Notifiable recipients.

When Should You Use It? 

Use lex_notifications whenever you need to decouple the act of triggering a notification from the act of delivering it. Typical scenarios:

  • E-commerce — order confirmed, shipment dispatched, invoice available.
  • Workflow / approval — content submitted for review, approved, rejected.
  • Account events — registration, password reset, login from new device.
  • System alerts — an extension notifies a backend user when a background job fails.
  • Cross-user messaging — a frontend user triggers a notification to another frontend user (e.g. a collaboration platform).
  • Editorial broadcasts — backend editors send announcements to groups of frontend users via the included module.
  • Any programmatic trigger — if something happens in your TYPO3 application and someone needs to know about it, this extension handles the delivery.

Core Concepts 

Notifiable
Any PHP object that uses the Notifiable trait. It gains notify() and notifyNow() methods and can be passed as the recipient of any notification.
Notification
A PHP class extending Lex\Notifications\Notification. It declares which channels to use (via()) and how to format the payload for each channel (toMail(), toDatabase(), toSlack(), …).
Channel
A delivery mechanism implementing ChannelInterface. A channel receives a notifiable and a notification, extracts the relevant payload, and delivers it (sends an email, writes a DB row, calls a webhook, …).
NotificationDispatcherInterface
The central dispatcher. Inject it anywhere in your TYPO3 code to send notifications without depending on concrete implementations.
ShouldQueue
A marker interface. When a notification class implements it, dispatching goes through Symfony Messenger so delivery happens asynchronously in a CLI worker — keeping your HTTP responses fast.

Architecture Overview 

┌──────────────────────────────────────────────────────────────┐
│  Any PHP code: Controller · Plugin · Service · Scheduler     │
│                                                              │
│  $user->notify(new OrderConfirmed($order));                  │
│  // or                                                       │
│  $dispatcher->send([$userA, $userB], new Announcement());    │
└─────────────────────────┬────────────────────────────────────┘
                          │
                          ▼
┌──────────────────────────────────────────────────────────────┐
│              NotificationManager (Dispatcher)                │
│                                                              │
│  • Iterates notifiable(s)                                    │
│  • Calls notification->via($notifiable) → channel list       │
│  • If ShouldQueue → wraps in Messenger message               │
│  • Otherwise → sends immediately via each channel            │
└───────┬──────────────────┬────────────────────┬─────────────┘
        │                  │                    │
        ▼                  ▼                    ▼
┌──────────────┐  ┌────────────────┐  ┌─────────────────────┐
│ EmailChannel │  │DatabaseChannel │  │  YourCustomChannel  │
│ (TYPO3 Mail) │  │ (Extbase / DB) │  │  (Slack, SMS, …)    │
└──────────────┘  └────────────────┘  └─────────────────────┘

        ▲                  ▲                    ▲
        │       Notifiable recipients            │
┌───────┴──────────────────┴────────────────────┴─────────────┐
│  FrontendUser · BackendUser · InlineClass · Any domain model │
└──────────────────────────────────────────────────────────────┘
Copied!

Notifications flow from any caller through the NotificationManager, which delegates to the channels declared in the notification's via() method. Each channel knows how to format and deliver the payload for its transport. The recipients can be any combination of objects using the Notifiable trait.

Screenshots 

Backend module — message list

The included backend module lists all composed messages with their send status, level indicator and action buttons.

Backend module — compose message

The create form lets editors write a message, select severity level, target recipients (individual frontend users or groups) and pick delivery channels.

Installation 

Requirements 

  • TYPO3 CMS 13.4 or 14 or higher
  • PHP 8.2 or higher
  • Composer-based TYPO3 installation (strongly recommended)

The extension requires two additional PHP packages that are pulled in automatically via Composer:

  • nesbot/carbon ^3.2 — human-readable date/time differences
  • illuminate/collections ^12.69 — fluent collection helpers

Installation via TER (non-Composer) 

  1. Download the extension from the TYPO3 Extension Repository.
  2. Upload and install it via Admin Tools > Extensions.
  3. Run Admin Tools > Maintenance > Analyze Database Structure to create the required tables.

Post-Installation Steps 

No configuration needed. Login in the backend, and verify that the module Notification Messages is present.

Database Tables 

The extension creates two tables:

Table

Purpose

tx_lexnotifications_domain_model_notification

Stores in-app (database-channel) notifications for frontend users. Each row represents one unread or archived notification.

tx_lexnotifications_domain_model_message

Stores messages composed in the backend module, including recipient lists, delivery channels and send status.

Configuration 

Extension Configuration 

The extension does not currently expose options in the TYPO3 Extension Configuration panel (Admin Tools > Settings > Extension Configuration). All behaviour is controlled through TypoScript or PHP configuration.

TypoScript 

The extension ships with empty constants.typoscript and setup.typoscript templates. These are reserved for future frontend plugin configuration. No TypoScript is required for the backend module or the developer API.

Symfony Messenger (Queue Configuration) 

The BackendUserSentMessageToFrontendUser notification implements the ShouldQueue interface, which means it is dispatched via the Symfony Messenger component by default.

To process the queue, configure a transport and run the messenger worker:

vendor/bin/typo3 messenger:consume
Copied!

Refer to the TYPO3 Core documentation on Symfony Messenger for full transport configuration options (Doctrine, Redis, AMQP, etc.).

If you want to send notifications synchronously (without a queue worker), call notifyNow() instead of notify() — see Developer Reference.

Backend Module Access 

The Web > Notification Messages module is registered under the web module group. Access is controlled via TYPO3's standard backend user/group permissions.

To grant a backend user group access:

  1. Go to System > Backend Users and edit the relevant user group.
  2. Under Access Lists > Modules, tick Web > Notification Messages.
  3. Save and clear caches.

Notification Levels 

Levels follow RFC 5424 severity constants:

Constant

Value

Meaning

NotificationLevel::LEVEL_INFO

Informational message

NotificationLevel::LEVEL_NOTICE

1

Normal but significant event

NotificationLevel::LEVEL_WARNING

2

Warning condition

NotificationLevel::LEVEL_ERROR

3

Error condition

NotificationLevel::LEVEL_CRITICAL

4

Critical condition

NotificationLevel::LEVEL_ALERT

5

Action must be taken immediately

NotificationLevel::LEVEL_EMERGENCY

6

System is unusable

Notification Channels 

Two built-in channels are available:

Constant

String key

Implementation class

NotificationChannel::CHANNEL_MAIL

mail

Lex\Notifications\Channel\EmailChannel

NotificationChannel::CHANNEL_DATABASE

database

Lex\Notifications\Channel\DatabaseChannel

Custom channels are automatically registered by implementing Lex\Notifications\Channel\ChannelInterface — no Services.yaml entry needed. See Adding Custom Channels for the full pattern.

Backend Module: Web > Notification Messages 

After installation, navigate to Web > Notification Messages in the TYPO3 backend. The module lets editors compose messages and deliver them to frontend users (individuals or groups) via email and/or database notifications.

Listing Messages 

The module overview displays all composed messages in a paginated list (50 per page). Each row shows:

  • Level — severity badge (Info, Notice, Warning, Error, …)
  • Subject — message title
  • Created — creation date and author
  • Sent at — timestamp of last send, or "Not sent" if pending
  • Actions — Send / Resend buttons

Use the filter bar at the top to narrow results by level.

Composing a Message 

Click the Create button (pencil icon) in the module button bar.

Field

Required

Description

Level

Yes

Severity of the notification (Info, Notice, Warning, …). Displayed as a badge in both the backend list and the notification email.

Subject

Yes

Short title of the message (max 255 characters).

Message

Yes

Full message body. Supports basic HTML.

Link

No

Optional URL or internal TYPO3 page link displayed as a call-to-action button in the notification.

Recipients

Yes

Select one or more frontend users or frontend user groups. All members of selected groups will receive the notification.

Excluded recipients

No

Frontend users to exclude from delivery (useful to target a group but skip specific members).

Channels

No

Select one or more delivery channels: Mail and/or Database. If left empty, both channels are used.

Click Save to store the message without sending it.

Sending Messages 

After saving (or from the list view), click the Send button. The module will:

  1. Resolve all recipient UIDs from the selected users and groups, minus any excluded recipients.
  2. Remove duplicates.
  3. Load NotifiableFrontendUser objects for each resolved UID.
  4. Instantiate a BackendUserSentMessageToFrontendUser notification.
  5. Call $dispatcher->send($recipients, $notification) — the same call you would make from your own PHP code.
  6. Record the sent_at timestamp on the message record.
  7. Display a flash message confirming how many recipients were notified.

Resending Messages 

Click Resend to dispatch a message again to the same recipients and channels. The sent_at timestamp is updated. Useful when delivery failed or you want to send a reminder.

Using the Module as a Code Sample 

The backend module is deliberately simple so that its source code is easy to read. If you need to send batch notifications from your own extension, study NotificationController::sendAction() — it shows the full pattern:

// Simplified version of what the module does:

$recipients = $this->resolveRecipients($message);   // returns Notifiable[]

$notification = new BackendUserSentMessageToFrontendUser(
    subject:  $message->getSubject(),
    message:  $message->getMessage(),
    level:    $message->getLevel(),
    link:     $message->getLink(),
    channels: $this->parseChannels($message),
);

$this->notificationDispatcher->send($recipients, $notification);
Copied!

Replace BackendUserSentMessageToFrontendUser with your own Notification subclass and $recipients with any collection of Notifiable objects to adapt this pattern to your use case.

Developer Reference 

The Notifiable Trait 

The Notifiable trait is the only requirement for a class to receive notifications. Add it to any PHP object — Extbase entity, plain PHP class, anonymous class — and that object immediately becomes a valid notification recipient.

use Lex\Notifications\Domain\Model\Ability\Notifiable;

class MyModel
{
    use Notifiable;
}

// That's it. Now you can do:
$instance = new MyModel();
$instance->notify(new SomeNotification());
Copied!

There is no registry, no database table for recipients, no configuration. Any object with the trait can be passed to the dispatcher.

Making a Model Notifiable 

For email delivery, also add HasRouteNotificationForMail. This trait provides the routeNotificationForMail() method, which the email channel calls to resolve the recipient's email address:

use Lex\Notifications\Domain\Model\Ability\HasRouteNotificationForMail;
use Lex\Notifications\Domain\Model\Ability\Notifiable;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class FrontendUser extends AbstractEntity
{
    use Notifiable;
    use HasRouteNotificationForMail;

    // // Retrieve from the class properties, database or other source
    public function getEmail(): string { return $this->email; }
    public function getFirstName(): ?string { return $this->firstName; }
    public function getLastName(): ?string { return $this->lastName; }
}
Copied!

The Notifiable trait exposes two methods:

// Dispatches via Symfony Messenger if the notification implements ShouldQueue
$user->notify(new OrderConfirmed($order));

// Always sends immediately, ignoring ShouldQueue
$user->notifyNow(new OrderConfirmed($order), ['mail', 'database']);
Copied!

Who Can Be a Recipient? 

Because the only requirement is the Notifiable trait, the recipient can be anything. Here are common patterns.

Frontend user notifies another frontend user

A collaboration feature where one user shares content with another:

// In a frontend plugin action
$sender = $this->notifiableFrontendUserRepository->findByUid($senderUid);
$recipients = $this->notifiableFrontendUserRepository->findByUids($recipientUids);
$this->notificationDispatcher->send($recipients, new ContentSharedWithYou($page, $sender));
Copied!

Extension notifies a backend user

A Scheduler task or service that alerts an admin when a background job fails:

// An inline class wrapping a backend user record — no persistent domain model needed
$admin = new class($backendUserRecord['email'], $backendUserRecord['realName']) {
    use Notifiable;
    use HasRouteNotificationForMail;

    protected string $firstName;
    protected string $lastName;

    public function __construct(protected readonly string $email, string $fullName) {
        $parts = explode(' ', trim($fullName), 2);
        $this->firstName = $parts[0] ?? '';
        $this->lastName  = $parts[1] ?? '';
    }
    public function getEmail(): string { return $this->email; }
    public function getFirstName(): ?string { return $this->firstName; }
    public function getLastName(): ?string { return $this->lastName; }
};
$admin->notifyNow(new SchedulerJobFailed($taskName, $errorMessage));
Copied!

Inline / anonymous notifiable (no database record needed)

Send a one-off notification to any email address without a domain model:

$recipient = new class('info@example.com') {
    use Notifiable;
    use HasRouteNotificationForMail;

    public function __construct(protected readonly string $email) {}
    public function getEmail(): string { return $this->email; }
    public function getFirstName(): ?string { return null; }
    public function getLastName(): ?string { return null; }
};

$recipient->notifyNow(new ContactFormReceived($formData));
Copied!

Multiple recipients of different types in one call

The dispatcher accepts an array of notifiables — they do not need to be the same class:

 $team = [$teamMemberA, $teamMemberB, ...]

$this->notificationDispatcher->send(
    $team,
    new ImportantAnnouncement($text),
);
Copied!

Each recipient's via() result can differ, so the notification class can adapt channels based on the notifiable type:

public function via(object $notifiable): array
{
    // Only store in-DB for actual frontend user records
    if ($notifiable instanceof NotifiableFrontendUser) {
        return [NotificationChannel::CHANNEL_MAIL, NotificationChannel::CHANNEL_DATABASE];
    }

    return [NotificationChannel::CHANNEL_MAIL];
}
Copied!

Creating a Notification 

Extend the abstract Notification class and implement the methods for each channel your notification uses:

namespace MyVendor\MyExtension\Notification;

use Lex\Notifications\Notification;
use Lex\Notifications\NotificationChannel;
use Lex\Notifications\NotificationLevel;
use TYPO3\CMS\Core\Mail\MailMessage;

final class OrderConfirmed extends Notification
{
    public function __construct(
        private readonly Order $order,
    ) {}

    /**
     * RFC 5424 severity level for this notification.
     * Optional - Only if you want to specify a notification level different from INFO
     */
    public function getLevel(): int
    {
        return NotificationLevel::LEVEL_INFO;
    }

    /**
     * Which channels to use. Receives the notifiable so you can adapt
     * the channel list per recipient type/preference.
     *
     * @return string[]
     */
    public function via(object $notifiable): array
    {
        return $notifiable->prefers_sms ? ['vonage'] : [NotificationChannel::CHANNEL_MAIL, NotificationChannel::CHANNEL_DATABASE];
    }

    /**
     * Optional - Payload for the email channel.
     * Build your own MailMessage if you want.
     */
    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage())
            ->subject('Order #' . $this->order->getNumber() . ' confirmed')
            ->html('<p>Thank you! Your order is being processed.</p>')
            ->to($notifiable->getEmail());
    }

    /**
     * Optional - Payload for the database channel.
     * Returned array is JSON-encoded and stored as-is.
     *
     * @return array<string, mixed>
     */
    public function toDatabase(object $notifiable): array
    {
        return [
            'level'    => $this->getLevel(),
            'subject'  => 'Order #' . $this->order->getNumber() . ' confirmed',
            'message'  => 'Your order has been received and is being processed.',
            'order_id' => $this->order->getUid(),
        ];
    }

    /**
     * Custom channel - Payload for the Slack channel.
     */
    public function toSlack(object $notifiable): SlackMessage
    {
        return (new SlackMessage)
            ->...
            ->...
            ->...;
    }
}
Copied!

Only implement the channel methods you actually use. If your notification only sends email, there is no need for toDatabase().

Queuing Notifications 

Implement the ShouldQueue marker interface to have your notification dispatched asynchronously via Symfony Messenger:

use Illuminate\Contracts\Queue\ShouldQueue;

final class OrderConfirmed extends Notification implements ShouldQueue
{
    // No extra methods needed — the interface is a marker only.
}
Copied!

When ShouldQueue is implemented, calling notify() wraps the notification in a NotificationQueued Messenger message. A CLI worker must be running to process the queue:

vendor/bin/typo3 messenger:consume async --time-limit=3600
Copied!

Call notifyNow() or sendNow() to bypass the queue and deliver immediately regardless of ShouldQueue.

Using the Dispatcher Directly 

Inject NotificationDispatcherInterface into any service, controller, or plugin. This is the recommended approach when you do not have a direct reference to a notifiable object, or when you need to send to multiple recipients:

use Lex\Notifications\NotificationDispatcherInterface;

final class OrderService
{
    public function __construct(
        private readonly NotificationDispatcherInterface $notifications,
        private readonly FrontendUserRepository $userRepository,
    ) {}

    public function completeOrder(Order $order): void
    {
        $buyer = $this->userRepository->findByUid($order->getBuyerUid());

        // Dispatches via Messenger queue if ShouldQueue is implemented
        $this->notifications->send($buyer, new OrderConfirmed($order));

        // Forces immediate delivery
        $this->notifications->sendNow($buyer, new OrderConfirmed($order));
    }
}
Copied!

Send to a batch of recipients in one call:

$subscribers = $this->frontendUserRepository->findByNewsletterGroup($groupId);

$this->notifications->send(
    $subscribers->toArray(),
    new MonthlyNewsletter($content),
);
Copied!

The dispatcher iterates each notifiable independently, so a failed delivery for one recipient does not block the others.

You can also target a single specific channel for one call using channel():

// Deliver only via the database channel, regardless of what via() returns
$this->notificationDispatcher->channel(NotificationChannel::CHANNEL_DATABASE)
    ->send($user, new InvoicePaid($invoice));
Copied!

Practical Use Cases 

Workflow approval alert to a backend user

// In a DataHandler hook or custom service
$responsible = new class('editor@example.com') {
    use \Lex\Notifications\Domain\Model\Ability\Notifiable;
    use \Lex\Notifications\Domain\Model\Ability\HasRouteNotificationForMail;
    public function __construct(protected readonly string $email) {}
    public function getEmail(): string { return $this->email; }
    public function getFirstName(): ?string { return null; }
    public function getLastName(): ?string { return null; }
};
$this->notifications->sendNow(
    $responsible,
    new ContentPendingReview($pageUid, $submitter),
);
Copied!

Frontend user triggers a notification to another frontend user

// In a frontend plugin action (e.g. a messaging feature)
$sender   = $this->notifiableFrontendUserRepository->findByUid($senderUid);
$receiver = $this->notifiableFrontendUserRepository->findByUid($receiverUid);

$receiver->notify(new NewMessageReceived($sender, $messageText));
Copied!

Extension notifies multiple channels for different severity levels

final class PaymentFailed extends Notification
{
    public function via(object $notifiable): array
    {
        // Critical failures go to mail + database + Slack
        return [
            NotificationChannel::CHANNEL_MAIL,
            NotificationChannel::CHANNEL_DATABASE,
            'slack',
        ];
    }
}
Copied!

Sending to a plain email without any domain model

$contact = new class('customer@example.com') {
    use \Lex\Notifications\Domain\Model\Ability\Notifiable;
    use \Lex\Notifications\Domain\Model\Ability\HasRouteNotificationForMail;
    public function __construct(protected readonly string $email) {}
};

$contact->notifyNow(new OrderReceiptEmail($order));
Copied!

Reading Database Notifications 

Inject DatabaseNotificationRepository to query stored notifications for the currently logged-in frontend user:

use Lex\Notifications\Domain\Repository\DatabaseNotificationRepository;

class NotificationController extends ActionController
{
    public function __construct(
        private readonly DatabaseNotificationRepository $notificationRepository,
    ) {}

    public function indexAction(): ResponseInterface
    {
        $uid = $this->getContext()->getAspect('frontend.user')->get('id');

        $this->view->assign(
            'notifications',
            $this->notificationRepository->findByNotifiable($uid),
        );
        return $this->htmlResponse();
    }

    public function markAllReadAction(): ResponseInterface
    {
        $uid = $this->getContext()->getAspect('frontend.user')->get('id');
        $this->notificationRepository->markAllAsReadForNotifiable($uid);
        return $this->redirect('index');
    }
}
Copied!

Available repository methods:

Method

Description

findByNotifiable(int $uid)

Returns all notifications for a notifiable UID, ordered by creation date descending.

markAllAsReadForNotifiable(int $uid)

Sets read_at to the current timestamp for every unread notification belonging to that UID.

removeAllForNotifiable(int $uid)

Permanently deletes all notifications for that UID.

DatabaseNotification Model 

Each stored notification exposes:

Getter

Type

Description

getType()

string

Fully-qualified notification class name.

getLevel()

int

RFC 5424 severity level.

getData()

string

Raw JSON payload as stored by toDatabase().

getDataAsArray()

array

Decoded payload as a PHP array.

getReadAt()

\DateTime|null

Read timestamp, or null if unread.

getCreatedAt()

\DateTime

Creation timestamp.

getDiffCreatedAtForHumans()

string

Human-readable relative time (e.g. "3 minutes ago").

markAsRead()

void

Sets readAt to the current date/time.

Adding Custom Channels 

Step 1 — implement the interface

namespace MyVendor\MyExtension\Notification\Channel;

use Lex\Notifications\Channel\ChannelInterface;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Lex\Notifications\Notification;

#[AutoconfigureTag('notifications.channel')]
final class SlackChannel implements ChannelInterface
{
    public function __construct(
        private readonly SlackClient $slack,
    ) {}

    /**
     * Optional. Provides a short string key used in via().
     * When omitted, the fully-qualified class name is used as the key.
     */
    public function getName(): string
    {
        return 'slack';
    }

    public function send(object $notifiable, Notification $notification): void
    {
        $payload = $notification->toSlack($notifiable);
        $this->slack->post($notifiable->getSlackWebhookUrl(), $payload);
    }
}
Copied!

Step 2 — return the key from via()

public function via(object $notifiable): array
{
    return ['slack', NotificationChannel::CHANNEL_MAIL];
}
Copied!

The NotificationManager resolves the channel by its key at send time. If getName() is not defined, use the fully-qualified class name as the key in via():

public function via(object $notifiable): array
{
    return [MyVendor\MyExtension\Notification\Channel\SlackChannel::class];
}
Copied!

Channel key lookup order:

  1. getName() — if the method exists on the channel class.
  2. Fully-qualified class name — as a fallback.

To ensure a custom channel is properly detected and registered into the manager's iterator, you must assign the notifications.channel tag to your class.

You can achieve this in one of three ways:

Option A: Declaration in Services.yaml

Add your concrete channel class manually with the required tag in your extension's Configuration/Services.yaml:

services:
    Lex\Notifications\MicrosoftTeams\Channel\TeamsChannel:
        tags: ['notifications.channel']
Copied!

Option B: Using #[AutoconfigureTag] in the channel class (Recommended)

Keep your YAML file clean by adding the Symfony attribute directly above your class definition:

use Lex\Notifications\Channel\ChannelInterface;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag('notifications.channel')]
final class TeamsChannel implements ChannelInterface
{
    // ... Your channel logic
}
Copied!

Option C: Using #[Autoconfigure] in the channel class

If you need to change other service options (like visibility) while registering the tag, you can pass the tags directly into the #[Autoconfigure] attribute:

use Lex\Notifications\Channel\ChannelInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

#[Autoconfigure(public: true, tags: ['notifications.channel'])]
final class TeamsChannel implements ChannelInterface
{
    // ... Your channel logic
}
Copied!

Customising Email Templates 

Email templates live in:

Resources/Private/
├── Layouts/Email/
│   ├── NotificationLayout.html   # HTML wrapper
│   └── NotificationLayout.txt    # Plain-text wrapper
└── Templates/Email/
    ├── BackendUserSentMessageToFrontendUser.html
    └── BackendUserSentMessageToFrontendUser.txt
Copied!

Override them in your site package by adjusting the Fluid template paths. The built-in templates receive these variables:

Variable

Type

Description

{level}

int

Notification severity level.

{subject}

string

Message subject/title.

{message}

string

Message body (may contain HTML).

{link}

string|null

Optional call-to-action URL or typolink string.

For your own Notification subclasses you can use a completely different template — simply return the rendered HTML from your toMail() method using whatever rendering approach fits your extension.

Changelog 

1.3.0 

  • [FEATURE] Custom channels implementing ChannelInterface can now be registered via a Symfony DI tag (notifications.channel).
  • [FEATURE] Optional getName(): string method on channel classes. When present, its return value is used as the channel key in via(). When absent, the fully-qualified class name is used as the key.
  • [FEATURE] NotificationManager now collects channels through #[AutowireIterator('notifications.channel')] and stores live service instances — channels benefit from full DI, including scoped services.
  • [BREAKING] NotificationManager::channel() now resolves from the injected service map instead of calling GeneralUtility::makeInstance(). Custom channels previously registered only as public services must add the notifications.channel tag (or implement ChannelInterface so the _instanceof rule picks them up automatically).
  • [CHANGE] Added TYPO3 14 compatibility (^13.4 || ^14).
  • [CHANGE] AbstractModuleController converted to constructor injection. Removed deprecated setter-injection methods (injectModuleTemplateFactory, injectPageRenderer, injectIconFactory, injectBackendUriBuilder).
  • [FIX] ?array $channels = null parameter type corrected across NotificationDispatcherInterface, NotificationManager, NotificationSender, and the Notifiable trait.
  • [FIX] TCA: removed deprecated interface key (dropped in TYPO3 13), cruser_id ctrl field (removed in TYPO3 13), and eval => 'trim' / eval => 'int' validators (removed in TYPO3 13).
  • [FIX] notifiable_id TCA field changed from type=input to type=number.
  • [FIX] SQL schema: removed int(11) display width, added missing link column in tx_lexnotifications_domain_model_message.

1.1.0 — 2024-01-01 

  • [FEATURE] Added Symfony Messenger queue integration via ShouldQueue marker interface.
  • [FEATURE] Added NotificationLevel constants following RFC 5424 (Info, Notice, Warning, Error, Critical, Alert, Emergency).
  • [FEATURE] Added resend action to the backend module.
  • [FEATURE] Pagination in the backend message list (50 items per page).
  • [FEATURE] Added level filter to the backend message list.
  • [FEATURE] French (fr) localization for all language files.
  • [FEATURE] Added getDiffCreatedAtForHumans() on DatabaseNotification using Carbon.
  • [CHANGE] Bumped TYPO3 requirement to 13.4+.
  • [CHANGE] Bumped PHP requirement to 8.2+.

1.0.0 — Initial Release 

  • Backend module for composing and sending messages to frontend users.
  • Email channel using TYPO3's mail system and Fluid templates.
  • Database channel for persistent in-app notifications.
  • Notifiable trait for Extbase domain models.
  • HasRouteNotificationForMail trait for email routing.
  • DatabaseNotificationRepository with findByNotifiable, markAllAsReadForNotifiable and removeAllForNotifiable methods.
  • Abstract Notification base class for custom notification types.
  • NotificationDispatcherInterface for dependency injection.
  • BackendUserSentMessageToFrontendUser built-in notification class.
  • English localization for all language files.