Memvalidasi nilai bidang formulir dan meratakannya sambil mempertahankan status interaktif
Sekilas
Bagian berjudul “Sekilas”Dalam produksi, alur formulir jarang meratakan masukan mentah. Pertama, Anda memvalidasi setiap nilai. Setelah itu, Anda menentukan bidang mana yang dibuat permanen dan mana yang tetap dapat diedit. NextPDF core menyediakan dua blok penyusun untuk pekerjaan ini: trait penulisan HasFormFields, yang menuliskan nilai bidang ke dalam dokumen saat Anda membuatnya, dan flattenForms(), yang mengubah setiap bidang menjadi grafis halaman statis dan menghilangkan formulir interaktif.
Resep ini menghubungkan kedua blok tersebut dengan langkah validasi di lapisan aplikasi, yang memang diserahkan core kepada Anda. Anda akan:
- Memvalidasi peta nilai terhadap aturan per bidang sebelum menuliskan bidang apa pun, sehingga nilai yang tidak valid tidak pernah sampai ke dokumen.
- Membangun satu kumpulan data tervalidasi, lalu merendernya dua kali — sekali sebagai salinan terkunci yang diratakan dan hanya-baca, dan sekali sebagai salinan interaktif yang dapat diedit — sehingga status bidang yang sama terbawa ke kedua keluaran.
Prasyarat: instalasi NextPDF core yang berfungsi (composer require nextpdf/core), serta telah membaca tuntas Membangun dan mengisi awal formulir PDF dan Meratakan bidang formulir, karena resep ini menggabungkan mekanisme penulisan bidang dan perataan dari keduanya.
Batas cakupan.
flattenForms()milik core adalah operasi untuk seluruh dokumen: ia meratakan semua bidang atau tidak sama sekali. API formulir core tidak menyediakan sakelar publik untuk meratakan per bidang maupun validator nilai bawaan. Jadi “ratakan sebagian, biarkan yang lain dapat diedit” dilakukan di lapisan aplikasi: validasi sekali, lalu render kumpulan data tervalidasi yang sama ke dalam dua dokumen. Resep ini mendokumentasikan pola tersebut; ia tidak menciptakan metode core per bidang.
Instalasi
Bagian berjudul “Instalasi”composer require nextpdf/coreAnda tidak memerlukan ekstensi tambahan. Trait penulisan formulir dan flattener sama-sama disertakan dalam core.
Tinjauan konseptual
Bagian berjudul “Tinjauan konseptual”Bidang formulir Acrobat (AcroForm) menyimpan nilai saat ini di entri V pada kamus bidangnya. flattenForms() membaca nilai V dari setiap bidang dan merendernya ke dalam aliran konten halaman pemiliknya — bidang teks menjadi teks BT ... Tj ... ET, kotak centang dan tombol radio menjadi jalur yang digambar, dan bidang pilihan merender item yang dipilih. Lalu ia menghapus entri katalog /AcroForm. Hasilnya adalah formulir non-interaktif: representasi statis dari bidang yang ditampilkan secara identik di pembaca mana pun, tanpa memerlukan kemampuan pengisian formulir (ISO 32000-2 12.7).
Dua fakta membentuk pola produksi:
-
Core tidak memvalidasi nilai bidang. Setiap metode penulisan (
textField(),comboBox(),checkBox(), dan selebihnya) menuliskan langsung nilai apa pun yang Anda berikan keV. Format email, apakah nilai termasuk opsi yang diizinkan, dan keberadaan bidang wajib adalah tanggung jawab aplikasi. Validasi sebelum Anda menulis, dan hentikan proses lebih awal saat ada pelanggaran alih-alih menghasilkan dokumen dengan nilai buruk yang sudah terbakukan. -
Perataan bersifat tidak dapat dibatalkan dan mencakup seluruh dokumen. Setelah Anda memanggil
flattenForms()dansave(), bidang-bidang tersebut menjadi grafis statis. Untuk tetap menyimpan salinan yang dapat diedit, Anda tidak membatalkan perataan; Anda merender kumpulan data tervalidasi untuk kedua kalinya tanpa memanggilflattenForms(). Kedua salinan berasal dari nilai tervalidasi yang sama, sehingga salinan terkunci dan salinan yang dapat diedit membawa status bidang yang identik.
Profil reproduksibilitasnya adalah structural: setiap dokumen menyertakan larik /ID pada trailer, yang dinormalisasi oleh pasca-proses sebelum dua hasil eksekusi dibandingkan.
Permukaan API
Bagian berjudul “Permukaan API”NextPDF\Core\Document (melalui NextPDF\Core\Concerns\HasFormFields):
textField(string $name, float $x, float $y, float $w, float $h, string $default = '', array $options = []): static— menuliskan bidang teks dengan nilai padadefault.comboBox(string $name, float $x, float $y, float $w, float $h, array $items, string $selected = ''): static— menuliskan tarik-turun dengan item yang dipilih padaselected.checkBox(string $name, float $x, float $y, float $size, bool $checked = false): static— menuliskan kotak centang dengan status padachecked.flattenForms(): static— mengubah nilai setiap bidang menjadi konten halaman statis dan menghilangkan AcroForm. Jika tidak ada bidang, ia tidak melakukan apa-apa (no-op). Secara internal, ia mendelegasikan keNextPDF\Form\FormFlattener.
NextPDF\Core\Concerns\HasOutput:
save(string $path): void— membangun dan menulis PDF. MelemparNextPDF\Exception\InvalidConfigExceptionketika jalur keluaran berupa pembungkus aliran (stream wrapper), mengandung byte null, atau menyebutkan direktori induk yang tidak ada.
Validator dalam contoh di bawah adalah kode aplikasi milik Anda sendiri, bukan simbol core. Core tidak memiliki API validasi nilai; karena itu, langkah validasi di sini dibuat eksplisit.
Contoh kode — Mulai cepat
Bagian berjudul “Contoh kode — Mulai cepat”Alur minimal ini memvalidasi peta nilai, menuliskan tiga bidang berdasarkan peta tersebut, lalu meratakan dan menyimpan satu salinan terkunci.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
/** @var array<string, string> $input Untrusted value map (e.g. from a request). */$input = [ 'full_name' => 'Ada Lovelace', 'country' => 'Taiwan',];
$allowedCountries = ['United Kingdom', 'Taiwan', 'Japan'];
// Validate before authoring: core writes whatever you pass, so reject early.if (trim($input['full_name']) === '') { throw new InvalidArgumentException('full_name must not be empty.');}if (filter_var($input['email'], FILTER_VALIDATE_EMAIL) === false) { throw new InvalidArgumentException('email is not a valid address.');}if (!in_array($input['country'], $allowedCountries, true)) { throw new InvalidArgumentException('country is not an allowed option.');}
$doc = Document::createStandalone();$doc->setTitle('Validated Form (locked copy)');$doc->addPage();
$doc->textField(name: 'full_name', x: 20, y: 30, w: 90, h: 8, default: $input['full_name']);$doc->textField(name: 'email', x: 20, y: 45, w: 90, h: 8, default: $input['email']);$doc->comboBox( name: 'country', x: 20, y: 60, w: 90, h: 8, items: $allowedCountries, selected: $input['country'],);
// Whole-document flatten: every field becomes static graphics.$doc->flattenForms();
$doc->save(__DIR__ . '/registration-locked.pdf');echo "Wrote registration-locked.pdf\n";Contoh kode — Produksi
Bagian berjudul “Contoh kode — Produksi”Alur produksi memisahkan validasi dari rendering. Kelas FieldRuleSet dengan tipe eksplisit memvalidasi peta nilai satu kali dan mengembalikan kumpulan data tervalidasi. Satu helper renderForm() menuliskan bidang-bidang dan dijalankan dua kali — dengan perataan untuk salinan terkunci, dan tanpa perataan untuk salinan yang dapat diedit. Kedua salinan berasal dari nilai tervalidasi yang sama, sehingga status interaktif terbawa ke keduanya. Harness membaca jalur keluaran dari NEXTPDF_COOKBOOK_LOCKED_OUTPUT dan NEXTPDF_COOKBOOK_EDITABLE_OUTPUT.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Exception\InvalidConfigException;
/** * A single validated form record. Immutable: once constructed, every value has * already passed the rule set, so the rendering step cannot re-introduce a bad * value. This is the "validate once, render many" boundary. */final readonly class ValidatedRegistration{ /** * @param non-empty-string $fullName * @param non-empty-string $email * @param non-empty-string $country */ public function __construct( public string $fullName, public string $email, public string $country, public bool $newsletter, ) {}}
/** * Application-layer field validation. Core performs no value validation, so the * rules live here. Each method throws on the first violation; the caller maps * the exception to an HTTP 422 or a user-facing message. */final class FieldRuleSet{ /** @var list<non-empty-string> */ private const array ALLOWED_COUNTRIES = ['United Kingdom', 'Taiwan', 'Japan', 'Germany'];
/** * @param array<string, string|bool> $input Untrusted value map. * * @throws InvalidArgumentException When any field fails its rule. */ public function validate(array $input): ValidatedRegistration { $fullName = $this->requireNonEmpty($input, 'full_name'); $email = $this->requireEmail($input, 'email'); $country = $this->requireAllowed($input, 'country', self::ALLOWED_COUNTRIES);
$newsletter = $input['newsletter'] ?? false; if (!is_bool($newsletter)) { throw new InvalidArgumentException('newsletter must be a boolean.'); }
return new ValidatedRegistration( fullName: $fullName, email: $email, country: $country, newsletter: $newsletter, ); }
/** * @param array<string, string|bool> $input * * @return non-empty-string * * @throws InvalidArgumentException */ private function requireNonEmpty(array $input, string $key): string { $value = $input[$key] ?? ''; if (!is_string($value) || trim($value) === '') { throw new InvalidArgumentException(sprintf('%s must not be empty.', $key)); }
return $value; }
/** * @param array<string, string|bool> $input * * @return non-empty-string * * @throws InvalidArgumentException */ private function requireEmail(array $input, string $key): string { $value = $input[$key] ?? ''; if (!is_string($value) || $value === '' || filter_var($value, FILTER_VALIDATE_EMAIL) === false) { throw new InvalidArgumentException(sprintf('%s is not a valid email address.', $key)); }
return $value; }
/** * @param array<string, string|bool> $input * @param list<non-empty-string> $allowed * * @return non-empty-string * * @throws InvalidArgumentException */ private function requireAllowed(array $input, string $key, array $allowed): string { $value = $input[$key] ?? ''; if (!is_string($value) || !in_array($value, $allowed, true)) { throw new InvalidArgumentException(sprintf('%s is not an allowed option.', $key)); }
return $value; }
/** @return list<non-empty-string> */ public function allowedCountries(): array { return self::ALLOWED_COUNTRIES; }}
/** * Author the same field layout from one validated record. When $flatten is * true, every field is baked into static page content and the AcroForm is * dropped; when false, the fields stay interactive and editable. Both paths * start from identical values, preserving the form's state across copies. */function renderForm(ValidatedRegistration $record, FieldRuleSet $rules, bool $flatten): Document{ $doc = Document::createStandalone(); $doc->setTitle($flatten ? 'Customer Registration (locked)' : 'Customer Registration (editable)'); $doc->addPage();
$doc->setFont('helvetica', 'B', 18); $doc->cell(0, 12, 'Customer Registration', newLine: true); $doc->ln(4);
$leftMargin = 15.0; $fieldX = 70.0; $fieldW = 120.0; $fieldH = 8.0; $rowSpacing = 12.0; $y = 40.0;
$doc->setFont('helvetica', '', 10);
$doc->setXY($leftMargin, $y); $doc->cell(50, $fieldH, 'Full Name:'); $doc->textField(name: 'full_name', x: $fieldX, y: $y, w: $fieldW, h: $fieldH, default: $record->fullName); $y += $rowSpacing;
$doc->setXY($leftMargin, $y); $doc->cell(50, $fieldH, 'Email:'); $doc->textField(name: 'email', x: $fieldX, y: $y, w: $fieldW, h: $fieldH, default: $record->email); $y += $rowSpacing;
$doc->setXY($leftMargin, $y); $doc->cell(50, $fieldH, 'Country:'); $doc->comboBox( name: 'country', x: $fieldX, y: $y, w: $fieldW, h: $fieldH, items: $rules->allowedCountries(), selected: $record->country, ); $y += $rowSpacing;
$doc->setXY($leftMargin, $y); $doc->cell(50, $fieldH, 'Newsletter:'); $doc->checkBox(name: 'newsletter', x: $fieldX, y: $y, size: 5, checked: $record->newsletter);
if ($flatten) { // Whole-document flatten: locked, read-only copy. $doc->flattenForms(); }
return $doc;}
/** @var array<string, string|bool> $input Untrusted value map (request payload). */$input = [ 'full_name' => 'Ada Lovelace', 'country' => 'Taiwan', 'newsletter' => true,];
$rules = new FieldRuleSet();
try { // Validate once. A violation aborts before any document is built. $record = $rules->validate($input);
// Render the validated dataset twice: locked, then editable. $locked = renderForm($record, $rules, flatten: true); $editable = renderForm($record, $rules, flatten: false);
$lockedPath = getenv('NEXTPDF_COOKBOOK_LOCKED_OUTPUT') ?: __DIR__ . '/registration-locked.pdf'; $editablePath = getenv('NEXTPDF_COOKBOOK_EDITABLE_OUTPUT') ?: __DIR__ . '/registration-editable.pdf';
$locked->save($lockedPath); $editable->save($editablePath);
echo "Wrote locked and editable registration copies\n";} catch (InvalidArgumentException $e) { // Validation failure: a bad value never reached the document. fwrite(STDERR, 'Form validation failed: ' . $e->getMessage() . "\n"); exit(1);} catch (InvalidConfigException $e) { // Output failure: bad path, missing directory, or stream wrapper. fwrite(STDERR, sprintf( 'PDF save failed for key "%s": %s' . "\n", $e->getConfigKey(), $e->getMessage(), )); exit(1);}Keluaran yang diharapkan:
Wrote locked and editable registration copiesSalinan terkunci terbuka tanpa bidang interaktif — nilainya tampil sebagai grafis statis. Salinan yang dapat diedit terbuka dengan nilai yang sama yang sudah terisi, dan setiap bidang tetap dapat diedit. Keduanya mencerminkan satu kumpulan data tervalidasi yang sama.
Kasus khusus & jebakan
Bagian berjudul “Kasus khusus & jebakan”- Validasi sebelum Anda menulis, bukan sesudahnya. Metode penulisan menuliskan nilai Anda ke
Vapa adanya. Tidak ada kait (hook) core yang menolak nilai cacat saatsave(), sehingga nilai cacat yang lolos tanpa validasi akan terbakukan ke dalam salinan yang diratakan tanpa jalur pemulihan. flattenForms()bersifat semua-atau-tidak-sama-sekali. Ia meratakan setiap bidang dalam dokumen. Untuk menjaga sebagian bidang tetap dapat diedit, render dokumen kedua tanpa memanggil perataan, sebagaimana dilakukan contoh produksi — jangan mengharapkan sakelar per bidang pada API core.- Urutan pemanggilan. Tuliskan setiap bidang terlebih dahulu, lalu panggil
flattenForms(), lalusave(). MemanggilflattenForms()tanpa bidang adalah no-op yang aman; memanggilnya setelahsave()tidak berpengaruh pada byte yang sudah ditulis. - Nama bidang harus unik. Dua bidang yang berbagi nama menjadi satu bidang logis dengan nilai bersama di pembaca yang patuh. Validasi keunikan nama saat nama tersebut ditentukan oleh data.
- Nilai kotak centang yang dianggap benar saat perataan. Kotak centang yang diratakan menggambar tanda centangnya ketika nilainya
Yes,On,1, atautrue; nilai kosong atauOffhanya menggambar kotaknya. Berikan boolean yang sesungguhnya kecheckBox()agar status yang dirender cocok dengan nilai tervalidasi Anda. - Bidang tanda tangan tidak pernah diratakan. Permukaan bidang
/Sigadalah tampilan yang dihasilkan dari muatan tanda tangannya, bukan nilai yang dapat dirender ulang, sehingga flattener melewatinya. Ratakan sebelum Anda menandatangani, jangan pernah sesudahnya. - Salinan yang dapat diedit tetap dapat diedit. Mempertahankan status pada salinan yang dapat diedit berarti penerima dapat mengubahnya. Perlakukan salinan terkunci sebagai catatan otoritatif dan salinan yang dapat diedit sebagai draf kerja.
Performa
Bagian berjudul “Performa”Validasi bersifat linear terhadap jumlah bidang dan berjalan satu kali untuk setiap kumpulan data. Setiap render menuliskan satu anotasi widget, ditambah satu tampilan, untuk tiap bidang. Langkah perataan menambahkan satu blok konten terbatas per bidang. Merender kumpulan data dua kali kira-kira menggandakan biaya per dokumen, tetapi tetap jauh di bawah anggaran 1500 ms / 64 MB untuk formulir dengan beberapa ratus bidang. Jika Anda hanya memerlukan satu keluaran, render sekali dan lewati proses kedua.
Catatan keamanan
Bagian berjudul “Catatan keamanan”- Validasi masukan tidak tepercaya di batas. Format email, apakah nilai termasuk opsi yang diizinkan, dan keberadaan bidang wajib ditegakkan dalam kode aplikasi karena core tidak menegakkannya. Lakukan escape atau normalisasi pada nilai apa pun yang berasal dari masukan tidak tepercaya sebelum nilai itu mencapai bidang, karena core menuliskannya apa adanya ke dalam dokumen.
- Perataan bukan kontrol akses. Nilai yang diratakan tidak dapat diedit di pembaca normal, tetapi tetap terlihat di konten halaman dan dapat diekstraksi dengan alat teks apa pun. Jangan memperlakukan perataan sebagai redaksi atau sebagai perlindungan bagi nilai sensitif.
- Salinan yang dapat diedit membawa data yang sama. Distribusikan hanya kepada pihak yang diizinkan untuk melihat dan mengubah nilai-nilai tersebut. Jika kontennya sensitif, gabungkan salah satu salinan dengan Mengenkripsi dengan izin, dan perhatikan peringatan bahwa hal itu bergantung pada kerja sama pembaca sebagaimana dijelaskan di sana: bit izin tidak menegakkan pembatasan baca.
- Gagal secara tertutup. Contoh produksi keluar dengan kode bukan-nol saat terjadi kegagalan validasi atau keluaran, alih-alih menulis dokumen yang sebagian atau tidak valid. Jangan pernah menelan pengecualian ini.
Konformansi
Bagian berjudul “Konformansi”| Pernyataan | Spesifikasi | Klausul | reference_id |
|---|---|---|---|
| Formulir yang diratakan adalah representasi non-interaktif (statis) dari bidang-bidangnya. | ISO 32000-2 | 12.7 |
NextPDF menghasilkan struktur statis yang dijelaskan oleh klausul yang dikutip; ini bukan pernyataan konformansi ISO 32000-2 secara menyeluruh. Aturan validasi dalam resep ini adalah kebijakan aplikasi, bukan persyaratan konformansi dari standar.
Lihat juga
Bagian berjudul “Lihat juga”- Membangun dan mengisi awal formulir PDF — menuliskan bidang dan menyetel nilai awalnya.
- Meratakan bidang formulir — perataan seluruh dokumen yang menjadi dasar resep ini.
- Menangani kesalahan dengan hierarki pengecualian NextPDF — menangkap kegagalan pada granularitas yang tepat.
- Mengenkripsi dengan izin — menambahkan kerahasiaan ketika data formulir bersifat sensitif.
- Modul formulir — referensi bidang formulir.