send($request); */ final readonly class ApiGateway { public function __construct( private HttpClient $httpClient, private CircuitBreakerManager $circuitBreakerManager, private ApiMetrics $metrics, private OperationTracker $operationTracker, private ?Logger $logger = null ) { } /** * Send an API request through the gateway * * @param ApiRequest $request The API request to send * @return ClientResponse The response from the API * @throws ApiGatewayException If the request fails after all retries * @throws CircuitBreakerException If the circuit breaker is open */ public function send(ApiRequest $request): ClientResponse { $service = $request->getEndpoint()->getServiceIdentifier(); $requestName = $request->getRequestName(); // Start performance tracking $operationId = "api_gateway.{$service}.{$requestName}." . uniqid(); $snapshot = $this->operationTracker->startOperation( operationId: $operationId, category: PerformanceCategory::HTTP, contextData: [ 'service' => $service, 'request_name' => $requestName, 'method' => $request->getMethod()->value, 'endpoint' => $request->getEndpoint()->toString(), ] ); $retryAttempts = 0; $circuitBreakerTriggered = false; // Log request start $this->logger?->info("[ApiGateway] Sending request", [ 'operation_id' => $operationId, 'service' => $service, 'request_name' => $requestName, 'method' => $request->getMethod()->value, 'endpoint' => $request->getEndpoint()->toString(), ]); // Get circuit breaker for this service $circuitBreaker = $this->circuitBreakerManager->getCircuitBreaker($service); $circuitBreakerConfig = CircuitBreakerConfig::forHttpClient(); // Check circuit breaker status (throws if open) try { $circuitBreaker->check($service, $circuitBreakerConfig); } catch (CircuitBreakerException $e) { $circuitBreakerTriggered = true; $this->logger?->warning("[ApiGateway] Circuit breaker open", [ 'operation_id' => $operationId, 'service' => $service, 'request_name' => $requestName, ]); // Record failure and metrics $completedSnapshot = $this->operationTracker->failOperation($operationId, $e); if ($completedSnapshot !== null) { $this->recordMetrics($service, $requestName, $completedSnapshot, false, 0, true); } throw $e; } // Convert ApiRequest to ClientRequest $clientRequest = $this->buildClientRequest($request); // Execute with retry strategy if configured $retryStrategy = $request->getRetryStrategy(); try { if ($retryStrategy !== null) { $result = $this->sendWithRetry($clientRequest, $retryStrategy, $service, $requestName, $operationId); $response = $result['response']; $retryAttempts = $result['attempts']; } else { $response = $this->httpClient->send($clientRequest); } // Record success with circuit breaker $circuitBreaker->recordSuccess($service, $circuitBreakerConfig); $this->logger?->info("[ApiGateway] Request successful", [ 'operation_id' => $operationId, 'service' => $service, 'request_name' => $requestName, 'status' => $response->status->value, 'retry_attempts' => $retryAttempts, ]); // Complete operation tracking and record metrics $completedSnapshot = $this->operationTracker->completeOperation($operationId); if ($completedSnapshot !== null) { $this->recordMetrics($service, $requestName, $completedSnapshot, true, $retryAttempts, false); } return $response; } catch (Throwable $exception) { // Record failure with circuit breaker $circuitBreaker->recordFailure($service, $exception, $circuitBreakerConfig); $this->logger?->error("[ApiGateway] Request failed", [ 'operation_id' => $operationId, 'service' => $service, 'request_name' => $requestName, 'error' => $exception->getMessage(), 'exception_class' => get_class($exception), 'retry_attempts' => $retryAttempts, ]); // Record failure and metrics $completedSnapshot = $this->operationTracker->failOperation($operationId, $exception); if ($completedSnapshot !== null) { $this->recordMetrics($service, $requestName, $completedSnapshot, false, $retryAttempts, $circuitBreakerTriggered); } throw new ApiGatewayException( "API request failed for {$requestName}", 0, $exception ); } } /** * Build ClientRequest from ApiRequest */ private function buildClientRequest(ApiRequest $request): ClientRequest { // Convert timeout Duration to seconds for ClientOptions $timeoutSeconds = $request->getTimeout()->toSeconds(); $options = new ClientOptions( timeout: $timeoutSeconds, connectTimeout: min(3, $timeoutSeconds), // Connect timeout max 3s or total timeout ); // Use factory method for JSON requests if payload is present if ($request instanceof HasPayload) { return ClientRequest::json( method: $request->getMethod(), url: $request->getEndpoint()->toString(), data: $request->getPayload(), options: $options )->with([ 'headers' => $request->getHeaders(), ]); } // Simple request without body return new ClientRequest( method: $request->getMethod(), url: $request->getEndpoint()->toString(), headers: $request->getHeaders(), body: '', options: $options ); } /** * Send request with retry strategy * * @return array{response: ClientResponse, attempts: int} */ private function sendWithRetry( ClientRequest $request, RetryStrategy $retryStrategy, string $service, string $requestName, string $operationId ): array { $attempt = 0; while (true) { try { if ($attempt > 0) { $this->logger?->info("[ApiGateway] Retry attempt", [ 'operation_id' => $operationId, 'service' => $service, 'request_name' => $requestName, 'attempt' => $attempt, ]); } $response = $this->httpClient->send($request); return [ 'response' => $response, 'attempts' => $attempt, ]; } catch (Throwable $exception) { $attempt++; if (!$retryStrategy->shouldRetry($exception, $attempt)) { $this->logger?->warning("[ApiGateway] Retry limit reached", [ 'operation_id' => $operationId, 'service' => $service, 'request_name' => $requestName, 'attempts' => $attempt, ]); throw $exception; } $delay = $retryStrategy->getDelay($attempt); $this->logger?->info("[ApiGateway] Retrying after delay", [ 'operation_id' => $operationId, 'service' => $service, 'request_name' => $requestName, 'attempt' => $attempt, 'delay_ms' => $delay->toMilliseconds(), ]); // Sleep before retry usleep($delay->toMicroseconds()); } } } /** * Record metrics from performance snapshot */ private function recordMetrics( string $service, string $requestName, \App\Framework\Performance\PerformanceSnapshot $snapshot, bool $success, int $retryAttempts, bool $circuitBreakerTriggered ): void { $this->metrics->recordRequest( service: $service, requestName: $requestName, durationMs: $snapshot->duration->toMilliseconds(), success: $success, retryAttempts: $retryAttempts, circuitBreakerTriggered: $circuitBreakerTriggered ); } }