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

إنشاء ملف PDF ضمن مهمة مُدرَجة في الطابور

لا ينبغي تنفيذ إنشاء ملفات ⁨PDF⁩ المكثف داخل خيط الطلب. يمنحك كل تكامل إطار عمل واجهة برمجة لإنشاء ملف ⁨PDF⁩ عبر الطابور؛ فهي تبنيه وتحفظه على عامل. يمكن لطلب ⁨HTTP⁩ أن يعود فور إرسال العمل. يغطي هذا الدليل المسار المُدرَج في الطابور لـ ⁨Laravel⁩ (GeneratePdfJob) و⁨Symfony⁩ (GeneratePdfMessage عبر ⁨Messenger⁩) و⁨CodeIgniter 4⁩ (GeneratePdfJob من خلال codeigniter4/queue).

المتطلبات المسبقة:

  • أساس ⁨NextPDF⁩ وتكامل إطار عمل واحد مُثبَّتان.
  • ناقل عمل مُهيَّأ: اتصال طابور في ⁨Laravel⁩، أو ناقل ⁨Messenger⁩ في ⁨Symfony⁩، أو طابور ⁨CodeIgniter 4⁩ مع تثبيت codeigniter4/queue.
  • عملية عامل قيد التشغيل لذلك الناقل.

يفترض هذا الدليل أن تطبيقك يحتوي على طابور مُهيَّأ مسبقًا. لإعداد الطابور أو ⁨Messenger⁩، راجِع وثائق إطار العمل لديك.

ثبِّت التكامل، ثم ثبِّت اعتمادية الطابور التي يحتاج إليها إطار عملك.

Terminal window
composer require nextpdf/laravel
Terminal window
composer require nextpdf/symfony symfony/messenger

يحتاج ⁨CodeIgniter⁩ إلى حزمة الطابور. يعلنها التكامل بوصفها اعتمادية للتطوير فقط، لذا اطلبها في التطبيق الذي يشغّل العمّال.

Terminal window
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⁩) + GeneratePdfHandlerGeneratePdfJob (مُعالِج الطابور)
الإرسالGeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure)MessageBusInterface::dispatch(new GeneratePdfMessage(...))service('queue')->push($queue, $name, $data)
شكل البنّاءcallable(PdfDocumentInterface): PdfDocumentInterfacePdfBuilderInterface::build(Document, array): Documentstatic fn(Document, array): Document تحت App\PdfBuilders
حماية المسار / المُدخَلتتحقق المهمة من مسار الإخراج على العامليتحقق ⁨DTO⁩ عند الإنشاء، ويعيد المُعالِج التحقق عند الاستهلاكتحصر المهمة المسار في WRITEPATH/pdfs/، وتسمح بفضاء أسماء البنّاء وفق قائمة سماح
سطح الإخفاقfailed() بعد tries؛ onFailure عند الإخفاق النهائياستراتيجية إعادة المحاولة في ⁨Messenger⁩؛ أخطاء تحقق مُنمَّطةInvalidArgumentException / QueueException

استخدم نمط الإرسال البسيط هذا في كل إطار عمل.

Laravel: dispatch GeneratePdfJob
<?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؛ تتحقق المهمة من المسار على العامل قبل أن تكتب الملف.

Symfony: dispatch GeneratePdfMessage from a controller
<?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);
}
}
CodeIgniter 4: push GeneratePdfJob by its registered name
<?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.

CodeIgniter 4: 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⁩ أدناه الإرسال مع ردَّي النداء كليهما.

Laravel: app/Jobs/DispatchMonthlyStatement.php
<?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⁩، نفِّذ البنّاء وسجِّله في مُحدِّد خدمات. يحصر ذلك المُعالِج في البنّائين المُسجَّلين.

Symfony: src/Pdf/InvoicePdfBuilder.php
<?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;
}
}
Symfony: config/services.yaml (builder locator)
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/.

CodeIgniter 4: app/PdfBuilders/InvoiceBuilder.php
<?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;
}
}

شغِّل العامل لكل إطار عمل.

Terminal window
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600
Terminal window
php 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⁩ الخاصة بها. تعيد صفحة كتاب الطبخ هذه ذكر الاستخدام وتُرجئ الاستشهادات إلى تلك الصفحات.