Tip
Use the table of contents in the left panel to navigate.
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());
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. It expects an $email
property on the class:
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;
protected string $email = '';
}
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']);
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
$recipient = $this->frontendUserRepository->findByUid($targetUid);
$recipient->notify(new ContentSharedWithYou($page, $sender));
Extension notifies a backend user
A Scheduler task or service that alerts an admin when a background job fails:
// BackendNotifiableUser wraps a TYPO3 backend user record
class BackendNotifiableUser
{
use Notifiable;
use HasRouteNotificationForMail;
public function __construct(
public readonly string $email,
public readonly string $username,
) {}
}
$admin = new BackendNotifiableUser(
email: $backendUserRecord['email'],
username: $backendUserRecord['username'],
);
$admin->notifyNow(new SchedulerJobFailed($taskName, $errorMessage));
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(public readonly string $email) {}
};
$recipient->notifyNow(new ContactFormReceived($formData));
Multiple recipients of different types in one call
The dispatcher accepts an array of notifiables — they do not need to be the same class:
$this->notificationDispatcher->send(
[$frontendUser, $backendAdmin, $externalEmail],
new ImportantAnnouncement($text),
);
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];
}
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.
*/
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.
*
* @return string[]
*/
public function via(object $notifiable): array
{
return [
NotificationChannel::CHANNEL_MAIL,
NotificationChannel::CHANNEL_DATABASE,
];
}
/**
* Payload for the email channel.
*/
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());
}
/**
* 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(),
];
}
}
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 TYPO3\CMS\Core\Messaging\ShouldQueue;
final class OrderConfirmed extends Notification implements ShouldQueue
{
// No extra methods needed — the interface is a marker only.
}
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
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));
}
}
Send to a batch of recipients in one call:
$subscribers = $this->frontendUserRepository->findByNewsletterGroup($groupId);
$this->notifications->send(
$subscribers->toArray(),
new MonthlyNewsletter($content),
);
The dispatcher iterates each notifiable independently, so a failed delivery for one recipient does not block the others.
Practical Use Cases
Workflow approval alert to a backend user
// In a DataHandler hook or custom service
$responsible = new BackendNotifiableUser(email: 'editor@example.com');
$this->notifications->sendNow(
$responsible,
new ContentPendingReview($pageUid, $submitter),
);
Frontend user triggers a notification to another frontend user
// In a frontend plugin action (e.g. a messaging feature)
$sender = $this->frontendUserRepository->findByUid($senderUid);
$receiver = $this->frontendUserRepository->findByUid($receiverUid);
$receiver->notify(new NewMessageReceived($sender, $messageText));
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',
];
}
}
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(public readonly string $email) {}
};
$contact->notifyNow(new OrderReceiptEmail($order));
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');
}
}
Available repository methods:
|
Method |
Description |
|---|---|
|
|
Returns all notifications for a notifiable UID, ordered by creation date descending. |
|
|
Sets |
|
|
Permanently deletes all notifications for that UID. |
DatabaseNotification Model
Each stored notification exposes:
|
Getter |
Type |
Description |
|---|---|---|
|
|
string |
Fully-qualified notification class name. |
|
|
int |
RFC 5424 severity level. |
|
|
string |
Raw JSON payload as stored by |
|
|
array |
Decoded payload as a PHP array. |
|
|
\DateTime|null |
Read timestamp, or |
|
|
\DateTime |
Creation timestamp. |
|
|
string |
Human-readable relative time (e.g. "3 minutes ago"). |
|
|
void |
Sets |
Adding Custom Channels
Implement ChannelInterface to create any delivery channel you need:
namespace MyVendor\MyExtension\Notification\Channel;
use Lex\Notifications\Channel\ChannelInterface;
use Lex\Notifications\Notification;
final class SlackChannel implements ChannelInterface
{
public function __construct(
private readonly SlackClient $slack,
) {}
public function send(object $notifiable, Notification $notification): void
{
$payload = $notification->toSlack($notifiable);
$this->slack->post($notifiable->getSlackWebhookUrl(), $payload);
}
}
Register the channel as a service in Services.yaml and return its string
key (e.g. 'slack') from your notification's via() method. The
NotificationManager will resolve it from the container automatically.
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
Override them in your site package by adjusting the Fluid template paths. The built-in templates receive these variables:
|
Variable |
Type |
Description |
|---|---|---|
|
|
int |
Notification severity level. |
|
|
string |
Message subject/title. |
|
|
string |
Message body (may contain HTML). |
|
|
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.