diff --git a/CHANGELOG.md b/CHANGELOG.md index f23177d..77a1f4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.2.2] - 2018-01-13 + +### Added +- Implemented Webhooks handling +- Implemented `webhook` and `webhooks` methods in the `Descriptor` facade + ## [1.2.1] - 2018-01-03 ### Fixed @@ -45,7 +51,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed - Package keywords at composer.json -[Unreleased]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.1...HEAD +[Unreleased]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.2...HEAD +[1.2.2]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.1...v1.2.2 [1.2.1]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.0...v1.2.1 [1.2.0]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.0.2...v1.1.0 diff --git a/README.md b/README.md index 57571eb..56a910d 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,83 @@ public function viewIssue(string $key): array } ``` +### Webhooks + +The plugin provides a way to handle incoming webhooks, it uses Laravel Events so you can use habitual way to use them. + +> If you don't familiar with Laravel Events, please take a look at [Laravel Docs](https://laravel.com/docs/5.5/events) + +There are two ways to define webhook listeners: + +1\. Define listeners in the `config/plugin.php` + +``` php +'webhooks' => [ + 'jira:issue_updated' => \App\Listeners\Webhooks\Issue\Created::class, + ... +] +``` + +2\. Define listeners using the `Webhook` facade, for example: + +``` php +Webhook::listen('jira:issue_created', function(\Illuminate\Http\Request $request) { + // ... +}); +``` + +As you can see, you can define event listener as a closure or as a string in Laravel-like syntax: + +``` php +Webhook::listen('jira:issue_created', \App\Listeners\Webhooks\Issue\Created::class); +Webhook::listen('jira:issue_created', 'App\Listeners\Webhooks\Issue\Created@handle'); +``` + +#### Example listener + +``` php +order... + } +} +``` + +> Your event listeners may also type-hint any dependencies they need on their constructors. +All event listeners are resolved via the Laravel service container, so dependencies will be injected automatically. + +The handling method have the following parameters: + +1. `$request` - Request instance with Webhooks payload. +1. `$tenant` - Authenticated Tenant model instance. + ### Console commands * `plugin:install` is a helper command that creates "dummy" tenant with fake data and publishes package resources (config, views, assets) diff --git a/composer.json b/composer.json index 406c8b0..c490b42 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,8 @@ "AtlassianConnectCore\\ServiceProvider" ], "aliases": { - "Descriptor": "AtlassianConnectCore\\Facades\\Descriptor" + "Descriptor": "AtlassianConnectCore\\Facades\\Descriptor", + "Webhook": "AtlassianConnectCore\\Facades\\Webhook" } } }, diff --git a/config/plugin.php b/config/plugin.php index d6148b6..80cabe4 100644 --- a/config/plugin.php +++ b/config/plugin.php @@ -114,5 +114,15 @@ | */ - 'safeDelete' => true + 'safeDelete' => true, + + /* + |-------------------------------------------------------------------------- + | The webhook listeners + |-------------------------------------------------------------------------- + | + | You can define here listeners of the webhook events + | + */ + 'webhooks' => [] ]; \ No newline at end of file diff --git a/src/Descriptor.php b/src/Descriptor.php index 12d0ea1..bd1cbe6 100644 --- a/src/Descriptor.php +++ b/src/Descriptor.php @@ -10,9 +10,11 @@ class Descriptor { /** + * Descriptor contents + * * @var array */ - private $contents = []; + protected $contents = []; /** * Descriptor constructor. @@ -21,7 +23,7 @@ class Descriptor */ public function __construct(array $contents = []) { - $this->contents = (!$contents ? $this->defaultContents() : $contents); + $this->contents = (empty($contents) ? $this->defaultContents() : $contents); } /** @@ -177,6 +179,49 @@ public function base() return $this; } + /** + * Add or replace a webhook + * + * @param string $name + * @param string $url + */ + public function webhook(string $name, string $url) + { + $webhooks = $this->get('modules.webhooks', []); + + // Go through existing webhooks and if there is a webhook with the same name, just replace a url + foreach ($webhooks as $key => $webhook) { + if(array_get($webhook, 'event') === $name) { + $this->set("modules.webhooks.$key.url", $url); + return; + } + } + + $webhooks[] = [ + 'event' => $name, + 'url' => $url + ]; + + $this->set('modules.webhooks', $webhooks); + } + + /** + * Define multiple webhooks + * + * [ + * 'jira:issue_created' => '/webhook-handler-url', + * ... + * ] + * + * @param array $webhooks + */ + public function webhooks(array $webhooks) + { + foreach ($webhooks as $name => $url) { + $this->webhook($name, $url); + } + } + /** * Default descriptor contents * diff --git a/src/Facades/Webhook.php b/src/Facades/Webhook.php new file mode 100644 index 0000000..323a671 --- /dev/null +++ b/src/Facades/Webhook.php @@ -0,0 +1,23 @@ + \Illuminate\Support\Facades\Auth::user(), + 'request' => $request + ]); + } } \ No newline at end of file diff --git a/src/Http/routes.php b/src/Http/routes.php index ed48296..898d00a 100644 --- a/src/Http/routes.php +++ b/src/Http/routes.php @@ -9,6 +9,7 @@ Route::group(['middleware' => 'jwt'], function () { Route::post('enabled', 'TenantController@enabled')->name('enabled'); Route::post('uninstalled', 'TenantController@uninstalled')->name('uninstalled'); + Route::post('webhook/{name}', 'TenantController@webhook')->name('webhook'); Route::get('hello', 'SampleController@index')->name('hello'); }); diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 101607b..83ef6f7 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -32,6 +32,7 @@ public function boot() $this->loadMigrations(); $this->loadConsoleCommands(); $this->loadViews(); + $this->loadWebhooks(); } /** @@ -102,12 +103,23 @@ protected function loadViews() $this->loadViewsFrom(__DIR__ . '/../resources/views', 'plugin'); } + /** + * Load webhook listeners + */ + protected function loadWebhooks() + { + foreach (config('plugin.webhooks', []) as $webhook => $listener) { + \AtlassianConnectCore\Facades\Webhook::listen($webhook, $listener); + } + } + /** * Register package facades */ protected function registerFacades() { $this->app->bind('descriptor', Descriptor::class); + $this->app->bind('webhook', Webhook::class); } /** diff --git a/src/Webhook.php b/src/Webhook.php new file mode 100644 index 0000000..c2d8bca --- /dev/null +++ b/src/Webhook.php @@ -0,0 +1,97 @@ +dispatcher = $dispatcher; + } + + /** + * Register a webhook listener + * + * @param string $name + * @param \Closure|string $listener Closure or class name + * + * @return void + */ + public function listen(string $name, $listener) + { + // Define a webhook in the descriptor + \AtlassianConnectCore\Facades\Descriptor::webhook($name, $this->url($name)); + + // Register event listener + $this->dispatcher->listen($this->eventName($name), $listener); + } + + /** + * Fire a webhook listeners + * + * @param string $name + * @param array $payload + * + * @return void + */ + public function fire(string $name, array $payload) + { + $this->dispatcher->fire($this->eventName($name), $payload); + } + + /** + * Get all listeners of the event + * + * @param string $name + * + * @return array + */ + public function getListeners(string $name): array + { + return $this->dispatcher->getListeners($this->eventName($name)); + } + + /** + * Create a webhook URL + * + * @param string $name Webhook name + * @param bool $absolute Whether need to return an absolute URL + * + * @return string + */ + public function url(string $name, bool $absolute = false) + { + return route('webhook', ['name' => $name], $absolute); + } + + /** + * Add a prefix to the event name + * + * @param string $name Webhook event name + * + * @return string + */ + private function eventName(string $name) + { + return 'webhook:' . $name; + } +} \ No newline at end of file diff --git a/tests/DescriptorTest.php b/tests/DescriptorTest.php index 8fe59f6..0a03064 100644 --- a/tests/DescriptorTest.php +++ b/tests/DescriptorTest.php @@ -136,6 +136,32 @@ public function testSetScopes() static::assertEquals($scopes, array_get($this->descriptor->contents(), 'scopes')); } + /** + * @covers Descriptor::webhook + * @covers Descriptor::webhooks + */ + public function testWebhook() + { + $this->descriptor->webhook('jira:issue_created', '/test'); + $this->descriptor->webhook('jira:issue_updated', '/test-update'); + + static::assertEquals([ + ['event' => 'jira:issue_created', 'url' => '/test'], + ['event' => 'jira:issue_updated', 'url' => '/test-update'] + ], array_get($this->descriptor->contents(), 'modules.webhooks')); + + $this->descriptor->webhooks([ + 'jira:issue_created' => '/test-create', + 'jira:issue_deleted' => '/test-delete' + ]); + + static::assertEquals([ + ['event' => 'jira:issue_created', 'url' => '/test-create'], + ['event' => 'jira:issue_updated', 'url' => '/test-update'], + ['event' => 'jira:issue_deleted', 'url' => '/test-delete'] + ], array_get($this->descriptor->contents(), 'modules.webhooks')); + } + /** * Create descriptor instance * diff --git a/tests/WebhookTest.php b/tests/WebhookTest.php new file mode 100644 index 0000000..c43276d --- /dev/null +++ b/tests/WebhookTest.php @@ -0,0 +1,46 @@ +webhook = app(\AtlassianConnectCore\Webhook::class); + } + + /** + * @covers Webhook::listen + * @covers Webhook::getListeners + */ + public function testListen() + { + $this->webhook->listen('jira:issue_created', function(\Illuminate\Http\Request $request) {}); + $this->webhook->listen('jira:issue_created', '\App\Listeners\IssueCreatedListener'); + + static::assertCount(2, $this->webhook->getListeners('jira:issue_created')); + } + + public function testUrl() + { + static::assertEquals( + '/webhook/jira:issue_deleted', + $this->webhook->url('jira:issue_deleted') + ); + + static::assertEquals( + 'http://localhost/webhook/jira:issue_deleted', + $this->webhook->url('jira:issue_deleted', true) + ); + } +} \ No newline at end of file