├── 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 | [![Run Tests](https://github.com/JustSteveKing/laravel-webhooks/actions/workflows/tests.yml/badge.svg)](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 | --------------------------------------------------------------------------------