diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..a7c44dd
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset = utf-8
+indent_size = 4
+indent_style = space
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{yml,yaml}]
+indent_size = 2
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..02419b4
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,14 @@
+# Path-based git attributes
+# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
+
+# Ignore all test and documentation with "export-ignore".
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/.travis.yml export-ignore
+/phpunit.xml.dist export-ignore
+/tests export-ignore
+/.editorconfig export-ignore
+/.php_cs.dist export-ignore
+/.github export-ignore
+/psalm.xml export-ignore
+
diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml
new file mode 100644
index 0000000..4cf285f
--- /dev/null
+++ b/.github/workflows/php-cs-fixer.yml
@@ -0,0 +1,23 @@
+name: Check & fix styling
+
+on: [push]
+
+jobs:
+ php-cs-fixer:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ github.head_ref }}
+
+ - name: Run PHP CS Fixer
+ uses: docker://oskarstark/php-cs-fixer-ga
+ with:
+ args: --config=.php_cs.dist --allow-risky=yes
+
+ - name: Commit changes
+ uses: stefanzweifel/git-auto-commit-action@v4
+ with:
+ commit_message: Fix styling
diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml
new file mode 100644
index 0000000..ec1935b
--- /dev/null
+++ b/.github/workflows/psalm.yml
@@ -0,0 +1,33 @@
+name: Psalm
+
+on:
+ push:
+ paths:
+ - '**.php'
+ - 'psalm.xml.dist'
+
+jobs:
+ psalm:
+ name: psalm
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '7.4'
+ extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
+ coverage: none
+
+ - name: Cache composer dependencies
+ uses: actions/cache@v2
+ with:
+ path: vendor
+ key: composer-${{ hashFiles('composer.lock') }}
+
+ - name: Run composer install
+ run: composer install -n --prefer-dist
+
+ - name: Run psalm
+ run: ./vendor/bin/psalm
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
new file mode 100644
index 0000000..cdf1660
--- /dev/null
+++ b/.github/workflows/run-tests.yml
@@ -0,0 +1,44 @@
+name: Tests
+
+on: [push, pull_request]
+
+jobs:
+ test:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: true
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ php: [7.4]
+ laravel: [8.*]
+ dependency-version: [prefer-lowest, prefer-stable]
+ include:
+ - laravel: 8.*
+ testbench: 6.*
+
+ name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Cache dependencies
+ uses: actions/cache@v2
+ with:
+ path: ~/.composer/cache/files
+ key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
+ coverage: none
+
+ - name: Install dependencies
+ run: |
+ composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
+ composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest
+
+ - name: Execute tests
+ run: vendor/bin/phpunit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0296b57
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+.idea
+.php_cs
+.php_cs.cache
+.phpunit.result.cache
+build
+composer.lock
+coverage
+docs
+phpunit.xml
+psalm.xml
+vendor
diff --git a/.php_cs.dist b/.php_cs.dist
new file mode 100644
index 0000000..c7d380c
--- /dev/null
+++ b/.php_cs.dist
@@ -0,0 +1,43 @@
+notPath('bootstrap/*')
+ ->notPath('storage/*')
+ ->notPath('resources/view/mail/*')
+ ->in([
+ __DIR__ . '/src',
+ __DIR__ . '/tests',
+ ])
+ ->name('*.php')
+ ->notName('*.blade.php')
+ ->ignoreDotFiles(true)
+ ->ignoreVCS(true);
+
+return PhpCsFixer\Config::create()
+ ->setRules([
+ '@PSR2' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ 'ordered_imports' => ['sortAlgorithm' => 'alpha'],
+ 'no_unused_imports' => true,
+ 'not_operator_with_successor_space' => true,
+ 'trailing_comma_in_multiline_array' => true,
+ 'phpdoc_scalar' => true,
+ 'unary_operator_spaces' => true,
+ 'binary_operator_spaces' => true,
+ 'blank_line_before_statement' => [
+ 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
+ ],
+ 'phpdoc_single_line_var_spacing' => true,
+ 'phpdoc_var_without_name' => true,
+ 'class_attributes_separation' => [
+ 'elements' => [
+ 'method',
+ ],
+ ],
+ 'method_argument_space' => [
+ 'on_multiline' => 'ensure_fully_multiline',
+ 'keep_multiple_spaces_after_comma' => true,
+ ],
+ 'single_trait_insert_per_statement' => true,
+ ])
+ ->setFinder($finder);
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..662ad4e
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+# Changelog
+
+All notable changes to `Webhooks` will be documented in this file
+
+## 1.0.0 - 202X-XX-XX
+
+- initial release
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..39195b9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,88 @@
+# Webhooks for Laravel
+
+[![Latest Version on Packagist](https://img.shields.io/packagist/v/viezel/webhooks.svg?style=flat-square)](https://packagist.org/packages/viezel/webhooks)
+[![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/viezel/webhooks/run-tests?label=tests)](https://github.com/viezel/webhooks/actions?query=workflow%3Arun-tests+branch%3Amaster)
+
+
+Simple and clear implementation of Webhooks.
+
+
+## Installation
+
+You can install the package via composer:
+
+```bash
+composer require viezel/webhooks
+```
+
+You can publish and run the migrations with:
+
+```bash
+php artisan vendor:publish --provider="Viezel\Webhooks\WebhooksServiceProvider" --tag="migrations"
+php artisan migrate
+```
+
+Add routes to your application. Below is a typical route configuration with auth, api prefix and naming.
+
+```php
+Route::middleware('auth:api')->prefix('api')->as('webhooks.api.')->group(function() {
+ Route::get('hooks', Viezel\Webhooks\Controllers\API\ListWebhooks::class)->name('list');
+ Route::get('hooks/events', Viezel\Webhooks\Controllers\API\ListWebhookEvents::class)->name('events');
+ Route::post('hooks', Viezel\Webhooks\Controllers\API\CreateWebhook::class)->name('create');
+ Route::post('hooks/{id}', Viezel\Webhooks\Controllers\API\DeleteWebhook::class)->name('delete');
+});
+```
+
+
+## Usage
+
+First, register Events in your application that should be exposed as Webhooks.
+To do so, your Events should implement the `ShouldDeliverWebhooks` interface.
+
+The interface has two methods, `getWebhookName` for giving the webhook a unique name,
+and `getWebhookPayload` to define the data send with the webhook.
+
+The following example shows how a Post Updated Event and its implementation:
+
+```php
+use App\Models\Post;
+use Viezel\Webhooks\Contracts\ShouldDeliverWebhooks;
+
+class PostUpdatedEvent implements ShouldDeliverWebhooks
+{
+ public function __construct(Post $post)
+ {
+ $this->post = $post;
+ }
+
+ public function getWebhookName(): string
+ {
+ return 'post:updated';
+ }
+
+ public function getWebhookPayload(): array
+ {
+ return [
+ 'post' => $this->post->toArray(),
+ 'extra' => [
+ 'foo' => 'bar'
+ ]
+ ];
+ }
+}
+```
+
+## Testing
+
+```bash
+composer test
+```
+
+## Credits
+
+- [Mads Møller](https://github.com/viezel)
+- [All Contributors](../../contributors)
+
+## License
+
+The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..df618a7
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,49 @@
+{
+ "name": "viezel/webhooks",
+ "description": "Webhooks for Laravel",
+ "keywords": ["webhooks"],
+ "homepage": "https://github.com/viezel/webhooks",
+ "license": "MIT",
+ "require": {
+ "php": "^7.4",
+ "illuminate/contracts": "^8.0",
+ "illuminate/queue": "^8.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^2.16",
+ "orchestra/testbench": "^6.0",
+ "phpunit/phpunit": "^9.3",
+ "vimeo/psalm": "^3.11"
+ },
+ "autoload": {
+ "psr-4": {
+ "Viezel\\Webhooks\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Viezel\\Webhooks\\Tests\\": "tests"
+ }
+ },
+ "scripts": {
+ "psalm": "vendor/bin/psalm",
+ "test": "vendor/bin/phpunit --colors=always",
+ "test-coverage": "vendor/bin/phpunit --coverage-html coverage",
+ "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes"
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Viezel\\Webhooks\\WebhooksServiceProvider"
+ ],
+ "aliases": {
+ "Webhooks": "Viezel\\Webhooks\\WebhooksFacade"
+ }
+ }
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
+}
diff --git a/database/migrations/create_webhooks_table.php.stub b/database/migrations/create_webhooks_table.php.stub
new file mode 100644
index 0000000..2305c73
--- /dev/null
+++ b/database/migrations/create_webhooks_table.php.stub
@@ -0,0 +1,27 @@
+string('id')->primary();
+ $table->text('description')->nullable();
+ $table->string('url');
+ $table->boolean('verify_ssl')->default(false);
+ $table->json('events');
+ $table->json('headers')->nullable();
+ $table->timestamps();
+ $table->softDeletes();
+ });
+ }
+
+ public function down()
+ {
+ Schema::dropIfExists('webhooks');
+ }
+}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..342d629
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,31 @@
+
+
+
+
+ tests
+
+
+
+
+ ./src
+
+
+
+
+
+
+
+
+
+
+
diff --git a/psalm.xml.dist b/psalm.xml.dist
new file mode 100644
index 0000000..c6df33e
--- /dev/null
+++ b/psalm.xml.dist
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Contracts/ShouldDeliverWebhooks.php b/src/Contracts/ShouldDeliverWebhooks.php
new file mode 100644
index 0000000..7ced9ce
--- /dev/null
+++ b/src/Contracts/ShouldDeliverWebhooks.php
@@ -0,0 +1,11 @@
+validated());
+
+ return Response::json($hook->toArray(), 201);
+ }
+}
diff --git a/src/Controllers/API/DeleteWebhook.php b/src/Controllers/API/DeleteWebhook.php
new file mode 100644
index 0000000..87f38e2
--- /dev/null
+++ b/src/Controllers/API/DeleteWebhook.php
@@ -0,0 +1,25 @@
+delete();
+
+ return Response::json([], 204);
+ }
+}
diff --git a/src/Controllers/API/ListWebhookEvents.php b/src/Controllers/API/ListWebhookEvents.php
new file mode 100644
index 0000000..25c4fe0
--- /dev/null
+++ b/src/Controllers/API/ListWebhookEvents.php
@@ -0,0 +1,23 @@
+paginate();
+ }
+}
diff --git a/src/Jobs/WebhookCall.php b/src/Jobs/WebhookCall.php
new file mode 100644
index 0000000..61fb054
--- /dev/null
+++ b/src/Jobs/WebhookCall.php
@@ -0,0 +1,44 @@
+webhook = $webhook;
+ $this->payload = $payload;
+ }
+
+ public function handle()
+ {
+ Http::timeout(10)
+ //->retry(3, 60)
+ ->withHeaders($this->webhook->headers ?? [])
+ ->withOptions([
+ 'verify' => $this->webhook->verify_ssl,
+ ])
+ ->asJson()
+ ->post($this->webhook->url, $this->payload);
+ }
+}
diff --git a/src/Listener/WebhookListener.php b/src/Listener/WebhookListener.php
new file mode 100644
index 0000000..9e456f0
--- /dev/null
+++ b/src/Listener/WebhookListener.php
@@ -0,0 +1,15 @@
+getWebhookName(), $event->getWebhookPayload());
+ }
+}
diff --git a/src/Models/Webhook.php b/src/Models/Webhook.php
new file mode 100644
index 0000000..0834917
--- /dev/null
+++ b/src/Models/Webhook.php
@@ -0,0 +1,45 @@
+ 'json',
+ 'headers' => 'json',
+ 'verify_ssl' => 'boolean',
+ ];
+
+ public static function trigger(string $event, array $payload): void
+ {
+ $hooks = self::query()->whereJsonContains('events', $event)->get();
+
+ if ($hooks->isEmpty()) {
+ return;
+ }
+
+ foreach ($hooks as $hook) {
+ WebhookCall::dispatch($hook, $payload);
+ }
+ }
+}
diff --git a/src/Requests/CreateWebhookRequest.php b/src/Requests/CreateWebhookRequest.php
new file mode 100644
index 0000000..75ce683
--- /dev/null
+++ b/src/Requests/CreateWebhookRequest.php
@@ -0,0 +1,28 @@
+ ['required', 'array'],
+ 'events.*' => ['string', 'distinct', Rule::in(WebhookRegistry::allEvents())],
+ 'headers' => ['nullable', 'array'],
+ 'url' => ['required', 'string'],
+ 'description' => ['nullable', 'string'],
+ 'verify_ssl' => ['nullable', 'boolean'],
+ ];
+ }
+}
diff --git a/src/Support/GeneratesIds.php b/src/Support/GeneratesIds.php
new file mode 100644
index 0000000..dff51f1
--- /dev/null
+++ b/src/Support/GeneratesIds.php
@@ -0,0 +1,33 @@
+getKey()) {
+ $model->setAttribute($model->getKeyName(), (string) Str::orderedUuid());
+ }
+ });
+ }
+
+ public function getIncrementing()
+ {
+ return false;
+ }
+
+ public function getKeyType()
+ {
+ return 'string';
+ }
+
+ public function getIdAttribute($value)
+ {
+ return (string) $value;
+ }
+}
diff --git a/src/WebhookRegistry.php b/src/WebhookRegistry.php
new file mode 100644
index 0000000..b6a7589
--- /dev/null
+++ b/src/WebhookRegistry.php
@@ -0,0 +1,38 @@
+newInstanceWithoutConstructor()
+ ->getWebhookName();
+ }
+}
diff --git a/src/WebhooksServiceProvider.php b/src/WebhooksServiceProvider.php
new file mode 100644
index 0000000..b90dbf2
--- /dev/null
+++ b/src/WebhooksServiceProvider.php
@@ -0,0 +1,33 @@
+app->runningInConsole()) {
+ $migrationFileName = 'create_webhooks_table.php';
+ if (! $this->migrationFileExists($migrationFileName)) {
+ $this->publishes([
+ __DIR__ . "/../database/migrations/{$migrationFileName}.stub" => database_path('migrations/' . date('Y_m_d_His', time()) . '_' . $migrationFileName),
+ ], 'migrations');
+ }
+ }
+ }
+
+ public static function migrationFileExists(string $migrationFileName): bool
+ {
+ $len = strlen($migrationFileName);
+ foreach (glob(database_path("migrations/*.php")) as $filename) {
+ if ((substr($filename, -$len) === $migrationFileName)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php
new file mode 100644
index 0000000..0d5266e
--- /dev/null
+++ b/tests/ExampleTest.php
@@ -0,0 +1,12 @@
+assertTrue(true);
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..4cfd6e6
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,36 @@
+set('database.default', 'sqlite');
+ $app['config']->set('database.connections.sqlite', [
+ 'driver' => 'sqlite',
+ 'database' => ':memory:',
+ 'prefix' => '',
+ ]);
+
+ /*
+ include_once __DIR__.'/../database/migrations/create_webhooks_table.php.stub';
+ (new \CreatePackageTable())->up();
+ */
+ }
+}