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

Firebase Notifier Laravel Package

symfony/firebase-notifier

Symfony Notifier integration for Firebase. Send push notifications via a DSN built from your Firebase service account JSON: client email, project ID, private key ID, and URL-encoded private key. Includes a helper script to convert your credentials file into DSN format.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony Notifier bridge, not a Laravel package. There is no service provider, no facade, no auto-discovery, and no config/firebase.php. Treat it as a low-level transport you wire into Laravel yourself.

Minimal install

composer require symfony/notifier symfony/firebase-notifier

Check the version that Composer resolves against your PHP version — the 8.x line requires PHP >= 8.4, 7.4.x aligns with PHP >= 8.2+. Run composer why symfony/notifier afterwards to confirm you didn't drag in a conflicting Symfony component (Laravel ships several).

Where to look first

  1. Symfony\Component\Notifier\Bridge\Firebase\Transport\FirebaseTransport — the thing that actually does the HTTP call. Read it before you trust it.
  2. Symfony\Component\Notifier\Bridge\Firebase\Notification\{AndroidNotification,IOSNotification,WebNotification} — the fluent payload builders.
  3. Symfony\Component\Notifier\Chatter / ChatterInterface — the entry point you'll inject.
  4. Upstream Symfony Notifier docs (the README here is a stub) for ChatMessage, Transports, and SentMessage.

Configuration

The DSN is the legacy Firebase credential format:

FIREBASE_DSN=firebase://USERNAME:PASSWORD@default

Add it to config/services.php (or a dedicated config/notifier.php) so it's config-cached and not read via env() at runtime:

// config/services.php
'firebase' => [
    'dsn' => env('FIREBASE_DSN'),
],

First use case — a topic push to every device subscribed to a news topic:

use Symfony\Component\Notifier\ChatterInterface;
use Symfony\Component\Notifier\Message\ChatMessage;
use Symfony\Component\Notifier\Bridge\Firebase\Notification\AndroidNotification;

$options = (new AndroidNotification('/topics/news', ['article_id' => 42]))
    ->icon('ic_news')
    ->sound('default')
    ->tag('news-42')
    ->color('#0b5fff')
    ->clickAction('OPEN_ARTICLE');

$chatter->send((new ChatMessage('New article published'))->options($options));

If that lands on a real device, you've validated the whole stack. If it doesn't, stop and read the Gotchas section before writing another line.

Implementation Patterns

1. Bind the transport once in a service provider

You need to build the Symfony Chatter yourself. Use the bridge's own factory rather than hand-rolling the transport:

// app/Providers/NotifierServiceProvider.php
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\Notifier\Chatter;
use Symfony\Component\Notifier\ChatterInterface;
use Symfony\Component\Notifier\Transport\Transports;
use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory;

public function register(): void
{
    $this->app->singleton(ChatterInterface::class, function ($app) {
        $factory = new FirebaseTransportFactory(HttpClient::create());

        $transports = new Transports([
            'firebase' => $factory->create(config('services.firebase.dsn')),
        ]);

        return new Chatter($transports);
    });
}

Verify the Chatter / FirebaseTransportFactory constructor signatures for the version you installed — they differ across Symfony majors (event dispatcher argument was added/reshuffled).

2. Wrap it in an application service, not a raw ChatterInterface injection

Injecting ChatterInterface into twenty controllers couples your domain to a Symfony component. Put one seam in front of it:

// app/Services/Push/PushSender.php
final class PushSender
{
    public function __construct(private ChatterInterface $chatter) {}

    public function send(PushMessage $message): void
    {
        $this->chatter->send(
            (new ChatMessage($message->title))->options($message->toPlatformOptions())
        );
    }
}

PushMessage is your own DTO (title, body, topic/token, data array, platform). Everything downstream — jobs, controllers, listeners — talks to PushSender. When the day comes that this bridge is unusable, you swap one class.

3. A Laravel Notification channel adapter

This is the highest-leverage pattern: make push a first-class Laravel notification channel so Notification::send() just works.

// app/Notifications/Channels/FirebaseChannel.php
namespace App\Notifications\Channels;

use App\Services\Push\PushSender;
use App\Services\Push\PushMessage;

class FirebaseChannel
{
    public function __construct(private PushSender $sender) {}

    public function send(object $notifiable, $notification): void
    {
        $message = $notification->toFirebase($notifiable);
        if (!$message instanceof PushMessage) {
            return;
        }

        // Notifiable supplies the target: a topic or a stored device token
        $this->sender->send($message->to($notifiable->pushTarget()));
    }
}

Then in a notification:

public function via($notifiable): array { return [FirebaseChannel::class]; }

public function toFirebase($notifiable): PushMessage
{
    return new PushMessage(
        title: 'Your order shipped',
        topic: '/topics/customer-'.$notifiable->id,
        data: ['order_id' => $this->order->id],
        platform: 'android',
    );
}

4. Queue everything

The transport makes a synchronous HTTP call. Never do that in a request cycle that also writes to the DB.

final class SendPush implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;
    public int $backoff = 30;

    public function __construct(public PushMessage $message) {}

    public function handle(PushSender $sender): void
    {
        $sender->send($this->message);
    }
}

5. Fan out per platform

A ChatMessage carries exactly one options object, so multi-platform sends are N messages, each with its own transport-relevant options:

foreach (['android' => AndroidNotification::class,
          'ios'     => IOSNotification::class,
          'web'     => WebNotification::class] as $platform => $class) {
    $options = (new $class($target, $data))
        ->icon($iconByPlatform[$platform])
        ->sound('default');

    dispatch(new SendPush(new PushMessage($title, $options)));
}

Do this in a batch/job-chain rather than looping synchronously — FCM topic sends are fast but you'll still serialise network round trips.

6. Testing: fake at your seam

There's no Notification::fake() for a Symfony transport. Two workable approaches:

  • Bind a fake PushSender in tests and assert on dispatched PushMessage DTOs — this is what you actually care about.
  • Use Symfony's in-memory/null transport if you want to exercise the Chatter wiring: Chatter with a Symfony\Component\Notifier\Transport\NullTransport, or a custom TransportInterface that records SentMessages.

Don't try to assert on raw Firebase JSON in feature tests — you'd be testing the bridge, not your app.

Gotchas and Tips

This bridge speaks the retired legacy FCM API. The README's option reference points at the old XMPP server reference and the DSN is username:password. Firebase retired the legacy HTTP and XMPP APIs in mid-2024 in favour of the HTTP v1 API, which authenticates with an OAuth2 bearer token derived from a service-account JSON. Verify — with a real device, on day one — that your project actually accepts this transport's request. If it doesn't, the package is a dead end for you regardless of how nice the AndroidNotification fluent API looks.

Credentials are not service-account credentials. If your Firebase console only offers you a service-account JSON file, you are looking at the wrong credential type for this bridge. Do not paste a JSON blob into FIREBASE_DSN and hope.

No auto-discovery means no php artisan config:cache surprises — but also no validation. A malformed DSN fails at $factory->create() time, which in the singleton binding above means it fails while resolving ChatterInterface, deep inside whatever job happens to run first. Add an explicit check in a health command or a small MakePushMessage-style test that boots the transport.

Exception type is Symfony's, not Laravel's. Catch Symfony\Component\Notifier\Exception\TransportExceptionInterface. If you only catch \Throwable in the job you'll lose the distinction between "Firebase said 401" (permanent, retrying is pointless) and "connection reset" (transient, retry). Map them accordingly, and consider $this->fail() on auth errors so the queue stops hammering.

Retries duplicate pushes. FCM is not idempotent. A 3-try job that actually delivered on attempt one but got a timeout reading the response will send three notifications. If duplicates are unacceptable, key on the tag/collapse_key-style option to make the client coalesce them, or accept at-least-once semantics explicitly.

No delivery receipts. Topic sends return "accepted", not "delivered". Do not build analytics, read-receipts, or "user saw this" logic on the return value of send(). The SentMessage gives you the message id at best.

Keep payloads small. FCM caps a message at roughly 4 KB. Your data array (second constructor argument) plus the notification block counts against it. Base64-encode nothing; send an ID and let the app fetch.

ChatMessage subject is the title. new ChatMessage('') in the README is misleading — pass your actual notification title there and let the options class carry body/icon/actions, or be explicit via ->title()-style options if the class supports it. An empty subject tends to produce a blank lock-screen notification.

Version pinning matters more than usual here. Symfony 8.x needs PHP 8.4; Laravel 10/11 on PHP 8.2 or 8.3 caps you at the 7.4.x line. Because this is a leaf package with ~0 dependents and a thin test surface upstream, pin an exact minor version and read composer update output for this package specifically.

No extension points, so hang your own. There's no event dispatcher on the bridge and no middleware. If you need metrics, audit logging, or per-user rate limiting, wrap PushSender — that's the only seam. spatie/laravel-activitylog or a simple Log::channel('push') call inside the wrapper is the practical answer.

Know when to bail. If you find yourself writing a service-account → OAuth2 token refresher, or patching the transport's endpoint, stop. kreait/firebase-php or laravel-notification-channels/fcm give you HTTP v1 with a Laravel-native notification channel and an active maintenance story. This package earns its place only if you're already running Symfony Notifier inside a Laravel app (e.g. a shared codebase) and already speak the legacy credential flow.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console