بث ملف PDF كبير مُولَّد كاستجابة HTTP
لمحة سريعة
قسم بعنوان «لمحة سريعة»تُولِّد ملف PDF كبيرًا داخل وحدة تحكم وتريد إعادة البايتات دون الاحتفاظ بنسخة كاملة ثانية في المخزن المؤقت للاستجابة. يوفِّر كل تكامل إطار عمل صيغ بث من مصنع PdfResponse الخاص به: streamInline() وstreamDownload(). تُعيد كل طريقة StreamedResponse من إطار العمل مع دالة استدعاء راجِع تكتب متن PDF إلى العميل في أجزاء ثابتة بحجم 64 KB.
اقرأ نموذج الذاكرة قبل اختيار هذا المسار. يبني المحرك المستند كاملًا في الذاكرة أولًا. تستدعي دالة الاستدعاء الراجِع المبثوثة getPdfData()، التي تُجسِّد ملف PDF بالكامل كسلسلة نصية واحدة، ثم تمر على تلك السلسلة في شرائح بحجم 64 KB. يوفِّر هذا المسار تكلفة الذروة للنسخة الثانية التي يحتفظ بها Illuminate\Http\Response أو Symfony\Component\HttpFoundation\Response المُخزَّن مؤقتًا بينما يقيس إطار العمل Content-Length. لا تقيس صيغة البث الطول، ولذلك تُغفِل Content-Length. كما أنها لا تحتفظ أبدًا بمتن الاستجابة وسلسلة المستند في الوقت نفسه. هذا ليس بثًا تزايديًّا حقيقيًّا: لا يملك NextPDF واجهة كاتب تزايدي، لذلك يُجسَّد المستند بالكامل قبل وصول أول بايت إلى المقبس.
قبل البدء، تأكَّد من توافر هذه العناصر:
- أساس NextPDF مُثبَّت، ومعه تكامل إطار عمل واحد،
nextpdf/laravelأوnextpdf/symfony، مُثبَّت ومُكتشَف. - أنت تعرف بالفعل كيفية توجيه طلب إلى وحدة تحكم في إطار العمل لديك.
- لقد قرأت إعادة ملف PDF مُولَّد من وحدة تحكم، التي تغطي مصنعَي
inline()وdownload()المُخزَّنَين مؤقتًا اللذَين تعتمد عليهما هذه الوصفة.
يركِّز هذا الدليل الإرشادي على نمط StreamedResponse المشترك بين Laravel وSymfony. يوفِّر CodeIgniter 4 أسماء الطرق نفسها streamInline() / streamDownload()، لكنه يغلِّف البايتات في CodeIgniter\HTTP\DownloadResponse بدلًا من StreamedResponse مُدار بدالة استدعاء راجِع. يوضِّح قسم الحالات الحدِّية هذا الفرق.
التثبيت
قسم بعنوان «التثبيت»ثبِّت التكامل الخاص بإطار العمل لديك. شغِّل أحد الأوامر التالية.
composer require nextpdf/laravelcomposer require nextpdf/symfonyبالنسبة إلى Laravel، انشُر التكوين بعد التثبيت.
php artisan vendor:publish --tag=nextpdf-configيسجِّل Symfony الحزمة عبر Flex. تأكَّد من الاكتشاف في صفحة التثبيت الخاصة بإطار العمل لديك قبل المتابعة.
نظرة عامة مفاهيمية
قسم بعنوان «نظرة عامة مفاهيمية»يستدعي مصنع الاستجابة المُخزَّن مؤقتًا، PdfResponse::download() أو PdfResponse::inline()، الدالة getPdfData()، ويخزِّن السلسلة المُعادة على كائن Response، ويضبط Content-Length من strlen(). ثم يحتفظ إطار العمل بتلك السلسلة طوال عمر الاستجابة. في المستندات الكبيرة، توجد سلسلة المستند وسلسلة متن الاستجابة في الذاكرة في الوقت نفسه.
يستخدم مصنع البث شكلًا مختلفًا. تُعيد PdfResponse::streamDownload() وPdfResponse::streamInline() كائن StreamedResponse مبنيًّا بدالة استدعاء راجِع. لا يستدعي إطار العمل دالة الاستدعاء الراجِع تلك إلا عندما يكون جاهزًا لإرسال المتن. داخل دالة الاستدعاء الراجِع، يستدعي التكامل getPdfData() مرة واحدة، ويقطِّع السلسلة المُعادة إلى أجزاء بحجم 64 KB، ثم يُخرِج كل جزء باستخدام echo متبوعًا بـ flush(). لا يحتفظ بنسخة دائمة ثانية من المتن، ولا يُصدِر ترويسة Content-Length.
حقيقتان تشكِّلان كل قرار في هذه الصفحة:
- البناء فوري، والنقل مُجزَّأ. تستدعي
getPdfData()علىNextPDF\Core\Documentالكاتب وتُعيد ملف PDF بأكمله كسلسلة نصية واحدة. يتحكَّم التجزيء بحجم 64 KB فقط في كيفية مغادرة البايتات المبنية مسبقًا العمليةَ. تُحدَّد ذروة الذاكرة بحجم مستند واحد مكتمل، لا بنافذة بث صغيرة. - لا توجد
Content-Length. لا يمكن لصيغة البث معرفة طول المتن دون بنائه داخل دالة الاستدعاء الراجِع، لذلك تُغفِل الترويسة. لن يرى شريط تقدُّم لدى العميل، أو طلبRange، أو وكيل حساس للطول، أي حجم. اختَرdownload()/inline()المُخزَّنَين مؤقتًا عندما يكون الطول المعروف أهمَّ من توفير نسخة الاستجابة.
احصل على المستند عبر مسار التحليل الاصطلاحي لإطار العمل:
- Laravel: حلِّل
NextPDF\Contracts\DocumentFactoryInterfaceمن الحاوية واستدعِcreate(). تُعيد هذه العمليةNextPDF\Core\Documentجديدًا، وهو النوع المحدَّد الذي تقبله مصانع البث. - Symfony: احقِن
NextPDF\Symfony\Service\PdfFactoryواستدعِcreate(). تُعيد هذه العمليةNextPDF\Core\Documentجديدًا مع تطبيق الإعدادات الافتراضية المُكوَّنة.
واجهة API
قسم بعنوان «واجهة API»| الاهتمام | Laravel | Symfony |
|---|---|---|
| مستند جديد | app(DocumentFactoryInterface::class)->create() | PdfFactory::create() |
| عرض مضمَّن مبثوث | PdfResponse::streamInline($doc, $name) | PdfResponse::streamInline($doc, $name) |
| تنزيل مبثوث | PdfResponse::streamDownload($doc, $name) | PdfResponse::streamDownload($doc, $name) |
| النوع المُعاد | Symfony\Component\HttpFoundation\StreamedResponse | Symfony\Component\HttpFoundation\StreamedResponse |
| استدعاء البناء داخل دالة الاستدعاء الراجِع | NextPDF\Core\Document::getPdfData() | NextPDF\Core\Document::getPdfData() |
| حجم الجزء | 64 KB (str_split حتمي) | 64 KB (حلقة substr حتمية) |
يوجد PdfResponse الخاص بـ Laravel في NextPDF\Laravel\Http\PdfResponse؛ ويوجد الخاص بـ Symfony في NextPDF\Symfony\Http\PdfResponse. يُعيد مصنعَا البث كلاهما النوع نفسه Symfony\Component\HttpFoundation\StreamedResponse. يطبِّق كلاهما مجموعة ترويسات تقوية الاستجابة الثابتة نفسها من مشروع أمان تطبيقات الويب المفتوح (OWASP) (X-Content-Type-Options: nosniff، X-Frame-Options: DENY، Content-Security-Policy: default-src 'none'، X-Robots-Tag: noindex, nofollow، Referrer-Policy: no-referrer)، ويطهِّر كلاهما اسم ملف التنزيل. لا تضِف تلك الترويسات بنفسك.
يستدعي كلا المصنعين الواجهة الأساسية الكامنة نفسها، NextPDF\Core\Document::getPdfData(): string، التي تبني ملف PDF الثنائي بأكمله وتُعيده. تكتب نظيرتها save(string $path): void البايتات نفسها إلى القرص عبر كاتب ذرّي. تستخدم هذه الوصفة getPdfData() لأن الهدف هو مقبس HTTP، وليس ملفًّا.
نموذج التعليمات البرمجية — بداية سريعة
قسم بعنوان «نموذج التعليمات البرمجية — بداية سريعة»إليك الحد الأدنى لإجراء تنزيل مبثوث في كل إطار عمل. تستخدم استدعاءات المستند الواجهة الأساسية نفسها؛ ولا يختلف سوى هيكل وحدة التحكم. يسلِّم مصنع البث إطارَ العمل دالة استدعاء راجِع، لذلك يعود إجراؤك فورًا. يُبنى المتن ويُفرَّغ عندما يرسل إطار العمل الاستجابة.
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use NextPDF\Contracts\DocumentFactoryInterface;use NextPDF\Laravel\Http\PdfResponse;use Symfony\Component\HttpFoundation\StreamedResponse;
final class ReportController extends Controller{ public function annualReport(): StreamedResponse { $document = app(DocumentFactoryInterface::class)->create(); $document->addPage(); $document->cell(0, 10, 'Annual report', newLine: true);
return PdfResponse::streamDownload($document, 'annual-report.pdf'); }}<?php
declare(strict_types=1);
namespace App\Controller;
use NextPDF\Symfony\Http\PdfResponse;use NextPDF\Symfony\Service\PdfFactory;use Symfony\Component\HttpFoundation\StreamedResponse;use Symfony\Component\Routing\Attribute\Route;
final class ReportController{ #[Route('/report', name: 'report_pdf')] public function annualReport(PdfFactory $pdf): StreamedResponse { $document = $pdf->create(); $document->addPage(); $document->cell(0, 10, 'Annual report', newLine: true);
return PdfResponse::streamDownload($document, 'annual-report.pdf'); }}للمعاينة في علامة تبويب المتصفح بدلًا من فرض التنزيل، استدعِ streamInline(...) بدلًا من streamDownload(...). تصبح Content-Disposition هي inline، وتبقى كل ترويسة أخرى كما هي.
نموذج التعليمات البرمجية — الإنتاج
قسم بعنوان «نموذج التعليمات البرمجية — الإنتاج»يحقن إجراء الإنتاج تبعياته، ويتحقق من صحة مدخل المسار، ويلتقط أكثر استثناء محدَّد قد يثيره البناء، ويسجِّل فئة الفشل دون تسريب أثر، ويُعيد خطأ بروتوكول نقل النص التشعبي (HTTP) معرَّفًا. يستخدم المثال أدناه حقن المُنشئ في Laravel. ويتبع المكافئ في Symfony الشكل نفسه، مع حقن PdfFactory لكل إجراء.
تعمل getPdfData() داخل دالة الاستدعاء الراجِع المبثوثة، لذلك يظهر أي استثناء تثيره بعد أن يكون إطار العمل قد بدأ في إرسال الترويسات. للحفاظ على فائدة معالجة الأخطاء، ابنِ المستند (الخطوة التي قد تفشل) قبل أن تُعيد الاستجابة، والتقِط فشل البناء هناك. عندئذٍ لا يحدث داخل دالة الاستدعاء الراجِع سوى النقل المُجزَّأ للبايتات المبنية مسبقًا.
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Response;use NextPDF\Contracts\DocumentFactoryInterface;use NextPDF\Core\Document;use NextPDF\Exception\NextPdfException;use NextPDF\Laravel\Http\PdfResponse;use Psr\Log\LoggerInterface;use Symfony\Component\HttpFoundation\StreamedResponse;
final class StatementController extends Controller{ private const int MAX_STATEMENT_ID = 9_999_999;
public function __construct( private readonly DocumentFactoryInterface $documents, private readonly LoggerInterface $logger, ) {}
public function show(int $statementId): StreamedResponse|Response { // Validate input at the boundary before any build work runs. if ($statementId < 1 || $statementId > self::MAX_STATEMENT_ID) { return new Response('Invalid statement identifier.', 422); }
try { // Build the whole document up front. getPdfData(), invoked inside // the streamed callback, materializes the full PDF in memory, so // do the failure-prone build here, where the catch can still set a // clean HTTP status before any byte is sent. $document = $this->buildStatement($statementId); $document->getPdfData(); } catch (NextPdfException $exception) { // Log the exception class, never the message or a stack trace, so // internal detail does not leak into the log sink. $this->logger->error('Statement PDF build failed', [ 'statement_id' => $statementId, 'exception' => $exception::class, ]);
return new Response('Could not generate the statement PDF.', 500); }
// The build succeeded. The streamed factory rebuilds the bytes inside // its callback and flushes them to the client in 64 KB chunks. return PdfResponse::streamDownload( $document, "statement-{$statementId}.pdf", ); }
private function buildStatement(int $statementId): Document { $document = $this->documents->create(); $document->addPage(); $document->cell(0, 10, "Statement #{$statementId}", newLine: true);
return $document; }}التقِط NextPDF\Exception\NextPdfException، وهي القاعدة المجرَّدة التي يمتدُّ منها كل استثناء في NextPDF، عندما تريد معالجًا واحدًا لأي فشل في البناء. وللاستجابة إلى أسباب محدَّدة، التقِط أولًا الأنواع الفرعية المحدَّدة التي قد تثيرها getPdfData(): NextPDF\Exception\PageLayoutException عندما يتعذَّر احتواء المحتوى في هندسة الصفحة، وNextPDF\Exception\CompressionException عندما يفشل ضغط الدفق، وNextPDF\Exception\InvalidConfigException عند وجود تكوين إخراج غير صالح. لا تكتب أبدًا كتلة catch فارغة. يسجِّل كل فرع هنا فئة الفشل ويُعيد حالة معرَّفة.
تحليل مستند جديد لكل إجراء يُبقي المصنع قابلًا للاستبدال في الاختبارات. لا تُعِد استخدام مثيل وحدة تحكم واحد لمستندَين غير مرتبطين داخل عملية عامل واحدة طويلة التشغيل، لأن حالة المحتوى القديمة قد تنتقل إلى ما يليها.
الحالات الحدِّية والمزالق
قسم بعنوان «الحالات الحدِّية والمزالق»- يُبنى المستند مرتين في نمط التحقق ثم البث. يستدعي النموذج الإنتاجي
getPdfData()مرة واحدة للتحقق من البناء، ثم يستدعيها المصنع مرة أخرى داخل دالة الاستدعاء الراجِع. هذه هي تكلفة نقل نقطة الفشل إلى ما قبل الترويسات. عندما يكون البناء المزدوج باهظ التكلفة لمستند معيَّن، تجاوَز فحص ما قبل البناء واقبَل أن فشل البناء داخل دالة الاستدعاء الراجِع سيبتر استجابة قد بدأت بالفعل. - لا توجد
Content-Length. تُغفِل صيغة البث الترويسة. لن تعمل أشرطة تقدُّم التنزيل وطلباتRange. استخدمdownload()/inline()المُخزَّنَين مؤقتًا عندما يكون الطول المعروف مطلوبًا. - الوكيل المُخزِّن مؤقتًا يُلغي الفائدة. أي وكيل عكسي أو مخزن مؤقت لإخراج PHP يلتقط المتن بأكمله قبل إعادة توجيهه يحتفظ بملف PDF الكامل مرة أخرى، مما يلغي النسخة التي جرى توفيرها. كوِّن الوكيل لبث استجابات
application/pdf، أو استخدم استجابة مُخزَّنة مؤقتًا في ذلك المسار. - CodeIgniter 4 ليس مبثوثًا بدالة استدعاء راجِع. يوفِّر تكامل CodeIgniter أسماء الطرق نفسها
streamInline()/streamDownload()، لكنها تُعيدCodeIgniter\HTTP\DownloadResponseيحتفظ بالمتن الكامل، وليسStreamedResponseمُدارًا بدالة استدعاء راجِع. ينطبق نمط StreamedResponse في هذه الصفحة على Laravel وSymfony فقط. - لا تكتب إلى المتن بعد الإعادة. تملك دالة الاستدعاء الراجِع المبثوثة الإخراج. لا تستخدم
echoولا تكتب إلى متن الاستجابة بنفسك بعد أن تُعيدStreamedResponseإلى إطار العمل. - المستندات المُوقَّعة تفشل بسرعة. استدعاء
getPdfData()على مستند مُعَدٍّ لتوقيع PAdES عالي المستوى يثيرNextPDF\Exception\NotImplementedExceptionبدلًا من إصدار ملف غير مُوقَّع. ابثّ الإخراج المُوقَّع عبر مسار التوقيع المُوثَّق، وليس عبر هذه الوصفة.
الأداء
قسم بعنوان «الأداء»يحدُّ البث من نسخة الاستجابة، لا من بناء المستند. تقارب ذروة الذاكرة حجم ملف PDF واحد مكتمل، لأن getPdfData() تُجسِّد المستند بأكمله قبل إرسال الجزء الأول. في المستندات الكبيرة حقًّا أو متعددة الصفحات، يهيمن البناء نفسه، لا النقل، على ميزانية الطلب. انقُل التوليد بعيدًا عن مسار الطلب باستخدام مهمة في قائمة الانتظار. راجِع توليد ملف PDF في مهمة بقائمة الانتظار.
حجم الجزء البالغ 64 KB ثابت وحتمي في كلا التكاملين. يتحكَّم في دقة النقل فقط، ولا يغيِّر إجمالي البايتات المُرسَلة أو ذروة الذاكرة. اختَر صيغة البث عندما تكون نسخة الاستجابة المُوفَّرة هي القيد ولا يكون شريط التقدُّم مطلوبًا. اختَر الصيغة المُخزَّنة مؤقتًا للاستجابات الصغيرة الحسَّاسة للكمون التي تستفيد من Content-Length معروف.
ملاحظات الأمان
قسم بعنوان «ملاحظات الأمان»- تحقَّق من صحة المدخل قبل البناء. يرفض إجراء الإنتاج مُعرِّفًا خارج النطاق برمز
422قبل تشغيل أي عمل بناء. لا تُدرِج أبدًا مدخلًا غير مُتحقَّق منه في البناء أو في اسم الملف. - تطهير اسم الملف مُطبَّق نيابةً عنك. يطهِّر كلا مصنعَي البث اسم الملف ويضيفان مجموعة ترويسات تقوية الاستجابة من OWASP. مرِّر قيمة تتحكَّم فيها، ودَع المصنع يطهِّرها كطبقة ثانية. لا ترمِّز اسم الملف يدويًّا.
- حُدَّ الذاكرة المتزامنة. بما أن ملف PDF بأكمله يُجسَّد في الذاكرة لكل طلب، فإن حركة المرور المتزامنة العالية تضاعف ذروة الذاكرة. افرض حدود الحجم والمعدَّل على المدخلات التي تقود البناء للتخفيف من هجوم حجب الخدمة باستنزاف الذاكرة.
- سجِّل فئة الفشل، لا الرسالة. تسجِّل كتلة catch
$exception::classومُعرِّف ارتباط، ولا تسجِّل أبدًا رسالة الاستثناء أو أثر المكدس. الأثر الخام في وجهة السجل تسريب للمعلومات. - لا توجد كتلة catch فارغة. يسجِّل كل فرع catch في هذه الصفحة ويُعيد استجابة خطأ معرَّفة.
المطابقة
قسم بعنوان «المطابقة»لا يقدِّم هذا الدليل أي ادِّعاء معياري بالمطابقة للمواصفات. كل صنف وطريقة وترويسة معروضة هي الواجهة العامة المُتحقَّق منها للتكامل المُسمَّى: NextPDF\Core\Document::getPdfData()، ومصنعا البث NextPDF\Laravel\Http\PdfResponse وNextPDF\Symfony\Http\PdfResponse، ونوع الإرجاع Symfony\Component\HttpFoundation\StreamedResponse. دلالات ترويسات تقوية الاستجابة وفق OWASP التي يطبِّقها المصنعان موثَّقة، مع مراجعها، في صفحة الأمان والتشغيل الخاصة بكل تكامل والمرتبطة ضمن قسم انظر أيضًا. تعيد صفحة دليل الوصفات هذه عرض الاستخدام، وتُحيل المراجع المعيارية إلى تلك الصفحات.
انظر أيضًا
قسم بعنوان «انظر أيضًا»- إعادة ملف PDF مُولَّد من وحدة تحكم: نظيرا
inline()وdownload()المُخزَّنان مؤقتًا. - توليد ملف PDF في مهمة في قائمة الانتظار: انقل البناء خارج خيط الطلب.
- استخدام Laravel في الإنتاج: وحدة تحكم موصولة بحقن التبعيات، ومجموعة ترويسات OWASP، وعقد ربط الحاوية.
- استخدام Symfony في الإنتاج: دالة الاستدعاء الراجِع المبثوثة، ومُصدِّر الأجزاء بحجم 64 KB، ومُحدِّد موضع الباني.