diff --git a/CHANGELOG.md b/CHANGELOG.md index 112c311..9ce44fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added - Coverage information +- Support for Laravel's notification channels ### Fixed - Require correct configuration of package by removing placeholder data diff --git a/README.md b/README.md index 52ba928..dcd05ae 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,15 @@ This command copies the configuration file to `config/pushwoosh.php`. Make sure you set up the `api_token` and `application_id` of the default application. + +## Usage + +You can use the wrapper to interact with the Pushwoosh SDK directly, or, you could add support for Pushwoosh to your +[Laravel notifications](https://laravel.com/docs/notifications). + +### Laravel Notifications + +If you are using Laravel's notification system, you can add `'pushwoosh'` to the `via()` response of a notification. +The channel name is also available as a class constant on the `Contextmapp\Pushwoosh\PushwooshChannel` class. +The notification class is expected to implement the `Contextmapp\Pushwoosh\Contracts\PushwooshNotification` contract. +Your notifiable classes should implement the `Contextmapp\Pushwoosh\Contracts\PushwooshNotifiable` contract. diff --git a/composer.json b/composer.json index a350664..127a74a 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,10 @@ "description": "Laravel package for integrating Pushwoosh", "type": "library", "license": "MIT", - "keywords": ["pushwoosh", "laravel"], + "keywords": [ + "pushwoosh", + "laravel" + ], "authors": [ { "name": "Raymond Jelierse", @@ -12,8 +15,9 @@ ], "require": { "php": "^7.0", - "illuminate/support": "5.1.* || 5.2.* || 5.3.* || 5.4.* || 5.5.*", - "gomoob/php-pushwoosh": "^1.0" + "gomoob/php-pushwoosh": "^1.0", + "illuminate/notifications": "5.3 - 5.6", + "illuminate/support": "5.1 - 5.6" }, "require-dev": { "phpunit/phpunit": "~6.0", @@ -29,6 +33,9 @@ "Contextmapp\\Pushwoosh\\Tests\\": "tests/" } }, + "suggest": { + "laravel-notification-channels/backport": "Required when using Laravel 5.1 or 5.2, drop-in replacement for 'illuminate/notifications'." + }, "extra": { "laravel": { "providers": [ @@ -38,5 +45,8 @@ "Pushwoosh": "Contextmapp\\Pushwoosh\\Facades\\Pushwoosh" } } + }, + "config": { + "sort-packages": true } } diff --git a/src/Contracts/PushwooshNotifiable.php b/src/Contracts/PushwooshNotifiable.php new file mode 100644 index 0000000..19b919c --- /dev/null +++ b/src/Contracts/PushwooshNotifiable.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Contextmapp\Pushwoosh\Contracts; + +use Gomoob\Pushwoosh\Model\Condition\ICondition; + +/** + * Implement this interface on the classes that include the {@link \Illuminate\Notifications\Notifiable} trait + * and should have routing for Pushwoosh notifications. + */ +interface PushwooshNotifiable +{ + /** + * Set the Pushwoosh conditions that target the intended user. + * + * @return ICondition[] + */ + public function routeNotificationForPushwoosh(): array; +} diff --git a/src/Contracts/PushwooshNotification.php b/src/Contracts/PushwooshNotification.php new file mode 100644 index 0000000..d86c688 --- /dev/null +++ b/src/Contracts/PushwooshNotification.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Contextmapp\Pushwoosh\Contracts; + +use Contextmapp\Pushwoosh\PushwooshMessage; + +/** + * Implement this interface on notifications that should pass through the Pushwoosh channel. + */ +interface PushwooshNotification +{ + /** + * Create the payload for the notification when routed through Pushwoosh. + * + * @param mixed $notifiable + * + * @return PushwooshMessage + */ + public function toPushwoosh($notifiable): PushwooshMessage; +} diff --git a/src/Exceptions/NotificationFailedException.php b/src/Exceptions/NotificationFailedException.php new file mode 100644 index 0000000..ca18dbb --- /dev/null +++ b/src/Exceptions/NotificationFailedException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Contextmapp\Pushwoosh\Exceptions; + +class NotificationFailedException extends \RuntimeException +{ +} diff --git a/src/PushwooshChannel.php b/src/PushwooshChannel.php new file mode 100644 index 0000000..f448d0c --- /dev/null +++ b/src/PushwooshChannel.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Contextmapp\Pushwoosh; + +use Contextmapp\Pushwoosh\Contracts\PushwooshNotifiable; +use Contextmapp\Pushwoosh\Contracts\PushwooshNotification; +use Contextmapp\Pushwoosh\Exceptions\NotificationFailedException; +use Gomoob\Pushwoosh\Exception\PushwooshException; +use Gomoob\Pushwoosh\Model\Notification\Android; +use Gomoob\Pushwoosh\Model\Notification\IOS; +use Gomoob\Pushwoosh\Model\Request\CreateMessageRequest; +use Illuminate\Notifications\Notification; + +/** + * Channel for dispatching notifications through Pushwoosh. + */ +class PushwooshChannel +{ + const NAME = 'pushwoosh'; + + private $manager; + + /** + * PushwooshChannel constructor. + * + * @param PushwooshManager $manager + */ + public function __construct(PushwooshManager $manager) + { + $this->manager = $manager; + } + + /** + * Send the notification through the Pushwoosh notification channel. + * + * @param mixed $notifiable + * @param \Illuminate\Notifications\Notification $notification + * + * @return void + */ + public function send($notifiable, Notification $notification) + { + if ((!$notifiable instanceof PushwooshNotifiable) || (!$notification instanceof PushwooshNotification)) { + return; + } + + $message = $notification->toPushwoosh($notifiable); + $request = $this->buildRequest($notifiable, $message); + + try { + $response = $this->manager->application($message->application)->createMessage($request); + } catch (PushwooshException $e) { + throw new NotificationFailedException('Failed to send notification to Pushwoosh', 0, $e); + } + + if (false === $response->isOk()) { + throw new NotificationFailedException('Failed to send notification to Pushwoosh', $response->getStatusCode()); + } + } + + /** + * Build the request to send to Pushwoosh. + * + * @param \Contextmapp\Pushwoosh\Contracts\PushwooshNotifiable $notifiable + * @param \Contextmapp\Pushwoosh\PushwooshMessage $message + * + * @return \Gomoob\Pushwoosh\Model\Request\CreateMessageRequest + */ + private function buildRequest(PushwooshNotifiable $notifiable, PushwooshMessage $message): CreateMessageRequest + { + $android = new Android(); + $ios = new IOS(); + + $notification = new \Gomoob\Pushwoosh\Model\Notification\Notification(); + $notification->setContent($message->content); + $notification->setData($message->data); + $notification->setAndroid($android); + $notification->setIOS($ios); + + if ($message->subject) { + $android->setHeader($message->subject); + } + + if ($message->increaseBadgeNumber) { + $android->setBadges('+1'); + $ios->setBadges('+1'); + } + + $notification->setConditions($notifiable->routeNotificationForPushwoosh()); + + $request = new CreateMessageRequest(); + $request->addNotification($notification); + + return $request; + } +} diff --git a/src/PushwooshMessage.php b/src/PushwooshMessage.php new file mode 100644 index 0000000..15744a7 --- /dev/null +++ b/src/PushwooshMessage.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Contextmapp\Pushwoosh; + +/** + * Message information for Pushwoosh notifications. + */ +class PushwooshMessage +{ + /** + * The Pushwoosh application to target with this message. + * + * @var string|null + */ + public $application; + + /** + * @var string|null + */ + public $subject; + + /** + * The content of the notification. + * + * @var string + */ + public $content; + + /** + * Should the application icon badge number on the recipient's device be increased? + * + * @var bool + */ + public $increaseBadgeNumber = true; + + /** + * Extra data to send along with the notification. + * + * @var array + */ + public $data = []; + + public function __construct(string $message = '') + { + $this->content = $message; + } + + public function message(string $message) + { + $this->content = $message; + + return $this; + } + + public function subject(string $subject) + { + $this->subject = $subject; + + return $this; + } + + public function application(string $application) + { + $this->application = $application; + + return $this; + } + + public function increaseBadgeNumber(bool $increaseBadgeNumber) + { + $this->increaseBadgeNumber = $increaseBadgeNumber; + + return $this; + } + + public function data(array $data) + { + $this->data = $data; + + return $this; + } +} diff --git a/src/PushwooshServiceProvider.php b/src/PushwooshServiceProvider.php index 83719b4..9b95ab7 100644 --- a/src/PushwooshServiceProvider.php +++ b/src/PushwooshServiceProvider.php @@ -12,6 +12,8 @@ namespace Contextmapp\Pushwoosh; use Gomoob\Pushwoosh\Client\Pushwoosh; +use Illuminate\Contracts\Foundation\Application; +use Illuminate\Notifications\ChannelManager; use Illuminate\Support\ServiceProvider; /** @@ -24,13 +26,19 @@ class PushwooshServiceProvider extends ServiceProvider /** * Perform post-registration booting of services. * + * @param \Illuminate\Notifications\ChannelManager + * * @return void */ - public function boot() + public function boot(ChannelManager $manager) { $this->publishes([ __DIR__.'/../config/pushwoosh.php' => config_path('pushwoosh.php'), ]); + + $manager->extend(PushwooshChannel::NAME, function (Application $app) { + return $app->make(PushwooshChannel::class); + }); } /** @@ -42,14 +50,11 @@ public function register() { $this->mergeConfigFrom(__DIR__.'/../config/pushwoosh.php', 'pushwoosh'); - $this->app->singleton(PushwooshManager::class, function ($app) { + $this->app->singleton(PushwooshFactory::class); + $this->app->singleton(PushwooshChannel::class); + $this->app->singleton(PushwooshManager::class, function (Application $app) { return new PushwooshManager($app['config']['pushwoosh'], $app[PushwooshFactory::class]); }); - - $this->app->singleton(PushwooshFactory::class, function () { - return new PushwooshFactory(); - }); - $this->app->singleton(Pushwoosh::class, function ($app) { return $app[PushwooshManager::class]->application(); }); @@ -61,8 +66,9 @@ public function register() public function provides() { return [ - PushwooshManager::class, + PushwooshChannel::class, PushwooshFactory::class, + PushwooshManager::class, Pushwoosh::class, ]; } diff --git a/tests/PushwooshChannelTest.php b/tests/PushwooshChannelTest.php new file mode 100644 index 0000000..d0488bd --- /dev/null +++ b/tests/PushwooshChannelTest.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Contextmapp\Pushwoosh\Tests; + +use Contextmapp\Pushwoosh\Contracts\PushwooshNotifiable; +use Contextmapp\Pushwoosh\Contracts\PushwooshNotification; +use Contextmapp\Pushwoosh\Exceptions\NotificationFailedException; +use Contextmapp\Pushwoosh\PushwooshChannel; +use Contextmapp\Pushwoosh\PushwooshManager; +use Contextmapp\Pushwoosh\PushwooshMessage; +use Gomoob\Pushwoosh\Client\Pushwoosh; +use Gomoob\Pushwoosh\Exception\PushwooshException; +use Gomoob\Pushwoosh\Model\Condition\IntCondition; +use Gomoob\Pushwoosh\Model\IResponse; +use Gomoob\Pushwoosh\Model\IRequest; +use Illuminate\Notifications\Notification; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class PushwooshChannelTest extends TestCase +{ + public function testSend() + { + $notifiable = $this->buildPushwooshNotifiable(); + $notification = $this->buildPushwooshNotification($notifiable); + $manager = $this->buildPushwooshManager(false, false); + + $channel = new PushwooshChannel($manager); + $channel->send($notifiable, $notification); + } + + public function testSendError() + { + $notifiable = $this->buildPushwooshNotifiable(); + $notification = $this->buildPushwooshNotification($notifiable); + $manager = $this->buildPushwooshManager(true, false); + + $this->expectException(NotificationFailedException::class); + + $channel = new PushwooshChannel($manager); + $channel->send($notifiable, $notification); + } + + public function testSendFails() + { + $notifiable = $this->buildPushwooshNotifiable(); + $notification = $this->buildPushwooshNotification($notifiable); + $manager = $this->buildPushwooshManager(false, true); + + $this->expectException(NotificationFailedException::class); + + $channel = new PushwooshChannel($manager); + $channel->send($notifiable, $notification); + } + + private function buildPushwooshManager(bool $throws, bool $fails): MockObject + { + $response = $this->createMock(IResponse::class); + $response->expects($throws ? $this->never() : $this->once())->method('isOk')->willReturn(!$fails); + $response->expects($throws || !$fails ? $this->never() : $this->once())->method('getStatusCode')->willReturn(500); + + $pushwoosh = $this->createMock(Pushwoosh::class); + $method = $pushwoosh->expects($this->once())->method('createMessage')->with($this->isInstanceOf(IRequest::class)); + if ($throws) { + $method->willThrowException(new PushwooshException); + } else { + $method->willReturn($response); + } + + $manager = $this->createMock(PushwooshManager::class); + $manager->expects($this->once())->method('application')->with(null)->willReturn($pushwoosh); + + return $manager; + } + + private function buildPushwooshNotifiable(): MockObject + { + $conditions = [IntCondition::create('userId')->eq('12')]; + + $notifiable = $this->createMock(PushwooshNotifiable::class); + $notifiable->expects($this->once())->method('routeNotificationForPushwoosh')->willReturn($conditions); + + return $notifiable; + } + + private function buildPushwooshNotification($notifiable): PushwooshNotification + { + $notification = $this->prophesize(Notification::class) + ->willImplement(PushwooshNotification::class); + $notification->toPushwoosh($notifiable)->shouldBeCalledTimes(1)->willReturn(new PushwooshMessage()); + + return $notification->reveal(); + } +}