إنشاء ملف PDF ضمن مهمة مُدرَجة في الطابور
نظرة سريعة
قسم بعنوان «نظرة سريعة»لا ينبغي تنفيذ إنشاء ملفات PDF المكثف داخل خيط الطلب. يمنحك كل تكامل إطار عمل واجهة برمجة لإنشاء ملف PDF عبر الطابور؛ فهي تبنيه وتحفظه على عامل. يمكن لطلب HTTP أن يعود فور إرسال العمل. يغطي هذا الدليل المسار المُدرَج في الطابور لـ Laravel (GeneratePdfJob) وSymfony (GeneratePdfMessage عبر Messenger) وCodeIgniter 4 (GeneratePdfJob من خلال codeigniter4/queue).
المتطلبات المسبقة:
- أساس NextPDF وتكامل إطار عمل واحد مُثبَّتان.
- ناقل عمل مُهيَّأ: اتصال طابور في Laravel، أو ناقل Messenger في Symfony، أو طابور CodeIgniter 4 مع تثبيت
codeigniter4/queue. - عملية عامل قيد التشغيل لذلك الناقل.
يفترض هذا الدليل أن تطبيقك يحتوي على طابور مُهيَّأ مسبقًا. لإعداد الطابور أو Messenger، راجِع وثائق إطار العمل لديك.
التثبيت
قسم بعنوان «التثبيت»ثبِّت التكامل، ثم ثبِّت اعتمادية الطابور التي يحتاج إليها إطار عملك.
composer require nextpdf/laravelcomposer require nextpdf/symfony symfony/messengerيحتاج CodeIgniter إلى حزمة الطابور. يعلنها التكامل بوصفها اعتمادية للتطوير فقط، لذا اطلبها في التطبيق الذي يشغّل العمّال.
composer require nextpdf/codeigniter codeigniter4/queueبالنسبة إلى Laravel، هيِّئ اتصال الطابور في config/nextpdf.php (queue.connection وqueue.queue وqueue.timeout)، ثم شغِّل عاملًا لهذا الاتصال.
نظرة مفاهيمية عامة
قسم بعنوان «نظرة مفاهيمية عامة»يستخدم كل تكامل النمط نفسه وفق أسلوب إطار العمل الذي ينتمي إليه:
- يوفّر Laravel
NextPDF\Laravel\Jobs\GeneratePdfJob، وهي مهمة من نوعShouldQueue. أرسِلها مع مسار إخراج وإغلاق بنّاء. يحصل الإغلاق على مستند يحلّه الحاوي ويُرجِع المستند المُهيَّأ. على العامل، تحفظ المهمة المستند المُرجَع إلى المسار. كما تقبل ردود نداء اختيارية للنجاح والإخفاق. - يوفّر Symfony
NextPDF\Symfony\Message\GeneratePdfMessage، وهي رسالةreadonlyتُرسَل على ناقل Messenger، إضافةً إلىGeneratePdfHandler. يحل المُعالِج بنّاءً باسم صنفه من مُحدِّد خدمات PSR-11. نفِّذNextPDF\Symfony\Message\PdfBuilderInterfaceلكل نوع مستند. - يوفّر CodeIgniter 4
NextPDF\CodeIgniter\Jobs\GeneratePdfJob، وهي مُسجَّلة تحت مفتاح اسم فيConfig\Queue::$jobHandlers. ادفع المهمة باسمها المُسجَّل مع مرجع بنّاء، ومسار إخراج، ومصفوفة سياق. البنّاء طريقة ساكنة محصورة في فضاء الأسماءApp\PdfBuilders.
تتبنّى التكاملات الثلاثة الموقف الأمني نفسه: التحقق من مسار الإخراج. يعيد Symfony وCodeIgniter التحقق منه عند الاستهلاك، لأن الحمولة قد تنتظر في الطابور بين الإرسال والتنفيذ. يعمل البنّاء على مستند جديد لدى العامل، لذا لا تشترك المهام المتزامنة في حالة المستند أبدًا.
سطح واجهة البرمجة
قسم بعنوان «سطح واجهة البرمجة»| الجانب | Laravel | Symfony | CodeIgniter 4 |
|---|---|---|---|
| الوحدة المُدرَجة في الطابور | GeneratePdfJob (ShouldQueue) | GeneratePdfMessage (DTO) + GeneratePdfHandler | GeneratePdfJob (مُعالِج الطابور) |
| الإرسال | GeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure) | MessageBusInterface::dispatch(new GeneratePdfMessage(...)) | service('queue')->push($queue, $name, $data) |
| شكل البنّاء | callable(PdfDocumentInterface): PdfDocumentInterface | PdfBuilderInterface::build(Document, array): Document | static fn(Document, array): Document تحت App\PdfBuilders |
| حماية المسار / المُدخَل | تتحقق المهمة من مسار الإخراج على العامل | يتحقق DTO عند الإنشاء، ويعيد المُعالِج التحقق عند الاستهلاك | تحصر المهمة المسار في WRITEPATH/pdfs/، وتسمح بفضاء أسماء البنّاء وفق قائمة سماح |
| سطح الإخفاق | failed() بعد tries؛ onFailure عند الإخفاق النهائي | استراتيجية إعادة المحاولة في Messenger؛ أخطاء تحقق مُنمَّطة | InvalidArgumentException / QueueException |
نموذج برمجي — بداية سريعة
قسم بعنوان «نموذج برمجي — بداية سريعة»استخدم نمط الإرسال البسيط هذا في كل إطار عمل.
<?php
declare(strict_types=1);
use NextPDF\Contracts\PdfDocumentInterface;use NextPDF\Laravel\Jobs\GeneratePdfJob;
GeneratePdfJob::dispatch( storage_path('app/reports/january-2026.pdf'), static fn (PdfDocumentInterface $document): PdfDocumentInterface => $document ->addPage() ->cell(0, 10, 'January report', newLine: true),);يجب أن ينتهي مسار الإخراج بـ .pdf؛ تتحقق المهمة من المسار على العامل قبل أن تكتب الملف.
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Pdf\InvoicePdfBuilder;use NextPDF\Symfony\Message\GeneratePdfMessage;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Messenger\MessageBusInterface;use Symfony\Component\Routing\Attribute\Route;
final class ReportController{ #[Route('/invoice/{id}/queue', name: 'invoice_queue')] public function queue(MessageBusInterface $bus, int $id): Response { $bus->dispatch(new GeneratePdfMessage( builderClass: InvoicePdfBuilder::class, outputPath: '/var/storage/invoices/' . $id . '.pdf', builderContext: ['invoice_id' => $id], ));
return new Response('PDF generation queued.', 202); }}<?php
declare(strict_types=1);
namespace App\Controllers;
use CodeIgniter\HTTP\ResponseInterface;
final class InvoiceController extends BaseController{ public function queueInvoice(int $id): ResponseInterface { service('queue')->push('pdf-queue', 'generate-pdf', [ 'builder' => 'App\PdfBuilders\InvoiceBuilder::build', 'outputPath' => WRITEPATH . 'pdfs/invoice-' . $id . '.pdf', 'context' => ['invoice_id' => $id], ]);
return $this->response ->setStatusCode(ResponseInterface::HTTP_ACCEPTED) ->setJSON(['status' => 'queued', 'invoice_id' => $id]); }}في CodeIgniter، ادفع مفتاح jobHandlers ('generate-pdf')، لا سلسلة صنف المهمة. سجِّل المُعالِج أولًا في app/Config/Queue.php.
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Queue\Config\Queue as BaseQueue;use NextPDF\CodeIgniter\Jobs\GeneratePdfJob;
final class Queue extends BaseQueue{ /** @var array<string, class-string> */ public array $jobHandlers = [ 'generate-pdf' => GeneratePdfJob::class, ];}نموذج برمجي — الإنتاج
قسم بعنوان «نموذج برمجي — الإنتاج»في الإنتاج، اربط إرسال العمل بردود نداء النجاح والإخفاق (Laravel)، أو ببنّاء مُسجَّل صراحةً ومُعالِج مُنمَّط (Symfony)، مع مُسجِّل PSR-3. يوضح مثال Laravel أدناه الإرسال مع ردَّي النداء كليهما.
<?php
declare(strict_types=1);
namespace App\Jobs;
use NextPDF\Contracts\PdfDocumentInterface;use NextPDF\Laravel\Jobs\GeneratePdfJob;use Psr\Log\LoggerInterface;use Throwable;
final class DispatchMonthlyStatement{ public function __construct(private readonly LoggerInterface $logger) {}
public function __invoke(int $accountId): void { // dispatch() is public static: it constructs the job from the // arguments it receives. Pass every argument — including the // callbacks — to the static call, not to a separately built instance. GeneratePdfJob::dispatch( storage_path("app/statements/{$accountId}.pdf"), static fn (PdfDocumentInterface $document): PdfDocumentInterface => $document ->addPage() ->cell(0, 10, "Statement for account {$accountId}", newLine: true), function (string $path) use ($accountId): void { $this->logger->info('Statement PDF written', [ 'account_id' => $accountId, 'path' => $path, ]); }, function (Throwable $exception) use ($accountId): void { $this->logger->error('Statement PDF failed', [ 'account_id' => $accountId, 'exception' => $exception::class, ]); }, ); }}يتلقى رد نداء النجاح مسار الإخراج. ويتلقى رد نداء الإخفاق الـ Throwable. تستنفد المهمة tries (الافتراضي 3) قبل أن يجري مسار الإخفاق. اضبط timeout من خلال nextpdf.queue.timeout. قيمتا tries وbackoff خاصيتان عامتان، لذا اشتق صنفًا فرعيًا من GeneratePdfJob لتغييرهما.
بالنسبة إلى Symfony، نفِّذ البنّاء وسجِّله في مُحدِّد خدمات. يحصر ذلك المُعالِج في البنّائين المُسجَّلين.
<?php
declare(strict_types=1);
namespace App\Pdf;
use NextPDF\Core\Document;use NextPDF\Symfony\Message\PdfBuilderInterface;
final class InvoicePdfBuilder implements PdfBuilderInterface{ /** @param array<string, mixed> $context */ public function build(Document $document, array $context): Document { $document->addPage(); $document->setFont('dejavusans', '', 12); $document->cell(0, 10, 'Invoice #' . $context['invoice_id']);
return $document; }}services: App\Pdf\InvoicePdfBuilder: ~
nextpdf.pdf_builder_locator: class: Symfony\Component\DependencyInjection\ServiceLocator arguments: - 'App\Pdf\InvoicePdfBuilder': '@App\Pdf\InvoicePdfBuilder' tags: ['container.service_locator']
NextPDF\Symfony\Message\GeneratePdfHandler: arguments: $builderLocator: '@nextpdf.pdf_builder_locator'بالنسبة إلى CodeIgniter، نفِّذ البنّاء بوصفه طريقة ساكنة ضمن App\PdfBuilders. ترفض المهمة أي مرجع بنّاء خارج فضاء الأسماء هذا، وأي مسار إخراج خارج WRITEPATH/pdfs/.
<?php
declare(strict_types=1);
namespace App\PdfBuilders;
use NextPDF\Core\Document;
final class InvoiceBuilder{ /** @param array<string, mixed> $context */ public static function build(Document $document, array $context): Document { $invoiceId = (int) ($context['invoice_id'] ?? 0);
$document->addPage(); $document->cell(0, 10, "Invoice #{$invoiceId}");
return $document; }}شغِّل العامل لكل إطار عمل.
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600php spark queue:work pdf-queueأعِد تدوير عمّال Laravel وSymfony بأعمار مُقيَّدة (--limit / --memory-limit / --time-limit) كي لا ينمو أي تخصيص ذاكرة متسرّب في إحدى الاعتماديات بلا حدود.
الحالات الحدّية والمزالق
قسم بعنوان «الحالات الحدّية والمزالق»- القيمة التي يُرجِعها البنّاء هي ما يُحفَظ. في كل تكامل، يحفظ العامل المستند الذي يُرجِعه البنّاء، لا النسخة التي حُلَّت أصلًا. أرجِع دائمًا المستند المُهيَّأ من البنّاء.
- يجري التحقق من المسار على العامل. يتحقق Symfony من مسار الإخراج عند الإنشاء ومرة أخرى عند الاستهلاك. يحصر CodeIgniter المسار في
WRITEPATH/pdfs/ويرفض مسارات الاجتياز والمسارات ذات البادئة الشقيقة. المسار الذي كان آمنًا عند الإرسال لكنه غير آمن عند الاستهلاك يُرفَض رغم ذلك. - يدفع CodeIgniter الاسم، لا الصنف. إذا دفعت
GeneratePdfJob::classبوصفه اسم المهمة، يرفضه الطابور عند الدفع. ادفع مفتاحjobHandlersبدلًا من ذلك. - يجب تمرير ردود نداء Laravel إلى الإرسال الساكن. إذا أنشأت نسخة من المهمة ثم استدعيت
$job->dispatch(...)، يتجاهل ذلك الاستدعاء النسخة وردود ندائها. مرِّر ردود النداء إلىGeneratePdfJob::dispatch(...). - سجلّات آمنة للعامل. سجل الخطوط سجل مفرد مقفل بعمر العملية، وسجل الصور ذاكرة مؤقتة مُقيَّدة. المستندات جديدة لكل مهمة. لا تطلب مستندًا مُشتركًا على العامل.
- التوقيع في العمّال. يتطلب الإخراج المُوقَّع أو إخراج PDF/A في مهمة طابور إصدارًا تجاريًا من NextPDF مُثبَّتًا في بيئة العامل. بدونه، تُحَل خدمة التوقيع إلى
null. تحقَّق من القيمة null قبل التوقيع.
الأداء
قسم بعنوان «الأداء»يزيل نقل الإنشاء إلى مهمة مُدرَجة في الطابور زمن بناء PDF الكامل من طلب HTTP. يعود الطلب بمجرد إرسال العمل. يوزِّع سجلّا الخطوط والصور تكلفة إعدادهما على عمر العامل، لذا تقتصر تكلفة كل مهمة على بناء المستند وإصدار المحتوى. حدِّد عدد المهام الجارية بما يتناسب مع مجموعة العمّال لديك، وهيِّئ preload_fonts مسبقًا (Laravel وSymfony) بحيث يحدث إحماء الخطوط مرة واحدة عند إقلاع العامل بدلًا من المهمة الأولى.
ملاحظات أمنية
قسم بعنوان «ملاحظات أمنية»- تكون حمولات الطابور قابلة للتأثر بالمهاجم عندما يكون الوسيط قابلًا للوصول، لذا عامِل مسار الإخراج ومرجع البنّاء في الحمولة بوصفهما غير موثوقَين. تفرض التكاملات ذلك عبر التحقق من المسار، وفي CodeIgniter عبر قائمة سماح لفضاء أسماء البنّاء.
- قيِّد أذونات نظام ملفات العامل على دليل الإخراج المقصود بوصفه دفاعًا في العمق. إذا اجتاز مسار مُعبَث به التحقق على نحوٍ ما، فلا يزال غير قادر على الإفلات من الدليل.
- سجِّل صنف الاستثناء ومُعرِّف ارتباط في رد نداء الإخفاق، ولا تسجِّل الرسالة أو التتبع أبدًا.
- لا تكتب أبدًا كتلة
catchفارغة. كل رد نداء إخفاق هنا يسجِّل ويحمل سياقًا.
تغطي صفحة الأمن والعمليات الخاصة بكل تكامل نموذج تهديد الطابور الكامل: التحقق من الحمولة، وقوائم سماح العناصر القابلة للاستدعاء، وحصر المسار.
المطابقة
قسم بعنوان «المطابقة»لا يقدّم هذا الدليل أي ادعاء معياري بشأن المعايير. كل استدعاء واجهة برمجة معروض هنا هو السطح العام المُتحقَّق منه للتكامل المُسمَّى. يعتمد المسار المُدرَج في الطابور على ضمانات ربط الحاوي: مستند جديد لكل حل وسجل الخطوط المقفل. توثِّق صفحات استخدام الإنتاج المرتبطة تحت «انظر أيضًا» تلك الضمانات مع استشهادات PSR الخاصة بها. تعيد صفحة كتاب الطبخ هذه ذكر الاستخدام وتُرجئ الاستشهادات إلى تلك الصفحات.
انظر أيضًا
قسم بعنوان «انظر أيضًا»- إرجاع ملف PDF مُولَّد من المتحكِّم — النظير المتزامن.
- استخدام Laravel في الإنتاج —
GeneratePdfJob، وردود النداء، وجدول إعدادات الطابور. - استخدام Symfony في الإنتاج — أمان عامل Messenger ومُحدِّد البنّاء.
- استخدام CodeIgniter في الإنتاج —
GeneratePdfJobوjobHandlersوحصر المسار.