├── LICENSE.txt ├── README.md ├── composer.json └── src ├── Exception ├── HttpException.php ├── InvalidRecipientException.php └── RuntimeException.php └── FCMChannel.php /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Ankur Kumar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FCM Notification Channel for Laravel 2 | 3 | [![Packagist](https://badgen.net/packagist/v/ankurk91/fcm-notification-channel)](https://packagist.org/packages/ankurk91/fcm-notification-channel) 4 | [![GitHub-tag](https://badgen.net/github/tag/ankurk91/fcm-notification-channel)](https://github.com/ankurk91/fcm-notification-channel/tags) 5 | [![License](https://badgen.net/packagist/license/ankurk91/fcm-notification-channel)](LICENSE.txt) 6 | [![Downloads](https://badgen.net/packagist/dt/ankurk91/fcm-notification-channel)](https://packagist.org/packages/ankurk91/fcm-notification-channel/stats) 7 | [![GH-Actions](https://github.com/ankurk91/fcm-notification-channel/workflows/tests/badge.svg)](https://github.com/ankurk91/fcm-notification-channel/actions) 8 | [![codecov](https://codecov.io/gh/ankurk91/fcm-notification-channel/branch/main/graph/badge.svg)](https://codecov.io/gh/ankurk91/fcm-notification-channel) 9 | 10 | Send [Firebase](https://firebase.google.com/docs/cloud-messaging) push notifications with Laravel php framework. 11 | 12 | ## Highlights 13 | 14 | * Using the latest Firebase HTTP v1 [API](https://firebase.google.com/docs/cloud-messaging/migrate-v1) 15 | * Send message to a topic or condition :wink: 16 | * Send message to a specific device or multiple devices (Multicast) 17 | * Send additional RAW data with notification 18 | * Supports multiple Firebase projects in single Laravel app:fire: 19 | * Invalid token handling with event and listeners 20 | * Fully tested package with automated test cases 21 | * Powered by battle tested [Firebase php SDK](https://firebase-php.readthedocs.io/) :rocket: 22 | 23 | ## Installation 24 | 25 | You can install this package via composer: 26 | 27 | ```bash 28 | composer require "ankurk91/fcm-notification-channel" 29 | ``` 30 | 31 | ## Configuration 32 | 33 | This package relies on [laravel-firebase](https://github.com/kreait/laravel-firebase) package to interact with Firebase 34 | services. Here is the minimal configuration you need in your `.env` file 35 | 36 | ```dotenv 37 | # relative or full path to the Service Account JSON file 38 | FIREBASE_CREDENTIALS=firebase-credentials.json 39 | ``` 40 | 41 | You will need to create a [service account](https://firebase.google.com/docs/admin/setup#initialize-sdk) 42 | and place the JSON file in your project root. 43 | 44 | Additionally, you can update your `.gitignore` file 45 | 46 | ```gitignore 47 | /firebase-credentials*.json 48 | ``` 49 | 50 | ## Usage 51 | 52 | You can use the FCM channel in the `via()` method inside your Notification class: 53 | 54 | ```php 55 | withDefaultSounds() 77 | ->withNotification([ 78 | 'title' => 'Order shipped', 79 | 'body' => 'Your order for laptop is shipped.', 80 | ]) 81 | ->withData([ 82 | 'orderId' => '#123' 83 | ]); 84 | } 85 | } 86 | ``` 87 | 88 | Prepare your Notifiable model: 89 | 90 | ```php 91 | hasMany(DeviceToken::class); 109 | } 110 | 111 | public function routeNotificationForFCM($notification): string|array|null 112 | { 113 | return $this->deviceTokens->pluck('token')->toArray(); 114 | } 115 | 116 | /** 117 | * Optional method to determine which message target to use 118 | * We will use TOKEN type when not specified 119 | * @see \Kreait\Firebase\Messaging\MessageTarget::TYPES 120 | */ 121 | public function routeNotificationForFCMTargetType($notification): ?string 122 | { 123 | return \Kreait\Firebase\Messaging\MessageTarget::TOKEN; 124 | } 125 | 126 | /** 127 | * Optional method to determine which Firebase project to use 128 | * We will use default project when not specified 129 | */ 130 | public function routeNotificationForFCMProject($notification): ?string 131 | { 132 | return config('firebase.default'); 133 | } 134 | } 135 | ``` 136 | 137 | ## Send to a topic or condition 138 | 139 | This package is not limited to sending notification to tokens. 140 | 141 | You can use Laravel's [on-demand](https://laravel.com/docs/9.x/notifications#on-demand-notifications) notifications to 142 | send push notification to a topic or condition or multiple tokens. 143 | 144 | ```php 145 | route('FCMTargetType', MessageTarget::TOPIC) 153 | ->notify(new ExampleNotification()); 154 | 155 | Notification::route('FCM', "'TopicA' in topics") 156 | ->route('FCMTargetType', MessageTarget::CONDITION) 157 | ->notify(new ExampleNotification()); 158 | 159 | Notification::route('FCM', ['token_1', 'token_2']) 160 | ->route('FCMTargetType', MessageTarget::TOKEN) 161 | ->notify(new ExampleNotification()); 162 | ``` 163 | 164 | ## Events 165 | 166 | You can consume Laravel's inbuilt notification [events](https://laravel.com/docs/9.x/notifications#notification-events) 167 | 168 | ```php 169 | [ 180 | //\App\Listeners\FCMNotificationSent::class, 181 | ], 182 | Events\NotificationFailed::class => [ 183 | \App\Listeners\FCMNotificationFailed::class, 184 | ], 185 | ]; 186 | } 187 | ``` 188 | 189 | Here is the example of the failed event listener class 190 | 191 | ```php 192 | channel !== FCMChannel::class) { 208 | return; 209 | } 210 | 211 | /** @var User $user */ 212 | $user = $event->notifiable; 213 | 214 | $invalidTokens = $this->findInvalidTokens($user); 215 | if (count($invalidTokens)) { 216 | $user->deviceTokens()->whereIn('token', $invalidTokens)->delete(); 217 | } 218 | } 219 | 220 | protected function findInvalidTokens(User $user): array 221 | { 222 | $tokens = Arr::wrap($user->routeNotificationFor('FCM')); 223 | if (! count($tokens)) { 224 | return []; 225 | } 226 | 227 | $project = $user->routeNotificationFor('FCMProject'); 228 | $response = Firebase::project($project)->messaging()->validateRegistrationTokens($tokens); 229 | 230 | return array_unique(array_merge($response['invalid'], $response['unknown'])); 231 | } 232 | } 233 | ``` 234 | 235 | Read more about validating device 236 | tokens [here](https://firebase-php.readthedocs.io/en/stable/cloud-messaging.html#validating-registration-tokens) 237 | 238 | Then; you may want to ignore this exception in your `app/Exceptions/Handler.php` 239 | 240 | ```php 241 | protected $dontReport = [ 242 | \NotificationChannels\FCM\Exception\InvalidRecipientException::class, 243 | ]; 244 | ``` 245 | 246 | ### Changelog 247 | 248 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 249 | 250 | ### Testing 251 | 252 | ```bash 253 | composer test 254 | ``` 255 | 256 | ### Security 257 | 258 | If you discover any security issue, please email `pro.ankurk1[at]gmail[dot]com` instead of using the issue tracker. 259 | 260 | ### Attribution 261 | 262 | The package is based on [this](https://github.com/kreait/laravel-firebase/pull/69) rejected PR 263 | 264 | ### License 265 | 266 | This package is licensed under [MIT License](https://opensource.org/licenses/MIT). 267 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ankurk91/fcm-notification-channel", 3 | "description": "Firebase push notification channel for Laravel", 4 | "keywords": [ 5 | "laravel", 6 | "notification", 7 | "firebase", 8 | "fcm" 9 | ], 10 | "homepage": "https://github.com/ankurk91/fcm-notification-channel", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "ankurk91", 15 | "homepage": "https://ankurk91.github.io", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.2", 21 | "illuminate/events": "^11.0 || ^12.0", 22 | "illuminate/notifications": "^11.0 || ^12.0", 23 | "illuminate/support": "^11.0 || ^12.0", 24 | "kreait/firebase-php": "^7.17", 25 | "kreait/laravel-firebase": "^6.0" 26 | }, 27 | "require-dev": { 28 | "orchestra/testbench": "^9.0 || ^10.0", 29 | "phpunit/phpunit": "^10 || ^11.0", 30 | "roave/better-reflection": "^6.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "NotificationChannels\\FCM\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "NotificationChannels\\FCM\\Tests\\": "tests/" 40 | } 41 | }, 42 | "config": { 43 | "sort-packages": true, 44 | "preferred-install": "dist" 45 | }, 46 | "scripts": { 47 | "test": "vendor/bin/phpunit", 48 | "test:coverage": "vendor/bin/phpunit --coverage-clover=coverage.xml" 49 | }, 50 | "extra": { 51 | "laravel": { 52 | "providers": [ 53 | ] 54 | } 55 | }, 56 | "minimum-stability": "dev", 57 | "prefer-stable": true 58 | } 59 | -------------------------------------------------------------------------------- /src/Exception/HttpException.php: -------------------------------------------------------------------------------- 1 | getMessage(), $exception->getCode(), $exception); 16 | $instance->errors = $exception->errors(); 17 | 18 | return $instance; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/InvalidRecipientException.php: -------------------------------------------------------------------------------- 1 | getMessage(), $exception->getCode(), $exception); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | |null|\Kreait\Firebase\Messaging\MulticastSendReport 36 | * 37 | * @throws FirebaseException 38 | */ 39 | public function send($notifiable, Notification $notification) 40 | { 41 | // Build the target 42 | [$targetType, $targetValue] = $this->getTarget($notifiable, $notification); 43 | 44 | // Build the message 45 | $message = $this->getMessage($notifiable, $notification); 46 | 47 | // Check if there is a target, otherwise return an empty array 48 | if (empty($targetValue)) { 49 | return null; 50 | } 51 | 52 | // Make the messaging client 53 | $client = $this->getFirebaseMessaging($notifiable, $notification); 54 | 55 | // Send the message 56 | try { 57 | // Send multicast 58 | if ($this->canSendToMulticast($targetType, $targetValue)) { 59 | return $client->sendMulticast($message, $targetValue); 60 | } 61 | 62 | // Set the target and type; since we are sure that target is single 63 | $message = $message->withChangedTarget($targetType, Arr::first($targetValue)); 64 | 65 | // Send to single target 66 | return $client->send($message); 67 | } catch (NotFound $exception) { 68 | $this->emitFailedEvent($notifiable, $notification, $exception); 69 | 70 | throw InvalidRecipientException::make($exception); 71 | } catch (MessagingException $exception) { 72 | $this->emitFailedEvent($notifiable, $notification, $exception); 73 | 74 | throw HttpException::sendingFailed($exception); 75 | } 76 | } 77 | 78 | /** 79 | * Multicast can only be sent to token type. 80 | * 81 | * @param string $targetType 82 | * @param array $targetValue 83 | * @return bool 84 | */ 85 | protected function canSendToMulticast(string $targetType, array $targetValue): bool 86 | { 87 | return $targetType === MessageTarget::TOKEN && count($targetValue) > 1; 88 | } 89 | 90 | /** 91 | * Get the message from notification. 92 | * 93 | * @param mixed $notifiable 94 | * @param Notification $notification 95 | * @return CloudMessage 96 | * 97 | * @throws RuntimeException 98 | */ 99 | protected function getMessage($notifiable, Notification $notification): CloudMessage 100 | { 101 | if (!method_exists($notification, 'toFCM')) { 102 | throw new RuntimeException('Notification class is missing toFCM method.'); 103 | } 104 | 105 | return $notification->toFCM($notifiable); 106 | } 107 | 108 | /** 109 | * Get the target and type from notifiable. 110 | * 111 | * @param mixed $notifiable 112 | * @param Notification $notification 113 | * @return array{ 114 | * targetType: string, 115 | * targetValue: array, 116 | * } 117 | */ 118 | protected function getTarget($notifiable, Notification $notification): array 119 | { 120 | $targetType = $notifiable->routeNotificationFor('FCMTargetType', $notification); 121 | $targetValue = $notifiable->routeNotificationFor('FCM', $notification); 122 | 123 | $targetType = (string)Str::of($targetType ?? MessageTarget::TOKEN)->lower(); 124 | $targetValue = Arr::wrap($targetValue); 125 | 126 | return [ 127 | $targetType, 128 | $targetValue, 129 | ]; 130 | } 131 | 132 | /** 133 | * Get firebase messaging instance for the configured project. 134 | * 135 | * @param mixed $notifiable 136 | * @param Notification $notification 137 | * @return MessagingClient 138 | */ 139 | protected function getFirebaseMessaging($notifiable, Notification $notification): MessagingClient 140 | { 141 | $project = $notifiable->routeNotificationFor('FCMProject', $notification); 142 | 143 | return Firebase::project($project ?? null)->messaging(); 144 | } 145 | 146 | /** 147 | * Dispatch failed event. 148 | * 149 | * @param mixed $notifiable 150 | * @param Notification $notification 151 | * @param Throwable $exception 152 | */ 153 | protected function emitFailedEvent($notifiable, Notification $notification, Throwable $exception): void 154 | { 155 | $this->events->dispatch(new NotificationFailed( 156 | $notifiable, 157 | $notification, 158 | self::class, 159 | [ 160 | 'message' => $exception->getMessage(), 161 | 'exception' => $exception, 162 | ] 163 | )); 164 | } 165 | } 166 | --------------------------------------------------------------------------------