mgilet/notification-bundle
Symfony bundle that adds a flexible notification system to your app. Attach notifications to any entity via a trait and interface, create and manage them with a service, and display lists and unread counts through built-in controllers and Twig helpers.
Read this first: it's a Symfony bundle, not a Laravel package. There is no service provider, no config/notifications.php, no facade. composer require mgilet/notification-bundle in a Laravel app gets you a bundle class nothing will ever boot. The 4.0 release (the first in years) fixes bugs and adds config, but it is still a Symfony bundle tied to the Symfony/Doctrine stack. Treat it as prior art and a data model spec, not a Laravel dependency.
If you are maintaining a Symfony app that already uses it:
composer require mgilet/notification-bundle and register Mgilet\NotificationBundle\MgiletNotificationBundle in config/bundles.php.bin/console doctrine:schema:update --dump-sql — the bundle ships three mapped entities: Notification (the message payload), NotifiableEntity (a polymorphic pointer to your user/account), and NotifiableNotification (the join table carrying the seen flag).User implement Mgilet\NotificationBundle\Model\NotifiableInterface and pull in NotifiableTrait (getIdentifier() / getClass()).Mgilet\NotificationBundle\Manager\NotificationManager, aliased to NotifierInterface) — confirm the exact id with bin/console debug:container | grep mgilet before wiring it, the alias naming changed across versions.Notification entity, no longer use resolve_target_entities. Instead add config/packages/mgilet_notification.yaml with mgilet_notification.notification_class: App\Entity\MyCustomNotification.First use case: the notification bell. On OrderShipped, create a Notification with a subject like order, a message, and a deep link, then addNotification($user, $notification). Render getUnseenNotificationCount($user) in the nav and getNotifications($user) on a /notifications page, each row exposing seen so you can style unread vs read.
If you're a Laravel developer (likely): don't install it. Copy the shape instead. Laravel's notifications table with notifiable_type/notifiable_id/read_at solves the same problem in one table, and Notifiable + Notification::send() + ->database channel gives you the bell in an afternoon. The one thing this bundle does better is subjects (see below) — worth stealing.
Wrap the notifier, don't scatter it. Inject NotifierInterface into one NotificationService of your own, with intention-revealing methods (notifyOrderShipped, notifyMention). The bundle API is generic and call sites become unreadable fast if you new Notification() inline everywhere.
// Symfony
class NotificationService {
public function __construct(private NotifierInterface $notifier) {}
public function orderShipped(User $user, Order $order): void {
$n = (new Notification())
->setSubject('order') // grouping key
->setMessage("Order #{$order->getId()} shipped")
->setLink($this->router->generate('order_show', ['id' => $order->getId()]));
$this->notifier->addNotification($user, $n);
}
}
Drive it from domain events, not controllers. Symfony EventDispatcher listeners (or Laravel listeners) keep the notifier out of your request path and let you queue the listener later. This is the single highest-leverage integration decision — everything else is CRUD.
Use subject as a category, not a title. It's the one genuinely nice idea in the bundle: addNotification($user, $n, 'order') lets you later mark an entire subject seen in one call, so "clear all order pings" is one operation. Laravel has no equivalent out of the box — emulate it with a type column and a scoped update(['read_at' => now()]).
Three patterns map cleanly to Laravel equivalents:
| Bundle | Laravel |
|---|---|
Notification entity (persisted per delivery) |
notifications row via toDatabase() |
NotifiableEntity (class + identifier) |
morph columns notifiable_type/notifiable_id |
NotifiableNotification.seen |
read_at timestamp |
addNotifications($user, array) |
Notification::send($user, $collection) |
markAllAsSeen($user) |
$user->unreadNotifications->markAsRead() |
Custom notification entity (4.0 pattern). If you need extra columns on the payload, subclass the shipped entity and point the bundle at it via YAML. The 4.0 release makes NotificationManager::createNotification() return your custom class, so factory-style code and instanceof checks finally work.
# config/packages/mgilet_notification.yaml
mgilet_notification:
notification_class: App\Entity\MyCustomNotification
// App\Entity\MyCustomNotification extends the bundle entity and adds fields
#[ORM\Entity]
class MyCustomNotification extends \Mgilet\NotificationBundle\Entity\Notification
{
#[ORM\Column(nullable: true)]
private ?string $category = null;
}
Polling, because there's no push here. The bundle gives you counts, not sockets. Expose a tiny JSON endpoint returning the unread count and poll it every 30–60s with throttling, or use Laravel's broadcast notifications + Echo if you're porting. Don't poll getNotifications() — poll the count and fetch the list lazily when the bell is opened.
Bulk-add in one transaction. addNotifications() exists for fan-out, but the manager flushes per call in some versions; wrap multi-recipient sends in $em->transactional() or you'll issue a query per user.
composer.json constraints against your Symfony 6/7 and PHP 8.2+ stack before upgrading. Expect ReturnTypeWillChange and Doctrine dbal 3 issues unless 4.0 widened them. Fork-patch budget still applies.resolve_target_entities. 4.0 explicitly says to delete the doctrine resolve_target_entities directive and replace it with mgilet_notification.notification_class. Leaving both in place gives you conflicting mappings or a doctrine:mapping:info duplicate. Run doctrine:schema:validate immediately after.|raw only on trusted, server-built strings.Notification is a persisted entity, not a value object. Calling addNotification() twice with equivalent content creates two rows. There is no dedupe, no shouldSend() gate, no rate limiting. If you need "one unread per (user, subject, entity)", implement it yourself with a lookup before insert.seen flag lives on the join row. That's the right design, but it means markAllAsSeen($user) is an UPDATE across NotifiableNotification, and loading a notifiable's notifications pulls two levels of relations — watch for N+1 when rendering a list. Add fetch="EXTRA_LAZY" or a repository query with an explicit join.NotifiableEntity behind; deleting a notification leaves join rows pointing at nothing. Add onDelete="CASCADE" on the join's foreign keys or a cleanup command. Laravel's morph relation has the same hazard — ->cascadeOnDelete().$this->getUser(). If they don't, don't route them — write your own controller that takes the notification id and checks ownership before flipping seen.getUnseenNotificationCount() runs on every nav render. Cache the integer in session/Redis keyed by user id and bust it on write; otherwise it's a COUNT(*) per page view.mgilet_notification.notification_class config (no more resolve_target_entities), decorate NotifierInterface to add logging or idempotency without touching the bundle, and override shipped templates under templates/bundles/MgiletNotificationBundle/.bin/console debug:container | grep -i mgilet to find the real service id, bin/console debug:config mgilet_notification to confirm your custom class is picked up, doctrine:schema:validate after overriding entities, and doctrine:mapping:info to confirm the join entity isn't mapped twice.NotifierInterface in unit tests and assert on addNotification calls; for integration use SQLite in-memory and doctrine:schema:create in setUp(). Don't test the manager's internals — test that your NotificationService produced the right subject, message, and link. Add one test asserting createNotification() returns your custom class if you rely on the 4.0 fix.How can I help you explore Laravel packages today?