Ir a la documentación
PHP

Clientes HTTP

cURL de forma predeterminada, cualquier cliente PSR-18 si prefieres usar el tuyo, y uno falso para las pruebas.

El predeterminado

Sin httpClient:, las solicitudes pasan por OpenEmail\Http\CurlHttpClient, que no necesita más que la extensión curl. Mantiene un handle de cURL por cliente, así que una segunda solicitud reutiliza la conexión que abrió la primera, y la página de configuración enumera sus opciones para proxies, autoridades de certificación y ajustes adicionales de cURL.

Sea lo que sea lo que mueva los bytes, el propio cliente hace todo lo demás: la credencial, las claves de idempotencia, los reintentos y su espera, el tiempo de espera de cada intento y la excepción de cada fallo. Un cliente HTTP solo envía una solicitud y devuelve la respuesta.

Cualquier cliente PSR-18

OpenEmail\Http\Psr18HttpClient envuelve cualquier cliente que implemente PSR-18, como Guzzle o Symfony HttpClient, así que las solicitudes pasan por el cliente que tu aplicación ya configura, con su middleware, sus registros y su configuración de proxy.

guzzle.php
use GuzzleHttp\Client;use OpenEmail\Http\Psr18HttpClient;use OpenEmail\OpenEmail; $client = new OpenEmail(httpClient: new Psr18HttpClient(new Client())); $client->me->ping();
symfony_http_client.php
use OpenEmail\Http\Psr18HttpClient;use OpenEmail\OpenEmail;use Symfony\Component\HttpClient\HttpClient;use Symfony\Component\HttpClient\Psr18Client; $http = new Psr18Client(HttpClient::create(['timeout' => 30, 'max_redirects' => 0])); $client = new OpenEmail(httpClient: new Psr18HttpClient($http)); $client->me->ping();

PSR-18 construye sus solicitudes con factorías PSR-17. El adaptador usa el propio cliente cuando también es una factoría, como lo es Psr18Client de Symfony, después php-http/discovery cuando está instalado, después nyholm/psr7 o las de Guzzle, y lanza InvalidArgumentException indicando qué instalar cuando no encuentra ninguna. Pasa requestFactory: y streamFactory: para elegirlas tú. El Psr18Client de Symfony también necesita las interfaces de PSR-18 y una implementación de PSR-17, así que instálalo con composer require symfony/http-client psr/http-client nyholm/psr7.

factories.php
use GuzzleHttp\Client;use Nyholm\Psr7\Factory\Psr17Factory;use OpenEmail\Http\Psr18HttpClient;use OpenEmail\OpenEmail; $factory = new Psr17Factory(); $client = new OpenEmail(httpClient: new Psr18HttpClient(new Client(), requestFactory: $factory, streamFactory: $factory)); $client->me->ping();
AspectoQué ocurre
Tiempos de esperaGuzzle recibe el timeout: del cliente en cada solicitud. PSR-18 no tiene forma de pasar un tiempo de espera a ningún otro cliente, así que define uno en el cliente que envuelves, como hace HttpClient::create(['timeout' => 30, 'max_redirects' => 0]) más arriba, o una solicitud que nunca responde puede esperar para siempre.
Status de errorUn cliente PSR-18 devuelve las respuestas 4xx y 5xx en lugar de lanzar una excepción, y el adaptador le indica a Guzzle que haga lo mismo, así que cada rechazo sigue convirtiéndose en la ApiException correcta.
Sin respuestaUna ClientExceptionInterface del cliente envuelto se convierte en una NetworkException que conserva su mensaje pero no la excepción, que contiene la solicitud y su cabecera Authorization. isTimeout() es true cuando el mensaje dice que la solicitud agotó el tiempo de espera, y con cualquier cliente que no sea Guzzle el mensaje indica que fue el tiempo de espera propio del cliente envuelto.
RedireccionesA Guzzle se le indica que no las siga, y el ejemplo de Symfony pone max_redirects a 0, así que una redirección se convierte en una ApiException, igual que con el cliente cURL predeterminado.

Tu propio cliente

httpClient: acepta cualquier cosa que implemente OpenEmail\Http\HttpClient, una interfaz con un solo método: send(HttpRequest $request): HttpResponse. Devuelve la respuesta para cualquier status, 4xx y 5xx incluidos, y lanza una excepción solo cuando no llegó ninguna respuesta.

ClaseLo que lleva
HttpRequestmethod, url, headers como un array de nombre y valor, body como cadena o null, y timeout en segundos, o null si no hay. header($name) lee una cabecera sin distinguir mayúsculas. var_dump(), print_r() y json_encode() muestran su cabecera Authorization como [redacted], pero var_export() y el dump() de Symfony muestran headers tal cual, así que nunca registres una solicitud con esos dos.
HttpResponseSe construye como new HttpResponse($status, $headers, $body). Pasa a minúsculas los nombres de las cabeceras, y header($name) lee una sin distinguir mayúsculas.
HttpClientExceptionLánzala desde send cuando no llegó ninguna respuesta. new HttpClientException($message, timeout: true) marca un tiempo de espera agotado.

Pruebas sin red

Un cliente falso registra lo que habría salido y responde con lo que necesite la prueba, así que nada sale de la máquina y la prueba puede comprobar exactamente lo que se envió.

fake_http_client.php
use OpenEmail\Http\HttpClient;use OpenEmail\Http\HttpRequest;use OpenEmail\Http\HttpResponse;use OpenEmail\OpenEmail; $fake = new class implements HttpClient {    public array $requests = [];     public function send(HttpRequest $request): HttpResponse    {        $this->requests[] = $request;         return new HttpResponse(            200,            ['content-type' => 'application/json'],            json_encode(['id' => 'msg_test', 'status' => 'sent', 'replayed' => false], JSON_THROW_ON_ERROR),        );    }}; $testClient = new OpenEmail(apiKey: 'oe_test_fake', httpClient: $fake, maxRetries: 0, disableUpdateNotice: true); $sent = $testClient->emails->send([    'from' => '[email protected]',    'to' => '[email protected]',    'subject' => 'Hi',    'text' => 'Hello',]); $request = $fake->requests[0]; echo $sent['status'], ' ', $request->method, ' ', $request->url, ' ', $request->header('Idempotency-Key'), PHP_EOL;var_dump($request);
  • Devuelve un status fuera de 2xx con el sobre de error de la API como cuerpo, por ejemplo {"error": {"type": "validation_error", "code": "invalid_parameter", "message": "..."}}, para obtener la subclase de ApiException correspondiente.
  • Lanza new HttpClientException('timed out', timeout: true) desde send para obtener una NetworkException cuyo isTimeout() es true. Cualquier otra Exception, como una RuntimeException, se convierte en una NetworkException cuyo isTimeout() es false.
  • Una LogicException y cualquier Error, como un TypeError, cuentan como errores de programación del cliente falso. Se lanzan sin cambios y nunca se reintentan.

Construye un cliente de prueba con maxRetries: 0 cuando simules fallos. Si no, un status reintentable o un fallo de red en una llamada que se puede repetir sin riesgo se intenta tres veces, con esperas reales entre medias.