jp/firebase-notification-bundle
Symfony bundle for sending Firebase Cloud Messaging push notifications from PHP apps. Configure credentials through bundle config, then dispatch messages to devices and topics with simple service calls, keeping FCM payload and device-token handling out of your controllers.
Start by confirming it is not a Laravel package. jp/firebase-notification-bundle is a Symfony2 bundle — it ships a Bundle subclass and a DependencyInjection\Extension, not a ServiceProvider. Laravel does not boot the FrameworkBundle, so there is no auto-discovery, no config/packages/*.yaml, no facade. Do not composer require it into a Laravel app: it will conflict on symfony/dependency-injection / symfony/http-kernel against the Symfony 6/7 components Laravel already pulls, and the PHP pins are Symfony2-era. Treat it as source to read, not a dependency to install.
Where to look first. Open src/ and find the class that builds the payload and issues the HTTP call — usually a Sender/Client/Firebase service plus its DI definition in Resources/config/services.yml. Two things decide everything:
https://fcm.googleapis.com/fcm/send with an Authorization: key=<server-key> header, it targets the legacy FCM HTTP API, which Google shut down in mid-2024. At that point the package is function-dead and you are extracting-and-porting, not adopting.composer.json, and whether it depends on google/apiclient. This tells you how much OAuth2 plumbing you'd have to add yourself.First use case (the only viable one in Laravel). Port the ~50 lines of payload construction and the send call into a plain class — App\Services\FirebaseNotifier — and register it in an App\Providers\NotificationServiceProvider. Replace the Symfony YAML config tree with a config/firebase.php array. Prerequisite: an FCM HTTP v1 service-account JSON, not a legacy server key. Validate the whole path with a one-message spike before building anything around it.
Extract behind an interface, not a package. Define App\Contracts\PushGateway with a send(DeviceToken|Topic, Payload): DeliveryResult method, and implement it with the extracted logic. This is the pattern that makes the rest of the assessment sane: FCM-specific code lives at the boundary, and swapping to kreait/laravel-firebase or laravel-notification-channels/fcm later is a config change.
Config pattern. A flat config/firebase.php returning project_id, credentials_path (env-driven, never committed), and sane defaults. Cache it with php artisan config:cache like any other Laravel config — the Symfony extension tree does not map 1:1, so translate explicitly rather than copying YAML keys.
Auth is yours to add. HTTP v1 needs an OAuth2 bearer token minted from the service account (scope https://www.googleapis.com/auth/firebase.messaging). Use google/auth or kreait to mint and cache the token for ~55 minutes — minting per send is the most common performance mistake in hand-rolled wrappers.
Device vs. topic sends. Keep them as distinct methods on the gateway, not a $to string with a /topics/ prefix. v1 uses message.topic for topics and message.token for single devices; conflating them produces silent 400s.
Wire it into Laravel's ergonomics. The day-to-day payoff is a custom NotificationChannel so product code writes $user->notify(new OrderShipped($order)) and never touches FCM. The channel resolves tokens, calls the gateway, and records the result. Keep payload DTOs framework-agnostic.
Queue by default. The bundle-era code sends inline during the request. Move every send to a ShouldQueue job, and chunk multicasts (v1 caps per-send device counts; treat batches of ~500 as the ceiling). A broadcast to 100k devices must never sit in a web request.
Test against a fake gateway. Assert payload shape and error handling with a bound fake PushGateway; mock the transport, not the network. There is no useful test suite upstream — you own coverage.
200 OK with an error code in the response body (MismatchSenderId, NotRegistered, etc.). Naive if ($response->isSuccessful()) drops messages silently. The extracted logic must parse the body.UNREGISTERED / INVALID_ARGUMENT per-token errors are the signal to delete the token row; without pruning, deliverability metrics drift and send cost rises on dead tokens.429 and Retry-After. Per-project rate limits are real; chunk multicasts and apply exponential backoff in the queued job rather than hammering the endpoint.401 at send time, not at deploy. Migrate to a service-account JSON before touching production.Extension::load() tree does not survive the port; do not try to preserve its shape. Model a small, explicit Laravel config array instead.data values must be strings. Enforce this in the DTO, not at the FCM boundary.name (message ID) so support can trace delivery.Http::withToken(...) — cheaper than porting sender logic that will be obsolete anyway.PushGateway so the next framework generation is a config swap, not a rewrite.How can I help you explore Laravel packages today?