Lewati ke konten

Memvalidasi nilai bidang formulir dan meratakannya sambil mempertahankan status interaktif

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.

Terminal window
composer require nextpdf/core

Anda tidak memerlukan ekstensi tambahan. Trait penulisan formulir dan flattener sama-sama disertakan dalam core.

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:

  1. Core tidak memvalidasi nilai bidang. Setiap metode penulisan (textField(), comboBox(), checkBox(), dan selebihnya) menuliskan langsung nilai apa pun yang Anda berikan ke V. 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.

  2. Perataan bersifat tidak dapat dibatalkan dan mencakup seluruh dokumen. Setelah Anda memanggil flattenForms() dan save(), 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 memanggil flattenForms(). 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.

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 pada default.
  • comboBox(string $name, float $x, float $y, float $w, float $h, array $items, string $selected = ''): static — menuliskan tarik-turun dengan item yang dipilih pada selected.
  • checkBox(string $name, float $x, float $y, float $size, bool $checked = false): static — menuliskan kotak centang dengan status pada checked.
  • 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 ke NextPDF\Form\FormFlattener.

NextPDF\Core\Concerns\HasOutput:

  • save(string $path): void — membangun dan menulis PDF. Melempar NextPDF\Exception\InvalidConfigException ketika 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.

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',
'email' => '[email protected]',
'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";

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',
'email' => '[email protected]',
'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 copies

Salinan 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.

  • Validasi sebelum Anda menulis, bukan sesudahnya. Metode penulisan menuliskan nilai Anda ke V apa adanya. Tidak ada kait (hook) core yang menolak nilai cacat saat save(), 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(), lalu save(). Memanggil flattenForms() tanpa bidang adalah no-op yang aman; memanggilnya setelah save() 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, atau true; nilai kosong atau Off hanya menggambar kotaknya. Berikan boolean yang sesungguhnya ke checkBox() agar status yang dirender cocok dengan nilai tervalidasi Anda.
  • Bidang tanda tangan tidak pernah diratakan. Permukaan bidang /Sig adalah 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.

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.

  • 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.
PernyataanSpesifikasiKlausulreference_id
Formulir yang diratakan adalah representasi non-interaktif (statis) dari bidang-bidangnya.ISO 32000-212.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.