├── LICENSE ├── README.md ├── composer.json ├── config └── laravel-firebase.php ├── docs └── DB_sync.md └── src ├── Channels └── FcmNotificationChannel.php ├── Collections └── SyncsWithFirebaseCollection.php ├── Exceptions └── FcmTargetNotSpecifiedException.php ├── Facades ├── FCM.php ├── FirebaseDb.php └── FirebaseJWT.php ├── FcmMessageBuilder.php ├── JWT.php ├── ModelSynchronizer.php ├── ReadonlyDatabase.php ├── RealtimeDb.php ├── Relations └── SyncWithFirebaseBelongsToMany.php ├── ServiceAccountCacheItemPool.php ├── ServiceProvider.php └── Traits ├── SyncRelatedWithFirebase.php └── SyncWithFirebase.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 plokko 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Firebase 2 | 3 | [![Build Status](https://travis-ci.org/plokko/laravel-firebase.svg?branch=master)](https://travis-ci.org/plokko/laravel-firebase) 4 | [![Packagist](https://img.shields.io/packagist/v/plokko/laravel-firebase.svg)](https://packagist.org/packages/plokko/laravel-firebase) 5 | [![Packagist](https://poser.pugx.org/plokko/laravel-firebase/d/total.svg)](https://packagist.org/packages/plokko/laravel-firebase) 6 | [![Packagist](https://img.shields.io/packagist/l/plokko/laravel-firebase.svg)](https://packagist.org/packages/plokko/laravel-firebase) 7 | 8 | Laravel Firebase integration 9 | 10 | This package includes: 11 | - Firebase OAuthV2.0 authentication, with token caching 12 | - Centralized ServiceAccount credential management 13 | - Firebase FCM Http V1 API and Firebase Realtime database REST api via OAuth authentication 14 | - Firebase JWT token generator (via php-jwt) 15 | - Automatic sync for Eloquent models to Firebase Realtime db 16 | - Automatic sync triggers on related model changes 17 | 18 | ## Installation 19 | 20 | Install via composer 21 | ```bash 22 | composer require plokko/laravel-firebase 23 | ``` 24 | The package will be auto registered in laravel >=5.5; 25 | **If you use laravel <5.5 follow the next two steps** 26 | 27 | 1. Add service provider to `config/app.php` in `providers` section 28 | ```php 29 | Plokko\LaravelFirebase\ServiceProvider::class, 30 | ``` 31 | 32 | 2. Register package facade in `config/app.php` in `aliases` section 33 | ```php 34 | Plokko\LaravelFirebase\Facades\LaravelFirebase::class, 35 | ``` 36 | Your file in `config/laravel-firebase.php` should now look like this: 37 | ```php 38 | env('FIREBASEDB_READONLY',false),//DEBUG 42 | 43 | /** 44 | * Firebase service account information, can be either: 45 | * - string : absolute path to serviceaccount json file 46 | * - string : content of serviceaccount (json string) 47 | * - array : php array conversion of the serviceaccount 48 | * @var array|string 49 | */ 50 | 'service_account' => base_path('.firebase-credentials.json'), 51 | 52 | /** 53 | * If set to true will enable Google OAuth2.0 token cache storage 54 | */ 55 | 'cache' => true, 56 | 57 | /** 58 | * Cache driver for OAuth token cache, 59 | * if null default cache driver will be used 60 | * @var string|null 61 | */ 62 | 'cache_driver' => null, 63 | 64 | /** 65 | * Specify if and what event to trigger if an invalid token is returned 66 | * @var string|null 67 | */ 68 | 'FCMInvalidTokenTriggerEvent' => null, 69 | ]; 70 | 71 | ``` 72 | 73 | ### Configuration 74 | Publish the configuration file via 75 | ```bash 76 | php artisan vendor:publish --provider="Plokko\LaravelFirebase\ServiceProvider" --tag="config" 77 | ``` 78 | 79 | 80 | 81 | ## Usage 82 | ### JWT token 83 | You can easly create a Firebase JWT token (for auth) with `FirebaseJWT::encode`: 84 | 85 | ```php 86 | FirebaseJWT::encode($uid,['optional'=>'custom-claims-array']); 87 | ``` 88 | 89 | ### FCM 90 | This package allows you to send FCM messages via FCM http v1 api 91 | #### Message builder 92 | You can easly build FCM Messages via the `FCM` facade: 93 | ```php 94 | 95 | FCM::notificationTitle('My notification title') 96 | ->notificationBody('my notification body...'); 97 | ->data(['notification' => 'data']) 98 | ->highPriority()//note: not all devices may use all the fields like priority or ttl 99 | ->ttl('20.5s') 100 | ->toDevice('my-device-fcm-token') // or toTopic('topic-name') or toCondition('condition-name') or toTarget(Target) 101 | ->send();//Submits the message 102 | ``` 103 | #### FCM Notification channel 104 | You can also send the FCM messages via the `FcmNotificationChannel` channel: 105 | ```php 106 | class TestFcmNotification extends Notification implements ShouldQueue 107 | { 108 | use Queueable; 109 | 110 | /** 111 | * Get the notification's delivery channels. 112 | * 113 | * @param mixed $notifiable 114 | * @return array 115 | */ 116 | public function via($notifiable) 117 | { 118 | return [FcmNotificationChannel::class]; 119 | } 120 | 121 | public function toFcm($notifiable) 122 | { 123 | return FCM::notificationTitle('Test notification') 124 | ->notificationBody('notification body...') 125 | ->toDevice($notifiable->deviceToken); 126 | } 127 | } 128 | ``` 129 | 130 | ### Real time database 131 | 132 | ##### Settings: 133 | You can enable read-only access to database setting 134 | ``` 135 | FIREBASEDB_READONLY=true 136 | ``` 137 | on your `.env` file, this is usefull for testing purpuses, the writes will not return any error but will not be executed on Firebase. 138 | #### Query the Realtime database 139 | To get an instance of the database use the `FirebaseDb` facade: 140 | 141 | ```php 142 | $test = FirebaseDb::getReference('test'); //get the reference for item /test 143 | $test->get('01');//Get /test/01 as an array 144 | $test01 = $test->getReference('01');//Get a reference for /test/01 145 | $test01->set('label','value');//Set /test/01/label = value 146 | ``` 147 | #### Sync models to Firebase 148 | [see Firebase database sync](docs/DB_sync.md) 149 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plokko/laravel-firebase", 3 | "description": "Laravel Firebase integration", 4 | "license": "MIT", 5 | "keywords": [ 6 | "laravel", 7 | "firebase", 8 | "fcm", 9 | "realtime database" 10 | ], 11 | "type": "library", 12 | "require": { 13 | "php": ">=7.1", 14 | "plokko/firebase-php":"^0.3.0", 15 | "illuminate/support": ">=5.4.0" 16 | }, 17 | "require-dev": { 18 | "orchestra/testbench": "~3.6.0", 19 | "phpunit/phpunit": "~7.0" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Plokko\\LaravelFirebase\\": "src" 24 | } 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "Plokko\\LaravelFirebase\\Tests\\": "tests" 29 | }, 30 | "files": [ 31 | "vendor/phpunit/phpunit/src/Framework/Assert/Functions.php" 32 | ] 33 | }, 34 | "scripts": { 35 | "phpunit": "phpunit" 36 | }, 37 | "extra": { 38 | "laravel": { 39 | "providers": [ 40 | "Plokko\\LaravelFirebase\\ServiceProvider" 41 | ], 42 | "aliases": { 43 | "FirebaseDb": "Plokko\\LaravelFirebase\\Facades\\FirebaseDb", 44 | "FirebaseJWT": "Plokko\\LaravelFirebase\\Facades\\FirebaseJWT", 45 | "FCM": "Plokko\\LaravelFirebase\\Facades\\FCM" 46 | } 47 | } 48 | }, 49 | "config": { 50 | "preferred-install": "dist", 51 | "sort-packages": true, 52 | "optimize-autoloader": true 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /config/laravel-firebase.php: -------------------------------------------------------------------------------- 1 | env('FIREBASEDB_READONLY', false), 8 | 9 | /** 10 | * Firebase service account information, can be either: 11 | * - string : absolute path to serviceaccount json file 12 | * - string : content of serviceaccount (json string) 13 | * - array : php array conversion of the serviceaccount 14 | * @var array|string 15 | */ 16 | 'service_account' => base_path('.serviceAccount.json'), 17 | 18 | /** 19 | * If set to true will enable Google OAuth2.0 token cache storage 20 | */ 21 | 'cache' => true, 22 | 23 | /** 24 | * Cache driver for OAuth token cache, 25 | * if null default cache driver will be used 26 | * @var string|null 27 | */ 28 | 'cache_driver' => null, 29 | 30 | /** 31 | * Specify if and what event to trigger if an invalid token is returned 32 | * @var string|null 33 | */ 34 | 'FCMInvalidTokenTriggerEvent' => null, 35 | 36 | /** 37 | * Specify wich Firebase Realtime DB URL to use 38 | * @var string 39 | */ 40 | 'default_db' => 'default', 41 | 'firebasedb_urls' => [ 42 | /** 43 | * Specify the Firebase Realtime DB URL, if not set https://.firebaseio.com/ will be used 44 | * @var string|null 45 | */ 46 | 'default' => rtrim(env('FIREBASEDB_URL', null), '/'), 47 | ], 48 | ]; 49 | -------------------------------------------------------------------------------- /docs/DB_sync.md: -------------------------------------------------------------------------------- 1 | # Firebase realtime database synching 2 | This module can automatically sync Eloquent models to Firebase Realtime database. 3 | 4 | To enable synching use the `SyncWithFirebase` trait in your Model 5 | ```php 6 | use Plokko\LaravelFirebase\Traits\SyncWithFirebase; 7 | 8 | class MyModel extends Model 9 | { 10 | use SyncWithFirebase; 11 | //... 12 | } 13 | ``` 14 | This model will add the `syncWithFirebase` method that manually syncs the model to Firebase and will be triggered at each model modification done throught eloquent(save,update,delete,restore). 15 | 16 | You can customize what will be synched with Firebase via the `toFirebase` method otherwise the output of toArray will be used instead 17 | ```php 18 | use Plokko\LaravelFirebase\Traits\SyncWithFirebase; 19 | 20 | class MyModel extends Model 21 | { 22 | use SyncWithFirebase; 23 | //... 24 | 25 | /** 26 | * Returns the data that will be synched with firebase 27 | * @return array 28 | **/ 29 | function toFirebase(){ 30 | return [ 31 | 'id' => $this->id, 32 | 'name' => $this->name, 33 | 'custom'=> 'value', 34 | //.... 35 | ]; 36 | } 37 | 38 | } 39 | ``` 40 | ### Changing reference name 41 | By default the model will be synchromized to Firebase using the table name (ex. User model using _users_ table will be sync to _/users_); 42 | you can change this behaivour by extending the `getFirebaseReferenceName` method and returning a custom reference name 43 | ```php 44 | 45 | /** 46 | * @return string reference name 47 | */ 48 | public function getFirebaseReferenceName(){ 49 | return 'my_custom_reference';// default : $this->getTable(); 50 | } 51 | ``` 52 | 53 | ## Sync related models 54 | Sometimes you want to serialize to firebase data from othe related models but the changes in the related model will not be automatically updated on the base model and vice-versa. 55 | You can extend the model synchronization to related models using the `SyncRelatedWithFirebase` trait in your Model and extend the `getRelationsToSyncWithFirebase()` function to return an array of relations you want to keep in sync 56 | ```php 57 | use Plokko\LaravelFirebase\Traits\SyncWithFirebase; 58 | 59 | class MyModel extends Model 60 | { 61 | use SyncWithFirebase, 62 | SyncRelatedWithFirebase; 63 | 64 | 65 | /** 66 | * Specifies the relations that needs to be automatically synched with firebase 67 | * @return array relations to be synched with firebase, default [] 68 | */ 69 | protected function getRelationsToSyncWithFirebase(){ 70 | return [ 71 | 'myRelation', 72 | 'myOtherRelation' 73 | ]; 74 | } 75 | public function myRelation(){ 76 | return $this->belongsToMany(OtherModel::class); 77 | } 78 | } 79 | ``` 80 | This trait will automatically sync related model every time a m-n relation is changed or the model is saved. 81 | 82 | ### Advanced sync options 83 | Sometimes the content of a model must be synchronized with an unrelated or you need to trigger the sync just in some conditions. 84 | 85 | You can customize what an when to trigger related mode synchromization by changing the return value of `getRelationsToSyncWithFirebase`: 86 | accepted value for the array values are: 87 | - simple: 'relation_name' - simple relation synching 88 | - with filter: 'relation_name' => function($query){} - filter the relationship 89 | - Custom query: function(){} - return a query to sync with Firebase 90 | 91 | ```php 92 | function getRelationsToSyncWithFirebase(){ 93 | return [ 94 | 'target',//Normal 95 | 'client'=>function($query){$query->where('is_premium','=',1);},//With filter 96 | function(){ return UnrelatedModel::where('condition','value');},//Custom query 97 | ]; 98 | } 99 | ``` 100 | -------------------------------------------------------------------------------- /src/Channels/FcmNotificationChannel.php: -------------------------------------------------------------------------------- 1 | toFcm($notifiable); 23 | $message->send(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Collections/SyncsWithFirebaseCollection.php: -------------------------------------------------------------------------------- 1 | syncWithFirebase($withRelated); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Exceptions/FcmTargetNotSpecifiedException.php: -------------------------------------------------------------------------------- 1 | serviceAccount = $serviceAccount; 37 | $this->message = new Message(); 38 | } 39 | 40 | /** 41 | * Set the invalid token event (overrides options set in configuration) 42 | * @param string $eventName 43 | * @return $this 44 | */ 45 | function setInvalidTokenEvent($eventName) 46 | { 47 | $this->invalidTokenEvent = $eventName; 48 | return $this; 49 | } 50 | 51 | 52 | function __get($k) 53 | { 54 | return $this->message->{$k}; 55 | } 56 | 57 | 58 | /** 59 | * Set the notification data 60 | * @param array $data 61 | * @return $this 62 | */ 63 | function data(array $data) 64 | { 65 | $this->message->data->fill($data); 66 | return $this; 67 | } 68 | 69 | /** 70 | * Set the notification title 71 | * @param string $title 72 | * @return $this 73 | */ 74 | function notificationTitle($title) 75 | { 76 | $this->message->notification->setTitle($title); 77 | return $this; 78 | } 79 | 80 | /** 81 | * Set the notification sound (Android/IOS); 82 | * set to "default" for default sound, null to remove 83 | * @param string $sound Sound name (use 'default' for default sound) 84 | * @param bool|string $applyApns Specify also in apns payload data for IOS, if true is used $sound will be used, with false apns will not be set otherwise the specified value will be used. 85 | * @return $this 86 | */ 87 | function notificationSound($sound, $apns = true) 88 | { 89 | $this->message->android->notification->sound = $sound; 90 | if ($apns !== false) { 91 | $this->setApnsApsData('sound', $apns === true ? $sound : $apns); 92 | } 93 | return $this; 94 | } 95 | 96 | /** 97 | * Set the notification body 98 | * @param string $body 99 | * @return $this 100 | */ 101 | function notificationBody($body) 102 | { 103 | $this->message->notification->setBody($body); 104 | return $this; 105 | } 106 | 107 | /** 108 | * Set the message priority 109 | * @param 'high'|"normal" $priority 110 | * @return $this 111 | */ 112 | function priority($priority) 113 | { 114 | if ($priority !== 'high' && $priority !== 'normal') 115 | throw new \InvalidArgumentException('Invalid priority value!'); 116 | $this->message->android->setPriority($priority); 117 | return $this; 118 | } 119 | /** 120 | * Set the message tag (Android/IOS) 121 | * @param string $tag 122 | * @return $this 123 | */ 124 | function notificationTag($tag) 125 | { 126 | $this->message->android->notification->tag = $tag; 127 | return $this; 128 | } 129 | 130 | 131 | /** 132 | * Set Android message options with a callback 133 | * @param Closure(\Plokko\Firebase\FCM\Message\AndroidNotification) $closure 134 | * @return $this 135 | */ 136 | function setAndroidNotification($closure) 137 | { 138 | $closure($this->android->notification); 139 | return $this; 140 | } 141 | 142 | /** 143 | * Set Android message options with a callback 144 | * @param Closure(\Plokko\Firebase\FCM\Message\AndroidConfig) $closure 145 | * @return $this 146 | */ 147 | function setAndroidConfig($closure) 148 | { 149 | $closure($this->android); 150 | return $this; 151 | } 152 | 153 | /** 154 | * Set high priority 155 | * @return $this 156 | */ 157 | function highPriority() 158 | { 159 | $this->priority('high'); 160 | return $this; 161 | } 162 | 163 | /** 164 | * Set normal priority 165 | * @return $this 166 | */ 167 | function normalPriority() 168 | { 169 | $this->priority('normal'); 170 | return $this; 171 | } 172 | 173 | /** 174 | * Set the time to live of the message 175 | * @param string $ttl TTL as a string (ex. '14.5s') 176 | * @return $this 177 | */ 178 | function ttl($ttl) 179 | { 180 | $this->message->android->ttl($ttl); 181 | return $this; 182 | } 183 | 184 | /** 185 | * Set Aps data in the Apns payload 186 | * @param string $key Key to set 187 | * @param mixed $value Value to set 188 | * @return $this 189 | */ 190 | function setApnsApsData($key, $value) 191 | { 192 | $this->message->apns->payload->aps->$key = $value; 193 | return $this; 194 | } 195 | 196 | /** 197 | * Set Apns payload data 198 | * @param string $key Key of the payload to set 199 | * @param mixed $value Value of the payload to set 200 | * @return $this 201 | */ 202 | function setApnsPayload($key, $value) 203 | { 204 | $this->message->apns->payload->$key = $value; 205 | return $this; 206 | } 207 | 208 | /** 209 | * Get Apns payload data 210 | * @param string $key Key of the payload to get 211 | * @return mixed 212 | */ 213 | function getApnsPayloadValue($key) 214 | { 215 | return $this->message->apns->payload->$key; 216 | } 217 | 218 | /** 219 | * Set Apns header data 220 | * @param string $key Key of the header to set 221 | * @param mixed $value Value of the header to set 222 | * @return $this 223 | */ 224 | function setApnsHeader($key, $value) 225 | { 226 | $this->message->apns->header->$key = $value; 227 | return $this; 228 | } 229 | 230 | /** 231 | * Get Apns header data 232 | * @param string $key Key of the header to get 233 | * @return mixed 234 | */ 235 | function getApnsHeaderValue($key) 236 | { 237 | return $this->message->header->$key; 238 | } 239 | 240 | /** 241 | * Set the message destination, 242 | * this field is MANDATORY to submit the message 243 | * @param Target $target 244 | * @return FcmMessageBuilder 245 | */ 246 | function toTarget(Target $target) 247 | { 248 | $this->message->setTarget($target); 249 | return $this; 250 | } 251 | 252 | /** 253 | * Set the message destination to a device token 254 | * @param string $token Target device FCM token 255 | * @return FcmMessageBuilder 256 | */ 257 | function toDevice($token) 258 | { 259 | return $this->toTarget(new Token($token)); 260 | } 261 | 262 | /** 263 | * Set the message destination to a topic name 264 | * @param string $topicName Target topic name 265 | * @return FcmMessageBuilder 266 | */ 267 | function toTopic($topicName) 268 | { 269 | return $this->toTarget(new Topic($topicName)); 270 | } 271 | 272 | /** 273 | * Set the message destination to a condition 274 | * @param string $condition Target condition 275 | * @return FcmMessageBuilder 276 | */ 277 | function toCondition($condition) 278 | { 279 | return $this->toTarget(new Condition($condition)); 280 | } 281 | 282 | /** 283 | * Get the request for sending 284 | * @internal 285 | * @return Request 286 | */ 287 | private function request() 288 | { 289 | return new Request($this->serviceAccount); 290 | } 291 | 292 | /** 293 | * Sends the message 294 | * If no target is specified a FcmTargetNotSpecifiedException will be thrown 295 | * @throws \GuzzleHttp\Exception\GuzzleException Generic http exception 296 | * @throws \Plokko\Firebase\FCM\Exceptions\FcmErrorException FCMError exception 297 | * @throws FcmTargetNotSpecifiedException will be thrown if no device target is specified 298 | */ 299 | function send() 300 | { 301 | 302 | if ($this->message->target === null) { 303 | throw new FcmTargetNotSpecifiedException(); 304 | } 305 | try { 306 | $this->message->send($this->request()); 307 | } catch (UnregisteredException $e) { 308 | if ($this->invalidTokenEvent) { 309 | event($this->invalidTokenEvent, $this->message->target); 310 | } 311 | throw $e; 312 | } 313 | 314 | } 315 | 316 | 317 | /** 318 | * Validate the message with Firebase without submitting it 319 | * @throws \GuzzleHttp\Exception\GuzzleException Generic http exception 320 | * @throws \Plokko\Firebase\FCM\Exceptions\FcmErrorException FCMError exception 321 | * @throws FcmTargetNotSpecifiedException will be thrown if no device target is specified 322 | */ 323 | function validate() 324 | { 325 | if ($this->message->target === null) { 326 | throw new FcmTargetNotSpecifiedException(); 327 | } 328 | try { 329 | $this->message->validate($this->request()); 330 | } catch (UnregisteredException $e) { 331 | if ($this->invalidTokenEvent) { 332 | event($this->invalidTokenEvent, $this->message->target); 333 | } 334 | throw $e; 335 | } 336 | 337 | } 338 | 339 | /** 340 | * Get FCM message class 341 | * @return Message 342 | */ 343 | public function getMessage() 344 | { 345 | return $this->message; 346 | } 347 | 348 | /** 349 | * Get FCM message payload 350 | * @return array 351 | */ 352 | public function getPayload() 353 | { 354 | return $this->message->getPayload(); 355 | } 356 | 357 | /** 358 | * Cast to array 359 | * @return array 360 | */ 361 | public function toArray() 362 | { 363 | return $this->getPayload(); 364 | } 365 | 366 | /** 367 | * Cast for JSON serialization 368 | * @return array 369 | */ 370 | public function jsonSerialize(): mixed 371 | { 372 | return $this->toArray(); 373 | } 374 | /** 375 | * Cast to string (as JSON string) 376 | * @return string 377 | */ 378 | function __toString() 379 | { 380 | return json_encode($this); 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/JWT.php: -------------------------------------------------------------------------------- 1 | serviceAccount = $serviceAccount; 17 | } 18 | 19 | /** 20 | * @param $uid 21 | * @param array $claims 22 | */ 23 | function encode($uid,array $claims=[]){ 24 | return $this->serviceAccount->encodeJWT($uid,$claims); 25 | } 26 | } -------------------------------------------------------------------------------- /src/ModelSynchronizer.php: -------------------------------------------------------------------------------- 1 | model = $model; 20 | } 21 | 22 | /** 23 | * Enables/disables synching with related models (if supported) 24 | * @param bool $withRelated 25 | * @return $this 26 | */ 27 | public function withRelated($withRelated=true){ 28 | $this->syncRelated = $withRelated && in_array(SyncRelatedWithFirebase::class,class_uses($this->model)); 29 | return $this; 30 | } 31 | 32 | private function getData($onlyChanged=false){ 33 | $data = $this->model->toFirebase(); 34 | if($onlyChanged){ 35 | $this->model->getDirty(); 36 | } 37 | return $data; 38 | } 39 | 40 | /** 41 | * @param mixed|null id primary key of the model 42 | * @return Reference 43 | */ 44 | private function getReference($id=null){ 45 | return FirebaseDb::getReference($this->model->getFirebaseReferenceName().'/'.($id?:$this->model->getKey())); 46 | } 47 | 48 | /** 49 | * @return Reference 50 | */ 51 | private function getBaseReference(){ 52 | return FirebaseDb::getReference($this->model->getFirebaseReferenceName()); 53 | } 54 | 55 | /** 56 | * 57 | */ 58 | public function create(){ 59 | $this->getReference()->set($this->getData()); 60 | if($this->syncRelated){ 61 | $this->model->syncRelatedWithFirebase(); 62 | } 63 | } 64 | 65 | 66 | public function delete(){ 67 | $this->getReference()->delete(); 68 | if($this->syncRelated){ 69 | $this->model->syncRelatedWithFirebase(); 70 | } 71 | } 72 | 73 | /** 74 | * @param Model $model model to be deleted 75 | static function deletes(Model $model,array $ids){ 76 | $ref = self::getBaseReference($model); 77 | foreach($ids AS $id){ 78 | $ref->delete($id); 79 | } 80 | } 81 | */ 82 | 83 | /** 84 | * @param array|null $updatedFields if present only the keys in array will be updated 85 | */ 86 | public function update(array $updatedFields=null){ 87 | $data = $this->getData(); 88 | if($updatedFields){ 89 | $data = array_intersect_key($data, array_flip($updatedFields)); 90 | } 91 | 92 | $this->getReference()->update('',$data); 93 | 94 | //TODO: just sync changed relations? 95 | if($this->syncRelated){ 96 | $this->model->syncRelatedWithFirebase(); 97 | } 98 | } 99 | 100 | 101 | } -------------------------------------------------------------------------------- /src/ReadonlyDatabase.php: -------------------------------------------------------------------------------- 1 | $dbName]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Relations/SyncWithFirebaseBelongsToMany.php: -------------------------------------------------------------------------------- 1 | syncParentWithFirebase(); 22 | 23 | // Sync attached entity 24 | $this->related->newQuery() 25 | ->where($this->parentKey,$id)->first() 26 | ->syncWithFirebase(); 27 | } 28 | 29 | function detach($ids = null, $touch = true) 30 | { 31 | parent::detach($ids,$touch); 32 | 33 | // Sync parent model 34 | $this->syncParentWithFirebase(); 35 | 36 | // Gets detached entities 37 | $items = $this->related->newQuery()->whereIn($this->relatedKey,is_array($ids)?$ids:[$ids])->get(); 38 | /**@var SyncsWithFirebaseCollection $items*/ 39 | // Sync detached entities 40 | $items->syncWithFirebase(); 41 | 42 | } 43 | 44 | 45 | public function sync($ids, $detaching = true) 46 | { 47 | // Temporary disable Firebase synching to avoid 48 | // triggering multiple attach/detach syncs on parent 49 | $this->_isSynching = false; 50 | parent::sync($ids,$detaching); 51 | $this->_isSynching = true; 52 | 53 | // Sync parent with firebase 54 | $this->syncParentWithFirebase(); 55 | 56 | // No need to sync related model, 57 | // they have been sync in attach/detach 58 | } 59 | 60 | public function updateExistingPivot($id, array $attributes, $touch = true) 61 | { 62 | parent::updateExistingPivot($id,$attributes,$touch); 63 | $this->syncParentWithFirebase(); 64 | 65 | // Sync updated entity 66 | $this->related->newQuery() 67 | ->where($this->parentKey,$id)->first() 68 | ->syncWithFirebase(); 69 | } 70 | 71 | protected function syncParentWithFirebase(){ 72 | if($this->_isSynching){ 73 | $this->parent->syncWithFirebase(); 74 | $this->parent->syncRelatedWithFirebase(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/ServiceAccountCacheItemPool.php: -------------------------------------------------------------------------------- 1 | ttl = $config && isset($config['ttl'])?$config['ttl']:null; 27 | 28 | $driver = $config && !empty($config['cache_driver'])?$config['cache_driver']:Cache::getDefaultDriver(); 29 | $this->cache = Cache::store($driver); 30 | //Add tagging if possible 31 | if($driver !=='file'){ 32 | try{ 33 | $this->cache = $this->cache->tags(['firebase','token']); 34 | }catch(\BadMethodCallException $e){} 35 | } 36 | } 37 | 38 | /** 39 | * Returns a Cache Item representing the specified key. 40 | * 41 | * This method must always return a CacheItemInterface object, even in case of 42 | * a cache miss. It MUST NOT return null. 43 | * 44 | * @param string $key 45 | * The key for which to return the corresponding Cache Item. 46 | * 47 | * @throws InvalidArgumentException 48 | * If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException 49 | * MUST be thrown. 50 | * 51 | * @return CacheItemInterface 52 | * The corresponding Cache Item. 53 | */ 54 | public function getItem(string $key): CacheItemInterface 55 | { 56 | return $this->cache->get(self::CACHE_PREFIX.$key)?:new Item($key); 57 | } 58 | 59 | /** 60 | * Returns a traversable set of cache items. 61 | * 62 | * @param string[] $keys 63 | * An indexed array of keys of items to retrieve. 64 | * 65 | * @throws InvalidArgumentException 66 | * If any of the keys in $keys are not a legal value a \Psr\Cache\InvalidArgumentException 67 | * MUST be thrown. 68 | * 69 | * @return array|\Traversable 70 | * A traversable collection of Cache Items keyed by the cache keys of 71 | * each item. A Cache item will be returned for each key, even if that 72 | * key is not found. However, if no keys are specified then an empty 73 | * traversable MUST be returned instead. 74 | */ 75 | public function getItems(array $keys = array()): iterable 76 | { 77 | $items = []; 78 | foreach($keys AS $k){ 79 | $items[] = $this->getItem($k); 80 | } 81 | return $items; 82 | } 83 | 84 | /** 85 | * Confirms if the cache contains specified cache item. 86 | * 87 | * Note: This method MAY avoid retrieving the cached value for performance reasons. 88 | * This could result in a race condition with CacheItemInterface::get(). To avoid 89 | * such situation use CacheItemInterface::isHit() instead. 90 | * 91 | * @param string $key 92 | * The key for which to check existence. 93 | * 94 | * @throws InvalidArgumentException 95 | * If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException 96 | * MUST be thrown. 97 | * 98 | * @return bool 99 | * True if item exists in the cache, false otherwise. 100 | */ 101 | public function hasItem($key): bool 102 | { 103 | return $this->cache->has(self::CACHE_PREFIX.$key); 104 | } 105 | 106 | /** 107 | * Deletes all items in the pool. 108 | * 109 | * @return bool 110 | * True if the pool was successfully cleared. False if there was an error. 111 | */ 112 | public function clear(): bool 113 | { 114 | $this->cache->flush(); 115 | return true; 116 | } 117 | 118 | /** 119 | * Removes the item from the pool. 120 | * 121 | * @param string $key 122 | * The key to delete. 123 | * 124 | * @throws InvalidArgumentException 125 | * If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException 126 | * MUST be thrown. 127 | * 128 | * @return bool 129 | * True if the item was successfully removed. False if there was an error. 130 | */ 131 | public function deleteItem($key): bool 132 | { 133 | return $this->cache->delete(self::CACHE_PREFIX.$key); 134 | } 135 | /** 136 | * Removes multiple items from the pool. 137 | * 138 | * @param string[] $keys 139 | * An array of keys that should be removed from the pool. 140 | * @throws InvalidArgumentException 141 | * If any of the keys in $keys are not a legal value a \Psr\Cache\InvalidArgumentException 142 | * MUST be thrown. 143 | * 144 | * @return bool 145 | * True if the items were successfully removed. False if there was an error. 146 | */ 147 | public function deleteItems(array $keys): bool 148 | { 149 | foreach($keys AS $key){ 150 | $this->deleteItem($key); 151 | } 152 | return true; 153 | } 154 | 155 | /** 156 | * Persists a cache item immediately. 157 | * 158 | * @param CacheItemInterface $item 159 | * The cache item to save. 160 | * 161 | * @return bool 162 | * True if the item was successfully persisted. False if there was an error. 163 | */ 164 | public function save(CacheItemInterface $item): bool 165 | { 166 | try { 167 | $this->cache->set(self::CACHE_PREFIX.$item->getKey(), $item,$this->ttl); 168 | } catch (InvalidArgumentException $e) { 169 | return false; 170 | } 171 | return true; 172 | } 173 | 174 | /** 175 | * Sets a cache item to be persisted later. 176 | * 177 | * @param CacheItemInterface $item 178 | * The cache item to save. 179 | * 180 | * @return bool 181 | * False if the item could not be queued or if a commit was attempted and failed. True otherwise. 182 | */ 183 | public function saveDeferred(CacheItemInterface $item): bool 184 | { 185 | $this->deferred[] = $item; 186 | return true; 187 | } 188 | 189 | /** 190 | * Persists any deferred cache items. 191 | * 192 | * @return bool 193 | * True if all not-yet-saved items were successfully saved or there were none. False otherwise. 194 | */ 195 | public function commit(): bool 196 | { 197 | foreach($this->deferred AS $item){ 198 | if(!$this->save($item)) 199 | return false; 200 | } 201 | return true; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 18 | self::CONFIG_PATH => config_path('laravel-firebase.php'), 19 | ], 'config'); 20 | } 21 | 22 | public function register() 23 | { 24 | $this->mergeConfigFrom( 25 | self::CONFIG_PATH, 26 | 'laravel-firebase' 27 | ); 28 | 29 | // Provides the Firebase ServiceAccount 30 | $this->app->singleton(ServiceAccount::class, function ($app) { 31 | $sa = new ServiceAccount(config('laravel-firebase.service_account')); 32 | // Add cache handler if cache is enabled 33 | if (config('laravel-firebase.cache')) { 34 | $sa->setCacheHandler(new ServiceAccountCacheItemPool()); 35 | } 36 | return $sa; 37 | }); 38 | 39 | // Provide Firebase Database 40 | $this->app->singleton(Database::class, RealtimeDb::class); 41 | $this->app->singleton(RealtimeDb::class, function ($app, array $opt = []) { 42 | $dbName = $opt['db'] ?? config('laravel-firebase.default_db'); 43 | $dbUrl = config('laravel-firebase.firebasedb_urls.' . $dbName); 44 | 45 | return (config('laravel-firebase.read_only')) ? 46 | new ReadonlyDatabase($app->make(ServiceAccount::class), $dbUrl) : 47 | new RealtimeDb($app->make(ServiceAccount::class), $dbUrl); 48 | }); 49 | 50 | $this->app->bind(FcmMessageBuilder::class, function ($app) { 51 | $fcm = new FcmMessageBuilder($app->make(ServiceAccount::class)); 52 | 53 | $event = config('laravel-firebase.FCMInvalidTokenTriggerEvent'); 54 | if ($event) { 55 | $fcm->setInvalidTokenEvent($event); 56 | } 57 | return $fcm; 58 | }); 59 | 60 | $this->app->singleton(JWT::class, function ($app) { 61 | return new JWT($app->make(ServiceAccount::class)); 62 | }); 63 | } 64 | 65 | 66 | public function provides() 67 | { 68 | return [ 69 | ServiceAccount::class, 70 | Database::class, 71 | ]; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/Traits/SyncRelatedWithFirebase.php: -------------------------------------------------------------------------------- 1 | function($query){/*filters*\/} - Filtered query 23 | * - function(){}:Model|SyncsWithFirebaseCollection - return a custom query to sync 24 | * @return array relations to be synched with firebase, default [] 25 | */ 26 | protected function getRelationsToSyncWithFirebase() 27 | { 28 | return []; 29 | } 30 | 31 | /** 32 | * Manually syncs all related models with Firebase 33 | * Note: only relations returned by getRelationsToSyncWithFirebase() will be synchronized 34 | * @param string $only only sync specified relation (if in getRelationsToSyncWithFirebase() array) 35 | */ 36 | final public function syncRelatedWithFirebase($only = null) 37 | { 38 | $related = $this->getRelationsToSyncWithFirebase(); 39 | if ($only) { 40 | if (in_array($only, $related)) { 41 | $related = [$only]; 42 | } elseif (array_key_exists($only, $related)) { 43 | $related = [$only => $related[$only]]; 44 | } else { 45 | return; 46 | } 47 | } 48 | foreach ($related as $k => $v) { 49 | $el = null; 50 | /**@var Model $el**/ 51 | 52 | // Get the related model to sync 53 | if (is_numeric($k)) { 54 | if (is_string($v)) { 55 | //Simple relationship array 56 | $el = $this->$v; 57 | } elseif (is_callable($v)) { 58 | //Custom query 59 | $el = $v()->get(); 60 | } 61 | } else { 62 | if (is_callable($v)) { 63 | //Query filter 64 | $el = $v($this->$k())->get(); 65 | } 66 | } 67 | 68 | // Sync the model IF not null 69 | if ($el) { 70 | $el->syncWithFirebase(); 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * Overrides the BelongsToMany default relationship. 77 | * 78 | * @param Builder $query 79 | * @param Model $parent 80 | * @param string $table 81 | * @param string $foreignPivotKey 82 | * @param string $relatedPivotKey 83 | * @param string $parentKey 84 | * @param string $relatedKey 85 | * @param string $relationName 86 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany 87 | */ 88 | protected function newBelongsToMany( 89 | Builder $query, 90 | Model $parent, 91 | $table, 92 | $foreignPivotKey, 93 | $relatedPivotKey, 94 | $parentKey, 95 | $relatedKey, 96 | $relationName = null 97 | ) { 98 | if (in_array($relationName, $this->getRelationsToSyncWithFirebase())) { 99 | return new SyncWithFirebaseBelongsToMany($query, $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName); 100 | } 101 | return parent::newBelongsToMany($query, $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Traits/SyncWithFirebase.php: -------------------------------------------------------------------------------- 1 | withRelated()->create(); 25 | }); 26 | 27 | static::updated(function ($model) { 28 | $sync = new ModelSynchronizer($model); 29 | $sync->withRelated()->update(); 30 | }); 31 | 32 | static::deleted(function ($model) { 33 | $sync = new ModelSynchronizer($model); 34 | $sync->withRelated()->delete(); 35 | }); 36 | if (in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses(self::class))) { 37 | static::restored(function ($model) { 38 | $sync = new ModelSynchronizer($model); 39 | $sync->withRelated()->create(); 40 | }); 41 | } 42 | } 43 | 44 | public function syncWithFirebase($withRelated = false) 45 | { 46 | $sync = new ModelSynchronizer($this); 47 | $sync->withRelated($withRelated)->create(); 48 | } 49 | 50 | /** 51 | * Automatically casts Collection to SyncsWithFirebaseCollection 52 | * to allow bulk syncWithFirebase 53 | * @param array $models 54 | * @return SyncsWithFirebaseCollection 55 | * @internal 56 | */ 57 | public function newCollection(array $models = []) 58 | { 59 | return new SyncsWithFirebaseCollection($models); 60 | } 61 | 62 | /** 63 | * Data to be synchronized with Firebase 64 | * @return array 65 | */ 66 | public function toFirebase() 67 | { 68 | if ($fresh = $this->fresh()) { 69 | return $fresh->toArray(); 70 | } 71 | return []; 72 | } 73 | 74 | /** 75 | * @return string reference name 76 | */ 77 | public function getFirebaseReferenceName() 78 | { 79 | return $this->getTable(); 80 | } 81 | 82 | 83 | } 84 | --------------------------------------------------------------------------------