vherbaut/laravel-inbound-webhooks
Composer 安装命令:
composer require vherbaut/laravel-inbound-webhooks
包简介
A Laravel package to handle inbound webhooks from any provider with signature validation, logging, and replay capabilities.
README 文档
README
A Laravel package to handle inbound webhooks from any provider with signature validation, logging, queue processing, and replay capabilities.
Features
- 🔐 Signature validation for Stripe, GitHub, Slack, Twilio, and custom HMAC
- 📦 Automatic storage of all incoming webhooks with full payload
- ⚡ Async processing via Laravel queues (responds 200 in < 100ms)
- 🔄 Replay webhooks for debugging and development
- 🗑️ Automatic cleanup with configurable retention
- 📊 Event mapping to dispatch custom Laravel events
- 🔌 Extensible - add your own drivers easily
Installation
composer require vherbaut/laravel-inbound-webhooks
Publish the config and migrations:
php artisan vendor:publish --tag=inbound-webhooks-config php artisan vendor:publish --tag=inbound-webhooks-migrations php artisan migrate
Quick Start
1. Configure your provider
Add your webhook secret to .env:
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
2. Register your webhook URL
Point your provider's webhook settings to:
https://your-app.com/webhooks/stripe
3. Listen for events
In your EventServiceProvider or a listener:
use Vherbaut\InboundWebhooks\Events\WebhookReceived; class EventServiceProvider extends ServiceProvider { protected $listen = [ WebhookReceived::class => [ HandleStripeWebhook::class, ], ]; }
// app/Listeners/HandleStripeWebhook.php class HandleStripeWebhook { public function handle(WebhookReceived $event): void { if ($event->provider() !== 'stripe') { return; } match ($event->eventType()) { 'payment_intent.succeeded' => $this->handlePaymentSucceeded($event), 'customer.subscription.deleted' => $this->handleSubscriptionCanceled($event), default => null, }; } protected function handlePaymentSucceeded(WebhookReceived $event): void { $paymentIntentId = $event->get('data.object.id'); $amount = $event->get('data.object.amount'); // Your logic here... } }
Configuration
Providers
Configure each provider in config/inbound-webhooks.php:
'providers' => [ 'stripe' => [ 'driver' => 'stripe', 'secret' => env('STRIPE_WEBHOOK_SECRET'), 'tolerance' => 300, // Timestamp tolerance in seconds ], 'github' => [ 'driver' => 'github', 'secret' => env('GITHUB_WEBHOOK_SECRET'), ], 'slack' => [ 'driver' => 'slack', 'signing_secret' => env('SLACK_SIGNING_SECRET'), ], 'twilio' => [ 'driver' => 'twilio', 'auth_token' => env('TWILIO_AUTH_TOKEN'), ], // Custom provider using generic HMAC 'acme' => [ 'driver' => 'hmac', 'secret' => env('ACME_WEBHOOK_SECRET'), 'algorithm' => 'sha256', 'header' => 'X-Acme-Signature', 'prefix' => 'sha256=', // Optional prefix 'event_key' => 'event_type', 'id_key' => 'webhook_id', ], ],
Event Mapping
Map specific webhook events to custom Laravel event classes:
'events' => [ 'stripe.payment_intent.succeeded' => \App\Events\PaymentReceived::class, 'stripe.customer.subscription.deleted' => \App\Events\SubscriptionCanceled::class, 'github.push' => \App\Events\GitHubPush::class, ],
Your custom event receives the webhook model:
// app/Events/PaymentReceived.php class PaymentReceived { public function __construct( public InboundWebhook $webhook ) {} }
Events
The package dispatches three events during the webhook lifecycle:
WebhookReceived
Dispatched when a webhook is received and ready for processing. This is the primary event for handling webhooks.
use Vherbaut\InboundWebhooks\Events\WebhookReceived; class HandleStripeWebhook { public function handle(WebhookReceived $event): void { // Access webhook data via helper methods $provider = $event->provider(); // "stripe" $eventType = $event->eventType(); // "payment_intent.succeeded" $payload = $event->payload(); // Full payload array $value = $event->get('data.object.id'); // Dot notation access } }
WebhookProcessed
Dispatched after a webhook has been successfully processed. Useful for metrics or cleanup.
use Vherbaut\InboundWebhooks\Events\WebhookProcessed; class UpdateMetrics { public function handle(WebhookProcessed $event): void { Metrics::increment("webhooks.{$event->webhook->provider}.success"); } }
WebhookFailed
Dispatched when webhook processing fails. Use for error notifications or logging.
use Vherbaut\InboundWebhooks\Events\WebhookFailed; class NotifyOnFailure { public function handle(WebhookFailed $event): void { Log::error('Webhook failed', [ 'provider' => $event->webhook->provider, 'event_type' => $event->webhook->event_type, 'error' => $event->exception->getMessage(), ]); // Send notification to Slack, email, etc. } }
Registering Listeners
Register your listeners in EventServiceProvider:
use Vherbaut\InboundWebhooks\Events\WebhookReceived; use Vherbaut\InboundWebhooks\Events\WebhookProcessed; use Vherbaut\InboundWebhooks\Events\WebhookFailed; protected $listen = [ WebhookReceived::class => [ HandleStripeWebhook::class, HandleGitHubWebhook::class, ], WebhookProcessed::class => [ UpdateWebhookMetrics::class, ], WebhookFailed::class => [ NotifyOnWebhookFailure::class, RetryFailedWebhook::class, ], ];
Queue Configuration
'queue' => [ 'connection' => env('INBOUND_WEBHOOKS_QUEUE_CONNECTION', 'default'), 'queue' => env('INBOUND_WEBHOOKS_QUEUE', 'webhooks'), ],
Storage
'storage' => [ 'store_payload' => true, // Store full payload (recommended for replay) 'retention_days' => 30, // Auto-prune after 30 days (null = forever) ],
Console Commands
Replay a webhook
# By UUID php artisan webhooks:replay 550e8400-e29b-41d4-a716-446655440000 # Process synchronously (useful for debugging) php artisan webhooks:replay 550e8400-e29b-41d4-a716-446655440000 --sync # Force replay even if already processed php artisan webhooks:replay 550e8400-e29b-41d4-a716-446655440000 --force
Prune old webhooks
# Use configured retention days php artisan webhooks:prune # Custom retention php artisan webhooks:prune --days=7 # Only prune failed webhooks php artisan webhooks:prune --status=failed # Only prune specific provider php artisan webhooks:prune --provider=stripe # Dry run (see what would be deleted) php artisan webhooks:prune --dry-run
Schedule automatic pruning in app/Console/Kernel.php:
$schedule->command('webhooks:prune')->daily();
Custom Drivers
Create custom drivers to integrate with any webhook provider not covered by the built-in drivers.
Registering a Custom Driver
Register your driver in the boot() method of a service provider:
// app/Providers/AppServiceProvider.php use Vherbaut\InboundWebhooks\Facades\InboundWebhooks; use App\Webhooks\Drivers\PayPalDriver; public function boot(): void { InboundWebhooks::extend('paypal', function (array $config) { return new PayPalDriver($config); }); }
Configuration
Add your provider configuration in config/inbound-webhooks.php:
'providers' => [ 'paypal' => [ 'driver' => 'paypal', // Must match the name used in extend() 'webhook_id' => env('PAYPAL_WEBHOOK_ID'), // Custom config keys 'client_id' => env('PAYPAL_CLIENT_ID'), 'client_secret' => env('PAYPAL_CLIENT_SECRET'), 'sandbox' => env('PAYPAL_SANDBOX', true), ], ],
Creating the Driver Class
Extend AbstractDriver and implement the required methods from DriverInterface:
// app/Webhooks/Drivers/PayPalDriver.php namespace App\Webhooks\Drivers; use Illuminate\Http\Request; use Vherbaut\InboundWebhooks\Drivers\AbstractDriver; use Vherbaut\InboundWebhooks\Exceptions\InvalidSignatureException; class PayPalDriver extends AbstractDriver { /** * Validate the webhook signature. * * @throws InvalidSignatureException */ public function validateSignature(Request $request): void { $transmissionId = $request->header('Paypal-Transmission-Id'); $timestamp = $request->header('Paypal-Transmission-Time'); $signature = $request->header('Paypal-Transmission-Sig'); $certUrl = $request->header('Paypal-Cert-Url'); if (! $transmissionId || ! $signature) { throw new InvalidSignatureException('Missing PayPal signature headers'); } // Build the expected signature string $webhookId = $this->config['webhook_id']; $payload = $request->getContent(); $crc32 = crc32($payload); $expectedSignature = "{$transmissionId}|{$timestamp}|{$webhookId}|{$crc32}"; // Verify with PayPal certificate (simplified example) if (! $this->verifyWithCertificate($expectedSignature, $signature, $certUrl)) { throw new InvalidSignatureException('Invalid PayPal webhook signature'); } } /** * Extract the event type from the webhook payload. */ public function getEventType(Request $request): ?string { return $request->input('event_type'); } /** * Extract the external ID (PayPal's webhook event ID). */ public function getExternalId(Request $request): ?string { return $request->input('id'); } /** * Define provider-specific headers to store for auditing. * * @return array<int, string> */ protected function getRelevantHeaders(): array { return [ 'Content-Type', 'Paypal-Transmission-Id', 'Paypal-Transmission-Time', 'Paypal-Transmission-Sig', 'Paypal-Cert-Url', ]; } private function verifyWithCertificate( string $data, string $signature, ?string $certUrl ): bool { // Your verification logic here return true; } }
Using AbstractDriver Helpers
The AbstractDriver class provides utility methods for common signature validation patterns:
class AcmeDriver extends AbstractDriver { public function validateSignature(Request $request): void { $signature = $request->header('X-Acme-Signature'); $payload = $request->getContent(); $secret = $this->config['secret']; if (! $signature) { throw new InvalidSignatureException('Missing signature header'); } // Use built-in HMAC computation $expected = $this->computeHmac($payload, $secret, 'sha256'); // Use timing-safe comparison to prevent timing attacks if (! $this->compareSignatures($expected, $signature)) { throw new InvalidSignatureException('Invalid signature'); } } // ... }
Available helper methods:
| Method | Description |
|---|---|
computeHmac($payload, $secret, $algorithm) |
Compute HMAC signature (default: sha256) |
compareSignatures($expected, $actual) |
Timing-safe string comparison |
getRelevantHeaders() |
Override to define storable headers |
getPayload($request) |
Override to customize payload extraction |
getStorableHeaders($request) |
Filters headers based on getRelevantHeaders() |
Custom Payload Extraction
Override getPayload() if your provider sends data in a non-standard format:
public function getPayload(Request $request): array { // Handle form-encoded webhooks (e.g., Twilio) if ($request->isJson()) { return $request->json()->all(); } return $request->all(); }
Full Driver Interface Reference
Your driver must implement all methods from DriverInterface:
interface DriverInterface { /** * Validate the webhook signature. * * @throws InvalidSignatureException */ public function validateSignature(Request $request): void; /** * Extract the event type from the webhook payload. */ public function getEventType(Request $request): ?string; /** * Extract the external ID (provider's webhook/event ID). */ public function getExternalId(Request $request): ?string; /** * Get the parsed payload from the request. */ public function getPayload(Request $request): array; /** * Get headers that should be stored with the webhook. */ public function getStorableHeaders(Request $request): array; }
Testing Custom Drivers
use App\Webhooks\Drivers\PayPalDriver; use Illuminate\Http\Request; use Vherbaut\InboundWebhooks\Exceptions\InvalidSignatureException; it('validates paypal webhook signature', function () { $driver = new PayPalDriver([ 'webhook_id' => 'WH-123', 'client_id' => 'test', 'client_secret' => 'secret', ]); $request = Request::create('/webhooks/paypal', 'POST', [], [], [], [ 'HTTP_PAYPAL_TRANSMISSION_ID' => 'abc123', 'HTTP_PAYPAL_TRANSMISSION_SIG' => 'valid-signature', 'HTTP_PAYPAL_TRANSMISSION_TIME' => '2024-01-15T10:00:00Z', ], json_encode(['event_type' => 'PAYMENT.CAPTURE.COMPLETED'])); // Should not throw $driver->validateSignature($request); }); it('rejects invalid signature', function () { $driver = new PayPalDriver(['webhook_id' => 'WH-123']); $request = Request::create('/webhooks/paypal', 'POST'); $driver->validateSignature($request); })->throws(InvalidSignatureException::class);
Querying Webhooks
use Vherbaut\InboundWebhooks\Models\InboundWebhook; // Get all failed Stripe webhooks $failed = InboundWebhook::provider('stripe') ->failed() ->latest() ->get(); // Get recent payment webhooks $payments = InboundWebhook::provider('stripe') ->eventType('payment_intent.succeeded') ->where('created_at', '>', now()->subDay()) ->get(); // Retry all failed webhooks InboundWebhook::failed()->each(function ($webhook) { $webhook->resetForRetry(); ProcessWebhook::dispatch($webhook); });
Testing
In your tests, you can simulate incoming webhooks:
use Vherbaut\InboundWebhooks\Models\InboundWebhook; // Create a webhook directly for testing $webhook = InboundWebhook::create([ 'provider' => 'stripe', 'event_type' => 'payment_intent.succeeded', 'payload' => [ 'type' => 'payment_intent.succeeded', 'data' => [ 'object' => [ 'id' => 'pi_123', 'amount' => 1000, ], ], ], ]); // Process it ProcessWebhook::dispatchSync($webhook);
License
MIT
vherbaut/laravel-inbound-webhooks 适用场景与选型建议
vherbaut/laravel-inbound-webhooks 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 1 次下载、GitHub Stars 达 1, 最近一次更新时间为 2025 年 12 月 28 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「api」 「github」 「stripe」 「inbound」 「laravel」 「webhooks」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 vherbaut/laravel-inbound-webhooks 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 vherbaut/laravel-inbound-webhooks 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 vherbaut/laravel-inbound-webhooks 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
Handles basic OAuth/OAuth2 authentication along with classes for common services
Flexible payment integration for Laravel, lunarphp, supporting multiple payment providers.
MediaWiki extension that allows embedding external content, specified by URL, into your wiki pages
A PSR-7 compatible library for making CRUD API endpoints
Rich payment solutions for Laravel framework. Paypal, payex, authorize.net, be2bill, omnipay, recurring paymens, instant notifications and many more
统计信息
- 总下载量: 1
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 1
- 点击次数: 27
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2025-12-28