mrad/notifications-bundle
A PHP notifications bundle that provides a reusable notification layer for your application, with support for multiple delivery channels, templated messages, and a clean API for triggering alerts from domain logic. Ideal for adding user-facing notifications without rebuilding delivery infrastructure...
Before writing a line, confirm the framework mismatch. mrad/notifications-bundle is a Symfony bundle (type: symfony-bundle, DependencyInjection/*Extension.php, Resources/config/services.yml), not a Laravel package. There is no ServiceProvider, no config/notifications.php to publish, and Composer auto-discovery will do nothing. Do not composer require it into a Laravel app and expect wiring — you will get either a resolution failure on symfony/framework-bundle or a stale Pusher SDK that breaks on PHP 8.
Read these four files in order (roughly 30 minutes total):
composer.json — confirms the Symfony constraint and the pinned pusher/pusher-php-server version (expect v2/v3 era).DependencyInjection/Configuration.php — the actual config keys you'll mirror (app_id, key, secret, cluster, sometimes encrypted).Resources/config/services.yml — the service graph, which reveals the one class that does real work.Your first use case should be the smallest thing that proves the wiring: a queued job that triggers a server-side event to a per-user private channel when a domain event fires (order shipped, invoice paid). Skip presence channels and client-side auth for now — the bundle gives you the least help there and you'll be writing that code yourself anyway.
Concretely, your onboarding is:
composer require pusher/pusher-php-server
php artisan make:provider NotificationServiceProvider
php artisan make:class Services/PusherNotifier
Then bind App\Contracts\Notifier to PusherNotifier in the provider, pull credentials from .env (PUSHER_APP_ID, PUSHER_APP_KEY, PUSHER_APP_SECRET, PUSHER_APP_CLUSTER), and mirror the bundle's payload shape only if a client already depends on it.
Wrap, don't inherit. The DI wiring is Symfony-specific and useless to you; the trigger logic is portable. Extract the $pusher->trigger($channel, $event, $payload) call into your own service behind an interface, so the transport is replaceable later (Reverb, Ably, Mercure, SNS).
interface Notifier {
public function notify(int|string $userId, string $event, array $payload): void;
}
Queue the dispatch. Never call trigger() inline from a controller. Wrap it in a ShouldQueue job so a Pusher timeout doesn't block the request, and so you get retries ($tries, $backoff) the original bundle doesn't offer.
class SendNotificationJob implements ShouldQueue {
use Queueable, SerializesModels;
public int $tries = 3;
public function __construct(
public int $userId,
public string $event,
public array $payload,
) {}
public function handle(Notifier $notifier): void {
$notifier->notify($this->userId, $this->event, $this->payload);
}
}
Event listener → notification. Keep domain code decoupled, which is the one design idea the bundle gets right. Register a listener on your domain event, dispatch the job from there. Domain code never imports Pusher.
Channel naming convention. Standardise early: private-user.{id} for per-user, private-org.{id} for tenant-wide. The bundle has no opinion on this; you'll live with whatever you pick, so pick once.
Payload shape. If you're porting from an existing client that consumed this bundle's payloads, preserve { title, body, icon, url } or whatever the bundle emits — read Resources/views/*.twig (the bundle renders message bodies via Twig, so the shape is discoverable there). Otherwise design fresh; there is no compatibility constraint.
Testing seam. Bind a FakeNotifier in your test service provider that records calls, and assert against it. This is trivial with the interface and impossible with the bundle's concrete service.
If you actually want Laravel-native broadcasting (recommended long-term), skip this package entirely: BROADCAST_DRIVER=pusher plus php artisan install:broadcasting gives you queueing, multiple channels, and Echo client-side, maintained by the framework.
Symfony bundle ≠ Laravel package. This is the biggest trap. If someone hands you this repo as a "Laravel notification package," correct the premise before you burn a day. Check composer.json type and require first — that resolves the question in 10 seconds.
SDK drift. The bundle pins an old pusher/pusher-php-server (v2/v3). That SDK predates current Channels auth, TLS defaults, and PHP 8 type semantics. Don't inherit its constraint — use the current SDK and adapt the wrapper's call signature (the trigger() API has been stable enough that this is usually a copy-paste).
Cluster config is mandatory. The Pusher client requires cluster; omitting it produces a cryptic connection error at trigger time, not at boot. Put it in .env and validate at provider boot.
Private channel auth. The bundle does server-side trigger only. Private/presence channels require the client to hit an auth endpoint on your app — you author /broadcasting/auth (or a custom route) and sign with the secret. The bundle gives you zero help here; use Laravel's Broadcast::auth() if you go native.
Fire-and-forget is the default failure mode. These bundles typically call trigger() with no return check. Silent notification loss is normal, not exceptional. Wrap the call, check the returned response, log failures with Log::warning, and emit a metric. Add a retry path via the queue — retrofitting delivery guarantees after notifications are load-bearing is far more expensive than doing it now.
Credential hygiene. Read the wrapper before any secret touches it — single-maintainer packages of this vintage sometimes inline config or log connection details. Rotate your Pusher secret after the spike regardless.
Config caching pitfall. If you do bridge it and read config via env() directly inside a service, php artisan config:cache will return null. Always read via config('services.pusher.*') and define the mapping in config/services.php.
Don't fork the repo. A fork converts a zero-cost read into a permanent maintenance liability with no upstream to rebase against. Port what you need into app/ and keep the license attribution in a comment.
Ramp-up cost is trust, not comprehension. The code is small and trivially readable. The time sink is justifying to yourself (and reviewers) why an unmaintained, zero-dependent wrapper routes production credentials. That conversation reliably takes longer than reimplementing it in an afternoon — plan for that, and have the native solution ready as the fallback answer.
How can I help you explore Laravel packages today?