symfony/webhook
Symfony Webhook simplifies sending and consuming webhooks in Symfony apps. It provides tools to define webhook endpoints, validate and process incoming payloads, and dispatch outgoing webhooks with a consistent, reusable workflow.
Install the Package:
composer require symfony/webhook
Add to composer.json if using Laravel’s autoloader:
"autoload": {
"psr-4": {
"App\\": "app/",
"Symfony\\Component\\Webhook\\": "vendor/symfony/webhook/"
}
}
Run composer dump-autoload.
First Use Case: Consuming a Webhook Create a route and middleware to validate and parse incoming webhooks:
// routes/web.php
Route::post('/stripe-webhook', [WebhookController::class, 'handleStripeWebhook']);
// app/Http/Middleware/ValidateWebhookSignature.php
use Symfony\Component\Webhook\Webhook;
public function handle($request, Closure $next) {
$webhook = new Webhook($request->getContent(), $request->headers->get('stripe-signature'));
if (!$webhook->validate(config('services.stripe.webhook_secret'))) {
abort(401, 'Invalid signature');
}
return $next($request);
}
Register the middleware in app/Http/Kernel.php:
protected $routeMiddleware = [
'validate.webhook' => \App\Http\Middleware\ValidateWebhookSignature::class,
];
First Use Case: Sending a Webhook
Use the Webhook class to send a payload with optional signing:
use Symfony\Component\Webhook\Webhook;
$webhook = new Webhook('https://example.com/webhook-endpoint');
$webhook->setPayload(['event' => 'order.paid', 'data' => $order]);
$webhook->setSecret(config('services.example.webhook_secret')); // Optional signing
$response = $webhook->send();
Signature Validation:
Use Symfony\Component\Webhook\Webhook to validate HMAC signatures for security:
$webhook = new Webhook($rawPayload, $request->headers->get('x-hub-signature-256'));
if (!$webhook->validate('your-secret-key')) {
abort(401);
}
Payload Parsing:
Decouple parsing logic by implementing RequestParserInterface:
use Symfony\Component\Webhook\RequestParserInterface;
class StripeRequestParser implements RequestParserInterface
{
public function parse(Request $request): RemoteEvent
{
$payload = json_decode($request->getContent(), true);
return new RemoteEvent('stripe', $payload['type'], $payload);
}
}
Register the parser in your service container:
$container->set('stripe.parser', StripeRequestParser::class);
Event-Driven Handling: Dispatch Laravel events after parsing:
$event = $this->parseWebhook($request);
event(new WebhookReceived($event));
Webhook class with Laravel’s HTTP client for async support:
$webhook = new Webhook('https://slack.com/webhook');
$webhook->setPayload(['text' => 'Alert: New order #'.$order->id]);
$webhook->setSecret(config('services.slack.signing_secret')); // Optional
$response = $webhook->send();
Webhook::sendWithRetry() (if using Symfony Messenger) or wrap in Laravel’s queue job:
class SendWebhookJob implements ShouldQueue
{
public function handle()
{
$webhook = new Webhook('https://api.example.com/webhook');
$webhook->sendWithRetry(3); // Retry 3 times
}
}
Service Provider:
Bind Symfony’s Webhook to Laravel’s container for dependency injection:
public function register()
{
$this->app->singleton(Webhook::class, function ($app) {
return new Webhook();
});
}
Facade: Create a facade for cleaner syntax:
// app/Facades/Webhook.php
public static function send(string $url, array $payload): Response
{
return app(Webhook::class)->send($url, $payload);
}
Usage:
Webhook::send('https://example.com', ['data' => $payload]);
Middleware for Inbound: Centralize webhook validation in middleware:
public function handle($request, Closure $next)
{
$webhook = new Webhook($request->getContent(), $request->header('x-signature'));
if (!$webhook->validate(config('webhooks.secret'))) {
abort(401);
}
return $next($request);
}
Use Laravel’s routing to dynamically handle multiple webhook providers:
Route::post('/webhooks/{provider}', [WebhookController::class, 'handle'])
->middleware('validate.webhook');
public function handle($provider, Request $request)
{
$parser = app("webhook.parser.$provider"); // Resolves to StripeRequestParser, etc.
$event = $parser->parse($request);
// Handle event...
}
Implement idempotency keys for outbound webhooks:
$webhook = new Webhook('https://example.com/webhook');
$webhook->setIdempotencyKey($order->id); // Ensures duplicate payloads are ignored
$webhook->send();
Offload webhook processing to queues for performance:
class ProcessWebhookJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public function handle()
{
$event = $this->parseWebhook($this->request);
// Process event...
}
}
Use Laravel’s HTTP tests with mock responses:
public function test_webhook_received()
{
$response = $this->postJson('/webhooks/stripe', $payload, [
'HTTP_X_STRIPE_SIGNATURE' => $this->generateSignature($payload),
]);
$response->assertOk();
}
var_dump($webhook->getSignature(), $request->header('x-signature')) to compare.// Debug signature generation
$secret = 'your-secret';
$payload = $request->getContent();
$expectedSignature = hash_hmac('sha256', $payload, $secret);
RequestParserInterface fails to parse JSON or malformed payloads.Log::debug('Raw payload:', [$request->getContent()]).json_decode($request->getContent(), true).class FallbackRequestParser implements RequestParserInterface
{
public function parse(Request $request): RemoteEvent
{
$payload = json_decode($request->getContent(), true) ?: [];
return new RemoteEvent('unknown', 'fallback', $payload);
}
}
php artisan queue:work --sleep=3 --tries=3.$webhook->sendWithRetry(3).Route::post('/webhook', [WebhookController::class, 'handle'])
->middleware('validate.webhook:stripe'); // Runs before controller
How can I help you explore Laravel packages today?