Menyematkan berkas dan membuat portofolio PDF
Sekilas pandang
Bagian berjudul “Sekilas pandang”Resep ini melampirkan satu atau beberapa berkas ke PDF dan, ketika Anda memiliki beberapa lampiran, menyusunnya sebagai portofolio PDF. Gunakan resep ini ketika dokumen perlu membawa bukti pendukung dalam berkas yang sama: faktur beserta lembar waktu yang mendasarinya, lembar data produk beserta ekspor Computer-Aided Design (CAD), atau catatan arsip yang menyimpan spreadsheet sumber di samping laporan yang dirender.
NextPDF menyediakan dua titik masuk pada objek dokumen. embedFile() membaca berkas dari disk; embedFileFromString() menyematkan bita dalam memori yang Anda hasilkan saat runtime. Keduanya mendaftarkan lampiran tersebut. Saat save(), mesin menulis tiap lampiran sebagai aliran berkas tersemat, membungkusnya dalam kamus spesifikasi berkas, dan menautkan setiap spesifikasi ke pohon nama EmbeddedFiles pada tingkat dokumen. ISO 32000-2 mendefinisikan pohon nama tersebut sebagai tempat aliran berkas tersemat dilekatkan ke dokumen secara keseluruhan melalui kamus nama.
Ini adalah kemampuan Core tanpa batasan komersial. Application Programming Interface (API) lampiran telah stabil sejak 1.0.0 dan berjalan di seluruh matriks backport 8.1-8.4.
Instalasi
Bagian berjudul “Instalasi”composer require nextpdf/core:^3Tidak diperlukan ekstensi opsional.
Tinjauan konseptual
Bagian berjudul “Tinjauan konseptual”Setiap lampiran melewati tiga struktur PDF. Memahaminya membantu Anda memeriksa keluaran dan men-debug berkas yang tidak sesuai.
- Aliran berkas tersemat. Bita mentah dari berkas yang dilampirkan dikompresi dengan Flate, lalu ditulis sebagai objek aliran dengan
/Typebernilai/EmbeddedFile. NextPDF mencatat ukuran asli, checksum MD5, dan tanggal modifikasi di kamus parameter aliran tersebut. Mesin mengodekan tipe Multipurpose Internet Mail Extensions (MIME) yang terdeteksi sebagai/Subtypealiran tersebut. - Kamus spesifikasi berkas. Pembungkus metadata yang membawa nama berkas yang ditampilkan (
/Fdan versi Unicode/UF), deskripsi yang dapat dibaca manusia (/Desc), referensi ke aliran tersemat (/EF), serta hubungan berkas dengan dokumen induk (/AFRelationship). - Pohon nama
EmbeddedFiles. Indeks tunggal pada tingkat dokumen yang memetakan nama setiap lampiran ke spesifikasi berkasnya. ISO 32000-2 mewajibkan setiap spesifikasi berkas yang dijangkau melalui pohon ini membawa entriEFyang nilainya mereferensikan aliran berkas tersemat. NextPDF membangun dan menyeimbangkan pohon ini untuk Anda saatsave().
Nilai hubungan penting untuk kesesuaian. PDF Association Application Note 0002 menyatakan bahwa berkas terkait memerlukan entri AFRelationship yang dipilih dari himpunan tetap PDF 2.0: Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema, atau Unspecified. NextPDF memodelkan himpunan tersebut sebagai enum AFRelationship dan menolak nilai lainnya. Pilih istilah yang menjelaskan alasan keberadaan berkas tersebut: lembar waktu di balik faktur adalah Source; kumpulan data yang dapat dibaca mesin di balik grafik adalah Data.
Sebuah portofolio PDF (disebut collection dalam ISO 32000-2) adalah lapisan berikutnya di atas struktur tersebut. Ketika dokumen membawa beberapa lampiran, kamus Collection katalog memberi tahu pembaca cara menyajikannya: tabel detail yang dapat diurutkan, tata letak ubin, atau amplop tersembunyi. ISO 32000-2 menjelaskan kamus Collection sebagai kontrol yang digunakan pemroses PDF untuk menyajikan lampiran berkas sebagai portofolio yang terorganisasi. NextPDF memodelkannya sebagai objek nilai CollectionDictionary, dengan CollectionSort untuk urutan kolom dalam tampilan detail.
Permukaan API
Bagian berjudul “Permukaan API”Metode tingkat dokumen disediakan oleh concern HasFileAttachments pada \NextPDF\Core\Document:
embedFile(string $path, string $description = ''): static— membaca berkas dari$pathdan melampirkannya. NextPDF mendeteksi tipe MIME dari ekstensi; hubungan default adalahUnspecified. Membaca berkas hingga 100 MB; gunakanembedFileFromString()untuk payload yang lebih besar. Mengembalikan dokumen agar dapat digunakan untuk chaining.embedFileFromString(string $data, string $filename, string $description = '', string $afRelationship = '/Unspecified'): static— melampirkan bita dalam memori dengan nama tampilan$filename. Berikan literalAFRelationship(dengan atau tanpa garis miring di awal) untuk menetapkan hubungan tersebut. Mengembalikan dokumen agar dapat digunakan untuk chaining.
Tipe pendukung berada di namespace \NextPDF\Navigation dan \NextPDF\Document:
\NextPDF\Navigation\AFRelationship— enum untuk delapan nilai hubungan yang valid.AFRelationship::coerce()menormalkan string atau case enum dan melempar pengecualian untuk nilai yang tidak dikenal.toPdfName()menghasilkan literal/Name.\NextPDF\Document\CollectionDictionary— membangun kamusCollectionkatalog. KonstantaVIEW_DETAILS,VIEW_TILE,VIEW_HIDDEN,VIEW_CUSTOM, danVIEW_NONEmemilih mode penyajian; konstruktor juga menerima nama dokumen awal dan pengurutan opsional.\NextPDF\Document\CollectionSort— objek nilai pengurutan kolom untuk portofolio tampilan detail.
Contoh kode — Mulai cepat
Bagian berjudul “Contoh kode — Mulai cepat”Contoh minimal ini melampirkan kumpulan data comma-separated values (CSV) yang dihasilkan ke halaman faktur dan mendeklarasikannya sebagai Source yang menjadi dasar pembuatan faktur tersebut.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Navigation\AFRelationship;
$doc = Document::createStandalone();$doc->addPage();$doc->setFont('helvetica', 'B', 18);$doc->cell(0, 12, 'Invoice INV-2026-0042', newLine: true);
// Attach the line-item dataset the invoice was rendered from.$csv = "sku,qty,unit_price\nA-100,3,49.00\nB-220,1,180.00\n";$doc->embedFileFromString( data: $csv, filename: 'line-items.csv', description: 'Source line items for INV-2026-0042', afRelationship: AFRelationship::Source->value,);
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/invoice-with-attachment.pdf');Pembaca menampilkan line-items.csv pada panel lampiran, dan hubungan tersebut menandainya sebagai sumber faktur.
Contoh kode — Produksi
Bagian berjudul “Contoh kode — Produksi”Contoh lengkap ini melampirkan berkas dari disk dan kumpulan data dalam memori, memvalidasi jalur disk terhadap direktori basis yang masuk daftar izin sebelum membacanya, serta membangun portofolio yang dapat diurutkan untuk lampiran tersebut. Contoh ini menangkap pengecualian NextPDF paling spesifik yang mungkin muncul dari jalur lampiran, lalu mengembalikan kode keluar yang terdefinisi, bukan menelan kegagalan begitu saja.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Document\CollectionDictionary;use NextPDF\Document\CollectionSort;use NextPDF\Exception\CompressionException;use NextPDF\Exception\InvalidConfigException;use NextPDF\Exception\PageLayoutException;use NextPDF\Navigation\AFRelationship;
/** * Resolve a caller-supplied filename against an allowed base directory. * * Rejects path traversal and stream wrappers so an embedded attachment can * never read outside the directory the application owns. Returns the * canonical absolute path, or null when the input escapes the base. * * @param non-empty-string $baseDir Absolute path to the allowed directory. * @param non-empty-string $userName Untrusted filename from the request. */function resolveWithinBase(string $baseDir, string $userName): ?string{ $base = \realpath($baseDir); if ($base === false) { return null; }
$candidate = \realpath($base . \DIRECTORY_SEPARATOR . \basename($userName)); if ($candidate === false || !\str_starts_with($candidate, $base . \DIRECTORY_SEPARATOR)) { return null; }
return $candidate;}
$attachmentsDir = __DIR__ . '/attachments';$requestedFile = 'timesheet-2026-05.pdf';
$safePath = resolveWithinBase($attachmentsDir, $requestedFile);if ($safePath === null) { \fwrite(\STDERR, "Rejected attachment path: outside the allowed directory\n"); exit(2);}
try { $doc = Document::createStandalone(); $doc->setTitle('Invoice INV-2026-0042 with supporting documents'); $doc->addPage(); $doc->setFont('helvetica', 'B', 18); $doc->cell(0, 12, 'Invoice INV-2026-0042', newLine: true);
// 1. A validated file from disk: the supporting timesheet. $doc->embedFile( $safePath, 'Timesheet supporting the billed hours', );
// 2. An in-memory dataset generated at runtime. $lineItems = "sku,qty,unit_price\nA-100,3,49.00\nB-220,1,180.00\n"; $doc->embedFileFromString( data: $lineItems, filename: 'line-items.csv', description: 'Machine-readable line items', afRelationship: AFRelationship::Data->value, );
// Present both attachments as a sortable details portfolio. The sort // keys reference columns declared in the portfolio /Schema; here the // built-in filename and modification-date fields order the view. $portfolio = new CollectionDictionary( view: CollectionDictionary::VIEW_DETAILS, initialDocument: 'line-items.csv', sort: new CollectionSort( keys: ['_Filename', '_ModDate'], ascending: [true, false], ), ); // $portfolio->toPdfDictionary() yields the catalog /Collection literal, // shared with the unencrypted-wrapper envelope path.
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/invoice-portfolio.pdf'; $doc->save($out);
echo "Wrote {$out} with 2 attachments and a details portfolio\n";} catch (PageLayoutException $e) { // Unreadable path, oversized file, null byte, or a MIME-type name that // exceeds the 127-byte PDF name limit. \fwrite(\STDERR, "Attachment rejected: {$e->getMessage()}\n"); exit(1);} catch (CompressionException | InvalidConfigException $e) { // The attachment data could not be compressed, or a config value was invalid. \fwrite(\STDERR, "Write failed: {$e->getMessage()}\n"); exit(1);}CollectionDictionary dan CollectionSort adalah objek nilai. Keduanya memvalidasi masukan saat konstruksi dan melakukan serialisasi menjadi literal katalog /Collection yang menggerakkan tampilan portofolio di pembaca.
Kasus tepi & jebakan
Bagian berjudul “Kasus tepi & jebakan”- Masukan jalur adalah tanggung jawab Anda.
embedFile()melindungi dari bita null dan stream wrapper serta menyelesaikan jalur sebenarnya, tetapi tidak menerapkan daftar izin untuk direktori basis. Jika jalur berasal dari permintaan, validasi terlebih dahulu, seperti yang dilakukan contoh produksi denganresolveWithinBase(). - Batas atas 100 MB hanya berlaku untuk
embedFile(). Berkas yang melebihi104,857,600bita menimbulkanPageLayoutException. Untuk payload yang lebih besar, alirkan sendiri bita tersebut dan teruskan keembedFileFromString(). - Nama tipe MIME yang panjang ditolak. Tipe MIME yang terdeteksi menjadi
/Subtypealiran tersemat, yaitu token nama PDF yang dibatasi hingga 127 bita oleh ISO 32000-2. Tipe yang sangat panjang (beberapa format Office mendekati 90 bita) tetap jauh di bawah batas, tetapi tipe yang diberikan secara manual dan melebihinya menimbulkanPageLayoutException. Biarkan mesin mendeteksi tipe dari ekstensi kecuali Anda memiliki alasan khusus untuk menggantinya. - Hubungan yang tidak dikenal melempar pengecualian.
AFRelationship::coerce()menolak nilai apa pun di luar himpunan tetap, bukan menurunkannya menjadiUnspecified. Berikan case enum (AFRelationship::Source->value) agar salah ketik tidak sampai ke runtime. - Nama berkas harus unik di dalam pohon nama. Dua lampiran dengan nama tampilan yang sama akan bertabrakan di indeks
EmbeddedFiles. Beri setiap lampiran nama berkas yang unik. _ModDatedicatat dalam Coordinated Universal Time (UTC).embedFile()membaca waktu modifikasi berkas dan menulisnya dengangmdate()sehingga fixture yang sama menghasilkan tanggal yang identik pada tingkat bita di seluruh mesin, terlepas dari pengaturan zona waktu.
Kinerja
Bagian berjudul “Kinerja”Setiap lampiran dikompresi satu kali dengan gzcompress() pada level 9 dan ditulis sebagai satu aliran tunggal saat save(). Kompresi mendominasi biaya dan berskala mengikuti ukuran payload yang dilampirkan, bukan konten halaman. Beberapa berkas pendukung kecil (kumpulan data, spreadsheet, satu lembar waktu PDF) tetap berada dalam anggaran 2000 ms / 64 MB. Untuk banyak lampiran berukuran besar, bita tersemat menjadi batas bawah penggunaan memori: sebuah lampiran 50 MB yang disimpan sebagai string menempati setidaknya sebesar itu sebelum kompresi. Utamakan embedFileFromString() dengan pembuatan berbasis potongan daripada memuat beberapa berkas besar sekaligus.
Pohon nama dibangun satu kali saat save(). Hingga 64 entri tetap berada dalam pohon datar dengan satu akar. Di luar itu, NextPDF mempartisi pohon menjadi rentang Kids dan Limits yang seimbang, sehingga biaya indeks tetap logaritmik untuk himpunan lampiran besar.
Catatan keamanan
Bagian berjudul “Catatan keamanan”- Validasi setiap jalur yang tidak tepercaya terhadap daftar izin. Penyematan membaca berkas apa pun yang dapat dijangkau oleh proses PHP. Tanpa pemeriksaan direktori basis, nama berkas yang dirancang khusus dapat mengubah lampiran menjadi Local File Inclusion (LFI). Contoh produksi menunjukkan pemeriksaan daftar izin; terapkan setiap kali nama berkas bukan konstanta saat kompilasi.
- Perlakukan bita yang dilampirkan sebagai tidak tepercaya di sisi yang mengonsumsinya. Bagi NextPDF, berkas tersemat bersifat opaque. Mesin tidak mengurai atau mengeksekusinya. Risiko berada di tempat berkas tersebut nantinya dibuka. Tetapkan hubungan dan deskripsi agar konsumen di hilir mengetahui isi setiap lampiran sebelum mengekstraknya.
- Jangan simpan rahasia di dalam lampiran atau deskripsi. Nama berkas, deskripsi, dan bita disimpan secara terbuka kecuali seluruh dokumen dienkripsi. Untuk melindungi lampiran, enkripsi dokumen dengan kebijakan izin (lihat resep terkait). Jangan sematkan kredensial, kunci, atau data pribadi yang tidak akan Anda tempatkan di halaman yang dirender.
- Tidak ada akses jaringan dalam resep ini. Setiap bita dibaca dari jalur lokal yang telah divalidasi atau dipasok di dalam memori.
Kesesuaian
Bagian berjudul “Kesesuaian”| Pernyataan | Spesifikasi | Klausul | reference_id |
|---|---|---|---|
Aliran berkas tersemat menempel pada dokumen melalui entri EmbeddedFiles di kamus nama. | ISO 32000-2 | 7.11.4 | |
Pohon nama EmbeddedFiles memetakan nama ke spesifikasi berkas yang entri EF-nya mereferensikan aliran berkas tersemat. | ISO 32000-2 | 7.7.4 | |
Berkas terkait memerlukan nilai AFRelationship dari himpunan tetap PDF 2.0. | PDF Association AN002 | 3 | |
Kamus Collection katalog mengontrol penyajian portofolio dari lampiran. | ISO 32000-2 | 7.11.6 |
Profil reproduktibilitas — struktural. /ID pada trailer, atom tanggal per-penyimpanan, dan /ModDate aliran tersemat bervariasi antar-eksekusi, sehingga perbandingan struktural menghapus nilai-nilai tersebut sebelum melakukan diff pada grafik objek. Resep ini menjelaskan cara NextPDF menghasilkan struktur tersebut. Resep ini tidak menyatakan kesesuaian PDF/A-4f secara menyeluruh, yang bergantung pada dokumen lengkap. Untuk profil arsip yang mewajibkan setiap lampiran mendeklarasikan hubungan dan deskripsi, lihat resep PDF/A-4.