Bỏ qua để đến nội dung

Kiểm tra giá trị trường biểu mẫu rồi làm phẳng mà vẫn giữ trạng thái tương tác

Một quy trình biểu mẫu trong môi trường thực tế hiếm khi làm phẳng trực tiếp dữ liệu nhập thô. Trước tiên, bạn kiểm tra từng giá trị. Sau đó, bạn quyết định trường nào trở thành cố định và trường nào vẫn có thể chỉnh sửa. NextPDF core cung cấp hai thành phần nền tảng cho công việc này: trait tạo trường HasFormFields, vốn ghi giá trị trường vào tài liệu ngay khi bạn tạo trường đó, và flattenForms(), vốn nung mọi trường thành đồ họa trang tĩnh rồi loại bỏ biểu mẫu tương tác.

Công thức này kết nối hai thành phần đó bằng một bước kiểm tra ở tầng ứng dụng, phần mà core chủ ý để bạn tự xử lý. Bạn sẽ:

  • Kiểm tra một bản đồ giá trị theo các quy tắc riêng cho từng trường trước khi bạn tạo bất kỳ trường nào, để một giá trị không hợp lệ không bao giờ lọt vào tài liệu.
  • Tạo một tập dữ liệu đã kiểm tra, rồi xuất tập đó hai lần — một lần làm phẳng (bản đã khóa, chỉ đọc) và một lần tương tác (bản có thể chỉnh sửa) — để cùng một trạng thái trường được giữ qua cả hai kết quả đầu ra.

Điều kiện cần: có bản cài đặt NextPDF core đang hoạt động (composer require nextpdf/core), cùng với việc đã đọc qua Tạo và điền sẵn biểu mẫu PDFLàm phẳng trường biểu mẫu, vì công thức này kết hợp cơ chế tạo trường và làm phẳng được trình bày trong hai bài đó.

Ranh giới phạm vi. Hàm flattenForms() của core là một thao tác trên toàn tài liệu: nó làm phẳng mọi trường hoặc không trường nào cả. API biểu mẫu của core không có công tắc làm phẳng công khai cho từng trường và không có trình kiểm tra giá trị tích hợp sẵn. Vì vậy, việc “làm phẳng một số, giữ những trường khác có thể chỉnh sửa” diễn ra ở tầng ứng dụng: kiểm tra một lần, rồi kết xuất cùng tập dữ liệu đã kiểm tra đó thành hai tài liệu. Công thức này mô tả mẫu xử lý đó; nó không tạo ra một phương thức core cho từng trường.

Terminal window
composer require nextpdf/core

Bạn không cần thêm tiện ích mở rộng nào. Cả trait tạo biểu mẫu và trình làm phẳng đều có sẵn trong core.

Một trường biểu mẫu Acrobat (AcroForm) lưu giá trị hiện tại trong mục V của từ điển trường. flattenForms() đọc giá trị V của từng trường và kết xuất giá trị đó vào luồng nội dung của trang chứa trường — trường văn bản trở thành văn bản BT ... Tj ... ET, hộp kiểm và nút chọn (radio) trở thành các đường vẽ, còn trường lựa chọn kết xuất mục đang được chọn. Sau đó, hàm xóa mục /AcroForm khỏi catalog. Kết quả là một biểu mẫu không tương tác: một biểu diễn tĩnh của các trường, hiển thị giống hệt nhau trong mọi trình đọc, không cần tính năng điền biểu mẫu (ISO 32000-2 12.7).

Hai điểm thực tế định hình mẫu xử lý này:

  1. Core không kiểm tra giá trị trường. Mỗi phương thức tạo (textField(), comboBox(), checkBox(), và các phương thức còn lại) ghi thẳng bất kỳ giá trị nào bạn truyền vào V. Định dạng email, việc giá trị có thuộc tập tùy chọn được phép, và việc trường bắt buộc có hiện diện hay không là trách nhiệm của ứng dụng. Hãy kiểm tra trước khi tạo, và dừng ngay khi có vi phạm thay vì xuất ra một tài liệu đã nung cứng một giá trị không hợp lệ.

  2. Việc làm phẳng là không thể đảo ngược và áp dụng cho toàn tài liệu. Sau khi bạn gọi flattenForms()save(), các trường trở thành đồ họa tĩnh. Để giữ thêm một bản có thể chỉnh sửa, bạn không thể hoàn tác việc làm phẳng — bạn kết xuất tập dữ liệu đã kiểm tra lần thứ hai mà không gọi flattenForms(). Cả hai bản đều khởi đầu từ cùng các giá trị đã kiểm tra, nên bản đã khóa và bản có thể chỉnh sửa mang trạng thái trường giống hệt nhau.

Hồ sơ khả năng tái lập là structural: mỗi tài liệu mang một mảng /ID ở trailer mà một bước hậu xử lý chuẩn hóa trước khi so sánh hai lần chạy.

NextPDF\Core\Document (qua NextPDF\Core\Concerns\HasFormFields):

  • textField(string $name, float $x, float $y, float $w, float $h, string $default = '', array $options = []): static — tạo một trường văn bản với giá trị nằm trong default.
  • comboBox(string $name, float $x, float $y, float $w, float $h, array $items, string $selected = ''): static — tạo một danh sách xổ xuống với mục được chọn nằm trong selected.
  • checkBox(string $name, float $x, float $y, float $size, bool $checked = false): static — tạo một hộp kiểm với trạng thái nằm trong checked.
  • flattenForms(): static — nung giá trị của mọi trường thành nội dung trang tĩnh và loại bỏ AcroForm. Không làm gì nếu không có trường nào. Bên trong, hàm ủy quyền cho NextPDF\Form\FormFlattener.

NextPDF\Core\Concerns\HasOutput:

  • save(string $path): void — dựng và ghi PDF. Ném NextPDF\Exception\InvalidConfigException khi đường dẫn đầu ra là một stream wrapper, chứa byte null hoặc trỏ tới một thư mục cha không tồn tại.

Trình kiểm tra trong các ví dụ bên dưới là mã ứng dụng do bạn sở hữu, không phải ký hiệu do core cung cấp. Core không có API kiểm tra giá trị, đó là lý do bước kiểm tra được nêu rõ ở đây.

Luồng tối giản này kiểm tra một bản đồ giá trị, tạo ba trường từ nó, rồi làm phẳng và lưu một bản đã khóa.

<?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";

Luồng thực tế tách bước kiểm tra khỏi bước kết xuất. Một FieldRuleSet được định kiểu rõ ràng sẽ kiểm tra bản đồ giá trị một lần và trả về một tập dữ liệu đã kiểm tra. Hàm trợ giúp renderForm() tạo các trường và chạy hai lần — có làm phẳng cho bản đã khóa, và không làm phẳng cho bản có thể chỉnh sửa. Cả hai bản đều đến từ cùng các giá trị đã kiểm tra, nên trạng thái tương tác được giữ qua cả hai. Trình chạy kiểm thử đọc các đường dẫn đầu ra từ NEXTPDF_COOKBOOK_LOCKED_OUTPUTNEXTPDF_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);
}

Kết quả mong đợi:

Wrote locked and editable registration copies

Khi mở bản đã khóa, bạn sẽ không thấy trường tương tác nào — các giá trị là đồ họa tĩnh. Bản có thể chỉnh sửa mở ra với cùng các giá trị được điền sẵn, và mọi trường vẫn có thể chỉnh sửa. Cả hai đều phản ánh cùng một tập dữ liệu đã kiểm tra.

  • Hãy kiểm tra trước khi bạn tạo, không phải sau đó. Các phương thức tạo ghi nguyên văn giá trị của bạn vào V. Không có hook nào của core từ chối một giá trị sai định dạng vào thời điểm save(), nên một giá trị đã bỏ qua bước kiểm tra sẽ bị nung vào bản đã làm phẳng mà không có cách khôi phục.
  • flattenForms() là tất cả hoặc không gì cả. Nó làm phẳng mọi trường trên tài liệu. Để giữ một số trường có thể chỉnh sửa, hãy kết xuất một tài liệu thứ hai mà không gọi làm phẳng, như mẫu thực tế đã làm — đừng mong có công tắc cho từng trường trong API của core.
  • Thứ tự gọi. Hãy tạo mọi trường trước, rồi gọi flattenForms(), rồi save(). Gọi flattenForms() khi không có trường nào là một thao tác an toàn không làm gì; gọi nó sau save() không có tác dụng gì lên các byte đã được ghi.
  • Tên trường phải là duy nhất. Hai trường dùng chung một tên sẽ trở thành một trường logic với giá trị dùng chung trong các trình đọc tuân thủ chuẩn. Hãy kiểm tra tính duy nhất của tên khi chúng được tạo từ dữ liệu.
  • Trạng thái đúng/sai của hộp kiểm khi làm phẳng. Một hộp kiểm sau khi làm phẳng sẽ vẽ dấu kiểm khi giá trị của nó là Yes, On, 1, hoặc true; một giá trị rỗng hoặc Off chỉ vẽ ô. Hãy truyền một giá trị boolean thực sự cho checkBox() để trạng thái được kết xuất khớp với giá trị đã kiểm tra của bạn.
  • Trường chữ ký không bao giờ được làm phẳng. Phần hiển thị của một trường /Sig là hình thức được tạo ra từ phần dữ liệu chữ ký của nó, không phải một giá trị có thể kết xuất lại, nên trình làm phẳng bỏ qua nó. Hãy làm phẳng trước khi bạn ký, đừng bao giờ làm sau khi ký.
  • Bản có thể chỉnh sửa vẫn là bản có thể chỉnh sửa. Giữ trạng thái trong bản có thể chỉnh sửa nghĩa là người nhận có thể thay đổi nó. Hãy xem bản đã khóa là bản ghi chính thức và bản có thể chỉnh sửa là bản nháp đang làm việc.

Việc kiểm tra có chi phí tuyến tính theo số lượng trường và chạy một lần cho mỗi tập dữ liệu. Mỗi lần kết xuất tạo một widget annotation và một hình thức hiển thị đi kèm cho mỗi trường. Bước làm phẳng thêm một khối nội dung có giới hạn cho mỗi trường. Kết xuất tập dữ liệu hai lần khiến tổng chi phí cho mỗi tập dữ liệu tăng khoảng gấp đôi, nhưng vẫn nằm gọn trong ngân sách 1500 ms / 64 MB cho các biểu mẫu vài trăm trường. Nếu bạn chỉ cần một kết quả đầu ra, hãy kết xuất một lần và bỏ qua lượt thứ hai.

  • Hãy kiểm tra dữ liệu nhập không tin cậy ở ranh giới. Định dạng email, việc giá trị có thuộc tập tùy chọn được phép, và việc trường bắt buộc có hiện diện hay không đều được thực thi trong mã ứng dụng vì core không thực thi chúng. Hãy escape hoặc chuẩn hóa mọi giá trị bắt nguồn từ dữ liệu nhập không tin cậy trước khi giá trị đó đi vào trường, vì core ghi nó nguyên văn vào tài liệu.
  • Làm phẳng không phải là kiểm soát truy cập. Một giá trị đã làm phẳng không thể chỉnh sửa trong trình đọc thông thường, nhưng nó vẫn hiển thị trong nội dung trang và có thể trích xuất bằng bất kỳ công cụ văn bản nào. Đừng coi làm phẳng là cách che giấu thông tin hoặc biện pháp bảo vệ cho các giá trị nhạy cảm.
  • Bản có thể chỉnh sửa mang cùng dữ liệu. Chỉ phân phối nó cho những bên được phép xem và thay đổi các giá trị đó. Khi nội dung nhạy cảm, hãy kết hợp một trong hai bản với Mã hóa với quyền hạn, và lưu ý cảnh báo về sự phụ thuộc vào trình đọc được mô tả ở đó: các bit quyền hạn không thực thi việc hạn chế đọc.
  • Hỏng theo hướng an toàn. Mẫu thực tế thoát với mã thoát khác không khi gặp lỗi kiểm tra hoặc lỗi đầu ra thay vì ghi một tài liệu thiếu hoặc không hợp lệ. Đừng bao giờ âm thầm bỏ qua các ngoại lệ này.
Phát biểuĐặc tảĐiều khoảnreference_id
Một biểu mẫu đã làm phẳng là một biểu diễn không tương tác (tĩnh) của các trường.ISO 32000-212.7

NextPDF tạo ra cấu trúc tĩnh được mô tả bởi điều khoản đã trích dẫn; nó không tuyên bố tuân thủ ISO 32000-2 đầy đủ. Các quy tắc kiểm tra trong công thức này là chính sách của ứng dụng, không phải một yêu cầu tuân thủ của tiêu chuẩn.