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

تنفيذ أنماط مخصصة لاستعادة الأخطاء وإعادة المحاولة

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

  • التدهور بسلاسة عند فشل خط — التقط NextPDF\Exception\FontNotFoundException، وتراجع إلى محرف مضمون، وتابع بناء المستند.
  • عارض احتياطي — عندما يرفض مسار Document::writeHtml() داخل العملية المدخلات، أعد المحاولة عبر Document::writeHtmlChrome()، وهو جسر ⁨Chrome⁩ في nextpdf/artisan.
  • إعادة المحاولة بـ ⁨HTML⁩ بديلة — عند حدوث NextPDF\Exception\HtmlParsingException أو NextPDF\Exception\CssResolutionBudgetExceededException، أعد المحاولة بنسخة ⁨HTML⁩ مبسّطة ومعروفة الصلاحية.
  • استعادة المستند الجزئي — اقرأ Document::getNumPages() بعد الفشل، واحفظ ما بُني فعليًا بدلًا من التخلص منه.

أنت تعرف بالفعل كيف تلتقط الاستثناء عند المستوى الصحيح. تتناول الصفحة المرافقة التعامل مع الأخطاء باستخدام تسلسل استثناءات ⁨NextPDF⁩ الهرمي التسلسل الهرمي نفسه. تعرض هذه الصفحة ما ينبغي فعله بعد الالتقاط.

تستهدف هذه الوصفة إصدار النواة مفتوح المصدر (⁨OSS⁩). كل واجهة برمجة تطبيقات (⁨API⁩) مذكورة هنا موجودة في nextpdf/core. الاعتمادية الاختيارية الوحيدة هي nextpdf/artisan لاستخدام احتياطي ⁨Chrome.⁩

Terminal window
composer require nextpdf/core:^3

تستخدم استراتيجية العارض الاحتياطي أيضًا جسر ⁨Chrome⁩:

Terminal window
composer require nextpdf/artisan

عند غياب nextpdf/artisan، يطرح Document::writeHtmlChrome() الاستثناء NextPDF\Exception\PageLayoutException بدلًا من التصيير. تتعامل استراتيجية الاحتياطي أدناه مع غياب الجسر بوصفه حالة أخرى قابلة للاستعادة.

تعتمد الاستعادة على حقيقتين بشأن ⁨NextPDF⁩، وكلتاهما متحقَّق منهما مقابل الشيفرة المصدرية.

يخبرك التسلسل الهرمي للاستثناءات بما يمكن استعادته. يمتد كل استثناء مجال من القاعدة المجردة NextPDF\Exception\NextPdfException، التي تمتد من RuntimeException وتنفّذ NextPDF\Contracts\ContextAwareExceptionInterface. التقط نوعًا فرعيًا محددًا لاختيار مسار استعادة مناسب لذلك الفشل:

  • يحمل FontNotFoundException التوابع getFontName() وgetSearchPaths() وwasFallbackAttempted() — وهذا يكفي لإعادة المحاولة بمحرف مختلف.
  • يحمل HtmlParsingException التوابع getRule() وgetPosition() وgetHtmlSnippet() — وهذا يكفي لتقرير ما إذا كانت إعادة المحاولة المبسّطة تستحق التنفيذ.
  • يحمل CssResolutionBudgetExceededException التوابع getVisits() وgetBudget() — وهذه إشارة إلى أن صفحة أنماط مجردة قد تتجاوز محدِّدًا مَرَضيًا.
  • حدّ مهم واحد: يمتد NextPDF\Support\DegradedException من RuntimeException مباشرةً، لا من NextPdfException. لذلك لا يلتقط catch (NextPdfException $e) رفضًا صادرًا عن سياسة التدهور. عندما تكون NextPDF\Contracts\DegradationPolicy النشطة Strict أو Balanced، التقط DegradedException صراحةً للتعافي منه.

يمكن فحص المستند أثناء بنائه. يكشف Document حالة بنائه عبر ملحقات للقراءة فقط. يعرض getNumPages() العدد الإجمالي للصفحات، بما في ذلك الصفحة النشطة غير المُفرَّغة، ويعيد getPage() الفهرس المبدوء من الصفر للصفحة الحالية. بعد فشل يحدث في منتصف البناء، اقرأ getNumPages() لمعرفة ما إذا كانت توجد أي صفحات كاملة، ثم استدعِ save() أو getPdfData() لإصدارها. يسجّل المحرك أيضًا أحداث تدهور غير قاتلة: يعيد getWarnings() قائمة من نوع list<NextPDF\Support\Warning>، ويبلّغ hasWarnings() عمّا إذا جُمِع أيٌّ منها، ويبلّغ hasDegradedParity() عمّا إذا تأثرت دقة المخرجات. تتيح هذه التوابع لروتين الاستعادة التمييز بين “نجح بنظافة” و”نجح بدقة منخفضة” دون تحليل استثناء.

تتحكم سياسة التدهور في الأحداث التي تعالجها بوصفها استثناءات، وفي الأحداث التي تعالجها بوصفها تحذيرات. يستخدم NextPDF\Core\Config افتراضيًا DegradationPolicy::Balanced، وهي سياسة تحذّر وتتابع عند التدهور المحدود، لكنها تطرح استثناءً عند وجود تأثير حاجب. لا تطرح DegradationPolicy::Permissive أبدًا، بل تجمع كل شيء في قناة التحذيرات. تطرح DegradationPolicy::Strict استثناءً عند أي خطر امتثال أو فقدان دلالي أو تأثير حاجب. اختر السياسة أولًا، ثم اكتب الاستعادة لأشكال الفشل التي تنتجها تلك السياسة.

تستخدم شيفرة الاستعادة أدناه الأعضاء المتحقَّق منها التالية:

  • NextPDF\Core\Document::createStandalone(?Config $config = null): self, addPage(), setFont(string $family, string $style = '', float $size = 12.0): static, cell(...), writeHtml(string $html): static, writeHtmlChrome(string $html, ?float $width = null, ?float $height = null): static, save(string $path): void, getPdfData(): string, getNumPages(): int, getPage(): int, getWarnings(): list<Warning>, hasWarnings(): bool, hasDegradedParity(): bool, addFontDirectory(string $directory): static.
  • NextPDF\Core\Config::withDegradationPolicy(DegradationPolicy $policy): self والقيمة الافتراضية degradationPolicy وهي DegradationPolicy::Balanced.
  • NextPDF\Contracts\DegradationPolicyStrict، Balanced، Permissive.
  • NextPDF\Exception\NextPdfException (القاعدة المجردة)، NextPDF\Exception\FontNotFoundException، NextPDF\Exception\HtmlParsingException، NextPDF\Exception\CssResolutionBudgetExceededException، NextPDF\Exception\WriterException، NextPDF\Exception\PageLayoutException.
  • NextPDF\Support\DegradedException (يحمل capability وpolicyNextPDF\Support\Capability (id، status، reason، isDegraded()NextPDF\Support\Warning، NextPDF\Support\WarningSeverity.

تلتقط أصغر استعادة مفيدة فشل خط مفقود، وتتراجع إلى محرف مضمون، ثم تتابع. يحذف هذا المقطع المعالجة الأوسع الموجودة في المثال الإنتاجي. للحصول على معالِج كامل يتضمن التسجيل وحدّ DegradedException، اقرأ المثال الإنتاجي أدناه.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Exception\FontNotFoundException;
$doc = Document::createStandalone();
$doc->addPage();
try {
// A face that may not be installed on every host.
$doc->setFont('CorporateSans', '', 12);
} catch (FontNotFoundException $e) {
// Recover: fall back to a face the engine always resolves.
$doc->setFont('helvetica', '', 12);
}
$doc->cell(0, 10, 'Rendered with a recovered font.', newLine: true);
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');

يربط المثال الكامل الاستراتيجيات الأربع كلها في خط تصيير واحد: احتياطي خط، واحتياطي عارض من المسار داخل العملية إلى ⁨Chrome⁩، وإعادة محاولة بـ ⁨HTML⁩ بديلة، واستعادة مستند جزئي اعتمادًا على getNumPages(). كما يحترم قناة مخرجات أداة التشغيل، ولا يلتقط أبدًا Exception عاريًا، ولا يترك كتلة catch فارغة.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Contracts\ContextAwareExceptionInterface;
use NextPDF\Contracts\DegradationPolicy;
use NextPDF\Core\Config;
use NextPDF\Core\Document;
use NextPDF\Exception\CssResolutionBudgetExceededException;
use NextPDF\Exception\FontNotFoundException;
use NextPDF\Exception\HtmlParsingException;
use NextPDF\Exception\NextPdfException;
use NextPDF\Exception\PageLayoutException;
use NextPDF\Exception\WriterException;
use NextPDF\Support\DegradedException;
/**
* A minimal structured sink. In production this is your PSR-3 logger; the
* exception class and its structured context become log fields.
*
* @param array<string, mixed> $context
*/
function logRecovery(string $message, array $context): void
{
fwrite(STDERR, $message . ' ' . json_encode($context, JSON_THROW_ON_ERROR) . "\n");
}
/**
* Resolve a usable font, degrading from the requested face to a guaranteed
* fallback. Returns the face actually applied so the caller can record it.
*
* @param non-empty-string $requested
* @param non-empty-string $fallback
*
* @return non-empty-string
*/
function applyFontWithFallback(Document $doc, string $requested, string $fallback): string
{
try {
$doc->setFont($requested, '', 12);
return $requested;
} catch (FontNotFoundException $e) {
// STRATEGY 1 — graceful degradation on a font failure.
logRecovery('Font unavailable; degrading to a guaranteed face', [
'exception' => $e::class,
'font_name' => $e->getFontName(),
'searched' => $e->getSearchPaths(),
'fallback' => $fallback,
]);
$doc->setFont($fallback, '', 12);
return $fallback;
}
}
/**
* Render HTML through the in-process pipeline, then through the Chrome bridge,
* then through a simplified HTML variant. Each layer recovers a more specific
* failure than the last.
*/
function renderHtmlWithRecovery(Document $doc, string $primaryHtml, string $simplifiedHtml): void
{
try {
// Primary path: the in-process HTML/CSS pipeline.
$doc->writeHtml($primaryHtml);
return;
} catch (CssResolutionBudgetExceededException $e) {
// STRATEGY 3 — retry with alternative HTML for a pathological selector.
logRecovery('CSS resolution budget exceeded; retrying with simplified HTML', [
'exception' => $e::class,
'visits' => $e->getVisits(),
'budget' => $e->getBudget(),
]);
$doc->writeHtml($simplifiedHtml);
return;
} catch (HtmlParsingException $e) {
// STRATEGY 2 — fall back to the Chrome renderer for input the
// in-process parser rejects. The Chrome bridge uses a browser CSS
// engine, so it may accept what the in-process parser would not.
logRecovery('In-process HTML parse failed; trying the Chrome fallback renderer', [
'exception' => $e::class,
'rule' => $e->getRule(),
'position' => $e->getPosition(),
]);
try {
$doc->writeHtmlChrome($primaryHtml);
return;
} catch (PageLayoutException $chromeError) {
// The Chrome bridge is absent (nextpdf/artisan not installed) or
// rejected the input. Last resort: the simplified HTML variant
// through the in-process pipeline.
logRecovery('Chrome fallback unavailable; retrying with simplified HTML', [
'exception' => $chromeError::class,
]);
$doc->writeHtml($simplifiedHtml);
return;
}
}
}
// --- Configure the degradation policy up front ---------------------------
// Balanced (the default) warns on bounded degradation and throws only on a
// blocking impact. A regulated workflow would choose DegradationPolicy::Strict.
$config = (new Config())->withDegradationPolicy(DegradationPolicy::Balanced);
$doc = Document::createStandalone($config);
$primaryHtml = '<h1>Quarterly report</h1><p>Body paragraph with rich styling.</p>';
$simplifiedHtml = '<h1>Quarterly report</h1><p>Body paragraph (simplified).</p>';
$outputPath = getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf';
try {
$doc->addPage();
$applied = applyFontWithFallback($doc, 'CorporateSans', 'helvetica');
$doc->cell(0, 12, 'Custom error recovery patterns', newLine: true);
renderHtmlWithRecovery($doc, $primaryHtml, $simplifiedHtml);
$doc->save($outputPath);
logRecovery('Document built', [
'font_applied' => $applied,
'pages' => $doc->getNumPages(),
'has_warnings' => $doc->hasWarnings(),
'degraded_parity' => $doc->hasDegradedParity(),
]);
} catch (DegradedException $e) {
// BOUNDARY: DegradedException extends RuntimeException directly, NOT
// NextPdfException, so the catch-all below would not have caught it.
// Under Strict/Balanced policy a blocking degradation lands here.
logRecovery('Capability degraded under the active policy; emitting a built partial', [
'exception' => $e::class,
'capability' => $e->capability->id,
'status' => $e->capability->status->value,
'reason' => $e->capability->reason ?? 'unknown',
'policy' => $e->policy->value,
]);
// STRATEGY 4 — partial-document recovery: save whatever pages exist.
if ($doc->getNumPages() > 0) {
$doc->save($outputPath);
}
} catch (WriterException $e) {
// Serialization or I/O failure: the in-memory document is valid but could
// not be written. Surface the stage so infrastructure can act on it.
logRecovery('PDF write failed; document was valid in memory', [
'exception' => $e::class,
'writer_state' => $e->getWriterState(),
'output_path' => $e->getOutputPath(),
]);
} catch (NextPdfException $e) {
// Catch-all for every other NextPDF\Exception\*. STRATEGY 4 again: if any
// complete pages were built before the failure, emit them rather than
// discarding the work.
$context = ['exception' => $e::class, 'pages' => $doc->getNumPages()];
if ($e instanceof ContextAwareExceptionInterface) {
$context += $e->getContext();
}
logRecovery('Unrecovered NextPDF failure; attempting a partial save', $context);
if ($doc->getNumPages() > 0) {
$doc->save($outputPath);
}
}
fwrite(STDERR, "Recovery pipeline complete.\n");

اترك STDOUT حرًا لأداة التشغيل. تذهب تشخيصات الاستعادة إلى STDERR، ويُكتب ملف صيغة المستندات المحمولة (⁨PDF⁩) إلى NEXTPDF_COOKBOOK_OUTPUT فقط.

  • رتّب كتل الالتقاط من الأخص إلى الأعم. تطابق ⁨PHP⁩ أول catch متوافقة. يؤدي وضع catch (NextPdfException $e) قبل catch (WriterException $e) إلى تحويل الكتلة المحددة إلى شيفرة ميتة، لأن WriterException يمتد من NextPdfException.
  • DegradedException يقع خارج التسلسل الهرمي. فهو يمتد من RuntimeException، لا من NextPdfException. خط معالجة يلتقط NextPdfException فقط يترك رفضًا صادرًا عن سياسة صارمة ينتشر دون التقاط. التقط DegradedException (أو RuntimeException أوسع) عندما تكون سياسة تدهور غير افتراضية نشطة.
  • يمكن أن يفشل احتياطي الخط أيضًا. إذا كان المحرف الاحتياطي نفسه غير مسجَّل، فإن استدعاء setFont() الثاني يطرح استثناءً من جديد. استخدم اسمًا مستعارًا من ⁨Base14⁩ مثل helvetica، الذي يحلّه المحرك دون البحث في نظام الملفات، أو سجّل محرفًا مرفقًا عبر addFontDirectory() عند بدء التشغيل ليكون الاحتياطي مضمونًا.
  • getNumPages() يحسب الصفحة النشطة غير المُفرَّغة. فهو يعيد عدد الصفحات المُفرَّغة زائد واحد عندما تكون هناك صفحة مفتوحة حاليًا. يتضمن “الحفظ الجزئي” الصفحة التي كانت تُبنى عند وقوع الفشل، وهذا غالبًا ما تريده. إذا كنت تحتاج إلى الصفحات المكتملة تمامًا فقط، فتفرّع بناءً على getPage() أيضًا.
  • يغيّر احتياطي ⁨Chrome⁩ الدقة، لا التوافر فقط. يستخدم المسار داخل العملية وجسر ⁨Chrome⁩ محركَي تخطيط مختلفين، لذلك قد يبدو المستند الذي يتراجع إلى ⁨Chrome⁩ مختلفًا. تعامل مع الاحتياطي بوصفه استعادة، لا بديلًا شفافًا، وسجّل أي مسار أنتج المخرجات.
  • يجب أن تستخدم إعادة المحاولة مدخلات معروفة الصلاحية. لا تنفع إعادة المحاولة بـ ⁨HTML⁩ المبسّطة إلا عندما تكون النسخة المبسّطة أبسط فعليًا: محددات متداخلة أقل، ولا سلاسل :has() تستنزف ميزانية الحل. إعادة المحاولة بالمدخلات نفسها التي فشلت بالفعل تعود إلى الاستثناء نفسه.
  • افحص التحذيرات بعد التشغيل النظيف. قد يكون التصيير الذي يعود دون طرح استثناء قد تدهور رغم ذلك. تحقق من hasDegradedParity() واقرأ getWarnings() قبل أن تعامل المخرجات بوصفها مطابقة بدقة البكسل؛ ففي ظل DegradationPolicy::Permissive يكون كل تدهور تحذيرًا، لا استثناءً أبدًا.
  • لا تضيف الاستعادة تكلفة إلا على مسار الفشل. يطرح ⁨NextPDF⁩ الاستثناءات في الحالات الاستثنائية، لذلك لا يدفع التصيير النظيف شيئًا مقابل إحاطتي try/catch.
  • يعيد احتياطي العارض تشغيل التصيير. تُتجاهل المحاولة داخل العملية وتبدأ محاولة ⁨Chrome⁩ من جديد، لذلك تكلّف عملية التصيير الاحتياطية، في أسوأ الحالات، زمنَي التصيير كليهما زائد رحلة الذهاب والإياب بين العمليات إلى ⁨Chrome.⁩ احسب لها حسابًا عند ضبط مهل الطلبات.
  • تحلّل إعادة المحاولة بـ ⁨HTML⁩ بديلة مستندًا ثانيًا. أبقِ النسخة المبسّطة صغيرة لتكون إعادة المحاولة قليلة التكلفة مقارنة بالمحاولة الأساسية.
  • يسلسل الحفظ الجزئي الصفحات التي بُنيت بالفعل. تتناسب تكلفته مع عدد الصفحات الباقية، لا مع العمل الذي فشل.
  • لا تعرض رسائل استثناءات خامًا أو مسارات نظام ملفات للمستخدمين النهائيين. تتضمن رسالة FontNotFoundException الأدلة التي جرى البحث فيها، ويتضمن WriterException مسار المخرجات؛ وكلاهما يسرّب بنية الخادم. سجّل السياق المُهيكل من جانب الخادم، وأعد رسالة عامة إلى المستدعي.
  • تعامل مع ⁨HTML⁩ المُعاد محاولتها بوصفها مدخلات غير موثوقة في كل محاولة. يتدفق كلٌّ من الاحتياطي وإعادة المحاولة بـ ⁨HTML⁩ المبسّطة عبر حدّ المدخلات نفسه؛ ويطبّق كل من المسار داخل العملية وجسر ⁨Chrome⁩ سياسته الأمنية الخاصة بـ ⁨HTML⁩، ولا تخفّف إعادة المحاولة ذلك التحقق. لا تفترض أن النسخة “المبسّطة” أكثر أمانًا لأنك أنت من أنشأها.
  • يظل الحفظ الجزئي عملية كتابة ملف. طبّق على المخرجات الجزئية القواعد نفسها المتعلقة بالتحقق من المسار والصلاحيات وموقع التخزين التي تطبّقها على المخرجات الكاملة. يرفض Document::save() أغلفة التدفق والبايتات الصفرية ويحلّ الدليل الأب لمنع اجتياز المسار، لكن الوجهة التي تمررها هي مسؤوليتك.

لا تقدّم هذه الوصفة أي ادعاء معياري بشأن المعايير. فهي تركّب واجهات ⁨NextPDF⁩ العامة للاستثناءات وفحص المستندات ضمن تدفق تحكم للاستعادة؛ ولا تؤكد سلوكًا محددًا في ⁨ISO 32000-2⁩ أو أي معيار آخر، لذلك لا تحمل أي كتلة citations:.

تُتحقَّق هذه الصفحة بملف تعريف إعادة الإنتاج الدلالي. يحمل المستند المستعاد في الذيل /ID وتاريخ تعديل يُعاد توليدهما عند كل حفظ، لذلك تتعذر المطابقة على مستوى البايت. مقارنة شجرة البناء التركيبية المجردة (⁨AST⁩) مع البيانات الوصفية فقط تبقى مستقرة عبر عمليات التشغيل.