Clients HTTP
cURL par défaut, n'importe quel client PSR-18 si vous préférez utiliser le vôtre, et un faux pour les tests.
Le client par défaut
Sans httpClient:, les requêtes passent par OpenEmail\Http\CurlHttpClient, qui n'a besoin que de l'extension curl. Il garde un handle cURL par client : une deuxième requête réutilise donc la connexion ouverte par la première, et la page Configuration liste ses options pour les proxys, les autorités de certification et les réglages cURL supplémentaires.
Quoi qui transporte les octets, le client lui-même fait tout le reste : l'identifiant, les clés d'idempotence, les réessais et leur temporisation, le délai de chaque tentative et l'exception de chaque échec. Un client HTTP ne fait qu'envoyer une requête et rendre la réponse.
N'importe quel client PSR-18
OpenEmail\Http\Psr18HttpClient enveloppe n'importe quel client qui implémente PSR-18, comme Guzzle ou Symfony HttpClient : les requêtes passent donc par le client que votre application configure déjà, avec son middleware, sa journalisation et ses réglages 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();PSR-18 construit ses requêtes avec des fabriques PSR-17. L'adaptateur utilise le client lui-même quand il est aussi une fabrique, comme l'est Psr18Client de Symfony, puis php-http/discovery quand il est installé, puis nyholm/psr7 ou celles de Guzzle, et lève InvalidArgumentException en nommant ce qu'il faut installer quand il n'en trouve aucune. Passez requestFactory: et streamFactory: pour les choisir vous-même. Le Psr18Client de Symfony a aussi besoin des interfaces PSR-18 et d'une implémentation PSR-17 : installez-le donc avec 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();| Aspect | Ce qui se passe |
|---|---|
| Délais | Guzzle reçoit le timeout: du client à chaque requête. PSR-18 n'offre aucun moyen de passer un délai à un autre client : définissez-en donc un sur le client que vous enveloppez, comme le fait HttpClient::create(['timeout' => 30, 'max_redirects' => 0]) ci-dessus, sinon une requête qui ne répond jamais peut attendre indéfiniment. |
| Statuts d'erreur | Un client PSR-18 renvoie les réponses 4xx et 5xx au lieu de lever une exception, et l'adaptateur demande à Guzzle d'en faire autant : chaque refus devient donc toujours la bonne ApiException. |
| Pas de réponse | Une ClientExceptionInterface venant du client enveloppé devient une NetworkException qui garde son message mais pas l'exception, car celle-ci contient la requête et son en-tête Authorization. isTimeout() vaut true quand le message indique que la requête a expiré, et pour tout client autre que Guzzle le message précise que c'était le délai propre au client enveloppé. |
| Redirections | Guzzle reçoit l'instruction de ne pas les suivre, et l'exemple Symfony fixe max_redirects à 0 : une redirection devient donc une ApiException, comme avec le client cURL par défaut. |
Votre propre client
httpClient: accepte tout ce qui implémente OpenEmail\Http\HttpClient, une interface à une seule méthode : send(HttpRequest $request): HttpResponse. Renvoyez la réponse pour chaque statut, 4xx et 5xx compris, et ne levez une exception que lorsqu'aucune réponse n'est arrivée.
| Classe | Ce qu'il contient |
|---|---|
| HttpRequest | method, url, headers sous forme de tableau de noms et de valeurs, body sous forme de chaîne ou null, et timeout en secondes, ou null pour aucun. header($name) lit un en-tête quelle que soit sa casse. var_dump(), print_r() et json_encode() affichent son en-tête Authorization sous la forme [redacted], mais var_export() et le dump() de Symfony affichent headers tel quel : ne journalisez donc jamais une requête avec ces deux-là. |
| HttpResponse | Se construit avec new HttpResponse($status, $headers, $body). Il met les noms d'en-têtes en minuscules, et header($name) en lit un quelle que soit sa casse. |
| HttpClientException | Levez-la depuis send quand aucune réponse n'est arrivée. new HttpClientException($message, timeout: true) signale un délai dépassé. |
Tester sans réseau
Un faux client enregistre ce qui serait parti et répond avec ce dont le test a besoin : rien ne quitte donc la machine, et le test peut vérifier exactement ce qui a été envoyé.
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);- Renvoyez un statut hors de 2xx avec l'enveloppe d'erreur de l'API comme corps, par exemple
{"error": {"type": "validation_error", "code": "invalid_parameter", "message": "..."}}, pour obtenir la sous-classe d'ApiExceptioncorrespondante. - Levez
new HttpClientException('timed out', timeout: true)depuissendpour obtenir uneNetworkExceptiondontisTimeout()vaut true. Toute autreException, comme uneRuntimeException, devient uneNetworkExceptiondontisTimeout()vaut false. - Une
LogicExceptionet touteError, comme uneTypeError, comptent comme des bugs du faux client. Elles sont relevées telles quelles et jamais réessayées.
Construisez un client de test avec maxRetries: 0 quand vous scénarisez des échecs. Sinon, un statut réessayable ou un échec réseau sur un appel qui peut être répété sans risque est tenté trois fois, avec de vraies attentes entre les tentatives.