کلاینتهای HTTP
بهطور پیشفرض cURL، هر کلاینت PSR-18 وقتی ترجیح میدهید کلاینت خودتان را به کار ببرید، و یک نمونهٔ ساختگی برای آزمونها.
پیشفرض
بدون httpClient:، درخواستها از OpenEmail\Http\CurlHttpClient میگذرند، که به چیزی جز افزونهٔ curl نیاز ندارد. برای هر کلاینت یک هندل cURL نگه میدارد، پس درخواست دوم از اتصالی که درخواست نخست باز کرده دوباره استفاده میکند، و صفحهٔ «پیکربندی» گزینههایش را برای پراکسیها، مراجع صدور گواهی و تنظیمات اضافی cURL فهرست میکند.
هر چه بایتها را جابهجا کند، باقی کارها را خودِ کلاینت انجام میدهد: اعتبارنامه، کلیدهای idempotency، تلاشهای دوباره و بکآف آنها، مهلت هر تلاش و استثنای هر شکست. یک کلاینت HTTP فقط یک درخواست میفرستد و پاسخ را پس میدهد.
هر کلاینت PSR-18
OpenEmail\Http\Psr18HttpClient هر کلاینتی را که PSR-18 را پیادهسازی کند، مانند Guzzle یا Symfony HttpClient، در بر میگیرد، پس درخواستها از کلاینتی میگذرند که برنامهٔ شما از پیش پیکربندی کرده، همراه با middleware، لاگگیری و تنظیمات پراکسی آن.
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 درخواستهایش را با factoryهای PSR-17 میسازد. آداپتور اگر خودِ کلاینت factory هم باشد، مانند Psr18Client در Symfony، از آن استفاده میکند، سپس اگر php-http/discovery نصب باشد از آن، سپس از nyholm/psr7 یا factoryهای خودِ Guzzle، و اگر هیچکدام را پیدا نکند InvalidArgumentException را پرتاب میکند و نام آنچه باید نصب شود را میآورد. برای اینکه خودتان آنها را انتخاب کنید، requestFactory: و streamFactory: را بدهید. Psr18Client در Symfony به رابطهای PSR-18 و یک پیادهسازی PSR-17 هم نیاز دارد، پس آن را با 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();| موضوع | چه رخ میدهد |
|---|---|
| مهلتها | Guzzle در هر درخواست timeout: کلاینت را دریافت میکند. PSR-18 راهی برای دادن مهلت به هیچ کلاینت دیگری ندارد، پس روی کلاینتی که در بر میگیرید یک مهلت تنظیم کنید، همانطور که HttpClient::create(['timeout' => 30, 'max_redirects' => 0]) در بالا میکند، وگرنه درخواستی که هرگز پاسخ نگیرد میتواند تا ابد منتظر بماند. |
| وضعیتهای خطا | یک کلاینت PSR-18 پاسخهای 4xx و 5xx را بهجای پرتاب استثنا برمیگرداند، و آداپتور به Guzzle هم میگوید همین کار را بکند، پس هر رد شدنی همچنان به ApiException درست تبدیل میشود. |
| بدون پاسخ | یک ClientExceptionInterface از کلاینتِ در برگرفتهشده به یک NetworkException تبدیل میشود که پیام آن را نگه میدارد اما خود استثنا را نه، چون آن استثنا درخواست و سرآیند Authorization آن را در خود دارد. وقتی پیام بگوید مهلت درخواست تمام شده، isTimeout() برابر true است، و برای هر کلاینتی جز Guzzle پیام میگوید که این مهلتِ خودِ کلاینتِ در برگرفتهشده بوده است. |
| تغییرمسیرها | به Guzzle گفته میشود آنها را دنبال نکند و نمونهٔ Symfony مقدار max_redirects را 0 میگذارد، پس یک تغییرمسیر، مانند کلاینت پیشفرض cURL، به ApiException تبدیل میشود. |
کلاینت خودتان
httpClient: هر چیزی را که OpenEmail\Http\HttpClient را پیادهسازی کند میپذیرد، رابطی با یک متد: send(HttpRequest $request): HttpResponse. برای هر وضعیتی، از جمله 4xx و 5xx، پاسخ را برگردانید، و فقط وقتی هیچ پاسخی نرسیده استثنا پرتاب کنید.
| کلاس | آنچه با خود دارد |
|---|---|
| HttpRequest | method، url، headers بهصورت آرایهای از نام و مقدار، body بهصورت رشته یا null، و timeout برحسب ثانیه، یا null برای بدون مهلت. header($name) یک سرآیند را بدون توجه به بزرگی و کوچکی حروف میخواند. var_dump()، print_r() و json_encode() سرآیند Authorization آن را بهصورت [redacted] نشان میدهند، اما var_export() و dump() در Symfony مقدار headers را همانطور که هست چاپ میکنند، پس هرگز یک درخواست را با این دو ثبت نکنید. |
| HttpResponse | بهصورت new HttpResponse($status, $headers, $body) ساخته میشود. نام سرآیندها را به حروف کوچک تبدیل میکند، و header($name) یک سرآیند را بدون توجه به بزرگی و کوچکی حروف میخواند. |
| HttpClientException | وقتی هیچ پاسخی نرسیده، آن را از send پرتاب کنید. new HttpClientException($message, timeout: true) یک پایان مهلت را علامت میزند. |
آزمون بدون شبکه
یک کلاینت ساختگی آنچه را که بیرون میرفت ثبت میکند و با هر چیزی که آزمون لازم دارد پاسخ میدهد، پس هیچ چیزی از دستگاه بیرون نمیرود و آزمون میتواند دقیقاً بررسی کند چه چیزی فرستاده شده است.
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);- وضعیتی بیرون از 2xx را با پاکت خطای API بهعنوان بدنه برگردانید، مانند
{"error": {"type": "validation_error", "code": "invalid_parameter", "message": "..."}}، تا زیرکلاس متناظرApiExceptionرا بگیرید. - از
sendیکnew HttpClientException('timed out', timeout: true)پرتاب کنید تا یکNetworkExceptionبگیرید کهisTimeout()آن true است. هرExceptionدیگری، مانندRuntimeException، به یکNetworkExceptionتبدیل میشود کهisTimeout()آن false است. - یک
LogicExceptionو هرError، مانندTypeError، باگهای خودِ نمونهٔ ساختگی به حساب میآیند. بیتغییر پرتاب میشوند و هرگز دوباره تلاش نمیشوند.
وقتی شکستها را در آزمون شبیهسازی میکنید، کلاینت آزمونی را با maxRetries: 0 بسازید. در غیر این صورت، یک وضعیت قابل تلاش دوباره یا یک شکست شبکه روی فراخوانیای که تکرارش بیخطر است سه بار امتحان میشود، با وقفههای واقعی در میانشان.