--- name: acc-create-psr18-http-client description: Generates PSR-18 HTTP Client implementation for PHP 8.5. Creates ClientInterface with request sending and exception handling. Includes unit tests. --- # PSR-18 HTTP Client Generator ## Overview Generates PSR-18 compliant HTTP client implementations for external API communication. ## When to Use - External API integrations - Microservice communication - HTTP-based service calls - Building HTTP client wrappers ## Template: HTTP Client ```php configureCurl($ch, $request); $response = curl_exec($ch); if ($response === false) { throw new NetworkException( $request, curl_error($ch), curl_errno($ch), ); } $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $headerString = substr($response, 0, $headerSize); $body = substr($response, $headerSize); return $this->buildResponse($statusCode, $headerString, $body); } finally { curl_close($ch); } } private function configureCurl($ch, RequestInterface $request): void { $options = [ CURLOPT_URL => (string) $request->getUri(), CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_TIMEOUT => $this->options['timeout'] ?? 30, CURLOPT_CONNECTTIMEOUT => $this->options['connect_timeout'] ?? 10, CURLOPT_CUSTOMREQUEST => $request->getMethod(), ]; // Set headers $headers = []; foreach ($request->getHeaders() as $name => $values) { $headers[] = $name . ': ' . implode(', ', $values); } $options[CURLOPT_HTTPHEADER] = $headers; // Set body $body = (string) $request->getBody(); if ($body !== '') { $options[CURLOPT_POSTFIELDS] = $body; } // SSL options if (isset($this->options['verify_ssl']) && !$this->options['verify_ssl']) { $options[CURLOPT_SSL_VERIFYPEER] = false; $options[CURLOPT_SSL_VERIFYHOST] = 0; } curl_setopt_array($ch, $options); } private function buildResponse(int $statusCode, string $headerString, string $body): ResponseInterface { $headers = $this->parseHeaders($headerString); return (new Response($statusCode)) ->withBody(new Stream($body)) ->withHeaders($headers); } private function parseHeaders(string $headerString): array { $headers = []; $lines = explode("\r\n", trim($headerString)); foreach ($lines as $line) { if (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $headers[trim($name)] = [trim($value)]; } } return $headers; } } ``` ## Template: Exceptions ```php request; } } final class RequestException extends \RuntimeException implements RequestExceptionInterface { public function __construct( private readonly RequestInterface $request, string $message = '', int $code = 0, ?\Throwable $previous = null, ) { parent::__construct($message, $code, $previous); } public function getRequest(): RequestInterface { return $this->request; } } ``` ## Template: Logging Client Decorator ```php $request->getMethod(), 'uri' => (string) $request->getUri(), ]; $this->logger->info('HTTP request starting', $context); $startTime = microtime(true); try { $response = $this->client->sendRequest($request); $duration = microtime(true) - $startTime; $this->logger->info('HTTP request completed', [ ...$context, 'status' => $response->getStatusCode(), 'duration_ms' => round($duration * 1000, 2), ]); return $response; } catch (\Throwable $e) { $this->logger->error('HTTP request failed', [ ...$context, 'error' => $e->getMessage(), ]); throw $e; } } } ``` ## Usage Example ```php 30]), $logger, ); // Send GET request $request = $factory->createRequest('GET', 'https://api.example.com/users') ->withHeader('Authorization', 'Bearer token'); $response = $client->sendRequest($request); $data = json_decode((string) $response->getBody(), true); // Send POST request $request = $factory->createRequest('POST', 'https://api.example.com/users') ->withHeader('Content-Type', 'application/json') ->withBody($factory->createStream(json_encode(['name' => 'John']))); $response = $client->sendRequest($request); ``` ## Requirements ```json { "require": { "psr/http-client": "^1.0", "psr/http-message": "^2.0" } } ```