.. include:: /Includes.rst.txt
.. _developer-api:
====================
Developer Reference
====================
.. contents::
:depth: 2
:local:
.. _notifiable-overview:
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.
.. code-block:: php
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:
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:
.. code-block:: php
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:
.. code-block:: php
// 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']);
.. _recipient-examples:
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:
.. code-block:: php
// 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:
.. code-block:: php
// 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:
.. code-block:: php
$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:
.. code-block:: php
$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:
.. code-block:: php
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:
Creating a Notification
=======================
Extend the abstract ``Notification`` class and implement the methods for each
channel your notification uses:
.. code-block:: php
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('
Thank you! Your order is being processed.
')
->to($notifiable->getEmail());
}
/**
* Payload for the database channel.
* Returned array is JSON-encoded and stored as-is.
*
* @return array
*/
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:
Queuing Notifications
=====================
Implement the ``ShouldQueue`` marker interface to have your notification
dispatched asynchronously via Symfony Messenger:
.. code-block:: php
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:
.. code-block:: bash
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:
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:
.. code-block:: php
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:
.. code-block:: php
$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:
Practical Use Cases
===================
**Workflow approval alert to a backend user**
.. code-block:: php
// 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**
.. code-block:: php
// 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**
.. code-block:: php
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**
.. code-block:: php
$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:
Reading Database Notifications
===============================
Inject ``DatabaseNotificationRepository`` to query stored notifications
for the currently logged-in frontend user:
.. code-block:: php
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:
.. t3-field-list-table::
:header-rows: 1
- :Method: Method
:Description: Description
- :Method: ``findByNotifiable(int $uid)``
:Description: Returns all notifications for a notifiable UID, ordered
by creation date descending.
- :Method: ``markAllAsReadForNotifiable(int $uid)``
:Description: Sets ``read_at`` to the current timestamp for every unread
notification belonging to that UID.
- :Method: ``removeAllForNotifiable(int $uid)``
:Description: Permanently deletes all notifications for that UID.
.. _database-notification-model:
DatabaseNotification Model
==========================
Each stored notification exposes:
.. t3-field-list-table::
:header-rows: 1
- :Getter: Getter
:Type: Type
:Description: Description
- :Getter: ``getType()``
:Type: string
:Description: Fully-qualified notification class name.
- :Getter: ``getLevel()``
:Type: int
:Description: RFC 5424 severity level.
- :Getter: ``getData()``
:Type: string
:Description: Raw JSON payload as stored by ``toDatabase()``.
- :Getter: ``getDataAsArray()``
:Type: array
:Description: Decoded payload as a PHP array.
- :Getter: ``getReadAt()``
:Type: \\DateTime\|null
:Description: Read timestamp, or ``null`` if unread.
- :Getter: ``getCreatedAt()``
:Type: \\DateTime
:Description: Creation timestamp.
- :Getter: ``getDiffCreatedAtForHumans()``
:Type: string
:Description: Human-readable relative time (e.g. "3 minutes ago").
- :Getter: ``markAsRead()``
:Type: void
:Description: Sets ``readAt`` to the current date/time.
.. _custom-channels:
Adding Custom Channels
======================
Implement ``ChannelInterface`` to create any delivery channel you need:
.. code-block:: php
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.
.. _email-templates:
Customising Email Templates
============================
Email templates live in:
.. code-block:: none
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:
.. t3-field-list-table::
:header-rows: 1
- :Variable: Variable
:Type: Type
:Description: Description
- :Variable: ``{level}``
:Type: int
:Description: Notification severity level.
- :Variable: ``{subject}``
:Type: string
:Description: Message subject/title.
- :Variable: ``{message}``
:Type: string
:Description: Message body (may contain HTML).
- :Variable: ``{link}``
:Type: string\|null
:Description: 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.