İçeriğe geç

Oluşturulan büyük bir PDF'yi HTTP yanıtı olarak akışla gönderme

Bir denetleyicide büyük bir PDF oluşturur ve baytları, yanıt arabelleğinde ikinci bir tam kopya tutmadan döndürmek istersiniz. Her çerçeve entegrasyonu, kendi PdfResponse fabrikasının akış varyantlarını içerir: streamInline() ve streamDownload(). Her yöntem, PDF gövdesini istemciye sabit 64 KB’lik parçalar hâlinde yazan bir geri çağırmayla birlikte bir çerçeve StreamedResponse nesnesi döndürür.

Bu yolu seçmeden önce bellek modelini inceleyin. Motor önce belgenin tamamını bellekte oluşturur. Akış geri çağırması, PDF’nin tamamını tek bir dize olarak somutlaştıran getPdfData() çağrısını yapar, ardından bu dize üzerinde 64 KB’lik dilimler hâlinde ilerler. İkinci kopyanın tepe maliyetinden kurtulursunuz; bu kopya, arabelleğe alınmış bir Illuminate\Http\Response veya Symfony\Component\HttpFoundation\Response nesnesinin, çerçeve Content-Length değerini ölçerken tuttuğu kopyadır. Akış varyantı uzunluğu ölçmez, bu nedenle Content-Length başlığını atlar. Yanıt gövdesini ve belge dizesini hiçbir zaman aynı anda tutmaz. Bu, gerçek artımlı akış değildir: NextPDF artımlı yazıcı yüzeyine sahip değildir; bu nedenle belge, ilk bayt sokete ulaşmadan önce tamamen somutlaştırılır.

Başlamadan önce şu parçaların yerinde olduğundan emin olun:

  • NextPDF core kuruludur; nextpdf/laravel veya nextpdf/symfony çerçeve entegrasyonlarından biri kurulmuş ve keşfedilmiştir.
  • Çerçevenizde bir isteği bir denetleyiciye nasıl yönlendireceğinizi zaten biliyorsunuz.
  • Şu sayfayı okudunuz: Bir denetleyiciden oluşturulan bir PDF döndürme; bu sayfa, bu reçetenin üzerine inşa edildiği arabelleğe alınmış inline() ve download() fabrikalarını kapsar.

Bu nasıl-yapılır kılavuzu, Laravel ve Symfony tarafından paylaşılan StreamedResponse desenine odaklanır. CodeIgniter 4 aynı streamInline() / streamDownload() yöntem adlarını sunar, ancak baytları bir CodeIgniter\HTTP\DownloadResponse nesnesine sarar; bu nesne, geri çağırma temelli bir StreamedResponse değildir. Uç durumlar bölümü bu farkı ele alır.

Çerçeveniz için entegrasyonu kurun. Aşağıdaki komutlardan birini çalıştırın.

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

Laravel için, kurulumdan sonra yapılandırmayı yayımlayın.

Terminal window
php artisan vendor:publish --tag=nextpdf-config

Symfony paketi Flex aracılığıyla kaydedilir. Devam etmeden önce çerçevenizin kurulum sayfasında keşfedildiğini doğrulayın.

Arabelleğe alınmış bir yanıt fabrikası, PdfResponse::download() veya PdfResponse::inline(), getPdfData() çağrısını yapar, döndürülen dizeyi bir Response nesnesinde saklar ve Content-Length değerini strlen() ile ayarlar. Çerçeve daha sonra bu dizeyi yanıtın ömrü boyunca tutar. Büyük bir belgede, belge dizesi ile yanıt gövdesi dizesi aynı anda bellekte bulunur.

Akış fabrikası farklı bir biçim kullanır. PdfResponse::streamDownload() ve PdfResponse::streamInline(), bir geri çağırmayla oluşturulmuş bir StreamedResponse döndürür. Çerçeve, bu geri çağırmayı yalnızca gövdeyi göndermeye hazır olduğunda çağırır. Geri çağırma içinde entegrasyon getPdfData() çağrısını bir kez yapar, döndürülen dizeyi 64 KB’lik parçalara böler, her parçayı echo ile yazar ve ardından flush() çağrısı yapar. Gövdenin ikinci bir kalıcı kopyasını tutmaz ve bir Content-Length başlığı yaymaz.

Bu sayfadaki her kararı iki olgu şekillendirir:

  • Oluşturma istekli, aktarım parçalıdır. getPdfData(), NextPDF\Core\Document üzerinde yazıcıyı çağırır ve PDF’nin tamamını tek bir dize olarak döndürür. 64 KB’lik parçalama yalnızca halihazırda oluşturulmuş baytların süreçten nasıl çıktığını denetler. Tepe bellek, küçük bir akış penceresiyle değil, tamamlanmış belgenin boyutuyla sınırlıdır.
  • Content-Length yok. Akış varyantı, gövdeyi geri çağırma içinde oluşturmadan gövde uzunluğunu bilemez, bu nedenle başlığı atlar. İstemci tarafındaki bir ilerleme çubuğu, bir Range isteği veya uzunluğa duyarlı bir proxy boyut bilgisi görmez. Bilinen bir uzunluk, yanıt kopyasından tasarruf etmekten daha önemli olduğunda arabelleğe alınmış download() / inline() seçeneğini seçin.

Belgeyi çerçevenin alışılmış çözümleme yolu üzerinden edinin:

  • Laravel: NextPDF\Contracts\DocumentFactoryInterface arayüzünü kapsayıcıdan çözümleyin ve create() çağrısını yapın. Bu çağrı, akış fabrikalarının kabul ettiği somut tür olan yeni bir NextPDF\Core\Document döndürür.
  • Symfony: NextPDF\Symfony\Service\PdfFactory sınıfını enjekte edin ve create() çağrısını yapın. Bu çağrı, yapılandırılmış varsayılanlar uygulanmış yeni bir NextPDF\Core\Document döndürür.
İlgi alanıLaravelSymfony
Yeni belgeapp(DocumentFactoryInterface::class)->create()PdfFactory::create()
Akışlı satır içiPdfResponse::streamInline($doc, $name)PdfResponse::streamInline($doc, $name)
Akışlı indirmePdfResponse::streamDownload($doc, $name)PdfResponse::streamDownload($doc, $name)
Döndürülen türSymfony\Component\HttpFoundation\StreamedResponseSymfony\Component\HttpFoundation\StreamedResponse
Geri çağırma içindeki oluşturma çağrısıNextPDF\Core\Document::getPdfData()NextPDF\Core\Document::getPdfData()
Parça boyutu64 KB (belirlenimci str_split)64 KB (belirlenimci substr döngüsü)

Laravel PdfResponse sınıfı NextPDF\Laravel\Http\PdfResponse konumunda bulunur; Symfony karşılığı ise NextPDF\Symfony\Http\PdfResponse konumundadır. Her ikisinin akış fabrikaları da aynı Symfony\Component\HttpFoundation\StreamedResponse türünü döndürür. İkisi de aynı sabit Open Web Application Security Project (OWASP) yanıt sıkılaştırma başlığı kümesini (X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Content-Security-Policy: default-src 'none', X-Robots-Tag: noindex, nofollow, Referrer-Policy: no-referrer) uygular ve indirme dosya adını arındırır. Bu başlıkları kendiniz eklemezsiniz.

Her iki fabrika da PDF ikilisinin tamamını oluşturup döndüren aynı temel core yüzeyini, NextPDF\Core\Document::getPdfData(): string, çağırır. Bunun kardeşi olan save(string $path): void, aynı baytları atomik bir yazıcı aracılığıyla diske yazar. Bu reçete getPdfData() kullanır; çünkü hedef bir dosya değil, bir HTTP soketidir.

Her çerçevedeki en yalın akışlı indirme eylemi aşağıdadır. Belge çağrıları aynı core yüzeyini kullanır; yalnızca denetleyici iskeleti farklıdır. Akış fabrikası çerçeveye bir geri çağırma verir; bu nedenle eyleminiz hemen geri döner. Gövde, çerçeve yanıtı gönderdiğinde oluşturulur ve boşaltılır.

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');
}
}

İndirmeyi zorlamak yerine bir tarayıcı sekmesinde önizlemek için streamInline(...) çağrısını streamDownload(...) yerine yapın. Content-Disposition başlığı inline olur ve diğer tüm başlıklar aynı kalır.

Bir üretim eylemi bağımlılıklarını enjekte eder, yol girişini doğrular, oluşturmanın yükseltebileceği en özel istisnayı yakalar, bir izleme sızdırmadan hata sınıfını günlüğe kaydeder ve tanımlanmış bir Hypertext Transfer Protocol (HTTP) hatası döndürür. Aşağıdaki örnek Laravel yapıcı enjeksiyonunu kullanır. Symfony karşılığı aynı biçimi izler; eylem başına PdfFactory enjekte edilir.

getPdfData() akış geri çağırması içinde çalışır; bu nedenle yükselttiği bir istisna, çerçeve başlıkları göndermeye başladıktan sonra ortaya çıkar. Hata işlemeyi yararlı tutmak için belgeyi, yani başarısız olabilecek adımı, yanıtı geri vermeden önce oluşturun ve oluşturma başarısızlığını orada yakalayın. Ardından geri çağırma içinde yalnızca halihazırda oluşturulmuş baytların parçalı aktarımı gerçekleşir.

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;
}
}

Herhangi bir oluşturma başarısızlığı için tek bir işleyici istediğinizde, her NextPDF istisnasının türediği soyut taban olan NextPDF\Exception\NextPdfException istisnasını yakalayın. Belirli nedenlere yanıt vermek için önce getPdfData() tarafından yükseltilebilecek somut alt türleri yakalayın: içerik sayfa geometrisine sığmadığında NextPDF\Exception\PageLayoutException, akış sıkıştırması başarısız olduğunda NextPDF\Exception\CompressionException ve geçersiz bir çıktı yapılandırması için NextPDF\Exception\InvalidConfigException. Hiçbir zaman boş bir catch bloğu yazmayın. Buradaki her dal, hata sınıfını günlüğe kaydeder ve tanımlanmış bir durum döndürür.

Eylem başına yeni bir belge çözümlemek, fabrikayı testlerde değiştirilebilir tutar. Uzun süre çalışan tek bir işçi sürecinde, ilgisiz iki belge için aynı denetleyici örneğini yeniden kullanmayın; çünkü eski içerik durumu taşınır.

  • Belge, önce-doğrula-sonra-akışla-gönder deseninde iki kez oluşturulur. Üretim örneği, oluşturmayı doğrulamak için getPdfData() çağrısını bir kez yapar; ardından fabrika bunu geri çağırma içinde tekrar çağırır. Bu, başarısızlık noktasını başlıkların önüne taşımanın maliyetidir. Belirli bir belgede çift oluşturma çok maliyetli olduğunda, ön oluşturma yoklamasını atlayın ve geri çağırma içindeki bir oluşturma başarısızlığının halihazırda başlamış bir yanıtı kesintiye uğratacağını kabul edin.
  • Content-Length yok. Akış varyantı başlığı atlar. İndirme ilerleme çubukları ve Range istekleri çalışmaz. Bilinen bir uzunluk gerektiğinde, arabelleğe alınmış download() / inline() seçeneğini kullanın.
  • Arabelleğe alan proxy yararı ortadan kaldırır. Gövdenin tamamını iletmeden önce yakalayan bir ters proxy veya PHP çıktı arabelleği, PDF’nin tamamını yeniden tutar ve bu da kaydedilen kopyayı ortadan kaldırır. Proxy’yi application/pdf yanıtlarını akışla gönderecek şekilde yapılandırın veya o yolda arabelleğe alınmış bir yanıt kullanın.
  • CodeIgniter 4 geri çağırma ile akış yapmaz. CodeIgniter entegrasyonu aynı streamInline() / streamDownload() yöntem adlarını sunar, ancak bunlar gövdenin tamamını tutan bir CodeIgniter\HTTP\DownloadResponse döndürür; bu nesne, geri çağırma temelli bir StreamedResponse değildir. Bu sayfadaki StreamedResponse deseni yalnızca Laravel ve Symfony için geçerlidir.
  • Geri döndükten sonra gövdeye yazmayın. Çıktı, akış geri çağırmasına aittir. Yanıt gövdesine kendiniz echo ile ya da başka bir yolla yazmayın; özellikle StreamedResponse nesnesini çerçeveye geri döndürdükten sonra bunu yapmayın.
  • İmzalı belgeler hızla başarısız olur. Üst düzey bir PAdES imzası için kurulmuş bir belge üzerinde getPdfData() çağrısı, imzasız bir dosya yaymak yerine NextPDF\Exception\NotImplementedException yükseltir. İmzalı çıktıyı bu reçete üzerinden değil, belgelenmiş imzalama yolu üzerinden akışla gönderin.

Akış, belge oluşturmayı değil, yanıt kopyasını sınırlar. Tepe bellek kabaca tamamlanmış bir PDF boyutundadır; çünkü getPdfData() ilk parçayı göndermeden önce belgenin tamamını somutlaştırır. Gerçekten büyük veya çok sayfalı bir belgede, istek bütçesine aktarım değil, oluşturmanın kendisi hâkim olur. Oluşturmayı kuyruğa alınmış bir işle istek iş parçacığından uzaklaştırın. Kuyruğa alınmış bir işte PDF oluşturma sayfasına bakın.

64 KB’lik parça boyutu her iki entegrasyonda da sabit ve belirlenimcidir. Yalnızca aktarım ayrıntı düzeyini denetler; gönderilen toplam baytları veya tepe belleği değiştirmez. Kısıt, tasarruf edilen yanıt kopyası olduğunda ve ilerleme çubuğu gerekmediğinde akış varyantını seçin. Bilinen bir Content-Length değerinden yararlanan küçük, gecikmeye duyarlı yanıtlar için arabelleğe alınmış varyantı seçin.

  • Oluşturmadan önce girişi doğrulayın. Üretim eylemi, herhangi bir oluşturma işi çalışmadan önce aralık dışındaki bir tanımlayıcıyı 422 ile reddeder. Doğrulanmamış girişi hiçbir zaman oluşturmaya veya dosya adına eklemeyin.
  • Dosya adı arındırması sizin için uygulanır. Her iki akış fabrikası da dosya adını arındırır ve OWASP yanıt sıkılaştırma başlığı kümesini ekler. Denetlediğiniz bir değer geçirin ve fabrikanın bunu ikinci bir katman olarak arındırmasına izin verin. Dosya adını elle kodlamayın.
  • Eşzamanlı belleği sınırlayın. PDF’nin tamamı istek başına bellekte somutlaştırıldığından, yüksek eşzamanlı trafik tepe belleği katlar. Bellek tükenmesi kaynaklı hizmet reddini azaltmak için, bir oluşturmayı yönlendiren girişlere boyut ve hız sınırları uygulayın.
  • İletiyi değil, hata sınıfını günlüğe kaydedin. catch bloğu, istisna iletisini veya yığın izini değil, $exception::class değerini ve bir ilişkilendirme tanımlayıcısını günlüğe kaydeder. Günlük havuzundaki ham bir izleme, bilgi sızıntısıdır.
  • Boş catch yok. Bu sayfadaki her catch dalı günlüğe kaydeder ve tanımlanmış bir hata yanıtı döndürür.

Bu kılavuz hiçbir normatif standart iddiasında bulunmaz. Gösterilen her sınıf, yöntem ve başlık, adı geçen entegrasyonun doğrulanmış genel yüzeyidir: NextPDF\Core\Document::getPdfData(), NextPDF\Laravel\Http\PdfResponse ve NextPDF\Symfony\Http\PdfResponse akış fabrikaları ve Symfony\Component\HttpFoundation\StreamedResponse dönüş türü. Fabrikaların uyguladığı OWASP yanıt sıkılaştırma başlığı semantiği, alıntılarıyla birlikte, Ayrıca bakınız altında bağlantılı her entegrasyonun güvenlik ve işlemler sayfasında belgelenmiştir. Bu Cookbook sayfası kullanımı yeniden ifade eder ve normatif alıntıları o sayfalara bırakır.