From ac464b247e60a042806c624c6ee5c69bc1cd38e5 Mon Sep 17 00:00:00 2001 From: Michael Babker Date: Sat, 21 Mar 2020 16:04:17 -0500 Subject: [PATCH] Add notification support --- CHANGELOG.md | 4 +- README.md | 90 ++++++++++- composer.json | 3 + config/twilio.php | 15 +- src/Notifications/Channels/TwilioChannel.php | 53 +++++++ src/Providers/TwilioProvider.php | 32 ++++ .../Channels/TwilioChannelTest.php | 142 ++++++++++++++++++ tests/Providers/TwilioProviderTest.php | 16 ++ tests/TwilioClientTest.php | 2 +- 9 files changed, 350 insertions(+), 7 deletions(-) create mode 100644 src/Notifications/Channels/TwilioChannel.php create mode 100644 tests/Notifications/Channels/TwilioChannelTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d0fa96..c8b4970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ # Changelog -## 1.0.1 (20??-??-??) +## 1.1.0 (20??-??-??) + +- Add a notification channel to send SMS messages diff --git a/README.md b/README.md index 305b18e..5113e2b 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,22 @@ Likewise, you will also need to register the facade in your `config/app.php` fil 'TwilioClient' => BabDev\Twilio\Facades\TwilioClient::class, ``` +## Configuration + +This package can be configured using environment variables in your `.env` file for its basic usage. For advanced use cases, you can publish this package's configuration with this command: + +```sh +php artisan vendor:publish --provider="BabDev\Twilio\Providers\TwilioProvider" --tag="config" +``` + +The below environment variables should be set: + +- `TWILIO_CONNECTION`: The name of the default Twilio API connection for your application; if using a single connection this does not need to be changed +- `TWILIO_NOTIFICATION_CHANNEL_CONNECTION`: If using Laravel's notifications system, the name of a Twilio API connection to use in the notification channel (defaulting to your default connection); if using a single connection this does not need to be changed +- `TWILIO_API_SID`: The Twilio API SID to use for the default Twilio API connection +- `TWILIO_API_AUTH_TOKEN`: The Twilio API authentication token to use for the default Twilio API connection +- `TWILIO_API_FROM_NUMBER`: The default sending phone number to use for the default Twilio API connection, note the sending phone number can be changed on a per-message basis + ## Usage Using the `ConnectionManager`, you can quickly create calls and send SMS messages through Twilio's REST API using their PHP SDK. By default, the `ConnectionManager` fulfills the `TwilioClient` contract and can be used by injecting the service into your class/method, retrieving the service from the container, or using the `TwilioClient` facade. @@ -73,14 +89,78 @@ final class MessagingController } ``` -## Multiple Connections +## Notifications -If your application uses multiple sets of REST API credentials for the Twilio API, you can add the data for multiple connections to the package's configuration. First, you should publish this package's configuration: +Messages can be sent as part of Laravel's [notifications system](https://laravel.com/docs/notifications). A notifiable (such as a User model) should include the "twilio" channel in its `via()` method. When routing the notification, the phone number the message should be sent to should be returned. -```sh -php artisan vendor:publish --provider="BabDev\Twilio\Providers\TwilioProvider" --tag="config" +```php +namespace App\Models; + +use Illuminate\Foundation\Auth\User as Authenticatable; +use Illuminate\Notifications\Notifiable; +use Illuminate\Notifications\Notification; + +class User extends Authenticatable +{ + use Notifiable; + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * + * @return array + */ + public function via($notifiable) + { + // This application's users can receive notifications by mail and Twilio SMS + return ['mail', 'twilio']; + } + + /** + * Get the notification routing information for the Twilio driver. + * + * @param Notification $notification + * + * @return string + */ + public function routeNotificationForTwilio($notification) + { + return $this->mobile_number; + } +} +``` + +For notifications that support being sent as an SMS, you should define a `toTwilio` method on the notification class. This method will receive a $notifiable entity and should return a string containing the message text. + +```php +namespace App\Notifications; + +use Illuminate\Notifications\Notification; + +final class PasswordExpiredNotification extends Notification +{ + /** + * Get the Twilio / SMS representation of the notification. + * + * @param mixed $notifiable + * + * @return string + */ + public function toTwilio($notifiable) + { + // The $notifiable in this example is your User model + return sprintf('Hello %s, this is a note that the password for your %s account has expired.', $notifiable->name, config('app.name')); + } +} ``` +Your default connection is used for the notification channel by default. If your application utilizes multiple Twilio API connections, you can set the connection which should be used using the `TWILIO_NOTIFICATION_CHANNEL_CONNECTION` environment variable. + +## Multiple Connections + +If your application uses multiple sets of REST API credentials for the Twilio API, you can add the data for multiple connections to the package's configuration. If you have not already, you will need to publish this package's configuration. + Then, in your newly created `config/twilio.php` file, you can add new connections to the `connections` array. Note, the default "twilio" connection has been created for you and uses environment variables by default. You are free to change the default connection for your application and the name of the "twilio" connection if desired. ```php @@ -89,6 +169,8 @@ Then, in your newly created `config/twilio.php` file, you can add new connection return [ 'default' => env('TWILIO_CONNECTION', 'twilio'), + 'notification_channel' => env('TWILIO_NOTIFICATION_CHANNEL_CONNECTION', env('TWILIO_CONNECTION', 'twilio')), + 'connections' => [ 'twilio' => [ 'sid' => env('TWILIO_API_SID', ''), diff --git a/composer.json b/composer.json index 2a1e3eb..51ce121 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,9 @@ "orchestra/testbench": "^4.0|^5.0", "phpunit/phpunit": "^8.0|^9.0" }, + "suggest": { + "illuminate/notifications": "To use Laravel's notifications component with this package" + }, "autoload": { "psr-4": { "BabDev\\Twilio\\": "src/" diff --git a/config/twilio.php b/config/twilio.php index 075b713..a6368fb 100644 --- a/config/twilio.php +++ b/config/twilio.php @@ -15,6 +15,19 @@ 'default' => env('TWILIO_CONNECTION', 'twilio'), + /* + |-------------------------------------------------------------------------- + | Notification Channel Connection Name + |-------------------------------------------------------------------------- + | + | Here you may specify which of the Twilio connections below you wish + | to use as the connection when using Laravel's notifications system. + | By default, this uses your default connection. + | + */ + + 'notification_channel' => env('TWILIO_NOTIFICATION_CHANNEL_CONNECTION', env('TWILIO_CONNECTION', 'twilio')), + /* |-------------------------------------------------------------------------- | Twilio Connections @@ -25,7 +38,7 @@ | all of which are required parameters for creating a connection to the | Twilio REST API. | - | To create a new connection, duplicate the "default" connection + | To create a new connection, duplicate the "twilio" connection | configuration as a new entry in your connections array and give | it a unique name. You can now access this connection through the | connection manager. diff --git a/src/Notifications/Channels/TwilioChannel.php b/src/Notifications/Channels/TwilioChannel.php new file mode 100644 index 0000000..81fb2a8 --- /dev/null +++ b/src/Notifications/Channels/TwilioChannel.php @@ -0,0 +1,53 @@ +twilio = $twilio; + } + + /** + * Send the given notification. + * + * @param mixed $notifiable + * @param Notification $notification + * + * @return MessageInstance|null + * + * @throws TwilioException on Twilio API failure + */ + public function send($notifiable, Notification $notification): ?MessageInstance + { + $to = $notifiable->routeNotificationFor('twilio', $notification); + + if (!$to) { + return null; + } + + $message = $notification->toTwilio($notifiable); + + if (!$message) { + return null; + } + + return $this->twilio->message($to, $message); + } +} diff --git a/src/Providers/TwilioProvider.php b/src/Providers/TwilioProvider.php index b05d72e..431b7b2 100644 --- a/src/Providers/TwilioProvider.php +++ b/src/Providers/TwilioProvider.php @@ -4,11 +4,15 @@ use BabDev\Twilio\ConnectionManager; use BabDev\Twilio\Contracts\TwilioClient; +use BabDev\Twilio\Notifications\Channels\TwilioChannel; use BabDev\Twilio\Twilio\Http\LaravelHttpClient; use GuzzleHttp\Client as Guzzle; +use Illuminate\Contracts\Config\Repository; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Support\DeferrableProvider; use Illuminate\Http\Client\Factory; +use Illuminate\Notifications\ChannelManager; +use Illuminate\Support\Facades\Notification; use Illuminate\Support\ServiceProvider; use Twilio\Http\Client as TwilioHttpClient; use Twilio\Http\CurlClient; @@ -55,6 +59,7 @@ public function register(): void { $this->registerConnectionManager(); $this->registerHttpClient(); + $this->registerNotificationChannel(); $this->mergeConfigFrom(__DIR__ . '/../../config/twilio.php', 'twilio'); } @@ -101,4 +106,31 @@ static function (Application $app): TwilioHttpClient { } ); } + + /** + * Registers the binding for the notification channel. + * + * @return void + */ + private function registerNotificationChannel(): void + { + Notification::resolved(static function (ChannelManager $manager): void { + $manager->extend( + 'twilio', + static function (Application $app): TwilioChannel { + /** @var Repository $config */ + $config = $app->make('config'); + + /** @var ConnectionManager $manager */ + $manager = $app->make(ConnectionManager::class); + + return new TwilioChannel( + $manager->connection( + $config->get('twilio.notification_channel', $config->get('twilio.default', 'twilio')) + ) + ); + } + ); + }); + } } diff --git a/tests/Notifications/Channels/TwilioChannelTest.php b/tests/Notifications/Channels/TwilioChannelTest.php new file mode 100644 index 0000000..1d9bca3 --- /dev/null +++ b/tests/Notifications/Channels/TwilioChannelTest.php @@ -0,0 +1,142 @@ +createMock(TwilioClient::class); + $twilio->expects($this->once()) + ->method('message') + ->willReturn($this->createMock(MessageInstance::class)); + + $notifiable = new class() + { + use Notifiable; + + public function via($notifiable) + { + return ['twilio']; + } + + public function routeNotificationForTwilio($notification) + { + return '+19418675309'; + } + }; + + $notification = new class() extends Notification + { + public function toTwilio($notifiable) + { + return 'This is a test'; + } + }; + + $this->assertInstanceOf(MessageInstance::class, (new TwilioChannel($twilio))->send($notifiable, $notification)); + } + + public function testANotificationIsNotSentWhenTheNotifiableDoesNotRouteToIt(): void + { + /** @var TwilioClient|MockObject $twilio */ + $twilio = $this->createMock(TwilioClient::class); + $twilio->expects($this->never()) + ->method('message'); + + $notifiable = new class() + { + use Notifiable; + + public function via($notifiable) + { + return []; + } + }; + + $notification = new class() extends Notification + { + public function toTwilio($notifiable) + { + return 'This is a test'; + } + }; + + $this->assertNull((new TwilioChannel($twilio))->send($notifiable, $notification)); + } + + public function testANotificationIsNotSentWhenTheNotifiableDoesNotProvideARecipient(): void + { + /** @var TwilioClient|MockObject $twilio */ + $twilio = $this->createMock(TwilioClient::class); + $twilio->expects($this->never()) + ->method('message'); + + $notifiable = new class() + { + use Notifiable; + + public function via($notifiable) + { + return ['twilio']; + } + + public function routeNotificationForTwilio($notification) + { + return null; + } + }; + + $notification = new class() extends Notification + { + public function toTwilio($notifiable) + { + return 'This is a test'; + } + }; + + $this->assertNull((new TwilioChannel($twilio))->send($notifiable, $notification)); + } + + public function testANotificationIsNotSentWhenTheNotificationDoesNotProvideAMessage(): void + { + /** @var TwilioClient|MockObject $twilio */ + $twilio = $this->createMock(TwilioClient::class); + $twilio->expects($this->never()) + ->method('message'); + + $notifiable = new class() + { + use Notifiable; + + public function via($notifiable) + { + return ['twilio']; + } + + public function routeNotificationForTwilio($notification) + { + return '+19418675309'; + } + }; + + $notification = new class() extends Notification + { + public function toTwilio($notifiable) + { + return null; + } + }; + + $this->assertNull((new TwilioChannel($twilio))->send($notifiable, $notification)); + } +} diff --git a/tests/Providers/TwilioProviderTest.php b/tests/Providers/TwilioProviderTest.php index 811be01..f53a77d 100644 --- a/tests/Providers/TwilioProviderTest.php +++ b/tests/Providers/TwilioProviderTest.php @@ -4,7 +4,9 @@ use BabDev\Twilio\ConnectionManager; use BabDev\Twilio\Contracts\TwilioClient; +use BabDev\Twilio\Notifications\Channels\TwilioChannel; use BabDev\Twilio\Providers\TwilioProvider; +use Illuminate\Notifications\ChannelManager; use Illuminate\Support\ServiceProvider; use Orchestra\Testbench\TestCase; @@ -23,6 +25,20 @@ public function testServicesAreRegistered(): void { $this->assertTrue($this->app->bound(ConnectionManager::class)); $this->assertSame(ConnectionManager::class, $this->app->getAlias(TwilioClient::class)); + $this->assertInstanceOf(TwilioChannel::class, $this->app->get(ChannelManager::class)->driver('twilio')); + } + + protected function getEnvironmentSetUp($app) + { + // Setup connections configuration + $app['config']->set( + 'twilio.connections.twilio', + [ + 'sid' => 'api_sid', + 'token' => 'api_token', + 'from' => '+15558675309', + ] + ); } protected function getPackageProviders($app) diff --git a/tests/TwilioClientTest.php b/tests/TwilioClientTest.php index 9b8f127..ef43341 100644 --- a/tests/TwilioClientTest.php +++ b/tests/TwilioClientTest.php @@ -11,7 +11,7 @@ use Twilio\Rest\Api\V2010\Account\MessageList; use Twilio\Rest\Client; -final class RestClientTest extends TestCase +final class TwilioClientTest extends TestCase { public function testTheSdkInstanceCanBeRetrieved(): void {