softspring/notification-bundle
Softspring Notification Bundle adds a flexible notification system to Symfony apps, with database storage, multiple delivery channels, templates, and user preferences for sending and managing notifications with minimal setup.
First, confirm this is the right package for your stack. Despite the composer/PHP framing, softspring/notification-bundle is a Symfony bundle, not a Laravel package. It ships a bundle class, a softspring_notification.yaml config tree, Doctrine entities, and Symfony event dispatcher wiring — all of which depend on the Symfony kernel. If the project you're assessing is a Laravel app, the practical answer is usually "use Laravel's built-in Notifiable trait plus laravel-notification-channels/*" and treat this bundle as reference material only. If you're in a Symfony codebase (or a mixed shop where a Symfony service emits notifications into a Laravel-adjacent system via HTTP/queue), keep reading.
Minimal path to a first notification:
composer require softspring/notification-bundle and register the bundle in config/bundles.php.softspring_notification in config/packages/.Notification through the bundle's factory, attach recipients and a template, then hand it to the dispatcher/manager.First use case to target: a transactional, persisted, asynchronously-delivered email (e.g. "order shipped"). It exercises the whole happy path — entity creation, template rendering, channel selection, transport send, and delivery-status persistence — which is exactly where the bundle's value over a raw MailerInterface call lives. Get that working before touching multi-channel or scheduling.
On v5.6.0 specifically: the release notes are a bare compare link with no described changes. It's a minor bump, so semver says no breaking changes, but it also means you have zero signal on what moved. Read https://github.com/softspring/notification-bundle/compare/v5.5.10...v5.6.0 yourself before pinning, and check whether the diff touches entities or config schema (see Gotchas).
Wrap the bundle behind your own application service. The bundle's manager/dispatcher is convenient but low-level. Introduce an App\Notification\Notifier that accepts a small DTO and internally constructs the bundle's notification object. This gives you one place to change when the bundle's API shifts (as it did somewhere between 5.5.x and 5.6.0), and it keeps bundle classes out of controllers and commands.
Persist-then-send, not send-then-hope. The entity-backed model is the reason to use this bundle. Treat the notification row as the source of truth: create it in a transaction with your domain change, and let the async worker flip its status. That gives you idempotency, retry, and an audit trail for free. Don't call the transport inline from the same code path that mutates business state — a mail timeout should never roll back an order.
Template resolution is the extensibility seam. Register notification templates per notification type and locale rather than inlining bodies. For anything dynamic, prefer a custom template/provider service over string concatenation in the notification factory; provider classes can be swapped per environment, which is how you get Mailhog in dev and a real transport in prod without touching notification definitions.
Channels as strategy, not conditionals. If you need "email always, SMS only for premium users, push only if the app is installed," express that as per-recipient channel resolution rather than an if ladder around the dispatcher. The bundle's channel abstraction is the intended place for that logic, and it keeps the notification definition stable as you add channels.
Queue integration. Dispatch notification creation synchronously and delivery asynchronously. Configure the bundle's async transport to your messenger/queue, and make the worker idempotent — re-processing a notification that already reached sent should be a no-op, not a duplicate email.
Testing. Assert on the notification entity and its recorded state, not on rendered HTML. Swap the transport for an in-memory/log transport in config/packages/test/ and assert "one notification of type X was created for recipient Y with channel Z." That survives template edits and bundle upgrades; snapshot-testing the email body does not.
Integration tip for Laravel-adjacent systems: if a Symfony service must notify users who live in a Laravel app, don't bridge at the bundle layer. Have the Symfony side emit a domain event to a queue and let Laravel's native notification system consume it. You get each framework's idiomatic tooling and a clean contract at the boundary.
The framework mismatch is the biggest pitfall. There is no service provider, no facade, no Notifiable trait. Code samples you find for "Laravel notifications" will not translate. Budget time for the conceptual translation: bundle config → config/packages/*.yaml, Doctrine entity → Eloquent model, Symfony event subscriber → Laravel listener.
v5.6.0 shipped with an empty changelog. A compare link with no described changes is a real risk signal for a 5.x minor: it may contain entity changes requiring a Doctrine migration, config key renames, or a bumped minimum PHP/Symfony constraint. Before upgrading: (a) read the GitHub diff, (b) run composer update --dry-run to spot dependency floor changes, (c) run doctrine:schema:validate and doctrine:migrations:diff on a scratch DB to see if the bundle's mapping drifted. Do this in a branch, not on main.
Entity/schema drift after upgrades. Because notifications are persisted, bundle upgrades can silently invalidate your schema. Any upgrade that touches Entity/ should be treated as a migration. Add a CI job that diffs the schema against the bundle's mapping so you catch this before deploy rather than on the first failed dispatch.
Config caching bites. Symfony caches the compiled container. After editing softspring_notification config — especially transport or template paths — run bin/console cache:clear in the target environment. A stale cache presents as "my config change did nothing," which is easy to misdiagnose as a bundle bug.
Failed sends can be invisible. If the async worker dies silently, notifications sit in a pending state forever. Instrument the states: alert on notifications pending beyond N minutes, and log the transport's response per attempt. The bundle records state; it won't page you.
Version pinning. With an opaque release history, pin to an exact version ("softspring/notification-bundle": "5.6.0") rather than a caret range, and upgrade deliberately. On a package where you can't read what changed, ^5.6 is a bet you don't need to take.
Extension points worth knowing before you fork anything: custom channel classes (implement the channel interface and tag the service), a custom template provider, event subscribers on the bundle's notification lifecycle events, and the console commands for inspecting/resending notifications. Most "the bundle doesn't do X" problems are a custom channel or provider, not a fork — check those first.
Debugging recipe: reproduce with the synchronous transport first to isolate rendering/template errors from transport/queue errors. Rendering failures and delivery failures look identical at the notification-record level, and separating them halves the search space.
How can I help you explore Laravel packages today?