spatie/laravel-webhook-client

repository·main·Indexed 22 days ago

https://github.com/spatie/laravel-webhook-client

A Laravel package for receiving, verifying, storing, and processing webhooks. It provides a structured lifecycle including signature verification via custom validators, request filtering through webhook profiles, database storage using the WebhookCall model, and asynchronous processing via queued jobs.

Tokens
7.5K
Snippets
23
Records
27
Agent score
78%

What's inside spatie/laravel-webhook-client

  1. How the webhook processing lifecycle works

    main

    The package follows a specific sequence when a webhook request is received:

    1. Signature Verification: The package checks if the request signature is valid using the configured signature_validator. If invalid, it throws an exception, fires an InvalidWebhookSignatureEvent, and discards the request (it is not stored).
    2. Webhook Profile: The request is passed to a WebhookProfile. This class determines if the specific request is interesting enough to be stored and processed. If it returns false, processing stops.
    3. Storage: If the profile allows it, the request is stored in the webhook_calls table using a WebhookCall model.
    4. Queued Processing: Once stored, the WebhookCall model is passed to a queued job (defined by process_webhook_job). This allows for a fast HTTP response to the sender. If queueing fails, the exception is stored in the exception attribute of the WebhookCall model.
    5. Webhook Response: Finally, a WebhookResponse class determines the HTTP response sent back to the sender (defaults to a 200 OK).
  2. Process webhooks programmatically without a controller

    main

    If you prefer not to use the provided route macro and controller, you can use the WebhookProcessor class directly within your own controller to handle the webhook lifecycle.

    $webhookConfig = new \Spatie\WebhookClient\WebhookConfig([
        'name' => 'webhook-sending-app-1',
        'signing_secret' => 'secret-for-webhook-sending-app-1',
        'signature_header_name' => 'Signature',
        'signature_validator' => \Spatie\WebhookClient\SignatureValidator\DefaultSignatureValidator::class,
        'webhook_profile' => \Spatie\WebhookClient\WebhookProfile\ProcessEverythingWebhookProfile::class,
        'webhook_response' => \Spatie\WebhookClient\WebhookResponse\DefaultRespondsTo::class,
        'webhook_model' => \Spatie\WebhookClient\Models\WebhookCall::class,
        'process_webhook_job' => '',
    ]);
    
    (new \Spatie\WebhookClient\WebhookProcessor($request, $webhookConfig))->process();
  3. Set up webhook routing and CSRF exclusion

    main

    Register your webhook endpoint in your routes/web.php (or equivalent) using the Route::webhooks method.

    Important: Because webhook senders cannot provide a CSRF token, you must exclude your webhook URL from CSRF protection.

    // In your routes file
    Route::webhooks('webhook-receiving-url');
  4. Prepare the database for webhook calls

    main

    The package stores webhook calls in the database by default. You must publish and run the migrations to create the webhook_calls table.

    php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-migrations"
    php artisan migrate
  5. Implement and configure a custom processing job

    main

    To handle the actual business logic of a webhook, you should extend Spatie\WebhookClient\Jobs\ProcessWebhookJob. The package will dispatch this job to your queue automatically after the webhook is stored.

    Inside the handle() method, you can access the stored data via $this->webhookCall.

    Register your custom job class in the process_webhook_job key of your webhook configuration.

    namespace App\Jobs;
    
    use Spatie\WebhookClient\Jobs\ProcessWebhookJob as SpatieProcessWebhookJob;
    
    class ProcessWebhookJob extends SpatieProcessWebhookJob
    {
        public function handle()
        {
            // $this->webhookCall // contains an instance of `WebhookCall`
    
            // perform the work here
        }
    }
  6. Register webhook routes with custom HTTP methods

    main

    By default, the webhooks route macro registers a POST route. You can change this to GET, PUT, PATCH, or DELETE by passing the method as the third argument. You can also pass an array of methods if the sender uses multiple methods on the same endpoint.

    // Single method
    Route::webhooks('receiving-url-for-app-1', 'webhook-sending-app-1', 'get');
    Route::webhooks('receiving-url-for-app-1', 'webhook-sending-app-1', 'put');
    Route::webhooks('receiving-url-for-app-1', 'webhook-sending-app-1', 'patch');
    Route::webhooks('receiving-url-for-app-1', 'webhook-sending-app-1', 'delete');
    
    // Multiple methods
    Route::webhooks('receiving-url-for-app-1', 'webhook-sending-app-1', ['post', 'put']);
  7. Configure the webhook client

    main

    Publish the configuration file to config/webhook-client.php to define how webhooks are handled. The package supports multiple webhook receiving endpoints via the configs array.

    Key configuration options include:

    • signing_secret: The secret used to verify the signature of incoming calls.
    • signature_header_name: The header containing the signature.
    • signature_validator: The class responsible for verifying the signature (must implement \Spatie\WebhookClient\SignatureValidator\SignatureValidator).
    • webhook_profile: Determines if a webhook should be stored and processed.
    • webhook_response: Determines the HTTP response sent back to the sender.
    • webhook_model: The model used to store webhook calls (must extend \Spatie\WebhookClient\Models\WebhookCall).
    • store_headers: An array of headers to store, or * to store all headers.
    • process_webhook_job: The class name of the job that will process the webhook (must extend \Spatie\WebhookClient\Jobs\ProcessWebhookJob).
    • delete_after_days: Number of days to keep webhook models before deletion (set to null to keep forever).
    php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-config"
  8. Migrate from v2 to v3

    main

    Upgrading from version 2 to version 3 requires database schema updates, configuration changes, and code updates due to renamed classes and moved jobs.

    1. Database Migration

    You must update your webhook_calls table to include url and headers columns, and ensure the payload column is compatible with JSON. Run the following command to create a migration:

    php artisan make:migration add_columns_to_webhook_calls

    Then, implement the migration as follows:

    Schema::table('webhook_calls', function (Blueprint $table) {
        $table->string('url')->nullable();
        $table->json('headers')->nullable();
        $table->json('payload')->change();
    });

    2. Configuration Update

    Add the store_headers key to each entry in the configs array within your webhook-client configuration file.

    3. Code Updates (Renames and Moves)

    • Events: The Spatie\WebhookClient\Events\InvalidSignature event has been renamed to Spatie\WebhookClient\Events\InvalidWebhookSignatureEvent. This new event now receives a Spatie\WebhookClient\WebhookConfig instance as a parameter.
    • Jobs: The Spatie\WebhookClient\ProcessWebhookJob class has been moved to Spatie\WebhookClient\Jobs\ProcessWebhookJob.
    <?php
    
    use Illuminate\Database\Migrations\Migration;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Support\Facades\Schema;
    
    class AddColumnsToWebhookCalls extends Migration
    {
        public function up(): void
        {
            Schema::table('webhook_calls', function (Blueprint $table) {
                $table->string('url')->nullable();
                $table->json('headers')->nullable();
                $table->json('payload')->change();
            });
        }
    
        public function down(): void
        {
            Schema::table('webhook_calls', function (Blueprint $table) {
                $table->dropColumn('url');
                $table->dropColumn('headers');
                $table->text('payload')->change();
            });
        }
    }
  9. Prune old webhook calls

    main

    The WebhookCall model uses Laravel's MassPrunable trait. You can configure how many days of webhook data to keep using the delete_after_days key in your configuration.

    To actually delete the old records, you must schedule the model:prune Artisan command in your routes/console.php (or wherever you define your schedule), explicitly passing the WebhookCall class.

    // config/webhook-client.php
    return [
        'configs' => [
            // ...
        ],
    
        'delete_after_days' => 30,
    ];
    
    // routes/console.php
    use Illuminate\Support\Facades\Schedule;
    use Spatie\WebhookClient\Models\WebhookCall;
    
    Schedule::command('model:prune', [
        '--model' => [WebhookCall::class],
    ])->daily();
  10. How WebhookConfig is resolved via routes

    main

    The package automatically resolves the correct WebhookConfig instance based on the current route name. When you define a route using Route::webhooks($url, $name), the package uses the $name to look up the corresponding configuration in webhook-client.configs.

    Resolution Logic:

    1. The package identifies the route name (which starts with webhook-client-).
    2. It extracts the configuration name from the route name.
    3. If add_unique_token_to_route_name is enabled, it strips the random token suffix to find the base configuration name.
    4. It fetches the WebhookConfig from the WebhookConfigRepository using that name.

    If no configuration matches the name extracted from the route, an InvalidConfig::couldNotFindConfig exception is thrown.

  11. How the default signature validation works

    main

    The DefaultSignatureValidator implements the SignatureValidator interface to verify that incoming webhook requests are authentic. It uses an HMAC SHA256 hash comparison between a signature provided in the request header and a locally computed signature.

    Validation Logic:

    1. It retrieves the signature from the request header specified by $config->signatureHeaderName.
    2. It retrieves the signing secret from $config->signingSecret.
    3. It computes a HMAC SHA256 hash of the raw request body ($request->getContent()) using the signing secret.
    4. It performs a timing-attack-safe comparison (hash_equals) between the computed signature and the signature from the header.

    Requirements:

    • A signing_secret must be configured in your webhook profile. If it is empty, an InvalidConfig::signingSecretNotSet() exception is thrown.
    • If the signature header is missing from the request, validation fails and returns false.
    // The validator uses the following logic internally:
    $computedSignature = hash_hmac('sha256', $request->getContent(), $signingSecret);
    return hash_equals($computedSignature, $signature);