This prototype implements a complete notification system similar to Laravel's database notifications, using Symfony 7.3, Doctrine ORM, and Symfony Messenger.
✅ Notification Entity with fields: id, user, title, body, url, createdAt, readAt ✅ Repository method to fetch unread notifications ✅ Messenger integration for async/sync notification processing ✅ Error handling with recoverable and unrecoverable exceptions ✅ REST API endpoints for creating and managing notifications ✅ Service wrapper for easy notification dispatching
src/
├── Entity/
│ ├── User.php # User entity
│ └── Notification.php # Notification entity with markAsRead() & isRead()
├── Repository/
│ ├── UserRepository.php
│ └── NotificationRepository.php # findUnreadForUser() method
├── Message/
│ └── NewNotificationMessage.php # Messenger message class
├── MessageHandler/
│ └── NewNotificationHandler.php # Handles notification creation
├── Service/
│ └── Notifier.php # Convenience service for dispatching
└── Controller/
└── NotificationController.php # API endpoints
config/packages/messenger.yaml # Updated with routing config
use App\Service\Notifier;
class YourController extends AbstractController
{
public function someAction(Notifier $notifier): Response
{
// Simple usage
$notifier->notifyUser(
userId: 1,
title: 'New Order',
body: 'You have a new order #12345',
url: '/orders/12345'
);
return $this->json(['status' => 'notification sent']);
}
}Create a demo notification:
curl -X POST http://localhost:8000/notifications/demoList unread notifications:
curl http://localhost:8000/notificationsMark notification as read:
curl -X POST http://localhost:8000/notifications/1/readThe system is currently configured for async processing (requires running messenger:consume).
For async (current configuration):
# Run this in a separate terminal
php bin/console messenger:consume asyncFor sync (immediate) processing:
Edit config/packages/messenger.yaml and change:
routing:
# App\Message\NewNotificationMessage: async # Comment this
App\Message\NewNotificationMessage: sync # Uncomment thisThe handler implements proper error handling:
- UnrecoverableMessageHandlingException: Thrown when user doesn't exist (won't retry)
- RecoverableMessageHandlingException: Thrown for transient errors like DB connection issues (will retry)
Retry strategy is configured in messenger.yaml:
- Max retries: 3
- Multiplier: 2 (exponential backoff)
# List failed messages
php bin/console messenger:failed:show
# Retry failed messages
php bin/console messenger:failed:retry-
Start the application:
symfony serve # or php -S localhost:8000 -t public/ -
Create a test notification:
curl -X POST http://localhost:8000/notifications/demo
-
If using async, start the worker:
php bin/console messenger:consume async -vv
-
Fetch unread notifications:
curl http://localhost:8000/notifications
-
Mark one as read:
curl -X POST http://localhost:8000/notifications/1/read
The migration created two tables:
users:
- id (PK)
- email (unique)
- name
notifications:
- id (PK)
- user_id (FK to users)
- title
- body
- url (nullable)
- created_at
- read_at (nullable)
- Authentication: Replace demo user logic with
$this->getUser()in controller - Security: Add proper access control to ensure users can only access their notifications
- Pagination: Add pagination to the list endpoint
- Real-time: Integrate with Mercure or Turbo Streams for live updates
- Notification Types: Create different notification classes for different types
- Email/SMS: Add additional handlers to send email/SMS for important notifications
- Preferences: Allow users to set notification preferences
- Batching: Add support for batching notifications
// In any service or controller
class OrderService
{
public function __construct(
private Notifier $notifier
) {}
public function completeOrder(Order $order): void
{
// ... order completion logic ...
// Notify the user
$this->notifier->notifyUser(
userId: $order->getUser()->getId(),
title: 'Order Completed',
body: sprintf('Your order #%s has been completed!', $order->getId()),
url: sprintf('/orders/%s', $order->getId())
);
}
}framework:
messenger:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
max_retries: 3
multiplier: 2
routing:
App\Message\NewNotificationMessage: asyncCurrently using Doctrine transport (stores messages in database). Can be switched to Redis, RabbitMQ, etc. by changing MESSENGER_TRANSPORT_DSN in .env.