redjanym/fcm-bundle
A Symfony bundle for Firebase Cloud Messaging (FCM) that provides simple services and configuration to send push notifications to Android and iOS devices through Google's FCM HTTP API.
First, the framework reality check. redjanym/fcm-bundle is a Symfony bundle, not a Laravel package. Laravel has no bundles.php, no config tree, and no autodiscovery for Symfony bundles — so you cannot composer require it and inject its services by class name the way a Symfony developer would. Two realistic paths:
redjanym/php-fcm-v1 — and bind it yourself. You get the same FcmClient/Message API without the bundle scaffolding.config/bundles.php, drop in config/packages/redjanym_fcm.yaml, and inject FcmClient directly.PHP 8+ as of v2.1.1. The package now declares PHP 8+ support, which finally lines it up with Laravel 9/10/11/12's own floor (8.0/8.1/8.2). Practically this means: if you were blocked before because Composer refused to install it on a PHP 8 runtime, that blocker is gone. If you are still pinned to PHP 7.4, you are stuck on an older release — in which case go straight to option 1 and skip the bundle entirely.
Steps for a Laravel app:
composer require redjanym/php-fcm-v1
storage/app/firebase/service-account.json.FCM_CREDENTIALS=storage/app/firebase/service-account.json to .env, and do not commit the JSON.$this->app->singleton(FcmClient::class, fn () => new FcmClient(
storage_path('app/firebase/service-account.json')
));
Where to look first: the README's usage block and the FcmClient / Message classes in vendor/redjanym/. They are small; read them before trusting them.
First use case: send a single-token notification in a queued job, not in a controller.
$message = new Message($deviceToken, [
'title' => 'Your order shipped',
'body' => 'Track it in the app',
], ['order_id' => (string) $order->id]);
$client->send($message);
Wrap it in a domain service, not raw calls. Keep FcmClient out of controllers and jobs' internals; expose something like PushNotifier::toUser(User $user, PushPayload $payload). That gives you one place to add logging, token pruning, and retries.
Use a Laravel Notification channel. A small FcmChannel that reads routeNotificationForFcm() returns $this from via(), and maps the notification's toFcm() array onto Message. This keeps push notifications alongside mail/database notifications and lets queueing come for free.
Handle multiple devices per user. Store device_tokens in their own table with user_id, token, platform, last_seen_at. Loop the tokens, send individually, and collect per-token failures rather than failing the whole batch.
Queue every send. HTTP v1 is a network call with OAuth token exchange on cold start. Use a job with $tries = 3 and backoff(); classify errors so only 5xx and timeouts retry — 4xx token errors should not.
Prune aggressively. When a response indicates UNREGISTERED or INVALID_ARGUMENT, delete the token row immediately. SENDER_ID_MISMATCH means a token from another Firebase project — flag it, don't retry.
Test without Firebase. Put your wrapper behind an interface (PushSender) and bind a NullPushSender / RecordingPushSender in tests. Do not let feature tests hit Google's endpoints.
Multi-tenant or multi-project: the client is bound to one service account. If you serve several Firebase projects, register named singletons (fcm.tenant_a) rather than mutating a shared client.
composer update will fail on that job rather than degrade gracefully. Align your runtime matrix before bumping, and pin redjanym/php-fcm-v1 to a compatible tag if you must straddle both.account_key_path, service_account_file, etc.) — it is the one required option, and a wrong key silently yields an unconfigured client.%kernel.project_dir% does not exist in Laravel. Use storage_path() / base_path(). A relative path resolves against the CLI's CWD, which breaks under queue workers and Octane.data values must be strings (ints/bools get rejected or stringified unpredictably). Total message size ≤ 4 KB; topic conditions have their own limits.android.notification, apns.payload.aps, webpush), collapse keys, TTL, and priority may not be exposed by Message. If you need them, either extend the message class or drop to google/auth + Guzzle and post the raw JSON yourself.errorCode — it is the only reliable way to distinguish "device gone" from "malformed payload"./run/secrets/fcm.json), not a file copied into the image. Rotate the key in Firebase if it ever lands in a log.composer.json, and be ready to replace it with ~50 lines of google/auth + Guzzle if maintenance stalls. The HTTP v1 API itself is simple; the library is not load-bearing.project_id inside it match the tokens you're sending? is the device token from the same Firebase project? These three account for most failures.How can I help you explore Laravel packages today?