diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 75c1ae4..ff4d74f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,13 +10,19 @@ jobs: illuminate-version: - 9 - 10 + - 11 php-version: - 8.0 - 8.1 - 8.2 + - 8.3 exclude: - illuminate-version: 10 php-version: 8.0 + - illuminate-version: 11 + php-version: 8.0 + - illuminate-version: 11 + php-version: 8.1 runs-on: ubuntu-latest @@ -42,7 +48,7 @@ jobs: illuminate/support:^${{ matrix.illuminate-version }} - name: Lint Code - run: vendor/bin/tlint + run: composer lint - name: Run tests - run: vendor/bin/phpunit + run: composer test diff --git a/.gitignore b/.gitignore index 4a3c8a0..e4edbce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /.envrc /.idea/ +/.phpunit.cache/ /.phpunit.result.cache /composer.lock /vendor diff --git a/.phpactor.json b/.phpactor.json new file mode 100644 index 0000000..4d42bbb --- /dev/null +++ b/.phpactor.json @@ -0,0 +1,4 @@ +{ + "$schema": "/phpactor.schema.json", + "language_server_phpstan.enabled": true +} \ No newline at end of file diff --git a/README.md b/README.md index 8b6eea3..6c5d784 100644 --- a/README.md +++ b/README.md @@ -1 +1,150 @@ # Service Bus Notifications Channel + +This is a Laravel package that provides notification channels for +sending notifications to the _RingierSA Service Bus_. + +## Installation + +Install the package into your project via composer: + +```bash +composer require ringiersa/service-bus-notifications-channel +``` + +## Configuration + +Add the following to `config/services.php` file: + +```php +'service_bus' => [ + 'enabled' => env('SERVICE_BUS_ENABLED', true), + 'from' => env('SERVICE_BUS_FROM'), + 'username' => env('SERVICE_BUS_USERNAME'), + 'password' => env('SERVICE_BUS_PASSWORD'), + 'version' => env('SERVICE_BUS_VERSION', '2.0.0'), + 'endpoint' => env('SERVICE_BUS_ENDPOINT', 'https://bus.staging.ritdu.tech/v1/'), +], +``` + +Add the following to the `.env` file: + +```dotenv +SERVICE_BUS_ENABLED=true +SERVICE_BUS_FROM=bus-node-id +SERVICE_BUS_USERNAME=bus-username +SERVICE_BUS_PASSWORD=bus-password +SERVICE_BUS_VERSION=2.0.0 +SERVICE_BUS_ENDPOINT=https://bus.staging.ritdu.tech/v1/ +``` + +You can get the `bus-node-id`, `bus-username` and `bus-password` from the _RingierSA_ Service Bus team. + +## usage + +Add something like the following example to a notification class: + +```php +use App\Models\Article; +use Illuminate\Notifications\Notifiable; +use Illuminate\Notifications\Notification; +use Illuminate\Queue\SerializesModels; +use RingierSA\ServiceBusNotificationsChannel\ServiceBusChannel; +use RingierSA\ServiceBusNotificationsChannel\ServiceBusEvent; + +class ArticleCreatedNotification extends Notification +{ + use SerializesModels; + + public function __construct(protected Article $article) + { + // + } + + public function toServiceBus(Notifiable $notifiable): ServiceBusEvent + { + return ServiceBusEvent::create('ArticleCreated') + ->withAction('user', $this->article->user_id) + ->withCulture('en') + ->withReference(uniqid()) + ->withPayload([ + 'article' => $this->article->toServiceBus(), + ]); + } + + public function via($notifiable) + { + return [ServiceBusChannel::class]; + } +} +``` + +Then use an anonymous notifiable to send the notification: + +```php +use Illuminate\Notifications\AnonymousNotifiable; +use Illuminate\Support\Facades\Notification; + +$article = Article::create([ + 'title' => 'My Article', + 'body' => 'This is my article content', + 'user_id' => 1, + // ... +]); + +(new AnonymousNotifiable)->notify(new MyNotification($article)); +``` + +That will use the `ServiceBusChannel` to send the notification to the _RingierSA_ Service Bus. + +## sqs usage + +The API endpoint is rate limited, so it's not suitable for high volume notifications. + +For high volume notifications, you can send directly to an `SQS` queue in the service bus. + +This removes the need to queue it in your app, and provides a more reliable way to send high volume notifications. + +Add the following to your config: + +```php +'service_bus' => [ + '...', + 'sqs' => [ + 'region' => env('SERVICE_BUS_SQS_REGION', 'eu-west-1'), + 'queue_url' => env('SERVICE_BUS_SQS_QUEUE_URL'), + 'key' => env('SERVICE_BUS_SQS_KEY'), + 'secret' => env('SERVICE_BUS_SQS_SECRET'), + ], +], +``` + +Also add the following to the `.env` file: + +```dotenv +SERVICE_BUS_SQS_REGION=eu-west-1 +SERVICE_BUS_SQS_QUEUE_URL=queue-url +SERVICE_BUS_SQS_KEY=key +SERVICE_BUS_SQS_SECRET=secret +``` + +The values for `queue-url`, `key` and `secret` can be obtained from the _RingierSA_ Service Bus team. + +The next change is to send the service bus notifications via the `ServiceBusSQSChannel` by changing the notification class: + +```php +use RingierSA\ServiceBusNotificationsChannel\ServiceBusSQSChannel; + +class ArticleCreatedNotification extends Notification +{ + // Everything else is the same as before + + public function via($notifiable) + { + return [ServiceBusSQSChannel::class]; + } +} +``` + +Now the notification will be sent directly to an `SQS` queue in the service bus, instead of via the API endpoint. + +We recommend you do not queue the notification. Send it `afterResponse`, the time to send the notification to `SQS` is minimal. diff --git a/composer.json b/composer.json index 99bbcf2..5038bbd 100644 --- a/composer.json +++ b/composer.json @@ -1,61 +1,63 @@ { - "name": "ringierimu/service-bus-notifications-channel", - "description": "Service Bus Notifications Channel", - "homepage": "https://github.com/RingierIMU/service-bus-notifications-channel", - "license": "MIT", - "authors": [ - { - "name": "RIMU Core", - "email": "tools@roam.africa", - "homepage": "http://ringier.tech", - "role": "Developer" - } + "name": "ringierimu/service-bus-notifications-channel", + "description": "Service Bus Notifications Channel", + "homepage": "https://github.com/RingierIMU/service-bus-notifications-channel", + "license": "MIT", + "authors": [ + { + "name": "RIMU Core", + "email": "tools@roam.africa", + "homepage": "http://ringier.tech", + "role": "Developer" + } + ], + "require": { + "php": "^8", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "illuminate/notifications": "^9 || ^10 || ^11", + "illuminate/support": "^9 || ^10 || ^11", + "guzzlehttp/guzzle": "^7", + "guzzlehttp/promises": "^2", + "guzzlehttp/psr7": "^1 || ^2" + }, + "require-dev": { + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "mockery/mockery": "^1", + "phpunit/phpunit": "^9 || ^10 || ^11", + "tightenco/tlint": "^8 || ^9", + "orchestra/testbench": "^7 || ^8 || ^9", + "nunomaduro/collision": "^6 || ^7 || ^8" + }, + "extra": { + "include_files": [ + "tests/Fixtures/Helpers.php" ], - "require": { - "php": "^8", - "ext-json": "*", - "ext-pcre": "*", - "ext-simplexml": "*", - "guzzlehttp/guzzle": "^7.2", - "guzzlehttp/promises": "^2", - "guzzlehttp/psr7": "^1 || ^2", - "illuminate/notifications": "^9 || ^10", - "illuminate/support": "^9 || ^10", - "ramsey/uuid": "^4" - }, - "require-dev": { - "ext-dom": "*", - "ext-openssl": "*", - "ext-pcntl": "*", - "ext-sockets": "*", - "mockery/mockery": "^1", - "phpunit/phpunit": "^9.5", - "tightenco/tlint": "^8 || ^9" - }, - "extra": { - "include_files": [ - "tests/Fixtures/Helpers.php" - ], - "laravel": { - "providers": [ - "Ringierimu\\ServiceBusNotificationsChannel\\ServiceBusServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Ringierimu\\ServiceBusNotificationsChannel\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "Ringierimu\\ServiceBusNotificationsChannel\\Tests\\": "tests/" - }, - "files": [ - "tests/helpers.php" - ] - }, - "scripts": { - "test": "vendor/bin/phpunit" + "laravel": { + "providers": [ + "Ringierimu\\ServiceBusNotificationsChannel\\ServiceBusServiceProvider" + ] } + }, + "autoload": { + "psr-4": { + "Ringierimu\\ServiceBusNotificationsChannel\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Ringierimu\\ServiceBusNotificationsChannel\\Tests\\": "tests/" + }, + "files": [ + "tests/helpers.php" + ] + }, + "scripts": { + "lint": "vendor/bin/tlint", + "test": "vendor/bin/testbench package:test" + } } diff --git a/phpunit.xml b/phpunit.xml index 1d9eada..da7de78 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,23 +1,5 @@ - - - - src/ - - + tests @@ -30,4 +12,9 @@ + + + src/ + + diff --git a/src/ServiceBusChannel.php b/src/ServiceBusChannel.php index 0a862b1..9dfd74c 100644 --- a/src/ServiceBusChannel.php +++ b/src/ServiceBusChannel.php @@ -33,11 +33,9 @@ public function __construct(array $config = []) { $this->config = $config ?: config('services.service_bus'); - $this->client = new Client( - [ - 'base_uri' => Arr::get($this->config, 'endpoint'), - ] - ); + $this->client = new Client([ + 'base_uri' => Arr::get($this->config, 'endpoint'), + ]); } /** @@ -60,16 +58,11 @@ public function send($notifiable, Notification $notification) if (Arr::get($this->config, 'enabled') == false) { if (!in_array($eventType, $dontReport)) { - Log::debug( - "$eventType service bus notification [disabled]", - [ - 'event' => $eventType, - 'params' => $params, - 'tags' => [ - 'service-bus', - ], - ] - ); + Log::debug("$eventType service bus notification [disabled]", [ + 'event' => $eventType, + 'params' => $params, + 'tags' => ['service-bus'], + ]); } return; @@ -93,31 +86,21 @@ public function send($notifiable, Notification $notification) ] ); - Log::info( - "$eventType service bus notification", - [ - 'event' => $eventType, - 'params' => $params, - 'tags' => [ - 'service-bus', - ], - 'status' => $response->getStatusCode(), - ] - ); + Log::info("$eventType service bus notification", [ + 'event' => $eventType, + 'params' => $params, + 'tags' => ['service-bus'], + 'status' => $response->getStatusCode(), + ]); } catch (RequestException $exception) { $code = $exception->getCode(); if (in_array($code, [401, 403])) { - Log::info( - "$code received. Logging in and retrying.", - [ - 'event' => $eventType, - 'params' => $params, - 'tags' => [ - 'service-bus', - ], - ] - ); + Log::info("$code received. Logging in and retrying.", [ + 'event' => $eventType, + 'params' => $params, + 'tags' => ['service-bus'], + ]); // clear the invalid token // Cache::forget($this->generateTokenKey()); @@ -145,45 +128,54 @@ public function send($notifiable, Notification $notification) */ private function getToken(): string { - return Cache::rememberForever( - $this->generateTokenKey(), - function () { - try { - $version = intval($this->config['version']); - - if ($version < 2) { - $response = $this->client->request( - 'POST', - $this->getUrl('login'), - [ - 'json' => Arr::only($this->config, ['username', 'password', 'venture_config_id']), - ] - ); - } else { - $response = $this->client->request( - 'POST', - $this->getUrl('login'), - [ - 'json' => Arr::only($this->config, ['username', 'password', 'node_id']), - ] - ); - } - - $body = json_decode((string) $response->getBody(), true); - - $code = (int) Arr::get($body, 'code', $response->getStatusCode()); - - switch ($code) { - case 200: - return $body['token']; - default: - throw CouldNotSendNotification::loginFailed($response); - } - } catch (RequestException $exception) { - throw CouldNotSendNotification::requestFailed($exception); + return Cache::rememberForever($this->generateTokenKey(), function () { + try { + $version = intval($this->config['version']); + + if ($version < 2) { + $response = $this->client->request( + 'POST', + $this->getUrl('login'), + [ + 'json' => Arr::only($this->config, [ + 'username', + 'password', + 'venture_config_id', + ]), + ] + ); + } else { + $response = $this->client->request( + 'POST', + $this->getUrl('login'), + [ + 'json' => Arr::only($this->config, [ + 'username', + 'password', + 'node_id', + ]), + ] + ); } + + $body = json_decode((string) $response->getBody(), true); + + $code = (int) Arr::get( + $body, + 'code', + $response->getStatusCode() + ); + + switch ($code) { + case 200: + return $body['token']; + default: + throw CouldNotSendNotification::loginFailed($response); + } + } catch (RequestException $exception) { + throw CouldNotSendNotification::requestFailed($exception); } - ); + }); } /** @@ -203,13 +195,10 @@ public function generateTokenKey() if ($version < 2) { return md5( 'service-bus-token' . - Arr::get($this->config, 'venture_config_id') + Arr::get($this->config, 'venture_config_id') ); } - return md5( - 'service-bus-token' . - Arr::get($this->config, 'node_id') - ); + return md5('service-bus-token' . Arr::get($this->config, 'node_id')); } } diff --git a/tests/ServiceBusChannelTest.php b/tests/ServiceBusChannelTest.php index 6504644..44f0e08 100644 --- a/tests/ServiceBusChannelTest.php +++ b/tests/ServiceBusChannelTest.php @@ -2,17 +2,10 @@ namespace Ringierimu\ServiceBusNotificationsChannel\Tests; -use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; use Illuminate\Notifications\AnonymousNotifiable; -use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Log; -use Mockery; -use Mockery\MockInterface; -use PHPUnit\Framework\TestCase; use Ringierimu\ServiceBusNotificationsChannel\Exceptions\CouldNotSendNotification; use Ringierimu\ServiceBusNotificationsChannel\ServiceBusChannel; -use stdClass; use Throwable; /** @@ -20,14 +13,6 @@ */ class ServiceBusChannelTest extends TestCase { - public function testShouldCreateServiceBusChannelInstance() - { - $this->mockAll(); - $serviceChannel = new ServiceBusChannel(config_v2()); - - $this->assertNotNull($serviceChannel); - } - /** * @throws GuzzleException * @throws CouldNotSendNotification @@ -37,43 +22,11 @@ public function testShouldThrowRequestExceptionOnSendEvent() { $this->expectException(CouldNotSendNotification::class); - $this->mockAll(); - Cache::shouldReceive('rememberForever') - ->andReturn(true); - Cache::shouldReceive('forget') - ->andReturn(true); - $serviceChannel = new ServiceBusChannel(); - $serviceChannel->send(new AnonymousNotifiable(), new TestNotification()); - } - - /** - * Mock classes, facades and everything else needed. - */ - private function mockAll() - { - Cache::shouldReceive('get') - ->once() - ->with((new ServiceBusChannel(config_v2()))->generateTokenKey()) - ->andReturn('value'); - - Log::shouldReceive('debug') - ->once() - ->andReturnNull(); - - Log::shouldReceive('info') - ->once() - ->andReturnNull(); - - Log::shouldReceive('error') - ->once() - ->andReturnNull(); - - Mockery::mock(Client::class, function (MockInterface $mock) { - $mock->shouldReceive('execute') - ->andReturn(new stdClass()) - ->once(); - }); + $serviceChannel->send( + new AnonymousNotifiable(), + new TestNotification() + ); } } diff --git a/tests/ServiceBusEventTest.php b/tests/ServiceBusEventTest.php index 702715c..9bf59c1 100644 --- a/tests/ServiceBusEventTest.php +++ b/tests/ServiceBusEventTest.php @@ -3,14 +3,9 @@ namespace Ringierimu\ServiceBusNotificationsChannel\Tests; use Carbon\Carbon; -use PHPUnit\Framework\TestCase; use Ringierimu\ServiceBusNotificationsChannel\Exceptions\InvalidConfigException; use Ringierimu\ServiceBusNotificationsChannel\ServiceBusEvent; -use Throwable; -/** - * Class ServiceBusEventTest. - */ class ServiceBusEventTest extends TestCase { public function testShouldCreateServiceBusEventInstance() @@ -27,9 +22,6 @@ public function testShouldCreateServiceBusEventInstanceViaStaticCall() $this->assertEquals('test', $serviceBus->getEventType()); } - /** - * @throws InvalidConfigException - */ public function testShouldThrowInvalidConfigException() { $this->expectException(InvalidConfigException::class); @@ -43,10 +35,6 @@ public function testShouldThrowInvalidConfigException() ->withResources('resources', ['data']); } - /** - * @throws InvalidConfigException - * @throws Throwable - */ public function testShouldAllocateAttributesToServiceBusObject() { $resource = [ @@ -74,10 +62,6 @@ public function testShouldAllocateAttributesToServiceBusObject() $this->assertEquals($resource, $serviceBusData['payload']['resource']); } - /** - * @throws InvalidConfigException - * @throws Throwable - */ public function testShouldAllocateAttributesToServiceBusObjectWithPayload() { $payload = [ @@ -106,10 +90,6 @@ public function testShouldAllocateAttributesToServiceBusObjectWithPayload() $this->assertEquals($payload, $serviceBusData['payload']); } - /** - * @throws InvalidConfigException - * @throws Throwable - */ public function testShouldReturnCorrectEventForSpecificVersion() { $serviceBusVersion1 = ServiceBusEvent::create('test', config_v1()) diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..23f270d --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,30 @@ +set('cache.default', 'array'); + $config->set('services.service_bus', [ + 'enabled' => true, + 'node_id' => '123456789', + 'username' => 'username', + 'password' => 'password', + 'version' => '2.0.0', + 'endpoint' => 'https://bus.staging.ritdu.tech/v1/', + ]); + }); + } +}