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

تصيير ملفات PDF بأمان داخل عامل طويل التشغيل

يُبقي عامل ⁨PHP⁩ (⁨PHP⁩: ⁨Hypertext Preprocessor⁩) طويل التشغيل (⁨RoadRunner⁩، ⁨Swoole⁩، ⁨Laravel Octane⁩) عمليةً واحدة قيد التشغيل عبر طلبات كثيرة. إذا حللت الخطوط نفسها وفككت ترميز الصور نفسها مع كل طلب، فأنت تهدر وقت المعالج وتزيد الذاكرة المقيمة. يتجنّب ⁨NextPDF⁩ هذه الكلفة بالفصل بين عُمرَين:

  • على مستوى عُمر العملية، مُشترَك: يحتفظ FontRegistry وImageRegistry بجداول الخطوط المُحلَّلة وذاكرات الصور المفكوكة الترميز. أنشئ السجلَّين مرة واحدة عند إقلاع العامل.
  • على مستوى عُمر الطلب، قابل للتخلّص منه: المستند Document الذي يُرجعه DocumentFactory::create(). ابنِه، واكتُبه، واتركه يغادر النطاق. عندها يستطيع جامع المهملات في ⁨PHP⁩ استرداد شجرة الكائنات كاملةً.

تعرض هذه الوصفة تسلسل إقلاع العامل، ومنطق المعالجة لكل طلب، وإعادة التعيين الدورية التي تُبقي ذروة الذاكرة مستقرة.

Terminal window
composer require nextpdf/core:^3

لا يتطلّب نمط العامل أي امتداد إضافي، كما أن بيئة تشغيل العامل (⁨RoadRunner⁩ / ⁨Swoole⁩ / ⁨Octane⁩) اختيارية. يمكنك تشغيل نمط المصنع نفسه داخل حلقة for من واجهة سطر الأوامر (⁨CLI⁩)، وهذا ما تختبره أداة الاختبار.

في شيفرة العامل، ابدأ بـDocumentFactory. أنشئه مرة واحدة باستخدام FontRegistry وImageRegistry مُشترَكَين:

  • FontRegistry::warmup() يُحلِّل ملفات الخطوط التي توفّرها ويُخزِّن الجداول المُحلَّلة في الذاكرة المؤقتة. FontRegistry::lock() يُجمِّد السجل حتى لا تستطيع شيفرة الطلب تعديل مجموعة الخطوط المُشترَكة. يبلّغ isLocked() عن الحالة الحالية. بعد قفل السجل، يصبح من الآمن مشاركته عبر الكوروتينات المتزامنة.
  • أنشئ ImageRegistry بميزانية maxCacheBytes. عند تجاوز الميزانية، يُخلي المُدخلات الأقل استخدامًا مؤخرًا. أما الصورة الأكبر من الميزانية فتتجاوز الذاكرة المؤقتة بدلًا من إرباكها.
  • ImageRegistry::reset() يُخلي كل صورة مُخزَّنة مع إبقاء السجل جاهزًا للاستخدام. يعيد الطلب التالي ملأه عند الحاجة. استدعِه بوتيرة منتظمة (كل ⁨N⁩ طلب، أو عندما يتجاوز memoryUsage() عتبةً محددة) لإرجاع علامة الذروة إلى خط الأساس.

كل مستند يُنشئه المصنع هو ملف مستقل بصيغة المستندات المحمولة (⁨PDF⁩). يعرّف ⁨ISO 32000-2⁩ §7.5.5 ذيل الملف الذي لم يُحدَّث قط بأنه لا يحوي مُدخل Prev، وكل طلب يتعامل معه العامل يُصدر هذا النوع من ملفات الجيل الأول. لذلك لا تتشارك الطلبات حالة المستند، رغم أنها تتشارك الذاكرة المؤقتة للخطوط والصور. يبقى وَسم الخط الفرعي BaseFont (⁨ISO 32000-2⁩ §9.6.4) ثابتًا عبر الطلبات لأن الخط المُحلَّل يقيم في السجل المُشترَك.

تستخدم هذه الوصفة سطح واجهة برمجة التطبيقات المُولَّد من ⁨PHPDoc⁩ لكل من NextPDF\Core\DocumentFactory وNextPDF\Typography\FontRegistry وNextPDF\Graphics\ImageRegistry وNextPDF\Support\MemoryReport. الأعضاء الأساسيون هم DocumentFactory::create() وFontRegistry::warmup() / lock() / isLocked() / memoryUsage() وImageRegistry::reset() / memoryUsage() وMemoryReport::$currentBytes / $peakBytes / $entryCount / utilizationPercent().

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
use NextPDF\Typography\FontRegistry;
// --- Worker boot (run ONCE, before the request loop) ---------------------
$fonts = new FontRegistry();
$fonts->lock(); // freeze the shared font set
$images = new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024);
$factory = new DocumentFactory($fonts, $images);
// --- Per request ---------------------------------------------------------
$doc = $factory->create();
$doc->setTitle('Worker output');
$doc->addPage();
$doc->setFont('helvetica', 'B', 16);
$doc->cell(0, 12, 'Generated in a shared-registry worker', newLine: true);
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');
// $doc leaves scope here → GC reclaims the whole document tree.

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

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
use NextPDF\Typography\FontRegistry;
// --- Worker boot: shared, process-lifetime registries --------------------
$fonts = new FontRegistry();
$fonts->lock(); // share-safe once locked
$images = new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024);
$factory = new DocumentFactory($fonts, $images);
$resetEvery = 4; // reset cadence in requests
$peakAfterReset = 0;
// --- Simulated request loop ---------------------------------------------
for ($request = 1; $request <= 12; $request++) {
$doc = $factory->create();
$doc->setTitle("Worker Request #{$request}");
$doc->addPage();
$doc->setFont('helvetica', 'B', 16);
$doc->cell(0, 12, "Worker Request #{$request}", newLine: true);
$doc->setFont('helvetica', '', 11);
$doc->cell(0, 8, 'Shared FontRegistry / ImageRegistry across requests.', newLine: true);
// The harness captures the LAST request's PDF via the side channel.
if ($request === 12) {
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');
} else {
$doc->getPdfData(); // force render, then drop
}
unset($doc); // explicit end-of-request
// Bound the cache high-water mark on a fixed cadence.
if ($request % $resetEvery === 0) {
$images->reset();
\gc_collect_cycles();
$report = $images->memoryUsage();
$peakAfterReset = \max($peakAfterReset, $report->currentBytes);
}
}
$final = $images->memoryUsage();
fwrite(STDERR, \sprintf(
"fonts.locked=%s images.entries=%d images.current=%dB peak_after_reset=%dB\n",
$fonts->isLocked() ? 'yes' : 'no',
$final->entryCount,
$final->currentBytes,
$peakAfterReset,
));

يبقى STDOUT متاحًا لأداة الاختبار؛ ويذهب نص التقدم إلى STDERR. يُكتب ملف ⁨PDF⁩ إلى NEXTPDF_COOKBOOK_OUTPUT فقط؛ ولا يُطبَع على المخرج أبدًا.

  • اقفل قبل أن تشارك. استدعِ FontRegistry::lock() عند الإقلاع. السجل الذي لا يزال قابلًا للتعديل عندما يلمسه كوروتينان يسبب حالة تسابق على البيانات. استخدم isLocked() كتأكيد ضمن فحص السلامة.
  • reset() ليس unset(). يُخلي ImageRegistry::reset() البيانات الثنائية المُخزَّنة ويُبقي السجل قابلًا للاستخدام، لذلك فهو الاستدعاء الدوري الصحيح. إذا أتلفت السجل وأعدت بناءه مع كل طلب، فستفقد فائدة الذاكرة المؤقتة المُشترَكة.
  • تجاوز الصورة المُفرطة الحجم. الصورة الأكبر من maxCacheBytes يُفك ترميزها عند كل استخدام ولا تُخزَّن أبدًا، لذلك لا تستطيع إزاحة مجموعة العمل. هذا مقصود. حدِّد حجم الميزانية لصورك الشائعة، لا للصورة الكبيرة النادرة.
  • يجب أن يغادر المستند النطاق. إذا احتفظت بالمستند Document في عضو ساكن، أو ارتباط حاوية طويل العمر، أو إغلاق يلتقطه العامل، فستبقى شجرة الكائنات كاملةً حيةً ولن يستطيع الجمع لكل طلب أن يعمل. استدعِ unset() أو اخرج من النطاق؛ فهذا إلزامي.
  • موضع gc_collect_cycles(). لا يعرف جامع الدورات في ⁨PHP⁩ حدود الطلبات. استدعِه بعد وتيرة إعادة التعيين، لا عند كل طلب. يحدّ هذا من علامة الذروة دون إضافة كلفة الجمع إلى المسار الساخن.
  • تحفُّظ حول الحتمية. تُعاد توليد الطوابع الزمنية للمستند وذيل /ID مع كل حفظ (⁨ISO 32000-2⁩ §14.3). لذلك تُقارَن ملفات ⁨PDF⁩ الملتقطة بالملف الشخصي الدلالي (شجرة التركيب المجردة (⁨AST⁩) البنيوية إضافةً إلى البيانات الوصفية، لا البايتات المتقلبة أبدًا). انظر “المطابقة”.
  • يجعل السجل المُشترَك تحليل الخطوط المتكرر وفك ترميز الصور كلفة إقلاع لمرة واحدة. يصبح العمل لكل طلب بعد ذلك تخطيطًا وتسلسلًا.
  • تُحدُّ ذروة الذاكرة المقيمة بـmaxCacheBytes إضافةً إلى مجموعة عمل مستند واحد قيد التنفيذ. تُعيد reset() لكل دورة الذاكرة المؤقتة إلى خط الأساس، فلا يُظهر العامل طويل العمر نمطًا سنّيًّا متصاعدًا.
  • تحدّ البيانات الأمامية performance_budget (wall_ms: 4000، peak_mb: 192) تشغيل أداة الاختبار لحلقة الطلبات الاثني عشر. تفرض أداة الاختبار هذه الميزانية؛ وهي ليست ضمانًا للمستندات الاعتباطية.
  • توفّر هذه الوصفة تغطية “الذاكرة/جمع المهملات” من قائمة الثغرات §4.3 للعنصر #31. الملف الداعم examples/14-worker-factory.php موجود، ويضيف tests/Cookbook/Php/WorkerSafeBatchRenderingRecipeTest.php تأكيد الذاكرة/جمع المهملات المفقود (الذروة لا تنمو عبر الدورات بعد إعادة التعيين).
  • يعالج نمط العامل مستندًا واحدًا لكل طلب، ولا يشارك إلا الذاكرة المؤقتة للخطوط المُحلَّلة والصور المفكوكة الترميز. لا يعبر محتوى المستند حدود الطلب. لا يستطيع أي طلب قراءة بيانات مستند طلب آخر عبر السجلات المُشترَكة.
  • لا يزال المُدخل غير الموثوق يتدفّق عبر حدود إدخال ⁨NextPDF⁩ المعتادة، ولا يخفّف نمط العامل متطلبات التحقق. عامِل مُدخل لغة ترميز النص الفائق (⁨HTML⁩) والأصول في كل طلب على أنها غير موثوقة، تمامًا كما تفعل في عملية لكل طلب.
العبارةالمواصفةالبند⁨reference_id⁩
يُعاد توليد تاريخ تعديل المستند مع كل حفظ، لذا فإن الإخراج لكل طلب ليس مستقرًا على مستوى البايت.⁨ISO 32000-2⁩§14.3
كل مستند عامل هو ملف لم يُحدَّث قط (لا يوجد Prev في الذيل)؛ ولا تتشارك الطلبات حالة المستند.⁨ISO 32000-2⁩§7.5.5
بادئة وَسم الخط الفرعي ثابتة عبر الطلبات لأن الخط المُحلَّل يقيم في السجل المُشترَك.⁨ISO 32000-2⁩§9.6.4

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