fresh/firebase-cloud-messaging-bundle
Fresh/FirebaseCloudMessagingBundle integrates Firebase Cloud Messaging into Symfony applications, providing configuration and services to send push notifications to Android, iOS, and web clients through FCM.
Before you type composer require, know what you're holding: this is a Symfony bundle wrapping the legacy FCM HTTP API (https://fcm.googleapis.com/fcm/send with Authorization: key=<server_key>), which Google shut down in June 2024. There is no ServiceProvider, no facade, no config publish tag. In a Laravel app this package cannot be consumed as-is — you either reach in and instantiate its internal sender service manually, or (correctly) read it as reference and write your own client.
Service/FirebaseCloudMessaging.php — the whole transport. Read top to bottom; it's small. Confirm whether it POSTs to the legacy endpoint or HTTP v1.DependencyInjection/Configuration.php — the only config keys (server_key, maybe api_url/timeout).Model/Message.php and Model/Notification.php — payload builders.Resources/config/services.yml — service ids (relevant only if you're on Symfony).composer.json — PHP constraint, Symfony component majors, Guzzle major. Do this before anything else; if it pins Symfony 3/4, Laravel 10/11 will refuse to resolve.composer require fresh/firebase-cloud-messaging-bundle --dry-run 2>&1 | head -30
If Composer throws a dependency resolution conflict with Laravel's pinned Symfony components, stop. It's not worth the adapter.
If you need push today on Laravel, the correct first move is not this package — it's kreait/firebase-php (FCM HTTP v1, OAuth2 service account) behind your own PushSender contract. This package's realistic first use case is: a 30-minute read to understand FCM payload shape, then close the tab.
PushSender contractDefine your own boundary so the transport is swappable — you'll be swapping it, trust me.
// app/Contracts/PushSender.php
namespace App\Contracts;
interface PushSender
{
public function send(PushMessage $message): SendResult;
}
// app/Support/Push/FirebaseV1PushSender.php
namespace App\Support\Push;
use App\Contracts\PushSender;
use Kreait\Firebase\Contract\Messaging;
use Kreait\Firebase\Messaging\CloudMessage;
class FirebaseV1PushSender implements PushSender
{
public function __construct(private Messaging $messaging) {}
public function send(PushMessage $message): SendResult
{
$cloud = CloudMessage::withTarget('token', $message->token)
->withNotification(['title' => $message->title, 'body' => $message->body])
->withData($message->data)
->withAndroidConfig(['priority' => 'high', 'ttl' => '3600s'])
->withApnsConfig(['headers' => ['apns-priority' => '10']]);
$report = $this->messaging->send($cloud);
return SendResult::fromFirebase($report); // maps UNREGISTERED/INVALID_ARGUMENT
}
}
// app/Jobs/SendPushNotification.php
class SendPushNotification implements ShouldQueue
{
use Queueable, InteractsWithQueue;
public int $tries = 5;
public function __construct(public PushMessage $message) {}
public function backoff(): array { return [5, 15, 60, 300]; }
public function handle(PushSender $sender): void
{
$sender->send($this->message);
}
public function retryUntil() { return now()->addMinutes(30); }
}
Dispatch from notifications or events:
SendPushNotification::dispatch($message)->onQueue('push');
The bundle has no Laravel provider. You'd manually bind its service:
// app/Providers/LegacyFcmServiceProvider.php
public function register(): void
{
$this->app->singleton(\Fresh\FirebaseCloudMessagingBundle\Service\FirebaseCloudMessaging::class, function () {
$client = new \GuzzleHttp\Client(['timeout' => 10]);
return new \Fresh\FirebaseCloudMessagingBundle\Service\FirebaseCloudMessaging(
config('services.fcm.server_key'),
$client,
config('services.fcm.api_url', 'https://fcm.googleapis.com/fcm/send'),
);
});
}
Then app(PushSender::class) binds an adapter. This will 401 or 410-Gone against current FCM if the bundle targets legacy. Verify before shipping anything.
// On SendResult failure codes
match ($result->errorCode) {
'UNREGISTERED', 'INVALID_ARGUMENT' => $device->delete(),
'QUOTA_EXCEEDED', 'UNAVAILABLE' => $this->release(60),
default => $this->fail($result->exception),
};
FCM HTTP v1 caps at 500 tokens per request. If you're fanning out to a segment, chunk:
collect($tokens)->chunk(500)->each(function ($chunk) {
$this->messaging->sendMulticast(
CloudMessage::new()->withNotification([...]),
$chunk->all()
);
});
Prefer a channel over direct job dispatch when the push is user-facing:
public function via($notifiable) { return [FcmChannel::class]; }
public function toFcm($notifiable): PushMessage
{
return new PushMessage($notifiable->fcm_token, 'Order shipped', '...');
}
If the bundle POSTs to fcm.googleapis.com/fcm/send with a server key, it produces silent 401s in Google's response — messages vanish with no delivery signal. There is no fallback, no compat layer. Treat "which FCM API does it target?" as the decisive day-one question. Legacy = disqualified.
php artisan vendor:publishYou will hand-wire everything. Any doc or AI suggestion that references config/firebase.php or a facade is hallucinated — it does not exist in this package.
Laravel pins specific Symfony majors (http-foundation, http-kernel, console, process). A bundle from this era typically requires Symfony 3/4/5. composer require will conflict. If it does install, it installs a version of Symfony that may break unrelated packages in your lockfile.
The bundle uses a static server key in config/dotenv. Current FCM requires OAuth2 service-account credentials with token refresh. If you're stuck on legacy during a transition, rotate the server key and be aware it's the "everyone with the key can spam your users" model. Never commit it; never log it.
apns-priority / apns-push-type if the payload is malformed."results":[{"error":"MismatchSenderId"}] all look different and mean different things.fcm.googleapis.com vs. fcm.googleapis.com/v1/projects/{id}/messages:send in the bundle source — this is your API generation clue.The bundle is a thin wrapper — there's no middleware, no event dispatch, no retry policy, no rate-limit handling, no logging hooks. If you need observability or backoff you're either forking it or writing it yourself. Given the ~200–300 LOC of transport code involved, writing it yourself is cheaper than maintaining a fork.
| Need | Use |
|---|---|
| FCM HTTP v1, batteries included | kreait/firebase-php |
| Laravel notification channel | laravel-notification-channels/fcm |
| Raw control, minimal deps | google/auth + Guzzle, ~200 LOC |
| Managed provider (metrics, segmentation) | OneSignal / Braze / Airship |
Treat this package as reference material for FCM payload structure and HTTP shape, not a dependency. The score of 0, zero dependents, no visible maintainer, and the decommissioned API it almost certainly targets add up to "do not ship." Build a PushSender interface today, back it with a maintained v1 client, and put the boundary in place before you need a second provider.
How can I help you explore Laravel packages today?