tomatophp/fcm-notifications
Tomatophp FCM Notifications is a Laravel package that simplifies sending push notifications via Firebase Cloud Messaging. It offers straightforward setup, configuration, and integration so you can deliver FCM messages to mobile and web users from your Laravel application.
Install and pin it — this is a young, single-maintainer package, so lock the version rather than floating on ^1.0:
composer require tomatophp/fcm-notifications
Read the source before you read the README. The package is small (a service provider, a channel class, a message builder, and a config file), and the README will not cover the edge cases you care about — multicast, token pruning, HTTP v1 vs. legacy. Open vendor/tomatophp/fcm-notifications/src/ and skim the channel's send() method first; that tells you exactly which FCM API surface it speaks and which exceptions it swallows.
Then:
php artisan vendor:publish --tag=fcm-config — confirm the tag in the provider's boot()).storage_path('app/firebase/credentials.json')), not a relative one.public function routeNotificationForFcm(): ?string
{
return $this->deviceTokens()->latest()->value('token');
}
First use case: order-shipped push. Add the channel to via(), implement toFcm(), and fire it from a listener after the shipment record is created:
public function via($notifiable): array
{
return [FcmChannel::class];
}
public function toFcm($notifiable)
{
return FcmMessage::create()
->title('Your order shipped')
->body("Tracking: {$this->shipment->tracking_number}")
->data(['order_id' => (string) $this->shipment->order_id]);
}
Send it to a real device on day one. Fakes and unit tests will not tell you whether the payload survives the Android/iOS background handlers.
Keep via() explicit. Return the channel class (FcmChannel::class) rather than a string alias — the string alias usually resolves through the package's provider, and if that provider isn't discovered (cached configs, package trimming in a monorepo) you get a silent no-send.
Always queue. Every send is an outbound HTTPS call to Google. Make the notification implement ShouldQueue, and prefer ShouldQueueAfterCommit if you're dispatching from inside a DB transaction — otherwise you can push a "shipped" notification before the row commits and your deep-link target 404s.
Separate notification payload from data payload. The package's builder typically maps title()/body() to the FCM notification block and data() to the data block. On Android, a notification block while the app is backgrounded is rendered by the OS and your onMessageReceived never runs; on iOS the same is true unless you add content-available/mutable-content. If your app needs to do work on receipt, send data-only and render locally.
Multi-device: fan out, don't fake it. A user with phone + tablet needs two sends. Prefer a morph-many device_tokens table over a single fcm_token column, then:
Notification::send($user->deviceTokens()->pluck('token'), new OrderShipped($order));
Doing it this way means you get per-token failure reporting for free instead of a single opaque batch result, and token pruning becomes a row delete.
Wrap it in one domain-level notification class. Define an abstract PushNotification that implements the plumbing (via(), toFcm() delegating to an abstract payload()), so feature code never imports the package. When you inevitably fork or swap the driver, you change one file.
Store the message ID. FCM returns a message name/ID per send. Persist it against the token row; it's the only handle you get when a user says "I didn't get the push."
Testing. Use Notification::fake() and assertSentTo($user, OrderShipped::class) for the application-level assertions, and assert payload shape by calling $notification->toFcm($user)->toArray() directly in a unit test — that's the part you control. Only the channel's HTTP call needs a real integration test, and it needs a real token.
Locales and topics. Set $notification->locale per recipient before notify() if you use translated copy; the builder usually reads from the notifiable's locale. For broadcast-style messaging (announcements, price drops), topics are far cheaper than iterating users — subscribe devices on login via your own call to the FCM admin API rather than expecting the package to manage subscriptions.
Legacy API risk — check this first. The last release is March 2024, and Google shut down the legacy FCM HTTP API mid-2024. If the channel posts to https://fcm.googleapis.com/fcm/send with a key=SERVER_KEY header, it is dead on arrival for new projects. Grep the source for fcm/send vs v1/projects. If it's legacy, you either fork the endpoint to HTTP v1 (straightforward — one URL, one OAuth token, one payload reshape) or use kreait/laravel-firebase + laravel-notification-channels/fcm instead.
Silent failures on dead tokens. FCM returns UNREGISTERED / INVALID_ARGUMENT for tokens that have been uninstalled or rotated. Small drivers frequently catch Throwable and log at debug. Wire up your own handling: catch per-token, delete the token row, and let the user's next app launch re-register. If the channel swallows exceptions, subclass it and override send().
Credentials path resolution bites under Octane and in CI. base_path()-relative paths break when the working directory changes; cached config (config:cache) freezes whatever relative path you had at cache time. Use absolute paths and re-cache in deploy.
Reserved data keys. Keys starting with google. or gcm. are stripped by FCM, and all values must be strings — an integer order_id will either fail validation or arrive as a string anyway. Cast at the boundary.
Android notification channels are mandatory. On API 26+ a push without a valid android_channel_id falls back to a default channel and may be silently suppressed by user settings. If the package doesn't expose an Android config block in the builder, add it — either by extending the message class or posting the full payload yourself.
iOS-specific config is usually missing. apns.payload.aps.sound, badge, and thread-id are what make iOS pushes feel native. Most minimal FCM drivers expose only title/body/image. Budget half a day to add an apns() escape hatch on the message builder.
4KB payload limit. Data-heavy notifications (full order JSON, image URLs with signatures) blow this quietly. Keep data to IDs and fetch on-device.
Debugging checklist, in order: (1) Notification::fake() proves the notification fired; (2) calling toFcm() directly proves the payload shape; (3) hitting the FCM v1 endpoint with curl and your service account proves credentials; (4) a token from FirebaseMessaging.getInstance().getToken() on a real device proves the device side. Ninety percent of "pushes don't work" is step 4 — a stale token in your DB, not a package bug.
Treat the dependency as vendored code. Zero stars, zero dependents, no release in over a year. Pin an exact version in composer.json, keep your wrapper class thin, and assume you may need to fork the two or three files that matter. The MIT license makes that a non-issue; the lack of maintainer activity makes it a certainty.
How can I help you explore Laravel packages today?