generateRequestId(); $rawPayload = $request->getRawBody(); try { // Step 1: Verify signature if (! $this->verifySignature($request, $rawPayload, $provider, $secret)) { $this->logger->warning('Webhook signature verification failed', [ 'provider' => $provider->toString(), 'request_id' => $requestId, 'ip' => $request->server->getClientIp(), 'user_agent' => $request->server->getUserAgent()?->toString(), ]); return $this->errorResponse('Invalid signature', 401, $requestId); } // Step 2: Parse payload $payload = WebhookPayload::fromRaw($rawPayload); $eventType = $payload->getEventType(); // Step 3: Check allowed events if (! empty($allowedEvents) && ! in_array($eventType, $allowedEvents, true)) { $this->logger->info('Webhook event not allowed', [ 'provider' => $provider->toString(), 'event_type' => $eventType, 'allowed_events' => $allowedEvents, 'request_id' => $requestId, ]); return $this->successResponse('Event ignored', $requestId); } // Step 4: Check idempotency $webhookId = $payload->getWebhookId(); if ($webhookId && $this->idempotencyService->isDuplicate($webhookId, $provider)) { $this->logger->info('Duplicate webhook request detected', [ 'webhook_id' => $webhookId, 'provider' => $provider->toString(), 'request_id' => $requestId, ]); return $this->successResponse('Webhook already processed', $requestId); } // Step 5: Mark as processing if ($webhookId) { $this->idempotencyService->markProcessing($webhookId, $provider); } // Step 6: Create and dispatch webhook received event $webhookEvent = WebhookReceived::create( provider: $provider, payload: $payload, endpoint: $request->getPath(), eventType: $eventType ); // Dispatch using EventDispatcher - returns array of handler results $results = $this->eventDispatcher->dispatch($webhookEvent); // Step 7: Mark as processed if ($webhookId) { $this->idempotencyService->markProcessed($webhookId, $provider); } $this->logger->info('Webhook processed successfully', [ 'provider' => $provider->toString(), 'event_type' => $eventType, 'webhook_id' => $webhookId, 'request_id' => $requestId, 'handlers_executed' => count($results), 'processing_time' => microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'], ]); return $this->successResponse('Webhook processed', $requestId, [ 'event_type' => $eventType, 'webhook_id' => $webhookId, 'handlers_executed' => count($results), ]); } catch (Exception $e) { // Step 8: Handle processing errors if (isset($webhookId)) { $this->idempotencyService->markFailed($webhookId, $provider, $e->getMessage()); } // Dispatch webhook failed event $failedEvent = WebhookFailed::create( provider: $provider, endpoint: $request->getPath(), error: $e->getMessage(), payload: isset($payload) ? $payload : null ); $this->eventDispatcher->dispatch($failedEvent); $this->logger->error('Webhook processing failed', [ 'provider' => $provider->toString(), 'request_id' => $requestId, 'error' => $e->getMessage(), 'webhook_id' => $webhookId ?? null, ]); return $this->errorResponse('Webhook processing failed', 500, $requestId); } } /** * Verify webhook signature using provider-specific verification */ private function verifySignature( HttpRequest $request, string $payload, WebhookProvider $provider, string $secret ): bool { $signatureHeader = $this->signatureVerifier->getSignatureHeader($provider); $signature = $request->headers->get($signatureHeader); if (empty($signature)) { return false; } return $this->signatureVerifier->verify($payload, $signature, $secret, $provider); } /** * Generate unique request ID for tracking */ private function generateRequestId(): string { return 'wh_' . bin2hex(random_bytes(8)) . '_' . time(); } /** * Create success response */ private function successResponse(string $message, string $requestId, array $data = []): JsonResult { return new JsonResult([ 'status' => 'success', 'message' => $message, 'request_id' => $requestId, 'timestamp' => date('c'), ...$data, ]); } /** * Create error response */ private function errorResponse(string $message, int $statusCode, string $requestId): JsonResult { return new JsonResult([ 'status' => 'error', 'message' => $message, 'request_id' => $requestId, 'timestamp' => date('c'), ], Status::from($statusCode)); } }