A Laravel-style notification system for TYPO3. Any PHP class can send a
notification to any object that uses the Notifiable trait — through any
combination of channels (email, database, Slack, …).
There is no constraint on the direction: a frontend user can notify another
frontend user, an extension can alert a backend user, a Scheduler task can
email an address with no domain model at all. If it uses Notifiable, it
can receive notifications.
A backend module (Web > Notifications) is included for editors who need to compose and send messages to frontend users. It also serves as a reference implementation for dispatching batch notifications from PHP code.
| Dependency | Version |
|---|---|
| TYPO3 CMS | ^13.4 |
| PHP | ^8.2 |
| nesbot/carbon | ^3.2 |
| illuminate/collections | ^12.69 |
composer require agencelex/notifications
vendor/bin/typo3 extension:setup
vendor/bin/typo3 upgrade:run
vendor/bin/typo3 cache:flushuse Lex\Notifications\Domain\Model\Ability\Notifiable;
use Lex\Notifications\Domain\Model\Ability\HasRouteNotificationForMail;
class FrontendUser extends AbstractEntity
{
use Notifiable;
use HasRouteNotificationForMail; // Needed for email delivery, remove if not needed
// When using HasRouteNotificationForMail
public function getEmail(): string { return 'john.doe@example.com'; } // Retrieve from local attributes, database or other source
public function getFirstName(): ?string { return null; }
public function getLastName(): ?string { return null; }
}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) {}
// Optional - If you want to specify a notification level different from INFO
public function getLevel(): int
{
return NotificationLevel::LEVEL_INFO;
}
// Specify delivery channels
public function via(object $notifiable): array
{
return [NotificationChannel::CHANNEL_MAIL, NotificationChannel::CHANNEL_DATABASE];
}
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());
}
public function toDatabase(object $notifiable): array
{
return [
'level' => $this->getLevel(),
'subject' => 'Order #' . $this->order->getNumber() . ' confirmed',
'message' => 'Your order has been received.',
];
}
}// From the notifiable itself
$user->notify(new OrderConfirmed($order)); // queued (if ShouldQueue)
$user->notifyNow(new OrderConfirmed($order)); // immediate
// From any service via the dispatcher
$this->notificationDispatcher->send($user, new OrderConfirmed($order));
$this->notificationDispatcher->sendNow($user, new OrderConfirmed($order));
// Multiple recipients
$this->notificationDispatcher->send([$userA, $userB], new Announcement());
$this->notificationDispatcher->sendNow([$userA, $userB], new Announcement());
// To a specific channel
$this->notificationDispatcher->channel(NotificationChannel::CHANNEL_DATABASE)->send($user, new InvoicePaid($invoice));Any object that uses the Notifiable trait — regardless of class hierarchy:
// Frontend user → frontend user
$sender = $this->notifiableFrontendUserRepository->findByUid($senderUid);
$recipients = $this->notifiableFrontendUserRepository->findByUids($recipientUids);
$this->notificationDispatcher->send($recipients, new ContentSharedWithYou($page, $sender));
// Extension → backend user (plain class, no DB record needed)
$admin = new class($backendEmail, $backendRealName) {
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($error));
// Any code → inline email recipient
$contact = new class($data) {
use Notifiable;
public function __construct(protected array $data) {}
};
$contact->notifyNow(new OrderReceiptEmail($order));It comes with 2 built-in channels:
| Key | Class | Description |
|---|---|---|
mail |
EmailChannel |
HTML/plain-text email via TYPO3 mail system |
database |
DatabaseChannel |
Persisted in-app notifications via Extbase |
Add any channel by implementing ChannelInterface:
final class SlackChannel implements ChannelInterface
{
public function send(object $notifiable, Notification $notification): void
{
$this->slack->post($notifiable->routeNotificationForSlack(), $this->buildJsonPayload(($notifiable, $notification));
}
}Return the channel's string key from via() and it will be resolved automatically.
Implement the ShouldQueue marker interface to dispatch via Symfony Messenger:
final class OrderConfirmed extends Notification implements ShouldQueue { }Run the worker:
vendor/bin/typo3 messenger:consumeCall notifyNow() / sendNow() to bypass the queue at any time.
| Constant | Value |
|---|---|
NotificationLevel::LEVEL_INFO |
0 |
NotificationLevel::LEVEL_NOTICE |
1 |
NotificationLevel::LEVEL_WARNING |
2 |
NotificationLevel::LEVEL_ERROR |
3 |
NotificationLevel::LEVEL_CRITICAL |
4 |
NotificationLevel::LEVEL_ALERT |
5 |
NotificationLevel::LEVEL_EMERGENCY |
6 |
Web > Notifications lets editors:
- Compose messages (subject, body, level, link)
- Target frontend users or groups, with optional exclusions
- Choose delivery channels (email and/or database)
- Send immediately or queue for later, and resend at any time
The module source (Classes/Controller/Backend/NotificationController.php)
is intentionally simple and can be used as a reference for dispatchingUserNotificationsController.php
batch notifications from PHP code.
// In a frontend plugin
$uid = $this->getContext()->getAspect('frontend.user')->get('id');
// Or, in Extbase controller
// $uid = $this->request->getAttribute('frontend.user')->getUserId();
$notifications = $this->notificationRepository->findByNotifiable($uid);
// Assign the notifications to the view
$this->view->assign('notifications', $notifications);
// Later, mark as read or remove all
$this->notificationRepository->markAllAsReadForNotifiable($uid);
$this->notificationRepository->removeAllForNotifiable($uid);# Code style
composer run cgl
# Static analysis
composer run phpstan
# Tests
composer run testFull documentation: https://docs.typo3.org/p/lex/notifications/1.1/en-us/
GPL-2.0-or-later — see LICENSE.