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.
use GuzzleHttp\Client;use OpenEmail\Http\Psr18HttpClient;use OpenEmail\OpenEmail; $client = new OpenEmail(httpClient: new Psr18HttpClient(new Client())); $client->me->ping();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.
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();| Aspeto | O que acontece |
|---|---|
| Timeouts | O 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 erro | Um 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 resposta | Um 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. |
| Redirecionamentos | O 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.
| Classe | O que contém |
|---|---|
| HttpRequest | method, 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. |
| HttpResponse | Construí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. |
| HttpClientException | Lance-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.
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 deApiExceptioncorrespondente. - Lance
new HttpClientException('timed out', timeout: true)a partir desendpara obter umNetworkExceptioncujoisTimeout()é true. Qualquer outraException, como umRuntimeException, torna-se umNetworkExceptioncujoisTimeout()é false. - Um
LogicExceptione qualquerError, como umTypeError, 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.