الأخطاء
كل إخفاق يرمي استثناءً. صنف للرفض، وصنف لغياب الرد، ومعرّف طلب على كل خطأ من API.
التقاط خطأ
use OpenEmail\Exception\ApiException;use OpenEmail\Exception\NetworkException; $message = [ 'from' => '[email protected]', 'to' => '[email protected]', 'subject' => 'Your September invoice', 'text' => 'Invoice attached.',]; try { $client->emails->send($message);} catch (ApiException $error) { if ($error->isValidation()) { error_log($error->errorCode . ' ' . $error->param . ' ' . $error->getMessage()); } if ($error->isPermission()) { $book = $client->addresses->listAll(); error_log('this key may send as ' . implode(', ', array_column($book->addresses, 'address'))); } if ($error->isRateLimited()) { error_log('try again in ' . $error->retryAfterSeconds . ' seconds'); } error_log($error->status . ' ' . $error->requestId); throw $error;} catch (NetworkException $error) { if ($error->isTimeout()) { error_log('no answer in time'); } throw $error;}الخطأ permission_error عند الإرسال يعود عادةً إلى نطاق الإرسال الخاص بالمفتاح، أي نطاق أو عنوان لم يُمنح له، لا إلى مساحة العمل، ولهذا يسجّل المثال ما يقوله addresses->listAll() عن العناوين التي يجوز لهذا المفتاح الإرسال باسمها.
لكل نوع من الرفض صنف فرعي خاص به، فيستطيع catch أن يختار حسب الصنف الأنواع التي يعالجها ويترك الباقي يصعد إلى الأعلى.
use OpenEmail\Exception\AuthenticationException;use OpenEmail\Exception\OpenEmailException;use OpenEmail\Exception\PermissionException;use OpenEmail\Exception\ValidationException; $message = [ 'from' => '[email protected]', 'to' => '[email protected]', 'subject' => 'Your September invoice', 'text' => 'Invoice attached.',]; try { $client->emails->send($message);} catch (ValidationException $error) { error_log($error->param . ': ' . $error->getMessage());} catch (AuthenticationException|PermissionException $error) { error_log('the key cannot do this: ' . $error->errorCode); throw $error;} catch (OpenEmailException $error) { error_log($error::class . ': ' . $error->getMessage()); throw $error;}الأصناف
كل الأصناف موجودة في OpenEmail\Exception.
| الصنف | متى |
|---|---|
| OpenEmailException | الواجهة التي ينفّذها كل استثناء ترميه الحزمة، فيلتقطها catch (OpenEmailException $error) كلها، بما فيها InvalidArgumentException. |
| ApiException | أجابت API، ولكن ليس بنجاح. يحمل status وtype وerrorCode وparam وdocUrl وrequestId وretryAfterSeconds وfields وbody. ويُرمى بصنفه هذا حين تكون قيمة type هي api_error، كما في أعطال الخادم، وبالصنف الفرعي المقابل لقيمة type فيما عدا ذلك. ويرث RuntimeException. |
| InvalidRequestException وAuthenticationException وPermissionException وNotFoundException وConflictException وValidationException وRateLimitException | أصناف فرعية من ApiException، واحد لكل type: invalid_request_error وauthentication_error وpermission_error وnot_found_error وconflict_error وvalidation_error وrate_limit_error. |
| NetworkException | لم تصل أي استجابة: DNS، أو TLS، أو اتصال مرفوض أو منقطع، أو انتهاء المهلة. يحمل getPrevious() الاستثناء الأصلي، وتكون قيمة isTimeout() هي true حين يكون انتهاء المهلة هو السبب. ويرث RuntimeException. |
| WebhookSignatureException | رفض OpenEmail::verifyWebhookSignature() عملية تسليم. ويرث UnexpectedValueException. |
| InvalidArgumentException | يُرمى قبل إرسال أي شيء: مفتاح مفقود أو مشوّه، أو baseUrl: غير صالح للاستخدام، أو معرّف فارغ. ويرث InvalidArgumentException الخاص بـ PHP نفسها، لأنه يعني أن الاستدعاء نفسه خاطئ. |
ما يحمله ApiException
getMessage()string- الجملة التي تكتبها API نفسها، مكتوبة لشخص، وتسمّي القيمة المخالفة إن وُجدت. ليست معرّفًا ثابتًا، فاعتمد على `errorCode` في التفرّع.
statusint or null- حالة HTTP للرد، ويعيدها `getCode()` أيضًا. وتكون null فقط حين يعود نجاح بشكل لم يستطع العميل قراءته.
typestring- واحدة من القيم الثماني في `OpenEmail\Constants\ErrorTypes`، وهي مجموعة ثابتة لن تكبر. وحين لا يسمّي المتن أيًّا منها، تُستنتج من الحالة.
errorCodestring- الإخفاق المحدد، مثل `from_address_forbidden` أو `invalid_email_address`. اسمه `errorCode` لأن PHP تحتفظ بـ `code` للرقم الذي يعيده `getCode()`. مجموعة مفتوحة تتوسع بالإضافة، فعامل الرمز الذي لا تعرفه وفق `type` الخاص به. وتكون القيمة `unrecognised_response` حين لا يكون المتن غلاف أخطاء API.
paramstring or null- الحقل الذي رُفض، كمسار منقوط مثل `to.0`، حين يسمّي الإخفاق حقلًا.
docUrlstring or null- صفحة عن هذا الإخفاق، حين تسمّي API صفحة.
requestIdstring or null- المعرّف الذي سجّل الخادم الطلب تحته، من المتن أو من الترويسة `x-request-id`.
retryAfterSecondsint, float or null- مدة الانتظار التي طلبها الخادم في `Retry-After`، بالثواني، سواء أرسل رقمًا أم تاريخًا. null حين لا يرسل شيئًا.
fieldsarray or null- مصفوفة واحدة لكل مشكلة، لكل منها `key` و`error`، مثل `['key' => 'email', 'error' => 'email']` حين يرفض `forms->subscribe` الإجابات بخطأ 422 `invalid_form_submission`. null حين لا يسرد الخطأ أي مشكلة.
bodymixed- استجابة الخطأ كاملة، بعد فك ترميزها. null حين تكون فارغة أو ليست JSON.
| الطريقة | تكون true عندما |
|---|---|
| isAuth() | تكون type بقيمة authentication_error، أي 401: لا مفتاح، أو نوع اعتماد خاطئ، أو مفتاح لم نُصدره. |
| isPermission() | permission_error، أي 403: مفتاح حقيقي ينقصه النطاق أو عنوان From الذي يحتاجه. |
| isScopeMissing() | تكون errorCode بقيمة insufficient_scope، وهو الخطأ 403 الذي يسمّي نطاقًا مفقودًا. |
| isInvalidRequest() | invalid_request_error، أي 400: طلب تعذّر فهمه. أما الرسالة التي تتجاوز سقف الحجم فتعود بالخطأ 422 message_too_large، ولذا فإن isValidation() هو التابع الذي يلتقطها. |
| isValidation() | validation_error، أي 422: رفضه المخطط، وparam تسمّي الحقل. |
| isNotFound() | not_found_error، أي 404: لا وجود لهذا المورد. |
| isConflict() | conflict_error، أي 409: تجاوز المورد النقطة التي كان يمكن عندها تنفيذ هذا عليه. |
| isRateLimited() | rate_limit_error، أي 429. وتحمل retryAfterSeconds مدة الانتظار عندما يحدّدها الخادم. |
| isServerError() | تكون status بقيمة 500 أو أعلى. اذكر requestId إن تواصلت مع الدعم. |
| isRetryable() | تكون status بقيمة 408 أو 429 أو 500 أو 502 أو 503 أو 504. |
| isStepUpRequired() | errorCode هو step_up_required، أي الـ 403 الذي يتلقاه رمز وصول OAuth قبل تغيير حساس إلى أن يتحقق الشخص من رمز. |
تقرأ معظم هذه التوابع type، وهو النصف الثابت من الغلاف، وكل صنف فرعي يمثّل type واحدًا. ويبقى errorCode سلسلة نصية، لأن API تضمن أنه مفتوح ويتوسع بالإضافة، فعامل الرمز الذي لا تعرفه وفق type الخاص به. فالقائمة المغلقة ستجعل ترقية الحزمة ثمنًا لقراءة نمط إخفاق جديد.
المتن الذي ليس غلاف أخطاء API يرمي مع ذلك ApiException، مع استنتاج type من الحالة وضبط errorCode على unrecognised_response. والاستجابة الناجحة التي متنها ليس JSON ترميه أيضًا.
يصف isRetryable() الحالة، لا استدعاءك. فالاستدعاء الذي يمكن تكراره بأمان تكون محاولته قد أُعيدت بالفعل قبل أن يرمي الاستثناء، والخطأ 429 مثل send_quota_exceeded أو ai_quota_exceeded يفشل بالطريقة نفسها إلى أن تتجدد حصته، فاعرضه على شخص بدل تكرار المحاولة في حلقة.
الاستثناء الذي يرميه عميل HTTP الخاص بك يصبح NetworkException بعد استنفاد أي محاولات يسمح بها الاستدعاء، مع الاستثناء الأصلي في getPrevious(). أما LogicException وأي Error، مثل TypeError، فتعني خطأً برمجيًا فيه، فتُرمى دون تغيير ولا تُعاد محاولتها أبدًا. ولا يحتفظ Psr18HttpClient من استثناء العميل الذي يغلّفه إلا برسالته، لأن ذلك الاستثناء يحمل الطلب وترويسة Authorization الخاصة به.
requestId
يحمل كل ApiException معرّف الطلب الذي أرسله الخادم، من متن الخطأ أو من الترويسة x-request-id، وهو الشيء الوحيد الذي يربط إخفاقك بسطر في سجل الخادم. أما الاستجابة الناجحة فتعيد المتن بعد فك ترميزه وحده، فلا يوجد فيها معرّف طلب لقراءته.