├── phpstan.neon
├── src
├── Contracts
│ ├── Factories
│ │ └── FactoryContract.php
│ ├── Signing
│ │ └── SigningContract.php
│ └── Builder
│ │ └── PendingWebhookContract.php
├── Facades
│ └── Webhook.php
├── Signing
│ └── WebhookSigner.php
├── Channels
│ └── WebhookChannel.php
├── Jobs
│ └── DispatchWebhookRequest.php
├── Factories
│ └── WebhookFactory.php
├── Providers
│ └── PackageServiceProvider.php
└── Builder
│ └── PendingWebhook.php
├── config
└── webhooks.php
├── phpunit.xml
├── LICENSE
├── pint.json
├── composer.json
└── README.md
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 |
3 | paths:
4 | - src/
5 |
6 | level: 9
7 |
8 | ignoreErrors:
9 |
10 | excludePaths:
11 |
12 | checkMissingIterableValueType: false
13 |
--------------------------------------------------------------------------------
/src/Contracts/Factories/FactoryContract.php:
--------------------------------------------------------------------------------
1 | [
7 | 'header' => env('WEBHOOK_HEADER', 'Signature'),
8 | 'key' => env('WEBHOOK_SIGNING_KEY'),
9 | ],
10 |
11 | 'user_agent' => [
12 | 'name' => env('WEBHOOK_USER_AGENT', 'Laravel_Webhooks')
13 | ],
14 |
15 | 'request' => [
16 | 'timeout' => env('WEBHOOK_TIMEOUT', 15),
17 | ]
18 | ];
19 |
--------------------------------------------------------------------------------
/src/Contracts/Signing/SigningContract.php:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | ./tests
8 |
9 |
10 |
11 |
12 |
13 | ./src
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/Signing/WebhookSigner.php:
--------------------------------------------------------------------------------
1 | key,
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Channels/WebhookChannel.php:
--------------------------------------------------------------------------------
1 | toWebhook($notifiable);
23 |
24 | return $webhook->send();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Jobs/DispatchWebhookRequest.php:
--------------------------------------------------------------------------------
1 | webhook->send();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Factories/WebhookFactory.php:
--------------------------------------------------------------------------------
1 | signer,
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Steve McDougall.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pint.json:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "psr12",
3 | "rules": {
4 | "align_multiline_comment": true,
5 | "array_indentation": true,
6 | "array_syntax": true,
7 | "blank_line_after_namespace": true,
8 | "blank_line_after_opening_tag": true,
9 | "combine_consecutive_issets": true,
10 | "combine_consecutive_unsets": true,
11 | "concat_space": true,
12 | "declare_parentheses": true,
13 | "declare_strict_types": true,
14 | "explicit_string_variable": true,
15 | "final_class": true,
16 | "final_internal_class": false,
17 | "fully_qualified_strict_types": true,
18 | "global_namespace_import": {
19 | "import_classes": true,
20 | "import_constants": true,
21 | "import_functions": true
22 | },
23 | "is_null": true,
24 | "lambda_not_used_import": true,
25 | "logical_operators": true,
26 | "mb_str_functions": true,
27 | "method_chaining_indentation": true,
28 | "modernize_strpos": true,
29 | "new_with_braces": true,
30 | "no_empty_comment": true,
31 | "not_operator_with_space": true,
32 | "ordered_traits": true,
33 | "protected_to_private": true,
34 | "simplified_if_return": true,
35 | "strict_comparison": true,
36 | "ternary_to_null_coalescing": true,
37 | "trim_array_spaces": true,
38 | "use_arrow_functions": true,
39 | "void_return": true,
40 | "yoda_style": true
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Providers/PackageServiceProvider.php:
--------------------------------------------------------------------------------
1 | publishes(
18 | paths: [
19 | __DIR__.'/../../config/webhooks.php' => config_path('webhooks.php'),
20 | ],
21 | groups: 'webhooks',
22 | );
23 | }
24 |
25 | public function register(): void
26 | {
27 | $this->app->singleton(
28 | abstract: SigningContract::class,
29 | concrete: fn () => new WebhookSigner(
30 | key: strval(config('webhooks.signing.key')),
31 | ),
32 | );
33 |
34 | $this->app->singleton(
35 | abstract: PendingWebhookContract::class,
36 | concrete: fn () => new PendingWebhook(
37 | signer: $this->app->make(
38 | abstract: SigningContract::class,
39 | ),
40 | signature: strval(config('webhooks.signing.key')),
41 | ),
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "juststeveking/laravel-webhooks",
3 | "description": "A simple webhook implementation for Laravel.",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "role": "Developer",
8 | "name": "Steve McDougall",
9 | "email": "juststevemcd@gmail.com",
10 | "homepage": "https://www.juststeveking.uk/"
11 | }
12 | ],
13 | "autoload": {
14 | "psr-4": {
15 | "JustSteveKing\\Webhooks\\": "src/"
16 | }
17 | },
18 | "autoload-dev": {
19 | "psr-4": {
20 | "JustSteveKing\\Webhooks\\Tests\\": "tests/"
21 | }
22 | },
23 | "require": {
24 | "php": "^8.2",
25 | "treblle/treblle-api-tools-laravel": "^0.0.1"
26 | },
27 | "require-dev": {
28 | "guzzlehttp/guzzle": "^7.5",
29 | "laravel/pint": "^1.9",
30 | "orchestra/testbench": "^8.5",
31 | "pestphp/pest": "^2.5.2",
32 | "phpstan/phpstan": "^1.10.14"
33 | },
34 | "scripts": {
35 | "pint": [
36 | "./vendor/bin/pint"
37 | ],
38 | "stan": [
39 | "./vendor/bin/phpstan analyse"
40 | ],
41 | "test": [
42 | "./vendor/bin/pest"
43 | ]
44 | },
45 | "extra": {
46 | "laravel": {
47 | "providers": [
48 | "JustSteveKing\\Webhooks\\Providers\\PackageServiceProvider"
49 | ],
50 | "aliases": {
51 | "Webhook": "JustSteveKing\\Webhooks\\Facades\\Webhook"
52 | }
53 | }
54 | },
55 | "config": {
56 | "sort-packages": true,
57 | "optimize-autoloader": true,
58 | "allow-plugins": {
59 | "pestphp/pest-plugin": true,
60 | "php-http/discovery": true
61 | }
62 | },
63 | "minimum-stability": "dev",
64 | "prefer-stable": true
65 | }
66 |
--------------------------------------------------------------------------------
/src/Contracts/Builder/PendingWebhookContract.php:
--------------------------------------------------------------------------------
1 | signed = true;
49 |
50 | return $this;
51 | }
52 |
53 | /**
54 | * Do not sign this webhook.
55 | *
56 | * @return PendingWebhook
57 | */
58 | public function notSigned(): PendingWebhook
59 | {
60 | $this->signed = false;
61 |
62 | return $this;
63 | }
64 |
65 |
66 | /**
67 | * Set the payload for the Webhook.
68 | *
69 | * @param array $payload
70 | * @return PendingWebhook
71 | */
72 | public function with(array $payload): PendingWebhook
73 | {
74 | $this->payload = $payload;
75 |
76 | return $this;
77 | }
78 |
79 | /**
80 | * Intercept the Http Request to override options.
81 | *
82 | * @param Closure $callback
83 | * @return PendingWebhook
84 | */
85 | public function intercept(Closure $callback): PendingWebhook
86 | {
87 | $userAgent = strval(config('webhooks.user_agent.name'));
88 |
89 | if ($this->request) {
90 | $this->request->withUserAgent(
91 | userAgent: $userAgent,
92 | );
93 |
94 | $this->request = $callback($this->request);
95 |
96 | return $this;
97 | }
98 |
99 | $this->request = Http::withUserAgent(
100 | userAgent: $userAgent,
101 | );
102 |
103 | $this->request = $callback($this->request);
104 |
105 | return $this;
106 | }
107 |
108 | /**
109 | * Dispatch the webhook to be sent on a Queue.
110 | *
111 | * @param string|null $queue
112 | * @return PendingDispatch|PendingClosureDispatch
113 | * @throws JsonException
114 | */
115 | public function queue(null|string $queue = null): PendingDispatch|PendingClosureDispatch
116 | {
117 | return dispatch(new DispatchWebhookRequest(
118 | webhook: $this,
119 | ))->onQueue(
120 | queue: $queue,
121 | );
122 | }
123 |
124 | /**
125 | * Send the webhook.
126 | *
127 | * @param Method $method
128 | * @return Response
129 | * @throws JsonException
130 | * @throws Exception
131 | */
132 | public function send(Method $method = Method::POST): Response
133 | {
134 | if (null === $this->request) {
135 | $this->intercept(
136 | fn (PendingRequest $request) => $request
137 | ->timeout(intval(config('webhooks.request.timeout'))),
138 | );
139 | }
140 |
141 | if ($this->signed) {
142 | /** @phpstan-ignore-next-line */
143 | $this->request->withHeaders(
144 | headers: [
145 | strval(config('webhooks.signing.header')) => $this->signer->sign(
146 | payload: $this->payload,
147 | ),
148 | ],
149 | );
150 | }
151 |
152 | /** @phpstan-ignore-next-line */
153 | return $this->request->send(
154 | method: $method->value,
155 | url: $this->url,
156 | options: [
157 | 'json' => $this->payload,
158 | ]
159 | )->throw();
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel Webhooks
2 |
3 |
4 | [![Latest Version][badge-release]][packagist]
5 | [![Software License][badge-license]][license]
6 | [](https://github.com/JustSteveKing/laravel-webhooks/actions/workflows/tests.yml)
7 | [![PHP Version][badge-php]][php]
8 | [![Total Downloads][badge-downloads]][downloads]
9 |
10 | [badge-release]: https://img.shields.io/packagist/v/juststeveking/laravel-webhooks.svg?style=flat-square&label=release
11 | [badge-license]: https://img.shields.io/packagist/l/juststeveking/laravel-webhooks.svg?style=flat-square
12 | [badge-php]: https://img.shields.io/packagist/php-v/juststeveking/laravel-webhooks.svg?style=flat-square
13 | [badge-downloads]: https://img.shields.io/packagist/dt/juststeveking/laravel-webhooks.svg?style=flat-square&colorB=mediumvioletred
14 |
15 | [packagist]: https://packagist.org/packages/juststeveking/laravel-webhooks
16 | [license]: https://github.com/juststeveking/laravel-webhooks/blob/main/LICENSE
17 | [php]: https://php.net
18 | [downloads]: https://packagist.org/packages/juststeveking/laravel-webhooks
19 |
20 |
21 | A simple webhook implementation for Laravel.
22 |
23 | ## Installation
24 |
25 | ```bash
26 | composer require juststeveking/laravel-webhooks
27 | ```
28 |
29 | ## Publishing config
30 |
31 | To publish the configuration file for this package, please run the following artisan command:
32 |
33 | ```bash
34 | php artisan vendor:publish --provider="JustSteveKing\Webhooks\Providers\PackageServiceProvider" --tag="webhooks"
35 | ```
36 |
37 | The config looks for a few ENV variables that you can set:
38 |
39 | - `WEBHOOK_HEADER` - The header key to send the signature as, this defaults to `Signature`
40 | - `WEBHOOK_SIGNING_KEY` - Your webhook signing key.
41 | - `WEBHOOK_USER_AGENT` - The user agent you want to set on your request, defaults to `Laravel_Webhooks`
42 | - `WEBHOOK_TIMEOUT` - The request timeout you want to set for sending the webhook, defaults to 15 seconds.
43 |
44 | ## Usage
45 |
46 | Using the webhook facade all you need to pass through is the URL you want to send the webhook to.
47 |
48 | ```php
49 | use JustSteveKing\Webhooks\Facades\Webhook;
50 |
51 | $webhook = Webhook::to(
52 | url: 'https://your-url.com/',
53 | )
54 | ```
55 |
56 | This will return a `PendingWebhook` for you to use. This will load the signing key in from your configuration.
57 |
58 | If you need/want to set the signing key per-webhook you will need to instantiate the `PendingWebhook` yourself:
59 |
60 | ```php
61 | use JustSteveKing\Webhooks\Builder\PendingWebhook;
62 | use JustSteveKing\Webhooks\Signing\WebhookSigner;
63 |
64 | $webhook = new PendingWebhook(
65 | url: 'https://your-url.com/',
66 | signer: new WebhookSigner(
67 | key: 'your-signing-key',
68 | ),
69 | );
70 | ```
71 |
72 | The Pending Webhook has the following properties in the constructor:
73 |
74 | - `url` - The URL you want to send the webhook to.
75 | - `signer` - An instance of the webhook signer you want to use. This must implement the `SigningContract`.
76 | - `payload` - You can pre-pass in the payload that you want to send in your webhook. This should be an `array`.
77 | - `signed` - You can pre-pass in whether you want this webhook to be signed or not, the default is `true`
78 | .- `signature` - You can pre-pass in the signature that you want to use to sign your Webhooks with.
79 | - `request` - You can pre-pass in a `PendingRequest` that you want to use to send your webhooks, this is useful when you need to attach an API token to your Webhooks.
80 |
81 | ## A simple example
82 |
83 | In the below example, we are sending a webhook to `https://your-url.com/` and sending it the first `Post` model that we find.
84 |
85 | ```php
86 | use JustSteveKing\Webhooks\Facades\Webhook;
87 |
88 | Webhook::to('https://your-url.com/')
89 | ->with(Post::query()->first()->toArray())
90 | ->send();
91 | ```
92 |
93 | ## A more complex example
94 |
95 | In this example we want to send a webhook to `https://your-url.com/`, again passing it the first `Post` model.
96 | However, this time we want to intercept the creation of the Request to attach the Bearer token for authentication.
97 | We then want to dispatch the sending of this webhook to the queue.
98 |
99 | ```php
100 | use Illuminate\Http\Client\PendingRequest;
101 | use JustSteveKing\Webhooks\Facades\Webhook;
102 |
103 | Webhook::to('https://your-url.com/')
104 | ->with(Post::query()->first()->toArray())
105 | ->intercept(fn (PendingRequest $request) => $request
106 | ->withToken('YOUR-BEARER-TOKEN'),
107 | )->queue('my-queue-name');
108 | ```
109 |
110 | ## Not signing the webhook
111 |
112 | If you don't need to sign the webhook.
113 |
114 | ```php
115 | use JustSteveKing\Webhooks\Facades\Webhook;
116 |
117 | Webhook::to('https://your-url.com/')->with(
118 | Post::query()->first()->toArray()
119 | )->notSigned()->send();
120 | ```
121 |
122 | ## Testing
123 |
124 | To run the test:
125 |
126 | ```bash
127 | composer run test
128 | ```
129 |
130 | ## Credits
131 |
132 | - [Steve McDougall](https://github.com/JustSteveKing)
133 | - [All Contributors](../../contributors)
134 |
135 | ## LICENSE
136 |
137 | The MIT License (MIT). Please see [License File](./LICENSE) for more information.
138 |
--------------------------------------------------------------------------------