تخطَّ إلى المستندات
PHP

عملاء HTTP

cURL افتراضيًا، وأي عميل PSR-18 إذا فضّلت استخدام عميلك الخاص، وعميل مزيّف للاختبارات.

الافتراضي

من دون httpClient: تمرّ الطلبات عبر OpenEmail\Http\CurlHttpClient، الذي لا يحتاج إلا إلى امتداد curl. ويحتفظ بمقبض cURL واحد لكل عميل، فيعيد الطلب الثاني استخدام الاتصال الذي فتحه الأول، وتسرد صفحة الإعداد خياراته للوكلاء وسلطات الشهادات وإعدادات cURL الإضافية.

أيًّا كان ما ينقل البايتات، يتولى العميل نفسه كل ما عدا ذلك: الاعتماد، ومفاتيح عدم التكرار، وإعادة المحاولات وفترات التراجع بينها، والمهلة لكل محاولة، والاستثناء لكل إخفاق. أما عميل HTTP فلا يفعل إلا إرسال طلب واحد وإعادة الاستجابة.

أي عميل PSR-18

يغلّف OpenEmail\Http\Psr18HttpClient أي عميل ينفّذ PSR-18، مثل Guzzle أو Symfony HttpClient، فتمرّ الطلبات عبر العميل الذي يضبطه تطبيقك أصلًا، بما فيه من middleware وتسجيل وإعدادات وكيل.

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 طلباته بمصانع PSR-17. ويستخدم المحوّل العميل نفسه حين يكون مصنعًا أيضًا، كما هو حال Psr18Client في Symfony، ثم php-http/discovery حين يكون مثبّتًا، ثم nyholm/psr7 أو مصانع Guzzle الخاصة، ويرمي InvalidArgumentException يسمّي ما يجب تثبيته حين لا يجد أيًّا منها. مرّر requestFactory: وstreamFactory: لتختارها بنفسك. ويحتاج Psr18Client في Symfony كذلك إلى واجهات PSR-18 وإلى تنفيذ لـ PSR-17، لذا ثبّته بالأمر 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();
المسألةما الذي يحدث
المهلاتيتلقى 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، فتصبح إعادة التوجيه ApiException، كما هو الحال مع عميل cURL الافتراضي.

عميلك الخاص

يأخذ httpClient: أي شيء ينفّذ OpenEmail\Http\HttpClient، وهي واجهة لها تابع واحد: send(HttpRequest $request): HttpResponse. أعد الاستجابة مع كل حالة، بما فيها 4xx و5xx، ولا ترمِ استثناءً إلا حين لا تصل أي استجابة.

الصنفما يحمله
HttpRequestmethod و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) حالة انتهاء المهلة.

الاختبار دون شبكة

يسجّل العميل المزيّف ما كان سيخرج ويجيب بما يحتاجه الاختبار، فلا يخرج شيء من الجهاز ويستطيع الاختبار أن يتحقق بالضبط مما أُرسل.

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);
  • أعد حالة خارج 2xx مع غلاف أخطاء API بوصفه المتن، مثل {"error": {"type": "validation_error", "code": "invalid_parameter", "message": "..."}}، لتحصل على الصنف الفرعي المطابق من ApiException.
  • ارمِ new HttpClientException('timed out', timeout: true) من send لتحصل على NetworkException قيمة isTimeout() فيه true. وأي Exception آخر، مثل RuntimeException، يصبح NetworkException قيمة isTimeout() فيه false.
  • يُعدّ LogicException وأي Error، مثل TypeError، أخطاءً برمجية في العميل المزيّف. فتُرمى دون تغيير ولا تُعاد محاولتها أبدًا.

ابنِ عميل الاختبار مع maxRetries: 0 حين تحاكي حالات الفشل. وإلا فإن الحالة القابلة لإعادة المحاولة أو فشل الشبكة في استدعاء يمكن تكراره بأمان يُجرَّب ثلاث مرات، مع فترات انتظار حقيقية بينها.