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

بث ملف 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 مُدار بدالة استدعاء راجِع. يوضِّح قسم الحالات الحدِّية هذا الفرق.

ثبِّت التكامل الخاص بإطار العمل لديك. شغِّل أحد الأوامر التالية.

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

بالنسبة إلى ⁨Laravel⁩، انشُر التكوين بعد التثبيت.

Terminal window
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 جديدًا مع تطبيق الإعدادات الافتراضية المُكوَّنة.
الاهتمام⁨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\StreamedResponseSymfony\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⁩، وليس ملفًّا.

نموذج التعليمات البرمجية — بداية سريعة

قسم بعنوان «نموذج التعليمات البرمجية — بداية سريعة»

إليك الحد الأدنى لإجراء تنزيل مبثوث في كل إطار عمل. تستخدم استدعاءات المستند الواجهة الأساسية نفسها؛ ولا يختلف سوى هيكل وحدة التحكم. يسلِّم مصنع البث إطارَ العمل دالة استدعاء راجِع، لذلك يعود إجراؤك فورًا. يُبنى المتن ويُفرَّغ عندما يرسل إطار العمل الاستجابة.

Laravel: app/Http/Controllers/ReportController.php
<?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');
}
}
Symfony: src/Controller/ReportController.php
<?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() داخل دالة الاستدعاء الراجِع المبثوثة، لذلك يظهر أي استثناء تثيره بعد أن يكون إطار العمل قد بدأ في إرسال الترويسات. للحفاظ على فائدة معالجة الأخطاء، ابنِ المستند (الخطوة التي قد تفشل) قبل أن تُعيد الاستجابة، والتقِط فشل البناء هناك. عندئذٍ لا يحدث داخل دالة الاستدعاء الراجِع سوى النقل المُجزَّأ للبايتات المبنية مسبقًا.

Laravel: app/Http/Controllers/StatementController.php
<?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⁩ التي يطبِّقها المصنعان موثَّقة، مع مراجعها، في صفحة الأمان والتشغيل الخاصة بكل تكامل والمرتبطة ضمن قسم انظر أيضًا. تعيد صفحة دليل الوصفات هذه عرض الاستخدام، وتُحيل المراجع المعيارية إلى تلك الصفحات.