SF
{ }
Symfony · Webhooks · Messenger · HMAC · PHP
Symfony Webhooks:
Externe Ereignisse typsicher verarbeiten

Webhooks von Stripe, GitHub, Shopify oder eigenen Diensten kommen als HTTP-POST-Requests an – ohne Typsicherheit, ohne Schema-Garantien und potenziell von gefälschten Absendern. Symfony bietet mit dem Webhook-Component, Messenger und HMAC-Signaturprüfung eine vollständige Infrastruktur, um externe Ereignisse sicher, typsicher und asynchron zu verarbeiten.

17 Min. Lesezeit HMAC · WebhookConsumer · Messenger · Retry · Typsicherheit Symfony 7.x · PHP 8.3+

1. Warum Webhooks typsichere Verarbeitung brauchen

Ein eingehender Symfony Webhook ist zunächst nichts anderes als ein HTTP-POST-Request mit einem JSON-Body. Die Herausforderung liegt nicht im Empfang, sondern in der sicheren Verarbeitung: Ist der Request wirklich von Stripe, GitHub oder dem erwarteten Absender? Entspricht der Payload dem erwarteten Schema oder hat der externe Dienst sein Format geändert? Wird die Anfrage synchron in unter 3 Sekunden beantwortet, auch wenn die Verarbeitungslogik mehrere Sekunden dauert? Ohne klare Architektur entstehen Controller, die JSON direkt parsen, keine Signatur prüfen und Business-Logik synchron im Request-Lifecycle ausführen – ein Rezept für schwer debugbare Fehler und Sicherheitsprobleme.

Die richtige Architektur für Symfony Webhooks trennt drei Verantwortlichkeiten sauber: Erstens die Eingangsprüfung (Signaturvalidierung, HTTP-Header-Auswertung, schnelle 200-Antwort), zweitens das Parsen (Mapping des JSON-Payloads auf typsichere Event-Objekte), drittens die Verarbeitung (Business-Logik, Datenbankoperationen, externe API-Calls). Symfony's Webhook-Component übernimmt die ersten zwei Schritte, Symfony Messenger übernimmt den dritten asynchron. Das Ergebnis: Der Webhook-Controller ist minimal, die Verarbeitungslogik ist isoliert testbar, und Fehler bei der Verarbeitung blockieren nicht den Eingang weiterer Webhooks.

Typsicherheit bei Webhooks bedeutet konkret: Statt $payload['data']['object']['amount'] mit direktem Array-Zugriff (der bei fehlendem Key einen null-Wert oder eine undefinierte Index-Warnung erzeugt) gibt es ein StripePaymentIntentEvent-DTO mit typisierten Eigenschaften. Wenn Stripe einen Webhook mit fehlendem amount-Feld sendet, fliegt beim Deserialisieren eine klare Exception, keine stille null-Verarbeitung. Dieser Ansatz macht Webhook-Handling testbar, vorhersehbar und wartbar.

2. Das Symfony Webhook Component installieren

Das Symfony Webhook-Component ist seit Symfony 6.3 verfügbar und wird über Composer als separates Paket installiert. Es bringt den WebhookController mit, der als zentraler Eintrittspunkt für alle eingehenden Webhooks dient, sowie das RemoteEvent-System, das geparste Webhook-Payloads typsicher repräsentiert. Für häufige Dienste wie Stripe, GitHub und Twilio gibt es bereits fertige Parser-Pakete, die das manuelle Schreiben eigener Parser ersparen.

Der WebhookController ist ein Services-First-Controller: Er wird über Routing-Konfiguration unter einem konfigurierbaren Pfad (standard: /webhook/{type}) registriert und leitet eingehende Requests an den passenden RequestParser weiter. Der Parser ist für einen spezifischen Dienst-Typ zuständig, prüft die Signatur, extrahiert das Event-Typ-Label aus den HTTP-Headern und deserialisiert den JSON-Payload in ein typisiertes RemoteEvent-Objekt. Dieses Objekt wird dann an einen RemoteEventConsumer weitergereicht, der die Business-Logik enthält – oder über Symfony Messenger asynchron versendet.


<?php
// Installation commands:
// composer require symfony/webhook
// composer require symfony/messenger    # for async processing
// composer require symfony/remote-event # pulled by webhook automatically

// config/routes/webhook.yaml — register the webhook entry controller
// webhook:
//   resource: '@WebhookBundle/config/routing.php'
//   prefix: /webhook

// Or manually in config/routes.yaml:
// stripe_webhook:
//   path: /webhook/stripe
//   controller: Symfony\Component\Webhook\Controller\MainController
//   defaults: { type: stripe }
//   methods: [POST]

// Check available webhook types after install:
// bin/console debug:router | grep webhook
// bin/console debug:container --tag=webhook.request_parser

3. HMAC-Signaturprüfung: Absender verifizieren

Die HMAC-Signaturprüfung ist der kritischste Sicherheitsschritt bei der Symfony Webhook-Verarbeitung. Ohne sie kann jeder beliebige Client POST-Requests an den Webhook-Endpunkt senden und Zahlungsbestätigungen, Bestellupdates oder Benutzer-Events simulieren. Der externe Dienst – etwa Stripe oder GitHub – signiert den Webhook-Payload mit einem gemeinsam geteilten Secret. Der Empfänger berechnet dieselbe HMAC-Signatur und vergleicht sie mit der im HTTP-Header übermittelten. Weichen sie ab, wird der Request mit 400 oder 403 abgewiesen, ohne die Business-Logik zu erreichen.

In Symfony implementiert man die Signaturprüfung im RequestParser, genauer in der Methode createRejectedResponse() und parse(). Die Methode parse() erhält das vollständige Request-Objekt und gibt ein RemoteEvent zurück oder wirft eine Exception, wenn die Signaturprüfung fehlschlägt. Wichtig: Der rohe Request-Body muss für die HMAC-Berechnung verwendet werden, nicht der geparste JSON-Array – weil die Signatur über den byte-genauen HTTP-Body berechnet wurde. $request->getContent() liefert den Roh-Body als String.

Die HMAC-Berechnung selbst verwendet hash_hmac('sha256', $rawBody, $secret) und vergleicht das Ergebnis mit dem Header-Wert über hash_equals() statt ===. Letzteres ist entscheidend für die Sicherheit: hash_equals() führt einen zeitkonstanten Vergleich durch, der keine Timing-Angriffe erlaubt, bei denen ein Angreifer aus der Antwortzeit Rückschlüsse auf teilweise korrekte Signaturen ziehen könnte. Dieser Symfony Webhook-Sicherheitsschritt kostet eine Zeile Code und verhindert eine ganze Klasse von Angriffsvektoren.

4. Eigene WebhookParser für verschiedene Dienste

Für jeden externen Dienst, von dem Symfony Webhooks empfangen werden, implementiert man einen eigenen RequestParserInterface. Die Klasse trägt das Service-Tag webhook.request_parser mit dem Typ-Attribut, das mit dem Routing-Typ übereinstimmt. Die wichtigste Methode ist parse(Request $request, #[SensitiveParameter] string $secret): RemoteEvent: Sie prüft die Signatur, extrahiert den Event-Typ aus dem Header oder dem Body, und erstellt ein typisiertes RemoteEvent-Objekt mit dem deserialisierten Payload.

Ein realistisches Szenario: GitHub sendet Webhooks mit dem Header X-GitHub-Event (Event-Typ) und X-Hub-Signature-256 (HMAC-SHA256-Signatur mit sha256=-Präfix). Der Parser liest beide Header, prüft die Signatur gegen das konfigurierte Secret und erstellt je nach Event-Typ (push, pull_request, release) ein unterschiedliches RemoteEvent-Objekt. Für Dienste mit vielen Event-Typen ist eine Factory oder ein Switch innerhalb des Parsers sinnvoll, die den rohen JSON-Body in den passenden typierten Event-DTO übersetzt.


<?php

declare(strict_types=1);

namespace App\Webhook\Parser;

use App\Webhook\Event\StripePaymentIntentEvent;
use App\Webhook\Event\StripeCheckoutSessionEvent;
use SensitiveParameter;
use Symfony\Component\HttpFoundation\ChainRequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\RemoteEvent\RemoteEvent;
use Symfony\Component\Webhook\Client\AbstractRequestParser;
use Symfony\Component\Webhook\Exception\RejectWebhookException;

/**
 * Parser for incoming Stripe webhook events.
 * Validates HMAC-SHA256 signature and maps payload to typed RemoteEvent objects.
 */
final class StripeRequestParser extends AbstractRequestParser
{
    /**
     * Only match POST requests to avoid processing GET health checks etc.
     */
    protected function getRequestMatcher(): RequestMatcherInterface
    {
        return new ChainRequestMatcher([
            new \Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher('POST'),
        ]);
    }

    /**
     * Validate the Stripe signature and parse the payload into a typed event.
     *
     * @throws RejectWebhookException if signature is invalid or payload is malformed
     */
    protected function doParse(Request $request, #[SensitiveParameter] string $secret): RemoteEvent
    {
        // Stripe sends the raw timestamp + payload hash in the Stripe-Signature header
        $signatureHeader = $request->headers->get('Stripe-Signature', '');
        $rawBody         = $request->getContent();

        if (!$this->isSignatureValid($rawBody, $signatureHeader, $secret)) {
            throw new RejectWebhookException(Response::HTTP_FORBIDDEN, 'Invalid Stripe webhook signature.');
        }

        $payload = json_decode($rawBody, true, flags: JSON_THROW_ON_ERROR);

        $eventType = $payload['type'] ?? null;
        if ($eventType === null) {
            throw new RejectWebhookException(Response::HTTP_BAD_REQUEST, 'Missing event type in Stripe payload.');
        }

        // Map Stripe event type to typed DTO — extend for more event types
        $eventObject = match (true) {
            str_starts_with($eventType, 'payment_intent.') => StripePaymentIntentEvent::fromPayload($payload),
            str_starts_with($eventType, 'checkout.session.') => StripeCheckoutSessionEvent::fromPayload($payload),
            default => new RemoteEvent($eventType, $payload['id'] ?? uniqid(), $payload),
        };

        return $eventObject;
    }

    /**
     * Verify Stripe HMAC-SHA256 signature using constant-time comparison.
     * Stripe signature format: t=timestamp,v1=signature
     */
    private function isSignatureValid(string $rawBody, string $signatureHeader, string $secret): bool
    {
        // Parse the Stripe-Signature header: "t=1234567890,v1=abc123..."
        $parts = [];
        foreach (explode(',', $signatureHeader) as $part) {
            [$key, $value] = explode('=', $part, 2) + ['', ''];
            $parts[$key] = $value;
        }

        $timestamp = $parts['t'] ?? null;
        $v1Signature = $parts['v1'] ?? null;

        if ($timestamp === null || $v1Signature === null) {
            return false;
        }

        // Stripe signs: timestamp + '.' + raw_payload
        $signedPayload = $timestamp . '.' . $rawBody;
        $expected      = hash_hmac('sha256', $signedPayload, $secret);

        // Constant-time comparison prevents timing attacks
        return hash_equals($expected, $v1Signature);
    }
}

5. Typsichere Webhook-Event-Objekte mit DTOs

Typsichere Webhook-Events in Symfony sind DTOs (Data Transfer Objects), die von RemoteEvent erben und alle relevanten Felder des externen Payloads als typisierte PHP-Eigenschaften abbilden. Statt $event->getPayload()['data']['object']['amount_received'] gibt es $event->amountReceived mit dem Typ int. Das eliminiert Array-Zugriffsfehler zur Laufzeit und macht den Code für IDEs vollständig analysierbar. Wenn Stripe ein Feld umbenennt oder entfernt, schlägt der Parser sofort fehl – nicht irgendwo mitten in der Verarbeitungslogik.

Die statische Factory-Methode fromPayload(array $payload) im DTO ist der Ort, wo unsichere Array-Daten in typisierte PHP-Werte verwandelt werden. Hier ist Fehlerbehandlung wichtig: Fehlt ein Pflichtfeld, wirft die Factory eine RejectWebhookException, damit der Controller mit 400 antworten kann, statt einen null-Wert in die Verarbeitungslogik zu schicken. Optionale Felder werden mit Nullable-Typen (?string) oder Default-Werten abgebildet. Mit dem Symfony Serializer lässt sich das JSON-Deserialisieren in DTOs auch automatisieren, wenn die DTO-Klassen vollständig getypt sind.

Für komplexe Webhook-Payloads – etwa Shopify-Bestellungen mit verschachtelten Line-Items, Adressen und Metadaten – lohnen sich Nested DTOs. Die Order-Payload wird in ein ShopifyOrderCreatedEvent deserialisiert, das selbst eine Liste von ShopifyLineItem-Objekten und ein ShopifyAddress-Objekt enthält. Der Symfony Serializer mit ObjectNormalizer und Type-Hints erledigt das Nesting automatisch, wenn die DTO-Klassen korrekt typisiert sind.

6. Asynchrone Verarbeitung mit Symfony Messenger

Die goldene Regel bei Symfony Webhooks: So schnell wie möglich mit 200 OK antworten und die eigentliche Verarbeitungslogik asynchron ausführen. Externe Dienste wie Stripe oder GitHub haben oft kurze Timeout-Fenster (5–30 Sekunden). Wenn die Verarbeitungslogik – Datenbankoperationen, E-Mail-Versand, externe API-Calls – länger dauert, kommt es zu Timeouts, und der externe Dienst wiederholt den Webhook erneut. Das führt zu Doppelverarbeitung, wenn keine Idempotenz implementiert ist.

Mit Symfony Messenger wird das RemoteEvent-Objekt als Message auf einen Queue geschrieben, sobald die Signaturprüfung erfolgreich war. Der Webhook-Endpunkt antwortet sofort mit 200 OK. Ein Worker-Prozess im Hintergrund (gestartet mit bin/console messenger:consume webhook) liest die Messages aus der Queue und führt die Verarbeitungslogik aus. Wenn die Verarbeitung fehlschlägt, wiederholt der Worker die Message automatisch gemäß der konfigurierten Retry-Strategie – ohne dass der externe Dienst denselben Webhook erneut senden muss.


<?php

declare(strict_types=1);

namespace App\Webhook\Consumer;

use App\Webhook\Event\StripePaymentIntentEvent;
use App\Service\OrderService;
use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer;
use Symfony\Component\RemoteEvent\Consumer\ConsumerInterface;
use Symfony\Component\RemoteEvent\RemoteEvent;

/**
 * Consumes Stripe payment_intent webhook events and updates order state.
 * This class is called asynchronously via Symfony Messenger after signature validation.
 */
#[AsRemoteEventConsumer('stripe')]
final readonly class StripeWebhookConsumer implements ConsumerInterface
{
    public function __construct(
        private OrderService $orderService,
    ) {}

    /**
     * Handle the incoming Stripe RemoteEvent.
     * Only processes payment_intent events — others are silently ignored.
     */
    public function consume(RemoteEvent $event): void
    {
        // Only handle typed events we explicitly know about
        if (!$event instanceof StripePaymentIntentEvent) {
            return;
        }

        // Dispatch to the correct handler based on Stripe event type
        match ($event->getName()) {
            'payment_intent.succeeded'       => $this->orderService->markAsPaid($event),
            'payment_intent.payment_failed'  => $this->orderService->markAsPaymentFailed($event),
            'payment_intent.canceled'        => $this->orderService->markAsCanceled($event),
            default                          => null, // Unknown sub-type — ignore safely
        };
    }
}

// config/packages/messenger.yaml — route remote events to async transport
// framework:
//   messenger:
//     transports:
//       webhook:
//         dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
//         retry_strategy:
//           max_retries: 3
//           delay: 1000
//           multiplier: 2
//     routing:
//       'Symfony\Component\RemoteEvent\RemoteEvent': webhook

7. Retry-Strategien und Fehlerbehandlung

Wenn die asynchrone Verarbeitung eines Symfony Webhooks fehlschlägt – etwa weil eine externe API nicht erreichbar ist oder eine Datenbankoperation scheitert – greift die Retry-Strategie von Symfony Messenger. Die Standardkonfiguration versucht die Verarbeitung maximal drei Mal, mit exponentiell wachsenden Wartezeiten: 1 Sekunde, 2 Sekunden, 4 Sekunden. Nach dem letzten fehlgeschlagenen Versuch landet die Message in der Failure-Transport (Dead Letter Queue), wo sie manuell inspiziert und erneut versendet werden kann.

Für die Unterscheidung zwischen transienten Fehlern (Netzwerk-Timeout, der sich wiederholt) und permanenten Fehlern (ungültiger Payload, der immer fehlschlagen wird) verwendet man die Klasse UnrecoverableMessageHandlingException. Wenn der Handler diese Exception wirft, wiederholt Messenger die Message nicht, sondern verschiebt sie sofort in die Failure-Transport. So verhindert man, dass ein dauerhaft ungültiger Webhook-Payload die Worker-Queue mit erfolglosen Wiederholungsversuchen belastet.

Monitoring der Webhook-Verarbeitung: Symfony Messenger stellt den Command bin/console messenger:failed:show bereit, der alle fehlgeschlagenen Messages in der Failure-Transport auflistet, und messenger:failed:retry zum manuellen Wiederholen. Für Produktionsumgebungen empfiehlt sich Laravel Horizon-ähnliches Monitoring mit Sentry oder einem eigenen Dashboard, das den Messenger-Status über die Datenbankstatistiken abfragt. Die Tabelle messenger_messages enthält alle ausstehenden und fehlgeschlagenen Messages mit Timestamps.

8. Idempotenz: doppelte Webhooks sicher abfangen

Externe Dienste garantieren oft "at-least-once"-Lieferung ihrer Webhooks: Der gleiche Event kann mehrfach ankommen, wenn die erste Antwort verloren geht oder das externe System einen Timeout erkennt und den Webhook wiederholt. Ohne Idempotenz-Prüfung wird dieselbe Bestellung zweimal als bezahlt markiert, derselbe Newsletter-Empfänger zweimal eingetragen oder dasselbe Kundenkonto zweimal erstellt. Das ist das zweithäufigste Webhook-Problem nach fehlender Signaturprüfung.

Die Lösung: Jeder Webhook-Event eines seriösen Dienstes trägt eine eindeutige Event-ID. Stripe-Events haben eine id wie evt_1234..., GitHub-Events haben einen X-GitHub-Delivery-Header. Diese ID wird beim ersten Verarbeitungsversuch in einer Idempotenz-Tabelle gespeichert. Bei wiederholtem Eingang prüft der Consumer, ob die ID bereits verarbeitet wurde, und gibt sofort zurück, ohne die Business-Logik erneut auszuführen. Diese Prüfung muss atomar sein – ein Unique-Index auf die Idempotenz-Tabelle verhindert Race Conditions bei parallelen Verarbeitungsversuchen.

Im Symfony Messenger-Kontext kann die Idempotenz-Prüfung als Middleware implementiert werden: Eine IdempotencyMiddleware prüft vor jedem Handler-Aufruf die Event-ID in der Datenbank. Ist sie bereits vorhanden, wird die Message als erfolgreich markiert (kein Fehler, kein Retry). Ist sie neu, wird die ID gespeichert und der Handler aufgerufen. Bei einem Handler-Fehler nach dem Speichern der ID wird die ID wieder entfernt, sodass der Retry die Business-Logik erneut ausführen kann.

9. Vergleich: Webhook-Verarbeitungsstrategien

Es gibt mehrere Ansätze für Symfony Webhooks, von der einfachen synchronen Verarbeitung bis zur vollständigen Messenger-Integration. Die Wahl hängt vom Webhook-Volumen, der Verarbeitungsdauer und den Anforderungen an Zuverlässigkeit ab.

Strategie Vorteil Nachteil Einsatz
Synchron im Controller Kein Setup nötig Timeout-Risiko, keine Retry Nur für sehr schnelle Operationen
Messenger + Database Retry, Monitoring, Persistence Datenbankabhängigkeit Empfohlen für die meisten Projekte
Messenger + Redis Sehr schnell, horizontal skalierbar Bei Absturz gehen Messages verloren Hohe Volumina, tolerante Systeme
Messenger + AMQP/SQS Garantierte Lieferung, Dead Letter Setup-Aufwand, externe Abhängigkeit Enterprise, kritische Events
Ohne Signaturprüfung Sicherheitslücke Niemals in Produktion

Die empfohlene Symfony Webhook-Architektur für die meisten Produktionsprojekte: Messenger mit Doctrine-Transport für Zuverlässigkeit und einfaches Monitoring. Der Doctrine-Transport speichert alle Messages in der Datenbank, überlebt Neustarts und ermöglicht manuelle Inspektion über SQL-Queries oder das messenger:failed:show-Command. Für sehr hohe Webhook-Volumina (über 1.000 Events pro Minute) ist ein externer Broker wie RabbitMQ oder AWS SQS mit dem AMQP-Transport besser geeignet.

Mironsoft

Symfony-Integration, Webhook-Architekturen und asynchrone Verarbeitung

Webhooks sicher und zuverlässig in Symfony integrieren?

Wir entwickeln sichere Webhook-Integrationen für Symfony-Projekte – von der HMAC-Signaturprüfung über typsichere Event-DTOs bis zur asynchronen Messenger-Verarbeitung mit Retry und Idempotenz-Garantie.

Webhook-Security

HMAC-Signaturprüfung, Replay-Schutz und Idempotenz für Stripe, GitHub und eigene Dienste

Async-Verarbeitung

Symfony Messenger mit Retry-Strategie, Dead Letter Queue und Monitoring für zuverlässige Webhook-Verarbeitung

Typsichere Events

RemoteEvent-DTOs für alle relevanten Dienst-Events mit vollständiger Typsicherheit und Testbarkeit

10. Zusammenfassung

Eingehende Symfony Webhooks robust zu verarbeiten bedeutet: HMAC-Signaturprüfung für jeden Request, typsichere Event-DTOs statt roher Array-Zugriffe, sofortige 200-Antwort mit asynchroner Verarbeitung über Symfony Messenger und Idempotenz-Prüfung gegen doppelte Verarbeitung. Das Symfony Webhook Component stellt die nötige Infrastruktur bereit, eigene RequestParser-Klassen und RemoteEventConsumer fügen die projekt-spezifische Logik hinzu. Die Kombination aus HMAC, Messenger-Retry und Idempotenz macht die Webhook-Verarbeitung produktionstauglich.

Die Architektur zahlt sich besonders bei wachsendem Webhook-Volumen aus: Neue Event-Typen werden als neue DTO-Klassen hinzugefügt, neue Dienste als neue Parser-Klassen. Der Webhook-Endpunkt selbst bleibt minimal und braucht bei Erweiterungen nicht angefasst werden. Testing ist auf allen Ebenen möglich: Unit-Tests für Parser und Consumer, Integrationstests mit gemockten HTTP-Requests und echten Event-Objekten.

Symfony Webhooks — Das Wichtigste auf einen Blick

Signaturprüfung

HMAC-SHA256 im RequestParser mit hash_equals() für zeitkonstanten Vergleich. Roh-Body für die Berechnung nutzen, nicht den geparsten JSON-Array.

Typsichere Events

RemoteEvent-DTOs mit fromPayload()-Factory statt direktem Array-Zugriff. Fehlende Pflichtfelder werfen RejectWebhookException für klare 400-Antworten.

Async mit Messenger

Sofort 200 antworten, Verarbeitung in die Queue. Retry-Strategie konfigurieren, UnrecoverableException für dauerhaft ungültige Payloads nutzen.

Idempotenz

Event-ID in Idempotenz-Tabelle speichern. Unique-Index verhindert Race Conditions. Bei wiederholtem Eingang sofort erfolgreich zurückgeben.

11. FAQ: Symfony Webhooks und externe Ereignisse

1Was ist das Symfony Webhook Component?
Infrastruktur für eingehende Webhooks seit Symfony 6.3: WebhookController, RequestParserInterface für Signaturprüfung und Parsing, RemoteEvent-System für typsichere Event-Objekte.
2Warum HMAC-Signaturprüfung?
Verifiziert den Absender. Ohne Prüfung kann jeder beliebige Client Payloads senden. hash_equals() verhindert Timing-Angriffe beim Signaturvergleich.
3Warum sofort 200 antworten?
Externe Dienste haben kurze Timeout-Fenster. Timeout führt zu Wiederholung und ohne Idempotenz zu Doppelverarbeitung. Messenger nimmt die Verarbeitung asynchron über.
4Was ist Idempotenz bei Webhooks?
Sicherstellen, dass dasselbe Event nur einmal verarbeitet wird. Event-ID in Tabelle mit Unique-Index speichern. Bei erneutem Eingang sofort erfolgreich zurückgeben.
5Eigenen RequestParser implementieren?
AbstractRequestParser erweitern, doParse() überschreiben: Signatur prüfen, Event-Typ extrahieren, RemoteEvent-Objekt zurückgeben. Service-Tag webhook.request_parser mit Typ-Attribut.
6Messenger für Webhook-Verarbeitung?
RemoteEvent als Message routen, Consumer mit ConsumerInterface implementieren. Retry-Strategie in messenger.yaml. UnrecoverableException für dauerhaft ungültige Payloads.
7Was tun bei doppelten Webhooks?
Idempotenz-Tabelle mit Event-ID und Unique-Index. Bei wiederholtem Eingang sofort zurückgeben ohne Geschäftslogik. Atomic: Unique-Index verhindert Race Conditions.
8Welcher Messenger-Transport empfohlen?
Doctrine-Transport für die meisten Projekte: persistent, überlebt Neustarts, einfach inspizierbar. RabbitMQ/SQS für sehr hohe Volumina. Redis schnell aber nicht crashsicher.
9Wie teste ich Webhook-Parser?
Unit: Request::create() mit Signatur-Header, parse() aufrufen, RemoteEvent-Typ prüfen. Integration: HTTP-POST via WebTestCase, Controller muss 200 zurückgeben und Message in Queue landen.
10Was ist UnrecoverableMessageHandlingException?
Signalisiert Messenger: kein Retry, direkt in Failure-Transport. Für dauerhaft ungültige Payloads oder Geschäftsfehler, die kein Retry löst. Spart Retry-Versuche für transiente Fehler.