edujugon/push-notification
A Laravel package for sending push notifications through Firebase Cloud Messaging (FCM). Deliver messages to Android and iOS devices—individually, by topic, or in groups—via a simple, expressive API with support for custom data payloads.
Install the package and let Laravel auto-discover the service provider:
composer require edujugon/push-notification
php artisan vendor:publish --provider="Edujugon\PushNotification\PushNotificationServiceProvider"
Version 5.5.0 adds Laravel 12 support, so the same install works on Laravel 11 and 12 without a composer.json constraint workaround; check that your PHP version satisfies the release's floor (Laravel 12 requires PHP 8.2+) before bumping. Note that "Laravel 12 support" is a framework-compatibility change only — the transport APIs (setService, setMessage, setDevicesToken, send) are unchanged, so nothing in your existing call sites needs rewriting.
This publishes config/pushnotification.php. That file is the first place to look — it defines one array per transport (gcm, fcm, apn) containing the API key / certificate, endpoint URL, and (for APN) the topic. Wire the secrets through .env immediately rather than editing the config directly:
// config/pushnotification.php
'fcm' => [
'apiKey' => env('FCM_API_KEY'),
'url' => 'https://fcm.googleapis.com/fcm/send',
],
'apn' => [
'certificate' => env('APN_CERT_PATH'),
'passPhrase' => env('APN_PASS_PHRASE'),
'url' => env('APN_URL', 'https://api.push.apple.com'),
'topic' => env('APN_TOPIC'),
],
Your first use case — send a notification to a single device — is three calls behind the facade:
use Edujugon\PushNotification\Facades\PushNotification;
$result = PushNotification::setService('fcm')
->setMessage([
'notification' => ['title' => 'Hello', 'body' => 'World'],
'data' => ['order_id' => 42],
])
->setDevicesToken([$deviceToken])
->send();
$result is an array with success (bool) and feedback (the raw provider response). Once that works end to end, the natural second step is sendByTopic() for the FCM/APN topic path, and reading src/Services/ to see exactly how each transport builds its payload — that directory is the ground truth when the README is ambiguous.
Wrap the facade in a single application service. Don't sprinkle PushNotification::setService(...) through controllers. Create an App\Services\PushSender that owns payload shaping, token selection, and response normalization. This gives you one place to swap in a newer API version later and keeps the package's array-based API out of your domain code.
Queue everything. Every send() is a synchronous HTTP call to Google/Apple. A controller that pushes to 5,000 tokens will time out. The idiomatic shape is a job per batch:
class SendPushBatch implements ShouldQueue
{
use InteractsWithQueue, Queueable;
public function __construct(public array $tokens, public array $payload) {}
public function handle(PushSender $sender): void
{
foreach (array_chunk($this->tokens, 1000) as $chunk) {
$sender->sendToDevices('fcm', $chunk, $this->payload);
}
}
}
Chunk by provider limit. FCM accepts up to 1,000 registration tokens per request; APN expects one token per request (the package will loop, but that loop is serial and slow). For APN-heavy apps, dispatch one job per token and let the queue workers fan out.
Model device tokens as first-class rows. Store user_id, token, platform, app_version, last_seen_at. Prune on last_seen_at rather than on errors — you'll get stale-token errors as a side effect, not as the primary signal.
Use data-only payloads when you need app logic to run. On FCM, ['data' => [...]] alone delivers silently and your app decides what to render; including notification lets the OS render it when the app is backgrounded. Mixing both causes duplicate notifications on Android — a very common production bug.
Override config per call with setConfig([]) when you need a second Firebase project (e.g. tenant isolation) without redeploying:
PushNotification::setService('fcm')
->setConfig(['apiKey' => $tenantKey])
->setMessage($payload)
->setDevicesToken($tokens)
->send();
Feature-test with a fake transport. The package has no built-in fake. Bind your PushSender interface in the container to a recording double in tests, and assert on the payload arrays rather than on network calls. On Laravel 11/12 the container binding in TestCase::setUp() behaves the same as before, so existing fakes port over cleanly.
https://fcm.googleapis.com/fcm/send server-key endpoint was shut down by Google; v5.5.0 is a Laravel-12 compatibility release only, so it does not by itself move you to HTTP v1 with OAuth. Inspect src/Services/Fcm/ before committing — if it's still legacy, you'll hit 404s in production and need the v1 path or a different package..pem certificate plus passphrase. Token-based (.p8) auth is generally preferable and, if unsupported, forces you to rotate certificates annually. Confirm which your version supports.https://api.push.apple.com vs https://api.sandbox.push.apple.com. Sending a dev build's token to production silently returns BadDeviceToken. Split via an env variable scoped per environment.PushSender before dispatch; trimming data at the last moment is much better than debugging a 400.$result['success'] and log $result['feedback'] in full. Providers embed per-token error codes (NotRegistered, Unregistered, BadDeviceToken) inside the feedback; you need those to prune tokens.send() in your own retry policy (e.g. ->retry(3, 1000)) and treat 5xx / 429 as retryable, 4xx-per-token as terminal.config:cache will freeze your .env values. Standard Laravel behavior, but easy to forget when rotating a Firebase key — always php artisan config:clear after deploy. This bites hardest on the Laravel 11/12 upgrade path, where cached config from the old app version is still on disk.setService() and setDevicesToken() every time rather than assuming defaults persist.curl using the same key/cert, (3) verify token freshness by round-tripping a token from the device to your DB. Nine times in ten the package is fine and the token is stale.composer.json already declares PSR-4 autoloading, so a custom PushSender binding is the cleanest seam.How can I help you explore Laravel packages today?