Looking to hire Laravel developers? Try LaraJobs

fcm-laravel maintained by amdadulhaq

Description
A simple, lightweight, fast Firebase Cloud Messaging (FCM) notification channel for Laravel.
Author
Last update
2026/09/11 21:59 (dev-main)
License
Downloads
25

Comments
comments powered by Disqus

FCM (Firebase Cloud Messaging) for Laravel

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads PHP Version Laravel Version Sponsor

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.