Files
michaelschiemer/src/Framework/Webhook/Processing/WebhookRequestHandler.php
Michael Schiemer e30753ba0e fix: resolve RedisCache array offset error and improve discovery diagnostics
- Fix RedisCache driver to handle MGET failures gracefully with fallback
- Add comprehensive discovery context comparison debug tools
- Identify root cause: WEB context discovery missing 166 items vs CLI
- WEB context missing RequestFactory class entirely (52 vs 69 commands)
- Improved exception handling with detailed binding diagnostics
2025-09-12 20:05:18 +02:00

205 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Webhook\Processing;
use App\Framework\Core\Events\EventDispatcher;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\Status;
use App\Framework\Logging\Logger;
use App\Framework\Router\Result\JsonResult;
use App\Framework\Webhook\Events\WebhookFailed;
use App\Framework\Webhook\Events\WebhookReceived;
use App\Framework\Webhook\Security\SignatureVerifier;
use App\Framework\Webhook\ValueObjects\WebhookPayload;
use App\Framework\Webhook\ValueObjects\WebhookProvider;
use Exception;
/**
* Central webhook request handler using EventDispatcher
* Handles incoming webhook requests, verifies signatures, and dispatches events
*/
final readonly class WebhookRequestHandler
{
public function __construct(
private SignatureVerifier $signatureVerifier,
private EventDispatcher $eventDispatcher,
private IdempotencyService $idempotencyService,
private Logger $logger
) {
}
/**
* Process incoming webhook request
* Returns JSON response with processing status
*/
public function handle(
HttpRequest $request,
WebhookProvider $provider,
string $secret,
array $allowedEvents = []
): HttpResponse {
$requestId = $this->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));
}
}