- 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
205 lines
6.9 KiB
PHP
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));
|
|
}
|
|
}
|