Saltar para a documentação
PHP

Clientes HTTP

cURL por omissão, qualquer cliente PSR-18 quando preferir usar o seu, e um falso para os testes.

O predefinido

Sem httpClient:, os pedidos passam por OpenEmail\Http\CurlHttpClient, que só precisa da extensão curl. Mantém um handle cURL por cliente, por isso um segundo pedido reutiliza a ligação que o primeiro abriu, e a página Configuração lista as suas opções para proxies, autoridades de certificação e definições extra do cURL.

Seja o que for que transporte os bytes, o próprio cliente faz todo o resto: a credencial, as chaves de idempotência, as repetições e o seu backoff, o timeout de cada tentativa e a exceção de cada falha. Um cliente HTTP apenas envia um pedido e devolve a resposta.

Qualquer cliente PSR-18

OpenEmail\Http\Psr18HttpClient envolve qualquer cliente que implemente PSR-18, como o Guzzle ou o Symfony HttpClient, por isso os pedidos passam pelo cliente que a sua aplicação já configura, com o seu middleware, logging e definições 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();

O PSR-18 constrói os seus pedidos com fábricas PSR-17. O adaptador usa o próprio cliente quando este também é uma fábrica, como o Psr18Client do Symfony, depois php-http/discovery quando está instalado, depois nyholm/psr7 ou as do próprio Guzzle, e lança InvalidArgumentException a indicar o que instalar quando não encontra nenhuma. Passe requestFactory: e streamFactory: para as escolher você mesmo. O Psr18Client do Symfony também precisa das interfaces PSR-18 e de uma implementação PSR-17, por isso instale-o com 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();
AspetoO que acontece
TimeoutsO Guzzle recebe o timeout: do cliente em cada pedido. O PSR-18 não tem forma de passar um timeout a qualquer outro cliente, por isso defina um no cliente que envolve, como faz HttpClient::create(['timeout' => 30, 'max_redirects' => 0]) acima, ou um pedido que nunca responda pode ficar à espera para sempre.
Estados de erroUm cliente PSR-18 devolve respostas 4xx e 5xx em vez de lançar exceções, e o adaptador diz ao Guzzle para fazer o mesmo, por isso cada recusa continua a tornar-se o ApiException certo.
Sem respostaUm ClientExceptionInterface do cliente envolvido torna-se um NetworkException que guarda a sua mensagem mas não a exceção, que contém o pedido e o seu cabeçalho Authorization. isTimeout() é true quando a mensagem diz que o pedido excedeu o tempo limite, e com qualquer cliente que não seja o Guzzle a mensagem indica que o tempo limite era o do próprio cliente envolvido.
RedirecionamentosO Guzzle é instruído a não os seguir, e o exemplo do Symfony define max_redirects como 0, por isso um redirecionamento torna-se uma ApiException, tal como acontece com o cliente cURL predefinido.

O seu próprio cliente

httpClient: aceita qualquer coisa que implemente OpenEmail\Http\HttpClient, uma interface com um único método: send(HttpRequest $request): HttpResponse. Devolva a resposta para todos os estados, incluindo 4xx e 5xx, e lance uma exceção só quando não chegou nenhuma resposta.

ClasseO que contém
HttpRequestmethod, url, headers como um array de nome e valor, body como string ou null, e timeout em segundos, ou null para nenhum. header($name) lê um cabeçalho sem distinguir maiúsculas de minúsculas. var_dump(), print_r() e json_encode() mostram o seu cabeçalho Authorization como [redacted], mas var_export() e o dump() do Symfony mostram headers tal como está, por isso nunca registe um pedido com esses dois.
HttpResponseConstruído como new HttpResponse($status, $headers, $body). Converte os nomes dos cabeçalhos para minúsculas, e header($name) lê um sem distinguir maiúsculas de minúsculas.
HttpClientExceptionLance-a a partir de send quando não chegou nenhuma resposta. new HttpClientException($message, timeout: true) marca um timeout.

Testar sem rede

Um cliente falso regista o que teria saído e responde com o que o teste precisar, por isso nada sai da máquina e o teste pode verificar exatamente o que foi enviado.

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);
  • Devolva um estado fora de 2xx com o envelope de erro da API como corpo, por exemplo {"error": {"type": "validation_error", "code": "invalid_parameter", "message": "..."}}, para obter a subclasse de ApiException correspondente.
  • Lance new HttpClientException('timed out', timeout: true) a partir de send para obter um NetworkException cujo isTimeout() é true. Qualquer outra Exception, como um RuntimeException, torna-se um NetworkException cujo isTimeout() é false.
  • Um LogicException e qualquer Error, como um TypeError, contam como bugs do falso. São lançados sem alterações e nunca são repetidos.

Construa um cliente de teste com maxRetries: 0 quando simular falhas. Caso contrário, um estado que admite repetição ou uma falha de rede numa chamada que é seguro repetir é tentado três vezes, com esperas reais pelo meio.