apility/laravel-fcm
A Laravel package for sending Firebase Cloud Messaging (FCM) push notifications to Android and iOS devices, with a simple API for targeting devices and topics, building payloads, and integrating FCM into Laravel applications.
apility/laravel-fcm is a maintained-fork lineage of brozot/laravel-fcm, so if you have used that package before, the API is identical (only the namespace changes to Apility\LaravelFCM). It wraps the legacy FCM HTTP API (fcm.googleapis.com/fcm/send) with builder objects, a facade, and a response wrapper that categorises tokens.
First, confirm the API is still reachable for your project. The package is archived and speaks the legacy server-key API, which Google shut down during 2024. If you are starting fresh, this package is not the right tool — migrate to the HTTP v1 API (e.g. kreait/firebase-php or laravel-notification-channels/fcm). The guidance below applies to working with existing installations.
Install and publish config:
composer require apility/laravel-fcm
php artisan vendor:publish --provider="Apility\LaravelFCM\FCMServiceProvider"
Then set credentials from the Firebase console (Project settings → Cloud Messaging → legacy server key):
FCM_SERVER_KEY=AAAA...
FCM_SENDER_ID=1234567890
Register the service provider only if you are on a Lumen-style setup or have auto-discovery disabled. Look first at config/fcm.php — forks in this lineage differ on key names (server_key, sender_id, http_client), so read the file rather than trusting an article.
First use case — send a notification to one device token:
use Apility\LaravelFCM\Facades\FCM;
use Apility\LaravelFCM\Message\OptionsBuilder;
use Apility\LaravelFCM\Message\PayloadNotificationBuilder;
use Apility\LaravelFCM\Message\PayloadDataBuilder;
$option = (new OptionsBuilder())->setTimeToLive(60 * 60)->build();
$notification = (new PayloadNotificationBuilder('Order shipped'))
->setBody('Your parcel is on its way')
->setSound('default')
->setClickAction('OPEN_ORDERS')
->build();
$data = (new PayloadDataBuilder())->addData(['order_id' => '1234'])->build();
$response = FCM::sendTo($deviceToken, $option, $notification, $data);
$response->numberSuccess(); // 1
Wrap it in a service, don't call the facade from controllers. The builder API is verbose; hide it behind something like PushService::toUser($user, $notification) that resolves device tokens from your device_tokens table and calls FCM::sendTo().
Multicast to many tokens. Pass an array as the first argument; the package batches the call:
$response = FCM::sendTo($tokens, $option, $notification, $data);
FCM caps a multicast request at 1,000 registration tokens. The package does not always chunk for you, so chunk on your side (array_chunk($tokens, 1000)) to avoid silent truncation.
Prune dead tokens from the response. This is the single most valuable part of the package and the main thing you should build on day one:
$response = FCM::sendTo($tokens, $option, $notification, $data);
if ($response->tokensToDelete()) {
DeviceToken::whereIn('token', $response->tokensToDelete())->delete();
}
if ($response->tokensToModify()) {
foreach ($response->tokensToModify() as $old => $new) {
DeviceToken::where('token', $old)->update(['token' => $new]);
}
}
if ($response->tokensToRetry()) {
// transient errors — requeue these tokens
}
Wrap this in a small HandleFcmResponse action so every call site gets consistent cleanup.
Topics for fan-out. Subscribe tokens to a topic rather than fanning out per-token when you push to segments:
FCM::subscribeToTopic('news', $tokens);
FCM::unsubscribeFromTopic('news', $tokens);
Subscription changes are not instantaneous — Google advises waiting a moment (or sending to a fresh subscription on the next push) before expecting delivery to new subscribers.
Queue it. FCM calls are network I/O; do them in a queued job with retries and exponential backoff. The package does not wrap itself in a queue, so a ShouldQueue job that catches FCMMessageException and re-throws on retryable errors is standard.
Data-only vs. notification payloads. Use PayloadNotificationBuilder for messages the OS renders, PayloadDataBuilder (or both) for silent messages your app handles. All data values must be strings — cast ints and booleans explicitly or json_encode them.
Testing. The facade is swappable: FCM::shouldReceive('sendTo')->andReturn($fakeDownstreamResponse);. Build fake DownstreamResponse objects in tests rather than mocking Guzzle, if the class is constructible.
composer.json; on newer Laravel you may need a composer.json conflict override or a fork. Budget a migration, don't plan around this package long-term.key=... header) was retired for new projects and stopped working for most existing ones in 2024. If sends return 401/403, check whether your project still has legacy credentials before digging into the code — many "package bug" reports are actually credential retirement.config/fcm.php is not cached consistently across forks. After editing FCM_SERVER_KEY, run php artisan config:clear. Symptom of a stale cache: requests going to an old project or 401s after a key rotation.sender_id matters less than you'd think. It's used in some upstream paths, not in the downstream send. Don't chase issues there unless you're doing topic/group operations.FCMMessageException is thrown for any non-2xx. Log getMessage() and the raw response ($downstreamResponse->getRawResponse() if available on your fork) — the raw body is where the real error code lives.setTimeToLive(0) means "deliver immediately or drop."->setCollapseKey('inbox') prevents stacking N notifications when a user is offline.http_client block in config and raise the timeout; otherwise you get spurious retries and duplicate sends.tokensToDelete() covers NotRegistered and InvalidRegistration. Do not hand-roll this check — the package classifies the error codes for you and gets the edge cases right.Apility\LaravelFCM\FCMServiceProvider manually and copy the config file into your config/ with the facade alias set in bootstrap/app.php; auto-discovery does not apply.sendToTopic, sendToGroup, isValidRegistrationToken exist in some versions and not others). Read vendor/apility/laravel-fcm/src/ once; it is small and will save you an hour of guessing.How can I help you explore Laravel packages today?