Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Php Firebase Cloud Messaging Laravel Package

redjanym/php-firebase-cloud-messaging

A lightweight PHP library for sending Firebase Cloud Messaging push notifications. Compose notification and data payloads and target device tokens, groups, or topics from Laravel or any PHP application.

Deep Wiki
Context7

Getting Started

1. Install and pin.

composer require redjanym/php-firebase-cloud-messaging

Pin an exact version in composer.json — this is a single-maintainer, low-star package, so you want upgrades to be a deliberate act.

2. Put the credential in config, never in code.

// config/services.php
'fcm' => [
    'key' => env('FCM_SERVER_KEY'),
],

Add FCM_SERVER_KEY= to .env and to .env.example (blank). Cache-config in deploy.

3. Register the client once as a singleton.

// app/Providers/AppServiceProvider.php
public function register(): void
{
    $this->app->singleton(\Redjanym\Fcm\Client::class, fn () => new \Redjanym\Fcm\Client(
        (string) config('services.fcm.key')
    ));
}

Never new Client(...) inside a controller — you lose testability and re-read config on every call.

4. Where to look first in vendor/redjanym/php-firebase-cloud-messaging/src/:

  • Message/ — DownstreamMessage, TopicMessage, DeviceGroupMessage, RawMessage, plus Notification and Data value objects.
  • Config/ — AndroidConfig, IosConfig, WebConfig for per-platform overrides.
  • Response/ — the typed response objects returned by send().
  • Exception/ — the dedicated exceptions you should be catching.

Read the constructor signatures in Message/ before writing anything; the argument order is the single most common source of "why is my payload empty" confusion. Verify exact class/method names against the repo README for your installed version — this package has churned its naming between releases.

5. First use case — one device token, one notification.

use Redjanym\Fcm\Client;
use Redjanym\Fcm\Message\DownstreamMessage;
use Redjanym\Fcm\Message\Notification;
use Redjanym\Fcm\Message\Data;

$message = new DownstreamMessage(
    $user->fcm_token,
    new Notification('Order shipped', 'Your order #1042 is on its way'),
    new Data([
        'order_id' => (string) $order->id, // data values must be strings
        'screen'   => 'orders',
    ]),
);

$response = app(Client::class)->send($message);

That is the whole happy path. Everything below is about making it survive production.

Implementation Patterns

Wrap it behind your own contract. Never let Redjanym\Fcm\* types leak into controllers, jobs, or models. One interface, one implementation, one binding:

namespace App\Contracts;

interface PushNotifier
{
    public function toToken(string $token, string $title, string $body, array $data = []): void;
    public function toTopic(string $topic, string $title, string $body, array $data = []): void;
}
namespace App\Notifications;

use Redjanym\Fcm\Client;
use Redjanym\Fcm\Message\{DownstreamMessage, Notification, Data};

class FcmPushNotifier implements PushNotifier
{
    public function __construct(private Client $client) {}

    public function toToken(string $token, string $title, string $body, array $data = []): void
    {
        $this->client->send(new DownstreamMessage(
            $token,
            new Notification($title, $body),
            new Data(array_map('strval', $data)),
        ));
    }

    public function toTopic(string $topic, string $title, string $body, array $data = []): void
    {
        $this->client->send(new TopicMessage(
            $topic,
            new Notification($title, $body),
            new Data(array_map('strval', $data)),
        ));
    }
}

This is the pattern that lets you swap to kreait/laravel-firebase later without touching 40 call sites, and it makes the package's low bus factor a non-event.

Always send from a queued job. FCM round-trips sit on the HTTP request path otherwise.

class SendPush implements ShouldQueue
{
    use Queueable, InteractsWithQueue;

    public int $tries = 3;
    public array $backoff = [10, 60, 300];
    public int $timeout = 15;

    public function __construct(
        public string $token,
        public string $title,
        public string $body,
        public array $data = [],
    ) {}

    public function handle(PushNotifier $push): void
    {
        $push->toToken($this->token, $this->title, $this->body, $this->data);
    }
}

Fan-out: chunk recipient lists. Never hand an unbounded array of tokens to a single message. Chunk at 1000 (the FCM multicast ceiling) and dispatch one job per chunk, or loop inside a dedicated fan-out job with Bus::batch():

foreach (array_chunk($tokens, 1000) as $chunk) {
    SendPushChunk::dispatch($chunk, $title, $body, $data);
}

Topic messaging for broadcasts, per-user device groups for multi-device.

  • Topic (TopicMessage) — app-wide announcements, marketing, anything where the audience is defined server-side. Subscribe tokens to a topic like user.{id} if you want per-user broadcasts without tracking device groups.
  • Device group (DeviceGroupMessage) — one notification_key per user, mapped to that user's phones/tablets. Remember the 20-tokens-per-key ceiling and that group membership (add/remove) is a separate API call — most projects find topics simpler and just maintain a token table.
  • Raw message (RawMessage) — when you want full control of the JSON body, e.g. data-only silent sync pushes with no notification block.

Inspect the response; don't assume success. Sending "succeeded" at the HTTP layer while individual tokens failed is normal:

$response = $client->send($message);

if ($response->getFailure() > 0) {
    foreach ($response->getResults() as $result) {
        $error = $result['error'] ?? null;
        if (in_array($error, ['NotRegistered', 'InvalidRegistration'], true)) {
            // prune this token from your DB
        }
    }
}

Treat NotRegistered/InvalidRegistration as a terminal outcome — do not throw and retry, or you burn retries on a permanently dead token. Only throw (and therefore retry) on transport/auth errors.

Platform config only where you need it. AndroidConfig (priority high, notification channel id, icon, sound), IosConfig (APNs headers, badge, sound, content-available for silent pushes), WebConfig (icon, click action). Pass them as trailing constructor args on the message. Keep a small config array in config/services.php and build these objects in one place rather than repeating 'high' and channel ids across jobs.

Testing. Http::fake() will not intercept this package — it talks Guzzle directly on its own client instance. Fake at your boundary instead:

$this->mock(PushNotifier::class)
    ->shouldReceive('toToken')->once()
    ->with($user->fcm_token, Mockery::any(), Mockery::any(), Mockery::any());

Then test the FCM-specific behaviour (payload shape, chunking, error mapping) in one focused test of the concrete FcmPushNotifier.

Gotchas and Tips

The number-one thing to verify: legacy API vs HTTP v1. This package historically wraps the legacy https://fcm.googleapis.com/fcm/send endpoint authenticated by a server key. Google retired that API — server keys no longer work for new projects, and the legacy endpoint was shut down. Before you build on this, confirm which transport the version you installed targets. If it's still the legacy server-key path, treat it as unusable for anything new and write the abstraction layer first, then drop in kreait/laravel-firebase (HTTP v1, service account JSON) behind your PushNotifier interface. Same shape, working auth.

Data payload values must be strings. FCM rejects (or silently mangles) integers, booleans, and nested arrays in the data block. Cast at the boundary — array_map('strval', $data) — and encode any structured value with json_encode() into a single string key. Nested arrays are not supported at all.

4 KB payload limit, total. Title + body + all data keys + platform config. Long notification bodies plus a fat data payload is the usual overrun. Keep data payloads to IDs and let the app fetch the rest.

One response for many recipients. A multicast request returns an aggregate success/failure count plus a per-index results array — the indices only line up with the tokens if you didn't reorder or dedupe between building and sending. Dedupe before sending.

Catch specifically. Wrap sends in try/catch for the package's own exceptions (Exception/) and let everything else bubble to the queue's failed() handler. Distinguish: bad/expired credential → don't retry, alert; network blip → retry with backoff; invalid token → prune, don't retry.

No retry, no logging, no rate limiting inside the package. You own all three. Add a Log::warning with the response's failure count and error codes in the job's handle(), and a failed(Throwable $e) method that logs the token (hashed) and reason.

Timeouts are yours to control. The underlying Guzzle client is constructed by the package, not injected — you can't easily swap in a handler stack or a custom timeout. So set public $timeout on the job and rely on queue-level supervision. Similarly, no Guzzle middleware means no built-in request logging; instrument at your job layer.

Secrets hygiene. The server key is a full-privilege credential for your Firebase project. .env only, never config('services.fcm.key') echoed in exception messages, never in a bug report. Rotate immediately if it lands in a log or a repo.

Token lifecycle is your job. FCM tokens rotate on reinstall, restore-from-backup, and app data clear. Store a token per device, keyed by an install id, and reconcile on every app launch where the client posts its current token. Delete on logout. The NotRegistered pruning above is your cleanup backstop — run it as a scheduled sanity sweep, not just inline.

Extension points are thin, and that's the point. There's no service provider, no publishable config, no facade, no events. That means zero framework coupling (good — it works in plain PHP too) and zero batteries included (you write the binding, the config, the queueing, the pruning). Accept that trade explicitly, or the project will accumulate new Client(...) calls scattered across controllers.

Bus factor. 37 stars, one maintainer, and a package that exists mostly to build a JSON array. The real code you're buying is the Message/Config builders and the response parsing — perhaps 300 lines. Keep the wrapper so thin that replacing it is a single-file change, and never let its objects appear in a model, migration, or API resource.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums