تخطَّ إلى المحتوى

الأخطاء بوصفها ميزة

Spec: ISO 9241-110, §5.6.4 Evidence: Code-backed

يتعامل ⁨NextPDF⁩ مع التسلسل الهرمي لاستثناءاته بوصفه سطح ⁨API⁩، مُصمَّمًا بالعناية نفسها التي تُصمَّم بها الطرائق التي تطلقه. تكون حالة الفشل محددة، ومُصنَّفة النوع، وقابلة للالتقاط عند مستوى الدقة الذي تحتاجه، وتحمل سياقًا مُهيكلًا لسجلاتك.

تعرض هذه الصفحة ذلك السطح كما يظهر في المصدر الفعلي للمحرك: النوع الأساس، والأنواع الفرعية المُصنَّفة النوع، والمنشئات المُسمَّاة التي تربط السبب الجذري بالرسالة، والسياق المُهيكل الذي يكشفه كل استثناء في ⁨NextPDF.⁩

رسالة الخطأ هي صوت المحرك حين يخاطبك في أسوأ لحظة ممكنة: بيئة الإنتاج، الساعة 2 ⁨a.m.⁩، ومستند كان ينبغي أن يُسلَّم. وما تقوله تلك الرسالة عندئذٍ هو ما يحدد هل تكون الخطوة التالية إصلاحًا أم تحقيقًا طويلًا.

إنّ RuntimeException: something went wrong العام لا يترك لك مسارًا واضحًا. يخبرك أنّ المحرك قد فشل، لكنه لا يخبرك بما فشل، ولا أين فشل، وبالتأكيد لا يخبرك بما ينبغي فعله. إرشادات العوامل البشرية صريحة في هذا الشأن. ينبغي أن يشرح الخطأ نفسه بما يكفي ليجعل إصلاحه الخطوة التالية البديهية، لا مشروع بحث ( Spec: ISO 9241-110, §5.6.4.3 ). الاستثناء الذي يسمّي السبب والعلاج ليس ترفًا. إنه الفرق بين إصلاح يستغرق خمس دقائق وإصلاح يستغرق خمس ساعات.

  • تمتد كل حالة فشل في ⁨NextPDF⁩ من نوع أساس مجرد واحد، NextPdfException، لذا يمكنك التقاط جميع أخطاء المكتبة بنوع واحد.
  • وتحته توجد أنواع فرعية محددة ومُصنَّفة النوع — خط لا يمكن العثور عليه، أو تهيئة غير صالحة، أو عملية توقيع فشلت — لتلتقط حالة الفشل التي يمكنك معالجتها بالضبط.
  • يطبّق كل استثناء في ⁨NextPDF⁩ واجهة ContextAwareExceptionInterface ويكشف getContext(): خريطة مُهيكلة آمنة للتسجيل، فلا تحلل أبدًا سلسلة رسالة لاستعادة التشخيصات.
  • الرسائل قابلة للتنفيذ: تربط المنشئات المُسمَّاة السبب الجذري الفعلي (وغالبًا الإصلاح) بالرسالة، بدلًا من الاكتفاء بقالب عام.
  • يوثّق كل صنف استثناء من يمكنه التصرف بشأنه — المطور، أو البنية التحتية، أو مُستدعي المكتبة — فيبدأ الفرز قبل أن تقرأ تتبُّع المكدس.

التسلسل الهرمي ضحل عن قصد. هناك نوع أساس واحد، وطبقة من الأنواع الخاصة بالمجال، وعقد يلتزم به كل واحد منها.

نوع أساس واحد، شامل الالتقاط بحكم التصميم. إنّ NextPdfException مجرد، يمتد من RuntimeException، ويطبّق ContextAwareExceptionInterface:

abstract class NextPdfException extends RuntimeException implements ContextAwareExceptionInterface
{
/** @return array<string, mixed> */
public function getContext(): array
{
return [];
}
}

جعله مجردًا قرار تصميمي. لن تلتقط أبدًا النوع الأساس المبهم عن طريق الخطأ، لأنه لا يُطلق مباشرةً أبدًا. إنما تلتقطه عمدًا، بوصفه شبكة أمان، وتلتقط نوعًا فرعيًا محددًا حين يمكنك فعل شيء محدد.

أنواع فرعية محددة ومُصنَّفة النوع. الخط المفقود ليس خطأً عامًا؛ إنه FontNotFoundException، ويحمل البيانات التي تحتاجها للتصرف:

final class FontNotFoundException extends NextPdfException
{
public function __construct(
private readonly string $fontName,
private readonly array $searchPaths,
private readonly bool $fallbackAttempted,
?Throwable $previous = null,
) {
parent::__construct(
\sprintf('Font "%s" not found. Searched: [%s].', $fontName, \implode(', ', $searchPaths)),
0,
$previous,
);
}
// getFontName(), getSearchPaths(), wasFallbackAttempted(), getContext()
}

تسمّي الرسالة الخط والمسارات الدقيقة التي جرى البحث فيها. لا تخمّن أي دليل كان مفقودًا؛ فالاستثناء يخبرك.

سياق مُهيكل، لا استخلاص من السلاسل النصية. يُرجِع كل استثناء خريطة بصيغة ⁨snake_case⁩ تقتصر على الأنواع الأولية، وآمنة لتسلسلها مباشرةً في سجل أو في حمولة ⁨APM⁩:

public function getContext(): array
{
return [
'config_key' => $this->configKey,
'given_value' => $this->givenValue,
'expected_type' => $this->expectedType,
];
}

العقد صريح بشأن السبب. يمكن لوسيط تسجيل أن يستدعي $logger->error($e->getMessage(), $e->getContext()) لأي استثناء مهما كان في NextPDF دون أن يحلل الرسالة أبدًا. الرسالة للبشر. والسياق للآلات. ولا ينبغي لأيٍّ منهما أن يؤدي دور الآخر.

رسائل قابلة للتنفيذ عبر المنشئات المُسمَّاة. هنا تتوقف الأخطاء عن كونها عرضية وتصبح مُصمَّمة. لا يقول SignatureException فقط “فشل التوقيع عند المستوى ⁨B-LT⁩”. إنه يقدّم منشئات مُسمَّاة تربط السبب الجذري الحقيقي، وغالبًا العلاج الدقيق، بالرسالة:

public static function tsaUrlEmpty(string $signatureLevel): self
{
return new self('', $signatureLevel, null,
'TSA endpoint URL is empty: pass a non-empty `tsaUrl` to the TsaClient '
. 'constructor (e.g. "https://timestamp.example.com/tsa") or remove the '
. 'TSA client wiring if no timestamping is required at this signature level');
}

تذكر الرسالة ما هو الخطأ وما ينبغي فعله بشأنه. توجد منشئات شقيقة لحزمة قدرات مفقودة، وعميل ⁨HTTP⁩ غائب، وخوارزمية مخصصة للملخص فقط اختيرت عن طريق الخطأ، ونوع مفتاح لا يطابق الخوارزمية، وغير ذلك. يحوّل كل واحد منها فئة من الفشل إلى جملة يمكن للمطور التصرف بناءً عليها دون قراءة مصدر المحرك.

حالات فشل صاخبة عن قصد. توجد بعض الاستثناءات تحديدًا لكي تتحول فجوة صامتة إلى فجوة صاخبة. يحمل NotImplementedException وسم feature قابلًا للبحث الآلي بـ ⁨grep⁩ ومرجع followUp:

final class NotImplementedException extends NextPdfException
{
public function __construct(
public readonly string $feature,
public readonly string $followUp,
?Throwable $previous = null,
) {
parent::__construct(
\sprintf('%s is not implemented in this release. %s', $feature, $followUp),
0, $previous,
);
}
}

المسار الذي يُبلَغ لكنه غير موصول يطلق هذا بدلًا من إرجاع عملية لاغية تبدو معقولة. الفكرة نفسها تقود StrictModeViolation، الذي تحمل أنواعه الفرعية وسمًا قصيرًا قابلًا للبحث بـ ⁨grep⁩ للبنية المنحرفة إضافةً إلى سياق اختياري للموضع والاستشهاد. يصبح الانحراف عن المواصفة توقفًا مُصنَّف النوع ومصحوبًا بسياقه، لا عرضًا خاطئًا بصمت.

بيانات وصفية للفرز في الصنف نفسه. يسمّي كل صنف استثناء من يمكنه التصرف بشأنه في كتلة توثيقه. على سبيل المثال، FontNotFoundException هو “المطور (تحقق من مسار الخط) أو البنية التحتية (أصلِح أذونات الملفات)”. InvalidConfigException هو “المطور (أصلِح التهيئة قبل استدعاء ⁨NextPDF⁩)”. NotImplementedException هو “مُستدعو المكتبة — إما إزالة الاستدعاء أو التثبيت على إصدار مستقبلي”. يبدأ الفرز قبل تتبُّع المكدس، لأن السؤال “هل هذا من مسؤوليتي أم من مسؤولية العمليات؟” له بالفعل إجابة مدوَّنة.

يلخص الجدول التصميم وما تمنحك إياه كل خاصية.

خاصية التصميمفي المصدرما يمنحك إياه
نوع أساس مجرد واحدNextPdfException (مجرد، يطبّق واجهة السياق)التقاط كل خطأ في المكتبة بنوع واحد، دون التقاط النوع الأساس المبهم عن طريق الخطأ
أنواع فرعية محددة ومُصنَّفة النوعFontNotFoundException، InvalidConfigException، SignatureException، …التقاط حالة الفشل التي يمكنك معالجتها بالضبط
سياق مُهيكلgetContext() — أنواع أولية بصيغة ⁨snake_case⁩ فقطالتسجيل أو الإرسال إلى ⁨APM⁩ دون تحليل سلسلة رسالة
رسائل قابلة للتنفيذالمنشئات المُسمَّاة تربط السبب الجذري + العلاججملة يمكنك التصرف بناءً عليها، لا قالب
صاخب عن قصدNotImplementedException، StrictModeViolationتحويل فجوة صامتة إلى توقف مُصنَّف النوع قابل للبحث بـ ⁨grep⁩
بيانات وصفية للفرز”قابل للتنفيذ من قِبَل:” في كتلة توثيق كل صنفمعرفة لمن المشكلة قبل قراءة التتبُّع

هذه الصفحة Evidence: Code-backed : كل صنف وتوقيع وشكل رسالة مقتبس من فضاء أسماء الاستثناءات في المحرك، وليس مُعادًا صياغته.

  • النوع الأساس المجرد وعقد ContextAwareExceptionInterface الخاص به، والأنواع الفرعية المُصنَّفة النوع، وشكل getContext()، ومنشئات SignatureException المُسمَّاة، جميعها مقتبسة حرفيًا من المصدر.
  • إنّ أسطر الفرز “قابل للتنفيذ من قِبَل:” هي عقود في كتل توثيق الأصناف ضمن الملفات نفسها.
  • المرتكز في العوامل البشرية هو Spec: ISO 9241-110 — §5.6.4.3، بشأن الأخطاء التي تشرح نفسها بما يكفي لإصلاحها، ومبدأ المتانة في مواجهة أخطاء الاستخدام في §6. يعامل المحرك المطور بوصفه المستخدم، والاستثناء بوصفه الواجهة التي يجب أن تستوفي تلك البنود.

التقِط على نطاق واسع بوصفه شبكة أمان، والتقِط على نحو محدد حيث يمكنك التصرف، ومرِّر السياق المُهيكل مباشرةً إلى مُسجِّلك — دون تحليل الرسالة.

<?php
declare(strict_types=1);
use NextPDF\Core\Document;
use NextPDF\Exception\FontNotFoundException;
use NextPDF\Exception\NextPdfException;
use Psr\Log\LoggerInterface;
function renderInvoice(LoggerInterface $logger): ?string
{
try {
$document = Document::createStandalone();
$document->setTitle('Invoice 2026-0042');
$document->addPage();
$document->setFont('BrandSans', '', 12);
$document->cell(0, 10, 'Thank you for your business.', newLine: true);
return $document->getPdfData();
} catch (FontNotFoundException $e) {
// Specific: we can recover — fall back to a built-in font.
// getContext() is log-safe structured data, not a parsed string.
$logger->warning($e->getMessage(), $e->getContext());
return null; // caller re-renders with 'helvetica'
} catch (NextPdfException $e) {
// Backstop: any other NextPDF failure, still with structured context.
$logger->error($e->getMessage(), $e->getContext());
return null;
}
}

تتعافى كتلة catch المحددة لأن نوع الاستثناء أخبرها بأن التعافي ممكن. وتسجّل شبكة الأمان سياقًا مُهيكلًا لكل ما عدا ذلك. ولا يقرأ التطبيق الرسالة في أي لحظة لمعرفة ما حدث.

القراءة الخاطئة المعتادة هي أن شجرة استثناءات عميقة تعني هندسة مفرطة، وأن نوع خطأ واحد سيكون أبسط. سيكون أبسط للمحرك وأسوأ لك. نوع واحد يعني أن كل حالة فشل تتحول إلى تتبُّع مكدس عام، وأن منطق التعافي يصبح مطابقة لسلسلة نصية. تلك المطابقة هشة؛ وأول إعادة صياغة للرسالة تكسرها. ينقل تسلسل هرمي صغير ومحدد تلك المعرفة إلى نظام الأنواع، حيث يمكن للمترجِم ولكتل catch لديك استخدامها.

سوء فهم ثانٍ هو أن الرسالة والسياق يكرران الشيء نفسه. هذا غير صحيح. الرسالة نص نثري لإنسان يقرأ سطر سجل. السياق خريطة مُصنَّفة النوع للتوجيه البرمجي أو التنبيه أو لوحات المعلومات. الخلط بينهما هو تحديدًا فخ تحليل السلاسل النصية الذي وُجِد عقد getContext() لإزالته.

التسلسل الهرمي ضحل عن قصد. لا يُنشئ ⁨NextPDF⁩ صنف استثناء متمايزًا لكل حالة فشل يمكن تصورها. إنما يُنشئ صنفًا حين يكون التقاط تلك الحالة بعينها أمرًا قد يفعله مُستدعٍ على نحو معقول. الإفراط في التقسيم سيستبدل مشكلة قائمة التقاط متضخمة بمشكلة تحليل السلاسل النصية.

إنّ getContext() مُهيكل للسجلات و ⁨APM⁩، لذا يُرجِع، بحكم العقد، أنواعًا أولية وقوائم من الأنواع الأولية فقط، دون كائنات متداخلة. إنه سياق تشخيصي، لا لقطة مُسلسلة من دواخل المحرك. كما أنه ليس صيغة نقل مستقرة لبناء مخططات خارجية بالاستناد إليها.

تصف هذه الصفحة السطح التصميمي للاستثناءات. تتطور المجموعة الدقيقة من الاستثناءات وحقولها مع المحرك. الأصناف والأشكال المقتبسة هنا محدَّثة حتى تاريخ هذه المراجعة، وهي توضيحية للعقد، لا فهرس مُجمَّد. العقد — نوع أساس واحد، وأنواع فرعية مُصنَّفة النوع، وسياق مُهيكل، ورسائل قابلة للتنفيذ — هو الجزء المستقر.

  • مدعوم بالشيفرة (مستوى الدليل) — صفحة تُدقَّق ادعاءاتها بالاستناد إلى المصدر الفعلي للمحرك، مقتبسةً منه لا مُعاد صياغتها.
  • استثناء مدرك للسياق — استثناء في ⁨NextPDF⁩ يطبّق ContextAwareExceptionInterface ويكشف getContext(). تُرجِع تلك الطريقة خريطة بصيغة ⁨snake_case⁩ من حقول تشخيصية أولية، آمنة لتسلسلها في سجل أو حمولة ⁨APM⁩ دون تحليل سلسلة الرسالة.
  • منشئ مُسمَّى — طريقة مصنع ساكنة (على سبيل المثال SignatureException::tsaUrlEmpty()) تبني استثناءً برسالة مرتبطة بسبب جذري محدد، وغالبًا بعلاجه.
  • ⁨PAdES⁩ — التوقيعات الإلكترونية المتقدمة لملفات ⁨PDF⁩، وهي عائلة ملامح ⁨ETSI⁩ لتوقيع ملفات ⁨PDF.⁩ يُفصَّل عند أول استخدام؛ ويُغطَّى بعمق في صفحات التوقيع.
  • ⁨TSA⁩ — سلطة الختم الزمني، وهي الخدمة الموثوقة التي تُصدِر أختام ⁨RFC 3161⁩ الزمنية المستخدَمة في ملامح ⁨PAdES⁩ الأعلى.