Skip to content

Commit 693a9d1

Browse files
authored
Webhook response customisation (#56)
* Add webhookresponse interface and default class * Create response inside processor, instead of controller * Update documentation * Fix StyleCI issue * Add 'Response' return type to interface method Co-authored-by: GuntherDebrauwer <[email protected]>
1 parent f15aa72 commit 693a9d1

11 files changed

+138
-15
lines changed

README.md

+38-11
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
[![Quality Score](https://img.shields.io/scrutinizer/g/spatie/laravel-webhook-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/spatie/laravel-webhook-client)
77
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-webhook-client.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-webhook-client)
88

9-
A webhook is a way for an app to provide information to another app about a specific event. The way the two apps communicate is with a simple HTTP request.
9+
A webhook is a way for an app to provide information to another app about a specific event. The way the two apps communicate is with a simple HTTP request.
1010

1111
This package allows you to receive webhooks in a Laravel app. It has support for [verifying signed calls](#verifying-the-signature-of-incoming-webhooks), [storing payloads and processing the payloads](#storing-and-processing-webhooks) in a queued job.
1212

1313
If you need to send webhooks, take a look at our [laravel-webhook-server](https://github.com/spatie/laravel-webhook-server) package.
1414

1515
## Support us
1616

17-
We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
17+
We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
1818

1919
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).
2020

@@ -69,6 +69,11 @@ return [
6969
*/
7070
'webhook_profile' => \Spatie\WebhookClient\WebhookProfile\ProcessEverythingWebhookProfile::class,
7171

72+
/*
73+
* This class determines the response on a valid webhook call.
74+
*/
75+
'webhook_response' => \Spatie\WebhookClient\WebhookResponse\DefaultResponse::class,
76+
7277
/*
7378
* The classname of the model to be used to store call. The class should be equal
7479
* or extend Spatie\WebhookClient\Models\WebhookCall.
@@ -87,7 +92,7 @@ return [
8792

8893
In the `signing_secret` key of the config file, you should add a valid webhook secret. This value should be provided by the app that will send you webhooks.
8994

90-
This package will try to store and respond to the webhook as fast as possible. Processing the payload of the request is done via a queued job. It's recommended to not use the `sync` driver but a real queue driver. You should specify the job that will handle processing webhook requests in the `process_webhook_job` of the config file. A valid job is any class that extends `Spatie\WebhookClient\ProcessWebhookJob` and has a `handle` method.
95+
This package will try to store and respond to the webhook as fast as possible. Processing the payload of the request is done via a queued job. It's recommended to not use the `sync` driver but a real queue driver. You should specify the job that will handle processing webhook requests in the `process_webhook_job` of the config file. A valid job is any class that extends `Spatie\WebhookClient\ProcessWebhookJob` and has a `handle` method.
9196

9297
### Preparing the database
9398

@@ -122,13 +127,13 @@ protected $except = [
122127

123128
## Usage
124129

125-
With the installation out of the way, let's take a look at how this package handles webhooks. First, it will verify if the signature of the request is valid. If it is not, we'll throw an exception and fire off the `InvalidSignatureEvent` event. Requests with invalid signatures will not be stored in the database.
130+
With the installation out of the way, let's take a look at how this package handles webhooks. First, it will verify if the signature of the request is valid. If it is not, we'll throw an exception and fire off the `InvalidSignatureEvent` event. Requests with invalid signatures will not be stored in the database.
126131

127132
Next, the request will be passed to a webhook profile. A webhook profile is a class that determines if a request should be stored and processed by your app. It allows you to filter out webhook requests that are of interest to your app. You can easily create [your own webhook profile](#determining-which-webhook-requests-should-be-stored-and-processed).
128133

129134
If the webhook profile determines that request should be stored and processed, we'll first store it in the `webhook_calls` table. After that, we'll pass that newly created `WebhookCall` model to a queued job. Most webhook sending apps expect you to respond very quickly. Offloading the real processing work allows for speedy responses. You can specify which job should process the webhook in the `process_webhook_job` in the `webhook-client` config file. Should an exception be thrown while queueing the job, the package will store that exception in the `exception` attribute on the `WebhookCall` model.
130135

131-
After the job has been dispatched, the controller will respond with a `200` status code.
136+
After the job has been dispatched, the request will be passed to a webhook response. A webhook response is a class that determines the HTTP response for the request. An 'ok' message response with `200` status code is returned by default, but you can easily create [your own webhook response](#creating-your-own-webhook-response).
132137

133138
### Verifying the signature of incoming webhooks
134139

@@ -142,7 +147,7 @@ If the `$computedSignature` does match the value, the request will be [passed to
142147

143148
### Creating your own signature validator
144149

145-
A signature validator is any class that implements `Spatie\WebhookClient\SignatureValidator\SignatureValidator`. Here's what that interface looks like.
150+
A signature validator is any class that implements `Spatie\WebhookClient\SignatureValidator\SignatureValidator`. Here's what that interface looks like.
146151

147152
```php
148153
use Illuminate\Http\Request;
@@ -154,7 +159,7 @@ interface SignatureValidator
154159
}
155160
```
156161

157-
`WebhookConfig` is a data transfer object that lets you easily pull up the config (containing the header name that contains the signature and the secret) for the webhook request.
162+
`WebhookConfig` is a data transfer object that lets you easily pull up the config (containing the header name that contains the signature and the secret) for the webhook request.
158163

159164
After creating your own `SignatureValidator` you must register it in the `signature_validator` in the `webhook-client` config file.
160165

@@ -183,9 +188,9 @@ After creating your own `WebhookProfile` you must register it in the `webhook_pr
183188

184189
### Storing and processing webhooks
185190

186-
After the signature is validated and the webhook profile has determined that the request should be processed, the package will store and process the request.
191+
After the signature is validated and the webhook profile has determined that the request should be processed, the package will store and process the request.
187192

188-
The request will first be stored in the `webhook_calls` table. This is done using the `WebhookCall` model.
193+
The request will first be stored in the `webhook_calls` table. This is done using the `WebhookCall` model.
189194

190195
Should you want to customize the table name or anything on the storage behavior, you can let the package use an alternative model. A webhook storing model can be specified in the `webhook_model`. Make sure you model extends `Spatie\WebhookClient\Models\WebhookCall`.
191196

@@ -203,13 +208,32 @@ class ProcessWebhookJob extends SpatieProcessWebhookJob
203208
public function handle()
204209
{
205210
// $this->webhookCall // contains an instance of `WebhookCall`
206-
211+
207212
// perform the work here
208213
}
209214
}
210215
```
211216

212-
You should specify the class name of your job in the `process_webhook_job` of the `webhook-client` config file.
217+
You should specify the class name of your job in the `process_webhook_job` of the `webhook-client` config file.
218+
219+
### Creating your own webhook response
220+
221+
A webhook response is any class that implements `\Spatie\WebhookClient\WebhookResponse\WebhookResponse`. This is what that interface looks like:
222+
223+
```php
224+
namespace Spatie\WebhookClient\WebhookResponse;
225+
226+
use Illuminate\Http\Request;
227+
use Spatie\WebhookClient\WebhookConfig;
228+
229+
interface WebhookResponse
230+
{
231+
public function respondToValidWebhookRequest(Request $request, WebhookConfig $config);
232+
}
233+
```
234+
235+
After creating your own `WebhookResponse` you must register it in the `webhook_response` key in the `webhook-client` config file.
236+
213237

214238
### Handling incoming webhook request for multiple apps
215239

@@ -224,6 +248,7 @@ return [
224248
'signature_header_name' => 'Signature-for-app-1',
225249
'signature_validator' => \Spatie\WebhookClient\SignatureValidator\DefaultSignatureValidator::class,
226250
'webhook_profile' => \Spatie\WebhookClient\WebhookProfile\ProcessEverythingWebhookProfile::class,
251+
'webhook_response' => \Spatie\WebhookClient\WebhookResponse\DefaultResponse::class,
227252
'webhook_model' => \Spatie\WebhookClient\Models\WebhookCall::class,
228253
'process_webhook_job' => '',
229254
],
@@ -233,6 +258,7 @@ return [
233258
'signature_header_name' => 'Signature-for-app-2',
234259
'signature_validator' => \Spatie\WebhookClient\SignatureValidator\DefaultSignatureValidator::class,
235260
'webhook_profile' => \Spatie\WebhookClient\WebhookProfile\ProcessEverythingWebhookProfile::class,
261+
'webhook_response' => \Spatie\WebhookClient\WebhookResponse\DefaultResponse::class,
236262
'webhook_model' => \Spatie\WebhookClient\Models\WebhookCall::class,
237263
'process_webhook_job' => '',
238264
],
@@ -262,6 +288,7 @@ $webhookConfig = new \Spatie\WebhookClient\WebhookConfig([
262288
'signature_header_name' => 'Signature',
263289
'signature_validator' => \Spatie\WebhookClient\SignatureValidator\DefaultSignatureValidator::class,
264290
'webhook_profile' => \Spatie\WebhookClient\WebhookProfile\ProcessEverythingWebhookProfile::class,
291+
'webhook_response' => \Spatie\WebhookClient\WebhookResponse\DefaultResponse::class,
265292
'webhook_model' => \Spatie\WebhookClient\Models\WebhookCall::class,
266293
'process_webhook_job' => '',
267294
]);

config/webhook-client.php

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
*/
3333
'webhook_profile' => \Spatie\WebhookClient\WebhookProfile\ProcessEverythingWebhookProfile::class,
3434

35+
/*
36+
* This class determines the response on a valid webhook call.
37+
*/
38+
'webhook_response' => \Spatie\WebhookClient\WebhookResponse\DefaultResponse::class,
39+
3540
/*
3641
* The classname of the model to be used to store call. The class should be equal
3742
* or extend Spatie\WebhookClient\Models\WebhookCall.

src/Exceptions/InvalidConfig.php

+8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Spatie\WebhookClient\ProcessWebhookJob;
77
use Spatie\WebhookClient\SignatureValidator\SignatureValidator;
88
use Spatie\WebhookClient\WebhookProfile\WebhookProfile;
9+
use Spatie\WebhookClient\WebhookResponse\WebhookResponse;
910

1011
class InvalidConfig extends Exception
1112
{
@@ -28,6 +29,13 @@ public static function invalidWebhookProfile(string $webhookProfile): InvalidCon
2829
return new static("`{$webhookProfile}` is not a valid webhook profile class. A valid web hook profile is a class that implements `{$webhookProfileInterface}`.");
2930
}
3031

32+
public static function invalidWebhookResponse(string $webhookResponse): InvalidConfig
33+
{
34+
$webhookResponseInterface = WebhookResponse::class;
35+
36+
return new static("`{$webhookResponse}` is not a valid webhook response class. A valid webhook response is a class that implements `{$webhookResponseInterface}`.");
37+
}
38+
3139
public static function invalidProcessWebhookJob(string $processWebhookJob): InvalidConfig
3240
{
3341
$abstractProcessWebhookJob = ProcessWebhookJob::class;

src/WebhookConfig.php

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Spatie\WebhookClient\Exceptions\InvalidConfig;
66
use Spatie\WebhookClient\SignatureValidator\SignatureValidator;
77
use Spatie\WebhookClient\WebhookProfile\WebhookProfile;
8+
use Spatie\WebhookClient\WebhookResponse\WebhookResponse;
89

910
class WebhookConfig
1011
{
@@ -18,6 +19,8 @@ class WebhookConfig
1819

1920
public WebhookProfile $webhookProfile;
2021

22+
public WebhookResponse $webhookResponse;
23+
2124
public string $webhookModel;
2225

2326
public string $processWebhookJobClass;
@@ -40,6 +43,11 @@ public function __construct(array $properties)
4043
}
4144
$this->webhookProfile = app($properties['webhook_profile']);
4245

46+
if (! is_subclass_of($properties['webhook_response'], WebhookResponse::class)) {
47+
throw InvalidConfig::invalidWebhookResponse($properties['webhook_response']);
48+
}
49+
$this->webhookResponse = app($properties['webhook_response']);
50+
4351
$this->webhookModel = $properties['webhook_model'];
4452

4553
if (! is_subclass_of($properties['process_webhook_job'], ProcessWebhookJob::class)) {

src/WebhookController.php

+1-3
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ class WebhookController
88
{
99
public function __invoke(Request $request, WebhookConfig $config)
1010
{
11-
(new WebhookProcessor($request, $config))->process();
12-
13-
return response()->json(['message' => 'ok']);
11+
return (new WebhookProcessor($request, $config))->process();
1412
}
1513
}

src/WebhookProcessor.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ public function process()
2626
$this->ensureValidSignature();
2727

2828
if (! $this->config->webhookProfile->shouldProcess($this->request)) {
29-
return;
29+
return $this->createResponse();
3030
}
3131

3232
$webhookCall = $this->storeWebhook();
3333

3434
$this->processWebhook($webhookCall);
35+
36+
return $this->createResponse();
3537
}
3638

3739
protected function ensureValidSignature()
@@ -64,4 +66,9 @@ protected function processWebhook(WebhookCall $webhookCall): void
6466
throw $exception;
6567
}
6668
}
69+
70+
protected function createResponse()
71+
{
72+
return $this->config->webhookResponse->respondToValidWebhookRequest($this->request, $this->config);
73+
}
6774
}
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Spatie\WebhookClient\WebhookResponse;
4+
5+
use Illuminate\Http\Request;
6+
use Spatie\WebhookClient\WebhookConfig;
7+
use Symfony\Component\HttpFoundation\Response;
8+
9+
class DefaultResponse implements WebhookResponse
10+
{
11+
public function respondToValidWebhookRequest(Request $request, WebhookConfig $config): Response
12+
{
13+
return response()->json(['message' => 'ok']);
14+
}
15+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Spatie\WebhookClient\WebhookResponse;
4+
5+
use Illuminate\Http\Request;
6+
use Spatie\WebhookClient\WebhookConfig;
7+
use Symfony\Component\HttpFoundation\Response;
8+
9+
interface WebhookResponse
10+
{
11+
public function respondToValidWebhookRequest(Request $request, WebhookConfig $config): Response;
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Spatie\WebhookClient\Tests\TestClasses;
4+
5+
use Illuminate\Http\Request;
6+
use Spatie\WebhookClient\WebhookConfig;
7+
use Spatie\WebhookClient\WebhookResponse\WebhookResponse;
8+
use Symfony\Component\HttpFoundation\Response;
9+
10+
class CustomWebhookResponse implements WebhookResponse
11+
{
12+
public function respondToValidWebhookRequest(Request $request, WebhookConfig $config): Response
13+
{
14+
return response()->json(['foo' => 'bar']);
15+
}
16+
}

tests/WebhookConfigTest.php

+13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Spatie\WebhookClient\Tests\TestClasses\ProcessWebhookJobTestClass;
99
use Spatie\WebhookClient\WebhookConfig;
1010
use Spatie\WebhookClient\WebhookProfile\ProcessEverythingWebhookProfile;
11+
use Spatie\WebhookClient\WebhookResponse\DefaultResponse;
1112

1213
class WebhookConfigTest extends TestCase
1314
{
@@ -49,6 +50,17 @@ public function it_validates_the_webhook_profile()
4950
new WebhookConfig($config);
5051
}
5152

53+
/** @test */
54+
public function it_validates_the_webhook_response()
55+
{
56+
$config = $this->getValidConfig();
57+
$config['webhook_response'] = 'invalid-webhook-response';
58+
59+
$this->expectException(InvalidConfig::class);
60+
61+
new WebhookConfig($config);
62+
}
63+
5264
/** @test */
5365
public function it_validates_the_process_webhook_job()
5466
{
@@ -68,6 +80,7 @@ protected function getValidConfig(): array
6880
'signature_header_name' => 'Signature',
6981
'signature_validator' => DefaultSignatureValidator::class,
7082
'webhook_profile' => ProcessEverythingWebhookProfile::class,
83+
'webhook_response' => DefaultResponse::class,
7184
'webhook_model' => WebhookCall::class,
7285
'process_webhook_job' => ProcessWebhookJobTestClass::class,
7386
];

tests/WebhookControllerTest.php

+14
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Illuminate\Support\Facades\Route;
99
use Spatie\WebhookClient\Events\InvalidSignatureEvent;
1010
use Spatie\WebhookClient\Models\WebhookCall;
11+
use Spatie\WebhookClient\Tests\TestClasses\CustomWebhookResponse;
1112
use Spatie\WebhookClient\Tests\TestClasses\EverythingIsValidSignatureValidator;
1213
use Spatie\WebhookClient\Tests\TestClasses\NothingIsValidSignatureValidator;
1314
use Spatie\WebhookClient\Tests\TestClasses\ProcessNothingWebhookProfile;
@@ -142,6 +143,19 @@ public function it_can_work_with_an_alternative_model()
142143
$this->assertEquals([], WebhookCall::first()->payload);
143144
}
144145

146+
/** @test */
147+
public function it_can_respond_with_custom_response()
148+
{
149+
config()->set('webhook-client.configs.0.webhook_response', CustomWebhookResponse::class);
150+
151+
$this
152+
->postJson('incoming-webhooks', $this->payload, $this->headers)
153+
->assertSuccessful()
154+
->assertJson([
155+
'foo' => 'bar',
156+
]);
157+
}
158+
145159
private function determineSignature(array $payload): string
146160
{
147161
$secret = config('webhook-client.configs.0.signing_secret');

0 commit comments

Comments
 (0)