amdadulhaq / fcm-laravel
A simple, lightweight, fast Firebase Cloud Messaging (FCM) notification channel for Laravel.
Fund package maintenance!
Requires
- php: ^8.2|^8.3|^8.4|^8.5
- google/auth: ^1.53
- illuminate/notifications: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- driftingly/rector-laravel: ^2.0
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
README
A simple, lightweight, fast Firebase Cloud Messaging (FCM) notification channel for Laravel. No Firebase Admin SDK, no bloat — just push notifications, configurable, with a fake for testing.
Requirements
- PHP 8.2, 8.3, 8.4, or 8.5
- Laravel 11, 12, or 13
Installation
composer require amdadulhaq/fcm-laravel
The service provider is auto-discovered. Publish the config file:
php artisan vendor:publish --tag=fcm-config
Configuration
Set FIREBASE_CREDENTIALS to the path of your Firebase service account JSON file (download it from Project settings → Service accounts in the Firebase console). The Firebase project ID is read from that file automatically — nothing else is required to start sending.
FIREBASE_CREDENTIALS=storage/app/firebase-adminsdk.json
See config/fcm.php for every option:
| Option | Env | Default | Purpose |
|---|---|---|---|
credentials |
FIREBASE_CREDENTIALS |
— | Path to the service account JSON. Absolute or relative to the app base path. |
http.timeout |
FCM_HTTP_TIMEOUT |
10 |
HTTP timeout (seconds) for the send request. |
android.priority |
FCM_ANDROID_PRIORITY |
high |
Applied to every message's android payload. Set to null to omit. |
android.channel_id |
FCM_ANDROID_CHANNEL_ID |
default_channel |
Android notification channel ID. Set to null to omit. |
apns.priority |
FCM_APNS_PRIORITY |
— | Sets apns.headers.apns-priority (5 or 10). Omitted from the payload unless set. |
debug |
FCM_DEBUG |
APP_DEBUG |
Logs skipped sends, rejected tokens, and delivery failures. Independent of the host app's app.debug. |
log_channel |
FCM_LOG_CHANNEL |
— | Log channel debug messages are written to. Defaults to the app's default channel. |
dry_run |
FCM_DRY_RUN |
false |
Builds and logs the message but never calls the FCM API; sendToToken() returns true as if it succeeded. Handy for local/staging. |
Notifications
Implement toFcm() on any notification, have it implement HasFcmPayload, and route it through the fcm channel:
use AmdadulHaq\Fcm\Contracts\HasFcmPayload; use Illuminate\Notifications\Notification; class OrderShipped extends Notification implements HasFcmPayload { public function via(object $notifiable): array { return ['database', 'fcm']; } public function toFcm(object $notifiable): array { return [ 'title' => 'Order shipped', 'body' => "Your order #{$this->order->id} is on its way!", 'data' => ['order_id' => (string) $this->order->id], ]; } }
The channel resolves device tokens via Laravel's standard routeNotificationFor('fcm', $notification) convention. Add a routeNotificationForFcm() method to the notifiable:
public function routeNotificationForFcm(): array { return $this->deviceTokens()->pluck('token')->all(); }
Sends to a token that FCM reports as unregistered or invalid throw internally and are reported through the AmdadulHaq\Fcm\Events\FcmTokenRejected event instead of stopping delivery to the notifiable's other tokens — listen for it to prune the dead token from wherever you store them:
use AmdadulHaq\Fcm\Events\FcmTokenRejected; class PruneRejectedFcmToken { public function handle(FcmTokenRejected $event): void { DeviceToken::query()->where('token', $event->token)->delete(); } }
Sending directly
For a one-off send outside the notification system, use FcmService directly:
use AmdadulHaq\Fcm\FcmService; app(FcmService::class)->sendToToken( token: $deviceToken, title: 'Order shipped', body: 'Your order is on its way!', data: ['order_id' => '123'], );
Testing
Use FcmService::fake() to swap the real service with an in-memory fake and assert on what would have been sent, without dispatching anything or hitting the network:
use AmdadulHaq\Fcm\FcmService; $fake = FcmService::fake(); // ... code under test that notifies via the "fcm" channel ... $fake->assertSent(fn (string $token, string $title, string $body, array $data) => $title === 'Order shipped'); $fake->assertSentTo($deviceToken); $fake->assertSentCount(1); $fake->assertNothingSent(); $fake->assertNotSent(fn (string $token) => $token === 'some-other-token');
$fake->rejectToken($token) makes sends to that token throw FcmUnregisteredTokenException (without a real FCM error response), so you can exercise the FcmTokenRejected pruning path in tests.
Running the package's own test suite
composer install composer test # Pest composer analyse # Larastan composer lint:check # Pint
License
MIT. See LICENSE.md.