diff --git a/composer.json b/composer.json index 3fc23c3..c295a54 100644 --- a/composer.json +++ b/composer.json @@ -10,9 +10,10 @@ } ], "require": { - "php" : ">=5.6.0", + "php" : ">=5.6.4", "guzzlehttp/guzzle": "^6.2", - "illuminate/support": "^5.2" + "illuminate/support": "^5.3", + "illuminate/notifications": "^5.3" }, "require-dev": { "phpunit/phpunit": "4.0.*" diff --git a/readme.md b/readme.md index d8c6197..cffae7a 100644 --- a/readme.md +++ b/readme.md @@ -39,28 +39,31 @@ composer require edujugon/push-notification Then you don't have to either register or add the alias, this package uses Package Auto-Discovery's feature, and should be available as soon as you install it via Composer. (Laravel < 5.5) Register the PushNotification service by adding it to the providers array. - - 'providers' => array( - ... - Edujugon\PushNotification\Providers\PushNotificationServiceProvider::class - ) +```php +'providers' => array( + ... + Edujugon\PushNotification\Providers\PushNotificationServiceProvider::class +) +``` (Laravel < 5.5) Let's add the Alias facade, add it to the aliases array. - - 'aliases' => array( - ... - 'PushNotification' => Edujugon\PushNotification\Facades\PushNotification::class, - ) +```php +'aliases' => array( + ... + 'PushNotification' => Edujugon\PushNotification\Facades\PushNotification::class, +) +``` Publish the package's configuration file to the application's own config directory - - php artisan vendor:publish --provider="Edujugon\PushNotification\Providers\PushNotificationServiceProvider" --tag="config" +``` +php artisan vendor:publish --provider="Edujugon\PushNotification\Providers\PushNotificationServiceProvider" --tag="config" +``` > Go to [laravel facade sample](https://github.com/edujugon/PushNotification#laravel-alias-facade) directly. ### Configuration -The default configuration for all Push service providers is located in Config/config.php +After publishing the configuration, all the Push service config details are located in config/pushnotification.php The default configuration parameters for **GCM** and **FCM** are : @@ -69,49 +72,72 @@ The default configuration parameters for **GCM** and **FCM** are : * apiKey => Your ApiKey You can dynamically update those values or adding new ones calling the method setConfig like so: - - $push->setConfig([ - 'priority' => 'high', - 'dry_run' => true, - 'time_to_live' => 3 - ]); +```php +$push->setConfig([ + 'priority' => 'high', + 'dry_run' => true, + 'time_to_live' => 3 +]); +``` The default configuration parameters for **APN** are: -*   ```certificate => __DIR__ . '/iosCertificates/yourCertificate.pem'``` -*   ```passPhrase => 'MyPassPhrase'``` +* ```certificate => __DIR__ . '/iosCertificates/yourCertificate.pem'``` +* ```passPhrase => 'MyPassPhrase'``` * ```passFile => __DIR__ . '/iosCertificates/yourKey.pem' //Optional``` * ```dry_run => false``` +(Make sure to set `dry_run` to `true` if you're using development *.pem certificate, and `false` for production) + Also you can update those values and add more dynamically +```php +$push->setConfig([ + 'passPhrase' => 'NewPass', + 'custom' => 'MycustomValue', + 'dry_run' => true +]); +``` + +Even you may update the url of the Push Service dynamically like follows: +```php +$push->setUrl('http://newPushServiceUrl.com'); +``` + +> Not update the url unless it's really necessary. + +You can specify the number of client-side attempts to APN before giving +up. The default amount is 3 attempts. You can override this value by +specifying `connection_attempts` in `setConfig()` assoc-array. Keep in +mind the default number of requested attempts is 3. + +If you prefer to retry indefinitely, set `connection_attempts` to zero. $push->setConfig([ 'passPhrase' => 'NewPass', 'custom' => 'MycustomValue', + 'connection_attempts' => 0, 'dry_run' => true ]); -Even you may update the url of the Push Service dynamically like follows: - - $puhs->setUrl('http://newPushServiceUrl.com'); - -> Not update the url unless it's really necessary. ## Usage - - $push = new PushNotification; +```php +$push = new PushNotification; +``` By default it will use GCM as Push Service provider. For APN Service: - - $push = new PushNotification('apn'); +```php +$push = new PushNotification('apn'); +``` For FCM Service: +```php +$push = new PushNotification('fcm'); +``` - $push = new PushNotification('fcm'); - Now you may use any method what you need. Please see the API List. @@ -200,7 +226,7 @@ object getFeedback() #### getUnregisteredDeviceTokens -`getUnregisteredDeviceTokens` method gets the devices' tokens that couldn't receive the notification because they aren't registered to the Push service provider. +`getUnregisteredDeviceTokens` method gets the devices' tokens that couldn't receive the notification because they aren't registered to the Push service provider. You may use it chaining it to `send` method or call it whenever after sending a notification. **Syntax** @@ -250,79 +276,82 @@ object sendByTopic($topic,$isCondition) GCM sample: ```php - $push->setMessage([ - 'notification' => [ - 'title'=>'This is the title', - 'body'=>'This is the message', - 'sound' => 'default' - ], - 'data' => [ - 'extraPayLoad1' => 'value1', - 'extraPayLoad2' => 'value2' - ] - ]) - ->setApiKey('Server-API-Key') - ->setDevicesToken(['deviceToken1','deviceToken2','deviceToken3'...]); -``` - -APN sample: - - $push->setMessage([ - 'aps' => [ - 'alert' => [ - 'title' => 'This is the title', - 'body' => 'This is the body' - ], - 'sound' => 'default' - +$push->setMessage([ + 'notification' => [ + 'title'=>'This is the title', + 'body'=>'This is the message', + 'sound' => 'default' ], - 'extraPayLoad' => [ - 'custom' => 'My custom data', + 'data' => [ + 'extraPayLoad1' => 'value1', + 'extraPayLoad2' => 'value2' ] - ]) + ]) + ->setApiKey('Server-API-Key') ->setDevicesToken(['deviceToken1','deviceToken2','deviceToken3'...]); +``` +APN sample: +```php +$push->setMessage([ + 'aps' => [ + 'alert' => [ + 'title' => 'This is the title', + 'body' => 'This is the body' + ], + 'sound' => 'default', + 'badge' => 1 + + ], + 'extraPayLoad' => [ + 'custom' => 'My custom data', + ] + ]) + ->setDevicesToken(['deviceToken1','deviceToken2','deviceToken3'...]); +``` or do it separately - - $push->setMessage([ - 'notification' => [ - 'title'=>'This is the title', - 'body'=>'This is the message', - 'sound' => 'default' - ], - 'data' => [ - 'extraPayLoad1' => 'value1', - 'extraPayLoad2' => 'value2' - ] - ]); - $push->setApiKey('Server-API-Key'); - $push->setDevicesToken(['deviceToken1' - ,'deviceToken2', - 'deviceToken3' - ]); - +```php +$push->setMessage([ + 'notification' => [ + 'title'=>'This is the title', + 'body'=>'This is the message', + 'sound' => 'default' + ], + 'data' => [ + 'extraPayLoad1' => 'value1', + 'extraPayLoad2' => 'value2' + ] + ]); +$push->setApiKey('Server-API-Key'); +$push->setDevicesToken(['deviceToken1' + ,'deviceToken2', + 'deviceToken3' +]); +``` If you want send the notification to only 1 device, you may pass the value as string. - - $push->setDevicesToken('deviceToken'); +```php +$push->setDevicesToken('deviceToken'); +``` ### Send the Notification Method send() can be also chained to the above methods. - - $push->setMessage([ - 'notification' => [ - 'title'=>'This is the title', - 'body'=>'This is the message', - 'sound' => 'default' - ], - 'data' => [ - 'extraPayLoad1' => 'value1', - 'extraPayLoad2' => 'value2' - ] - ]) - ->setApiKey('Server-API-Key') - ->setDevicesToken(['deviceToken1','deviceToken2','deviceToken3'...]) - ->send(); +```php +$push->setMessage([ + 'notification' => [ + 'title'=>'This is the title', + 'body'=>'This is the message', + 'sound' => 'default' + ], + 'data' => [ + 'extraPayLoad1' => 'value1', + 'extraPayLoad2' => 'value2' + ] + ]) + ->setApiKey('Server-API-Key') + ->setDevicesToken(['deviceToken1','deviceToken2','deviceToken3'...]) + ->send(); +``` ### Send the Notification by Topic (**FCM** only) @@ -345,7 +374,7 @@ $response = $push->setMessage(['message'=>'Hello World']) ### Understanding Gcm and Fcm Message Payload -#### Notification Message +#### Notification Message Add a `notification` key when setting the message in `setMessage` method. like follows: @@ -356,7 +385,7 @@ $push->setMessage([ 'body'=>'This is the message', 'sound' => 'default' ] - ); + ); ``` You may add some extra payload adding a `data` key when setting the message in `setMessage` method. @@ -375,7 +404,7 @@ $push->setMessage([ ]); ``` -#### Data Message +#### Data Message By default this package sends the notification as Data Message. So no need to add a `data` key. Just leave it without main keys. @@ -384,7 +413,7 @@ $push->setMessage([ 'title'=>'This is the title', 'body'=>'This is the message', 'myCustomVAlue' => 'value' - ]); + ]); ``` The above example is like you were sending the following: @@ -404,16 +433,18 @@ For more details, have a look at [gcm/fcm notification paypload support](https:/ ### Getting the Notification Response If you want to get the push service response, you can call the method `getFeedback`: - +```php $push->getFeedback(); +``` Or again, chain it to the above methods: - +```php $push->setMessage(['body'=>'This is the message','title'=>'This is the title']) ->setApiKey('Server-API-Key') ->setDevicesToken(['deviceToken1','deviceToken2','deviceToken3'...]) ->send() ->getFeedback(); +``` It will return an object with the response. @@ -422,7 +453,7 @@ It will return an object with the response. Any time you send a notification, it will check if APN server has any feedback for your certificate. If so, the responses are merged to our feedback like below: -``` +```php class stdClass#21 (4) { public $success => int(0) @@ -452,30 +483,108 @@ class stdClass#21 (4) { ### Get Unregistered Devices tokens After sending a notification, you may retrieve the list of unregistered tokens - - $push->getUnregisteredDeviceTokens(); +```php +$push->getUnregisteredDeviceTokens(); +``` This method returns an array of unregistered tokens from the Push service provider. If there isn't any unregistered token, it will return an empty array. ### Laravel Alias Facade After register the Alias Facade for this Package, you can use it like follows: - - PushNotification::setService('fcm') - ->setMessage([ - 'notification' => [ - 'title'=>'This is the title', - 'body'=>'This is the message', - 'sound' => 'default' - ], - 'data' => [ - 'extraPayLoad1' => 'value1', - 'extraPayLoad2' => 'value2' - ] - ]) - ->setApiKey('Server-API-Key') - ->setDevicesToken(['deviceToken1','deviceToken2','deviceToken3'...]) - ->send() - ->getFeedback(); +```php +PushNotification::setService('fcm') + ->setMessage([ + 'notification' => [ + 'title'=>'This is the title', + 'body'=>'This is the message', + 'sound' => 'default' + ], + 'data' => [ + 'extraPayLoad1' => 'value1', + 'extraPayLoad2' => 'value2' + ] + ]) + ->setApiKey('Server-API-Key') + ->setDevicesToken(['deviceToken1','deviceToken2','deviceToken3'...]) + ->send() + ->getFeedback(); +``` It would return the Push Feedback of the Notification sent. + +### Notification channels + +#### Formatting Push Notifications +If a notification supports being sent as an push message, you should define `toApn` and/or `toFcm`/`toGcm` methods on the notification class. This method will receive a `$notifiable` entity and should return a `Edujugon\PushNotification\Messages\PushMessage` instance: + +```php +public function toApn($notifiable) +{ + return new PushMessage('Hello world'); +} +``` + +#### Customizing The Title and Body +```php +public function toApn($notifiable) +{ + return (new PushMessage) + ->title('Hello world') + ->body('...'); +} +``` + +#### Customizing The Notification Sound +```php +public function toApn($notifiable) +{ + return (new PushMessage) + ->body('Hello world') + ->sound('default'); +} +``` + +#### Customizing The Badge Number +```php +public function toApn($notifiable) +{ + return (new PushMessage) + ->body('Hello world') + ->sound('default') + ->badge(7); +} +``` + +#### Passing Service Config +```php +public function toApn($notifiable) +{ + return (new PushMessage) + ->body('Hello world') + ->config(['dry_run' => false]); +} +``` + +#### Add it to the notification channels +```php +public function via($notifiable) +{ + return [ApnChannel::class]; +} +``` +>Don't forget the use statement at the top of the class + +#### Routing Push Notifications +Just define `routeNotificationForApn` and/or `routeNotificationForFcm`/`routeNotificationForGcm` methods on the entity +```php +/** + * Route notifications for the Apn channel. + * + * @return string|array + */ +public function routeNotificationForApn() +{ + return $this->ios_push_token; +} +``` diff --git a/src/Apn.php b/src/Apn.php index a940134..23ca60e 100644 --- a/src/Apn.php +++ b/src/Apn.php @@ -6,6 +6,8 @@ class Apn extends PushService implements PushServiceInterface { + const MAX_ATTEMPTS = 3; + /** * Url for development purposes * @@ -39,6 +41,16 @@ class Apn extends PushService implements PushServiceInterface */ private $feedbackUrl; + /** + * The number of attempts to re-try before failing. + * Set to zero for unlimited attempts. + * + * @var int + */ + private $maxAttempts = self::MAX_ATTEMPTS; + + private $attempts = 0; + /** * Apn constructor. */ @@ -63,7 +75,7 @@ public function setConfig(array $config) parent::setConfig($config); $this->setProperGateway(); - + $this->setRetryAttemptsIfConfigured(); } /** @@ -73,19 +85,69 @@ public function setConfig(array $config) */ private function setProperGateway() { - if(isset($this->config['dry_run'])) - { - if($this->config['dry_run']){ + if (isset($this->config['dry_run'])) { + if ($this->config['dry_run']) { $this->setUrl($this->sandboxUrl); $this->feedbackUrl = $this->feedbackSandboxUrl; - }else { + } else { $this->setUrl($this->productionUrl); $this->feedbackUrl = $this->feedbackProductionUrl; } } } + /** + * Configure re-try attempts. + * + * @return void + */ + private function setRetryAttemptsIfConfigured() + { + if (isset($this->config['connection_attempts']) && + is_numeric($this->config['connection_attempts'])) { + $this->maxAttempts = $this->config['connection_attempts']; + } + } + + /** + * Determines whether the connection attempts should be unlimited. + * + * @return bool + */ + private function isUnlimitedAttempts() + { + return $this->maxAttempts == 0; + } + + /** + * Check if can retry a connection + * + * @return bool + */ + private function canRetry() + { + if ($this->isUnlimitedAttempts()) { + return true; + } + + $this->attempts++; + + return $this->attempts < $this->maxAttempts; + } + + /** + * Reset connection attempts + * + * @return $this + */ + private function resetAttempts() + { + $this->attempts = 0; + + return $this; + } + /** * Provide the unregistered tokens of the notification sent. * @@ -96,10 +158,12 @@ public function getUnregisteredDeviceTokens(array $devices_token) { $tokens = []; - if(! empty($this->feedback->tokenFailList)) - $tokens = $this->feedback->tokenFailList; - if(!empty($this->feedback->apnsFeedback)) - $tokens = array_merge($tokens,array_pluck($this->feedback->apnsFeedback,'devtoken')); + if (!empty($this->feedback->tokenFailList)) { + $tokens = $this->feedback->tokenFailList; + } + if (!empty($this->feedback->apnsFeedback)) { + $tokens = array_merge($tokens, array_pluck($this->feedback->apnsFeedback, 'devtoken')); + } return $tokens; } @@ -111,9 +175,12 @@ public function getUnregisteredDeviceTokens(array $devices_token) */ private function messageNoExistCertificate() { - $response = ['success' => false, 'error' => "Please, add your APN certificate to the iosCertificates folder." . PHP_EOL]; + $response = [ + 'success' => false, + 'error' => "Please, add your APN certificate to the iosCertificates folder." . PHP_EOL + ]; - $this->setFeedback(json_decode(json_encode($response), FALSE)); + $this->setFeedback(json_decode(json_encode($response))); } /** @@ -122,11 +189,9 @@ private function messageNoExistCertificate() */ private function existCertificate() { - if(isset($this->config['certificate'])) - { + if (isset($this->config['certificate'])) { $certificate = $this->config['certificate']; - if(!file_exists($certificate)) - { + if (!file_exists($certificate)) { $this->messageNoExistCertificate(); return false; } @@ -151,16 +216,18 @@ private function composeStreamSocket() $certificate = $this->config['certificate']; stream_context_set_option($ctx, 'ssl', 'local_cert', $certificate); - if(isset($this->config['passPhrase'])) - { + if (isset($this->config['passPhrase'])) { $passPhrase = $this->config['passPhrase']; - if(!empty($passPhrase)) stream_context_set_option($ctx, 'ssl', 'passphrase', $passPhrase); + if (!empty($passPhrase)) { + stream_context_set_option($ctx, 'ssl', 'passphrase', $passPhrase); + } } - if(isset($this->config['passFile'])) - { + if (isset($this->config['passFile'])) { $passFile = $this->config['passFile']; - if(file_exists($passFile)) stream_context_set_option($ctx, 'ssl', 'local_pk', $passFile); + if (file_exists($passFile)) { + stream_context_set_option($ctx, 'ssl', 'local_pk', $passFile); + } } return $ctx; @@ -175,28 +242,41 @@ private function composeStreamSocket() */ private function openConnectionAPNS($ctx) { + $fp = false; // Open a connection to the APNS server - try{ + try { $fp = stream_socket_client( - $this->url, $err, - $errstr, 60, STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, $ctx); - }catch (\Exception $e){ - //if stream socket can't be established, try again - return $this->openConnectionAPNS($ctx); - } + $this->url, + $err, + $errstr, + 60, + STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, + $ctx + ); + + stream_set_blocking($fp, 0); + + if (!$fp) { + $response = ['success' => false, 'error' => "Failed to connect: $err $errstr" . PHP_EOL]; + + $this->setFeedback(json_decode(json_encode($response))); - stream_set_blocking ($fp, 0); + } - if (!$fp) - { - $response = ['success' => false, 'error' => "Failed to connect: $err $errstr" . PHP_EOL]; + } catch (\Exception $e) { + //if stream socket can't be established, try again + if ($this->canRetry()) { + return $this->openConnectionAPNS($ctx); + } - $this->setFeedback(json_decode(json_encode($response), FALSE)); + $response = ['success' => false, 'error' => 'Connection problem: ' . $e->getMessage() . PHP_EOL]; + $this->setFeedback(json_decode(json_encode($response))); - return false; + } finally { + $this->resetAttempts(); + return $fp; } - return $fp; } /** @@ -205,14 +285,16 @@ private function openConnectionAPNS($ctx) * @param array $message * @return \stdClass APN Response */ - public function send(array $deviceTokens,array $message) + public function send(array $deviceTokens, array $message) { /** * If there isn't certificate returns the feedback. * Feedback has been loaded in existCertificate method if no certificate found */ - if(!$this->existCertificate()) return $this->feedback; + if (!$this->existCertificate()) { + return $this->feedback; + } // Encode the payload as JSON $payload = json_encode($message); @@ -220,23 +302,23 @@ public function send(array $deviceTokens,array $message) //When sending a notification we prepare a clean feedback $feedback = $this->initializeFeedback(); - foreach ($deviceTokens as $token) - { + foreach ($deviceTokens as $token) { /** * Open APN connection */ $ctx = $this->composeStreamSocket(); $fp = $this->openConnectionAPNS($ctx); - if(!$fp) return $this->feedback; + if (!$fp) { + return $this->feedback; + } // Build the binary notification - //Check if the token is numeric no to get PHP Warnings with pack function. - if (ctype_xdigit($token)) { + //Check if the token is numeric not to get PHP Warnings with pack function. + if (ctype_xdigit($token)) { $msg = chr(0) . pack('n', 32) . pack('H*', $token) . pack('n', strlen($payload)) . $payload; - }else - { + } else { $feedback['tokenFailList'][] = $token; $feedback['failure'] += 1; continue; @@ -244,13 +326,13 @@ public function send(array $deviceTokens,array $message) $result = fwrite($fp, $msg, strlen($msg)); - if (!$result) - { + if (!$result) { $feedback['tokenFailList'][] = $token; $feedback['failure'] += 1; - }else + } else { $feedback['success'] += 1; + } // Close the connection to the server if ($fp) { @@ -267,18 +349,16 @@ public function send(array $deviceTokens,array $message) /** * Merge the apn feedback to our custom feedback if there is any. */ - if(!empty($apnsFeedback)) - { - $feedback = array_merge($feedback,$apnsFeedback); + if (!empty($apnsFeedback)) { + $feedback = array_merge($feedback, $apnsFeedback); - $feedback = $this->updateCustomFeedbackValues($apnsFeedback, $feedback,$deviceTokens); + $feedback = $this->updateCustomFeedbackValues($apnsFeedback, $feedback, $deviceTokens); } //Set the global feedback - $this->setFeedback(json_decode(json_encode($feedback), FALSE)); + $this->setFeedback(json_decode(json_encode($feedback))); return $this->feedback; - } /** @@ -291,30 +371,43 @@ public function apnsFeedback() { $feedback_tokens = array(); - if(!$this->existCertificate()) return $feedback_tokens; + if (!$this->existCertificate()) { + return $feedback_tokens; + } //connect to the APNS feedback servers $ctx = $this->composeStreamSocket(); // Open a connection to the APNS server - try{ + try { $apns = stream_socket_client($this->feedbackUrl, $errcode, $errstr, 60, STREAM_CLIENT_CONNECT, $ctx); - }catch (\Exception $e){ - //if stream socket can't be established, try again - return $this->apnsFeedback(); - } - //Read the data on the connection: - while(!feof($apns)) { - $data = fread($apns, 38); - if(strlen($data)) { - $feedback_tokens['apnsFeedback'][] = unpack("N1timestamp/n1length/H*devtoken", $data); + //Read the data on the connection: + while (!feof($apns)) { + $data = fread($apns, 38); + if (strlen($data)) { + $feedback_tokens['apnsFeedback'][] = unpack("N1timestamp/n1length/H*devtoken", $data); + } + } + fclose($apns); + + } catch (\Exception $e) { + //if stream socket can't be established, try again + if ($this->canRetry()) { + return $this->apnsFeedback(); } - } - fclose($apns); - return $feedback_tokens; + $response = [ + 'success' => false, + 'error' => 'APNS feedback connection problem: ' . $e->getMessage() . PHP_EOL + ]; + $this->setFeedback(json_decode(json_encode($response))); + + } finally { + $this->resetAttempts(); + return $feedback_tokens; + } } /** @@ -326,18 +419,17 @@ public function apnsFeedback() { * * @return array $feedback */ - private function updateCustomFeedbackValues($apnsFeedback, $feedback,$deviceTokens) + private function updateCustomFeedbackValues($apnsFeedback, $feedback, $deviceTokens) { //Add failures amount based on apple feedback to our custom feedback $feedback['failure'] += count($apnsFeedback['apnsFeedback']); //apns tokens - $apnsTokens = array_pluck($apnsFeedback['apnsFeedback'],'devtoken'); + $apnsTokens = array_pluck($apnsFeedback['apnsFeedback'], 'devtoken'); - foreach ($deviceTokens as $token) - { - if(in_array($token, $apnsTokens)){ + foreach ($deviceTokens as $token) { + if (in_array($token, $apnsTokens)) { $feedback['success'] -= 1; $feedback['tokenFailList'][] = $token; } diff --git a/src/Channels/ApnChannel.php b/src/Channels/ApnChannel.php new file mode 100644 index 0000000..6a731a1 --- /dev/null +++ b/src/Channels/ApnChannel.php @@ -0,0 +1,43 @@ + [ + 'alert' => [ + 'title' => $message->title, + 'body' => $message->body, + ], + 'category' => $message->category, + 'sound' => $message->sound, + ], + ]; + + if (! empty($message->extra)) { + $data['extraPayLoad'] = $message->extra; + } + + if (is_numeric($message->badge)) { + $data['aps']['badge'] = $message->badge; + } + + return $data; + } +} diff --git a/src/Channels/FcmChannel.php b/src/Channels/FcmChannel.php new file mode 100644 index 0000000..77b3529 --- /dev/null +++ b/src/Channels/FcmChannel.php @@ -0,0 +1,14 @@ +title != null || $message->body != null || $message->click_action != null) { + $data = [ + 'notification' => [ + 'title' => $message->title, + 'body' => $message->body, + 'sound' => $message->sound, + 'click_action' => $message->click_action, + ], + ]; + + // Set custom badge number when isset in PushMessage + if (! empty($message->badge)) { + $data['notification']['badge'] = $message->badge; + } + + // Set icon when isset in PushMessage + if (! empty($message->icon)) { + $data['notification']['icon'] = $message->icon; + } + } + + if (! empty($message->extra)) { + $data['data'] = $message->extra; + } + + return $data; + } +} diff --git a/src/Channels/PushChannel.php b/src/Channels/PushChannel.php new file mode 100644 index 0000000..1642938 --- /dev/null +++ b/src/Channels/PushChannel.php @@ -0,0 +1,142 @@ +push = $push; + } + + /** + * Send the given notification. + * + * @param mixed $notifiable + * @param \Illuminate\Notifications\Notification $notification + * @return void + */ + public function send($notifiable, Notification $notification) + { + if (! $to = $notifiable->routeNotificationFor($this->notificationFor())) { + return; + } + + $message = $this->buildMessage($notifiable, $notification); + + $data = $this->buildData($message); + + $this->push($this->pushServiceName(), $to, $data, $message); + } + + /** + * Send the push notification. + * + * @param string $service + * @param mixed $to + * @param array $data + * @param \Edujugon\PushNotification\Messages\PushMessage $message + * @return mixed + */ + protected function push($service, $to, $data, PushMessage $message) + { + $this->push->setMessage($data) + ->setService($service) + ->setDevicesToken($to); + + if (! empty($message->config)) { + $this->push->setConfig($message->config); + + if (! empty($message->config['apiKey'])) { + $this->push->setApiKey($message->config['apiKey']); + } + } + + $feedback = $this->push->send() + ->getFeedback(); + + $this->broadcast(); + + return $feedback; + } + + /** + * Format the message. + * + * @param mixed $notifiable + * @param \Illuminate\Notifications\Notification $notification + * @return \Edujugon\PushNotification\Messages\PushMessage + */ + protected function buildMessage($notifiable, Notification $notification) + { + $message = call_user_func_array([$notification, $this->getToMethod()], [$notifiable]); + + if (is_string($message)) { + $message = new PushMessage($message); + } + + return $message; + } + + /** + * Get the method name to get the push notification representation of the notification. + * + * @return string + */ + protected function getToMethod() + { + return 'to' . ucfirst($this->pushServiceName()); + } + + /** + * Format push service name for routing notification. + * + * @return string + */ + protected function notificationFor() + { + return ucfirst(strtolower($this->pushServiceName())); + } + + /** + * Build the push payload data. + * + * @param \Edujugon\PushNotification\Messages\PushMessage $message + * @return array + */ + abstract protected function buildData(PushMessage $message); + + /** + * BroadCast NotificationPushed event + */ + protected function broadcast() + { + if (function_exists('broadcast')) { + broadcast(new NotificationPushed($this->push)); + } elseif (function_exists('event')) { + event(new NotificationPushed($this->push)); + } + } + + /** + * Get push notification service name. + * + * @return string + */ + abstract protected function pushServiceName(); + +} diff --git a/src/Contracts/PushServiceInterface.php b/src/Contracts/PushServiceInterface.php index 74627bb..4be1f97 100644 --- a/src/Contracts/PushServiceInterface.php +++ b/src/Contracts/PushServiceInterface.php @@ -1,18 +1,16 @@ push = $push; + } +} diff --git a/src/Exceptions/PushNotificationException.php b/src/Exceptions/PushNotificationException.php new file mode 100644 index 0000000..07b7e20 --- /dev/null +++ b/src/Exceptions/PushNotificationException.php @@ -0,0 +1,7 @@ +addRequestHeaders(); $data = $this->buildData($topic, $message, $isCondition); @@ -46,12 +44,12 @@ public function sendByTopic($topic,$message, $isCondition = false) $json = $result->getBody(); - $this->setFeedback(json_decode($json)); + $this->setFeedback(json_decode($json, false, 512, JSON_BIGINT_AS_STRING)); } catch (\Exception $e) { $response = ['success' => false, 'error' => $e->getMessage()]; - $this->setFeedback(json_decode(json_encode($response), FALSE)); + $this->setFeedback(json_decode(json_encode($response))); } finally { return $this->feedback; @@ -70,6 +68,6 @@ protected function buildData($topic, $message, $isCondition) { $condition = $isCondition ? ['condition' => $topic] : ['to' => '/topics/' . $topic]; - return array_merge($condition , $this->buildMessage($message)); + return array_merge($condition, $this->buildMessage($message)); } } \ No newline at end of file diff --git a/src/Gcm.php b/src/Gcm.php index bdbbd82..46cb3a4 100644 --- a/src/Gcm.php +++ b/src/Gcm.php @@ -35,7 +35,7 @@ public function __construct() } /** - * Provide the unregistered tokens of the notification sent. + * Provide the unregistered tokens of the sent notification. * * @param array $devices_token * @return array $tokenUnRegistered @@ -45,18 +45,17 @@ public function getUnregisteredDeviceTokens(array $devices_token) /** * If there is any failure sending the notification */ - if($this->feedback && isset($this->feedback->failure)) - { - + if ($this->feedback && isset($this->feedback->failure)) { $unRegisteredTokens = $devices_token; /** * Walk the array looking for any error. * If no error, unset it from all token list which will become the unregistered tokens array. */ - foreach ($this->feedback->results as $key => $message) - { - if(! isset($message->error)) unset( $unRegisteredTokens[$key] ); + foreach ($this->feedback->results as $key => $message) { + if (!isset($message->error)) { + unset($unRegisteredTokens[$key]); + } } return $unRegisteredTokens; @@ -72,14 +71,14 @@ public function getUnregisteredDeviceTokens(array $devices_token) * @param $message * @return array */ - protected function addRequestFields($deviceTokens, $message){ + protected function addRequestFields($deviceTokens, $message) + { $params = $this->cleanConfigParams(); $message = $this->buildMessage($message); - return array_merge($params,$message,['registration_ids' => $deviceTokens]); - + return array_merge($params, $message, ['registration_ids' => $deviceTokens]); } /** @@ -88,9 +87,10 @@ protected function addRequestFields($deviceTokens, $message){ */ protected function buildMessage($message) { - // if NO notification and data keys, then set Data Message as default. - if(!array_key_exists('data',$message) && !array_key_exists('notification',$message)) + // if no notification nor data keys, then set Data Message as default. + if (!array_key_exists('data', $message) && !array_key_exists('notification', $message)) { return ['data' => $message]; + } return $message; } @@ -100,16 +100,16 @@ protected function buildMessage($message) * * @return array */ - private function cleanConfigParams(){ - + private function cleanConfigParams() + { /** * Add the params you want to be removed from the push notification */ $paramsToBeRemoved = ['apiKey']; - return array_filter($this->config,function($key) use($paramsToBeRemoved){ - return !in_array($key,$paramsToBeRemoved); - },ARRAY_FILTER_USE_KEY); + return array_filter($this->config, function ($key) use ($paramsToBeRemoved) { + return !in_array($key, $paramsToBeRemoved); + }, ARRAY_FILTER_USE_KEY); } /** @@ -117,7 +117,8 @@ private function cleanConfigParams(){ * * @return array */ - protected function addRequestHeaders(){ + protected function addRequestHeaders() + { return [ 'Authorization' => 'key=' . $this->config['apiKey'], 'Content-Type:' =>'application/json' @@ -126,17 +127,19 @@ protected function addRequestHeaders(){ /** * Send Push Notification + * * @param array $deviceTokens * @param array $message + * * @return \stdClass GCM Response */ - public function send(array $deviceTokens,array $message) + public function send(array $deviceTokens, array $message) { - $fields = $this->addRequestFields($deviceTokens,$message); + $fields = $this->addRequestFields($deviceTokens, $message); $headers = $this->addRequestHeaders(); - try - { + + try { $result = $this->client->post( $this->url, [ @@ -147,18 +150,16 @@ public function send(array $deviceTokens,array $message) $json = $result->getBody(); - $this->setFeedback(json_decode($json)); + $this->setFeedback(json_decode($json, false, 512, JSON_BIGINT_AS_STRING)); return $this->feedback; - }catch (\Exception $e) - { + } catch (\Exception $e) { $response = ['success' => false, 'error' => $e->getMessage()]; - $this->setFeedback(json_decode(json_encode($response), FALSE)); + $this->setFeedback(json_decode(json_encode($response))); return $this->feedback; } - } -} \ No newline at end of file +} diff --git a/src/Messages/PushMessage.php b/src/Messages/PushMessage.php new file mode 100644 index 0000000..30bdc0d --- /dev/null +++ b/src/Messages/PushMessage.php @@ -0,0 +1,179 @@ +body = $body; + } + + /** + * Set the message body. + * + * @param string $body + * @return $this + */ + public function body($body) + { + $this->body = $body; + + return $this; + } + + /** + * Set the message title. + * + * @param string $title + * @return $this + */ + public function title($title) + { + $this->title = $title; + + return $this; + } + + /** + * Set the message icon. + * + * @param string $icon + * @return $this + */ + public function icon($icon) + { + $this->icon = $icon; + + return $this; + } + + /** + * Set the notification sound. + * + * @param string $sound + * @return $this + */ + public function sound($sound) + { + $this->sound = $sound; + + return $this; + } + + /** + * The action associated with a user click on the notification.(Android) + * + * @param string $click_action + * @return $this + */ + public function clickAction($click_action) + { + $this->click_action = $click_action; + + return $this; + } + + /** + * The action associated with a user click on the notification.(iOS) + * + * @param string $click_action + * @return $this + */ + public function category($category) + { + $this->category = $category; + + return $this; + } + + /** + * Set the notification badge. + * + * @param integer $badge + * @return $this + */ + public function badge($badge) + { + $this->badge = $badge; + + return $this; + } + + /** + * Set message extra data. + * + * @param array $extra + * @return $this + */ + public function extra(array $extra) + { + $this->extra = $extra; + + return $this; + } + + /** + * Set message config. + * + * @param array $config + * @return $this + */ + public function config(array $config) + { + $this->config = $config; + + return $this; + } +} diff --git a/src/Providers/PushNotificationServiceProvider.php b/src/Providers/PushNotificationServiceProvider.php index 82e3d81..41cd0d2 100644 --- a/src/Providers/PushNotificationServiceProvider.php +++ b/src/Providers/PushNotificationServiceProvider.php @@ -29,8 +29,7 @@ public function boot() */ public function register() { - $this->app->singleton('edujugonPushNotification',function($app) - { + $this->app->singleton('edujugonPushNotification', function ($app) { return new PushNotification(); }); } diff --git a/src/PushNotification.php b/src/PushNotification.php index 6264e30..6e7305f 100644 --- a/src/PushNotification.php +++ b/src/PushNotification.php @@ -1,7 +1,6 @@ servicesList)) $service = $this->defaultServiceName; + if (!array_key_exists($service, $this->servicesList)) { + $service = $this->defaultServiceName; + } $this->service = is_null($service) ? new $this->servicesList[$this->defaultServiceName] : new $this->servicesList[$service]; - } /** * Set the Push Service to be used. - * + * * @param $serviceName * @return $this */ - public function setService($serviceName){ + public function setService($serviceName) + { + if (!array_key_exists($serviceName, $this->servicesList)) { + $serviceName = $this->defaultServiceName; + } - if(!array_key_exists($serviceName,$this->servicesList)) $serviceName = $this->defaultServiceName; - $this->service = new $this->servicesList[$serviceName]; return $this; @@ -103,7 +105,9 @@ public function setDevicesToken($deviceTokens) public function setApiKey($api_key) { // if apn doesn't do anything - if(!$this->service instanceof Apn) $this->service->setApiKey($api_key) ; + if (!$this->service instanceof Apn) { + $this->service->setApiKey($api_key); + } return $this; } @@ -135,7 +139,7 @@ public function setUrl($url) /** *Get the unregistered tokens of the notification sent. - * + * * @return array $tokenUnRegistered */ public function getUnregisteredDeviceTokens() @@ -145,7 +149,7 @@ public function getUnregisteredDeviceTokens() /** * Give the Push Notification Feedback after sending a notification. - * + * * @return mixed */ public function getFeedback() @@ -158,12 +162,11 @@ public function getFeedback() * * @return $this */ - public function send(){ - - $this->service->send($this->deviceTokens,$this->message); + public function send() + { + $this->service->send($this->deviceTokens, $this->message); return $this; - } /** @@ -173,10 +176,11 @@ public function send(){ */ public function sendByTopic($topic, $isCondition = false) { - if($this->service instanceof Fcm) - $this->service->sendByTopic($topic, $this->message, $isCondition); + if ($this->service instanceof Fcm) { + $this->service->sendByTopic($topic, $this->message, $isCondition); + } - return $this; + return $this; } /** @@ -185,19 +189,16 @@ public function sendByTopic($topic, $isCondition = false) * @param $property * @return mixed / null */ - public function __get($property){ - - if(property_exists($this,$property)) - { + public function __get($property) + { + if (property_exists($this, $property)) { return $this->$property; } - if(property_exists($this->service,$property)) - { + if (property_exists($this->service, $property)) { return $this->service->$property; } return null; - } -} \ No newline at end of file +} diff --git a/src/PushService.php b/src/PushService.php index 0cfde04..c6c8ff5 100644 --- a/src/PushService.php +++ b/src/PushService.php @@ -1,7 +1,7 @@ config = array_replace($this->config,$config); + $this->config = array_replace($this->config, $config); } /** * Initialize the configuration for the chosen push service // gcm,etc.. - * Check if config_path exist as function - * + * * @param $service + * + * @throws PushNotificationException + * * @return mixed */ public function initializeConfig($service) { - if(function_exists('config_path')) - { - if(file_exists(config_path('pushnotification.php'))) - { - $configuration = include(config_path('pushnotification.php')); - return $configuration[$service]; - } + if (function_exists('config_path') && + file_exists(config_path('pushnotification.php')) && + function_exists('app') + ) { + $configuration = app('config')->get('pushnotification'); + } else { + $configuration = include(__DIR__ . '/Config/config.php'); } - $configuration = include(__DIR__ . '/Config/config.php'); - + if (!array_key_exists($service, $configuration)) { + throw new PushNotificationException("Service '$service' missed in config/pushnotification.php"); + } return $configuration[$service]; } @@ -94,8 +97,9 @@ protected function initializeFeedback() * @param $property * @return mixed|null */ - public function __get($property){ - return property_exists($this,$property) ? $this->$property : null; + public function __get($property) + { + return property_exists($this, $property) ? $this->$property : null; } } \ No newline at end of file diff --git a/tests/PushNotificationTest.php b/tests/PushNotificationTest.php index 9472d57..e44bcdd 100644 --- a/tests/PushNotificationTest.php +++ b/tests/PushNotificationTest.php @@ -10,7 +10,7 @@ public function push_notification_instance_creation_without_argument_set_gcm_as_ { $push = new PushNotification(); - $this->assertInstanceOf('Edujugon\PushNotification\Gcm',$push->service); + $this->assertInstanceOf('Edujugon\PushNotification\Gcm', $push->service); } /** @test */ @@ -25,7 +25,7 @@ public function assert_send_method_returns_an_stdClass_instance() $push = $push->send(); - $this->assertInstanceOf('stdClass',$push->getFeedback()); + $this->assertInstanceOf('stdClass', $push->getFeedback()); } /** @test */ @@ -56,47 +56,47 @@ public function assert_unregistered_device_tokens_is_an_array() ->setMessage(['message' =>'hello world']) ->send(); - $this->assertInternalType('array',$push->getUnregisteredDeviceTokens()); - + $this->assertInternalType('array', $push->getUnregisteredDeviceTokens()); } /** @test */ - public function set_and_get_service_config(){ + public function set_and_get_service_config() + { /** GCM */ $push = new PushNotification(); $push->setConfig(['time_to_live' => 3]); - $this->assertArrayHasKey('time_to_live',$push->config); - $this->assertArrayHasKey('priority',$push->config); //default key - $this->assertInternalType('array',$push->config); + $this->assertArrayHasKey('time_to_live', $push->config); + $this->assertArrayHasKey('priority', $push->config); //default key + $this->assertInternalType('array', $push->config); /** APNS */ $pushAPN = new PushNotification('apn'); $pushAPN->setConfig(['time_to_live' => 3]); - $this->assertArrayHasKey('time_to_live',$pushAPN->config); - $this->assertArrayHasKey('certificate',$pushAPN->config); //default key - $this->assertInternalType('array',$pushAPN->config); + $this->assertArrayHasKey('time_to_live', $pushAPN->config); + $this->assertArrayHasKey('certificate', $pushAPN->config); //default key + $this->assertInternalType('array', $pushAPN->config); } /** @test */ - public function set_message_data(){ - + public function set_message_data() + { $push = new PushNotification(); $push->setMessage(['message' =>'hello world']); - $this->assertArrayHasKey('message',$push->message); - - $this->assertEquals('hello world',$push->message['message']); + $this->assertArrayHasKey('message', $push->message); + $this->assertEquals('hello world', $push->message['message']); } /** @test */ - public function send_method_in_apn_service(){ + public function send_method_in_apn_service() + { $push = new PushNotification('apn'); $message = [ @@ -119,8 +119,8 @@ public function send_method_in_apn_service(){ $push = $push->send(); //var_dump($push->getFeedback()); - $this->assertInstanceOf('stdClass',$push->getFeedback()); - $this->assertInternalType('array',$push->getUnregisteredDeviceTokens()); + $this->assertInstanceOf('stdClass', $push->getFeedback()); + $this->assertInternalType('array', $push->getUnregisteredDeviceTokens()); } /** @test */ @@ -141,11 +141,11 @@ public function apn_dry_run_option_update_the_apn_url() $push->setConfig(['dry_run'=>false]); - $this->assertEquals('ssl://gateway.push.apple.com:2195',$push->url); + $this->assertEquals('ssl://gateway.push.apple.com:2195', $push->url); $push->setConfig(['dry_run'=>true]); - $this->assertEquals('ssl://gateway.sandbox.push.apple.com:2195',$push->url); + $this->assertEquals('ssl://gateway.sandbox.push.apple.com:2195', $push->url); } @@ -161,9 +161,8 @@ public function fcm_assert_send_method_returns_an_stdClass_instance() $push = $push->send(); - $this->assertEquals('https://fcm.googleapis.com/fcm/send',$push->url); - $this->assertInstanceOf('stdClass',$push->getFeedback()); - + $this->assertEquals('https://fcm.googleapis.com/fcm/send', $push->url); + $this->assertInstanceOf('stdClass', $push->getFeedback()); } /** @test */ @@ -171,17 +170,16 @@ public function if_push_service_as_argument_is_not_valid_user_gcm_as_default() { $push = new PushNotification('asdf'); - $this->assertInstanceOf('Edujugon\PushNotification\Gcm',$push->service); - - + $this->assertInstanceOf('Edujugon\PushNotification\Gcm', $push->service); } + /** @test */ public function get_available_push_service_list() { $push = new PushNotification(); - $this->assertCount(3,$push->servicesList); - $this->assertInternalType('array',$push->servicesList); + $this->assertCount(3, $push->servicesList); + $this->assertInternalType('array', $push->servicesList); } /** @test */ @@ -189,10 +187,10 @@ public function if_argument_in_set_service_method_does_not_exist_set_the_service $push = new PushNotification(); $push->setService('asdf')->send(); - $this->assertInstanceOf('Edujugon\PushNotification\Gcm',$push->service); + $this->assertInstanceOf('Edujugon\PushNotification\Gcm', $push->service); $push->setService('fcm'); - $this->assertInstanceOf('Edujugon\PushNotification\Fcm',$push->service); + $this->assertInstanceOf('Edujugon\PushNotification\Fcm', $push->service); } /** @test */ @@ -207,14 +205,14 @@ public function get_feedback_after_sending_a_notification() ->send() ->getFeedback(); - $this->assertInstanceOf('stdClass',$response); + $this->assertInstanceOf('stdClass', $response); } /** @test */ public function apn_feedback() { - $push = new PushNotification('apn'); + $message = [ 'aps' => [ 'alert' => [ @@ -232,9 +230,8 @@ public function apn_feedback() ]); $push->send(); - $this->assertInstanceOf('stdClass',$push->getFeedback()); - $this->assertInternalType('array',$push->getUnregisteredDeviceTokens()); - + $this->assertInstanceOf('stdClass', $push->getFeedback()); + $this->assertInternalType('array', $push->getUnregisteredDeviceTokens()); } @@ -249,7 +246,7 @@ public function allow_apikey_from_config_file() ->send() ->getFeedback(); - $this->assertInstanceOf('stdClass',$response); + $this->assertInstanceOf('stdClass', $response); } @@ -277,20 +274,21 @@ public function fake_unregisteredDevicesToken_with_apn_feedback_response_merged_ ] ] ]; - $merge = array_merge($primary,$array); - $obj = json_decode(json_encode($merge), FALSE); + $merge = array_merge($primary, $array); + $obj = json_decode(json_encode($merge)); $tokens = []; - if(! empty($obj->tokenFailList)) + if (! empty($obj->tokenFailList)) { $tokens = $obj->tokenFailList; - if(!empty($obj->apnsFeedback)) - $tokens = array_merge($tokens,array_pluck($obj->apnsFeedback,'devtoken')); + } + if (!empty($obj->apnsFeedback)) { + $tokens = array_merge($tokens, array_pluck($obj->apnsFeedback, 'devtoken')); + } //var_dump($tokens); } - /** @test */ public function send_a_notification_by_topic_in_fcm() { $push = new PushNotification('fcm'); @@ -301,7 +299,7 @@ public function send_a_notification_by_topic_in_fcm() ->sendByTopic('test') ->getFeedback(); - $this->assertInstanceOf('stdClass',$response); + $this->assertInstanceOf('stdClass', $response); } /** @test */ @@ -312,9 +310,76 @@ public function send_a_notification_by_condition_in_fcm() $response = $push->setMessage(['message'=>'Hello World']) ->setApiKey('asdfasdffasdfasdfasdf') ->setConfig(['dry_run' => false]) - ->sendByTopic("'dogs' in topics || 'cats' in topics",true) + ->sendByTopic("'dogs' in topics || 'cats' in topics", true) ->getFeedback(); - $this->assertInstanceOf('stdClass',$response); + $this->assertInstanceOf('stdClass', $response); + } + + /** @test */ + public function apn_connection_attempts_default() + { + $push = new PushNotification('apn'); + + $push->setConfig(['dry_run' => true]); + + $key = 'connection_attempts'; + $this->assertArrayNotHasKey($key, $push->config); } -} \ No newline at end of file + + /** @test */ + public function set_apn_connect_attempts_override_default() + { + $push = new PushNotification('apn'); + + $expected = 0; + $push->setConfig([ + 'dry_run' => true, + 'connection_attempts' => $expected, + ]); + + $key = 'connection_attempts'; + $this->assertArrayHasKey($key, $push->config); + $this->assertEquals($expected, $push->config[$key]); + } + + /** @test */ + public function apn_connect_attempts_bailout_badcert() + { + $push = new PushNotification('apn'); + + $tmp_name = tempnam(sys_get_temp_dir(), 'apn-tmp'); + $fh = fopen($tmp_name, 'w'); + fwrite($fh, 'badcert'); + fclose($fh); + + $expected = 0; + + // ZZZ: intentional failure use-case so let's not + // waste time attemping to push with a bad cert. + $push->setConfig([ + 'dry_run' => true, + 'connection_attempts' => 1, + 'certificate' => $tmp_name, + ]); + + $message = [ + 'aps' => [ + 'alert' => [ + 'title' => '1 Notification test', + 'body' => 'Just for testing purposes' + ], + 'sound' => 'default' + ] + ]; + + $push->setMessage($message) + ->setDevicesToken(['507e3adaf433ae3e6234f35c82f8a43ad0d84218bff08f16ea7be0869f066c0312']); + + $push = $push->send(); + $this->assertInstanceOf('stdClass', $push->getFeedback()); + + unlink($tmp_name); + } + +}