Проверка значений полей формы и сведение с сохранением интерактивного состояния
Коротко о главном
Заголовок раздела «Коротко о главном»Промышленный конвейер обработки форм редко сводит необработанный ввод. Сначала вы проверяете каждое значение. Затем решаете, какие поля станут постоянными, а какие останутся редактируемыми. Для этой работы NextPDF core предоставляет два базовых механизма: трейт создания полей HasFormFields, который записывает значение поля в документ при создании, и flattenForms(), который запекает каждое поле в статическую графику страницы и удаляет интерактивную форму.
Этот рецепт дополняет эти два механизма шагом проверки на уровне приложения, который core намеренно оставляет вам. Вы:
- Проверите карту значений по правилам каждого поля до создания любого поля, чтобы недопустимое значение никогда не попало в документ.
- Сформируете один проверенный набор данных, затем выведете его дважды: один раз в сведённом виде (заблокированная копия только для чтения) и один раз в интерактивном (редактируемая копия), чтобы одно и то же состояние полей переносилось в оба результата.
Необходимые условия: рабочая установка NextPDF core (composer require nextpdf/core), а также знакомство с разделами Создание и предварительное заполнение PDF-формы и Сведение полей формы, механику создания полей и сведения из которых объединяет этот рецепт.
Граница области применения.
flattenForms()в core — операция уровня всего документа: она сводит либо все поля, либо ни одного. В API форм core нет публичного переключателя сведения для отдельных полей и нет встроенного средства проверки значений. Поэтому сценарий “свести одни, оставить другие редактируемыми” реализуется на уровне приложения: проверьте один раз, затем отрисуйте один и тот же проверенный набор данных в два документа. Этот рецепт описывает данный шаблон; он не выдумывает метод core для отдельных полей.
Установка
Заголовок раздела «Установка»composer require nextpdf/coreДополнительное расширение не требуется. Трейт создания форм и средство сведения входят в core.
Концептуальный обзор
Заголовок раздела «Концептуальный обзор»Поле формы Acrobat (AcroForm) хранит текущее значение в элементе V своего словаря поля. flattenForms() считывает значение V каждого поля и отрисовывает его в поток содержимого страницы-владельца: текстовые поля становятся текстом BT ... Tj ... ET, флажки и переключатели — нарисованными контурами, а поля выбора отрисовывают выбранный элемент. Затем она удаляет элемент каталога /AcroForm. Результат — неинтерактивная форма: статическое представление полей, которое отображается одинаково в любой программе просмотра и не требует возможности заполнения формы (ISO 32000-2 12.7).
Промышленный шаблон определяется двумя фактами:
-
Core не проверяет значения полей. Каждый метод создания (
textField(),comboBox(),checkBox()и остальные) записывает любое переданное вами значение прямо вV. Формат адреса электронной почты, принадлежность к допустимым вариантам и наличие обязательных полей — задачи приложения. Проверяйте значения перед созданием и быстро завершайте работу с ошибкой при нарушении, вместо того чтобы выпускать документ с запечённым в него недопустимым значением. -
Сведение необратимо и затрагивает весь документ. После вызова
flattenForms()иsave()поля становятся статической графикой. Чтобы сохранить ещё и редактируемую копию, вы не отменяете сведение, а отрисовываете проверенный набор данных второй раз, не вызываяflattenForms(). Обе копии создаются из одних и тех же проверенных значений, поэтому заблокированная и редактируемая копии несут идентичное состояние полей.
Профиль воспроизводимости — structural: каждый документ содержит в трейлере массив /ID, который финальный проход нормализует перед сравнением двух прогонов.
Поверхность API
Заголовок раздела «Поверхность API»NextPDF\Core\Document (через NextPDF\Core\Concerns\HasFormFields):
textField(string $name, float $x, float $y, float $w, float $h, string $default = '', array $options = []): static— создаёт текстовое поле, значение которого задаётся вdefault.comboBox(string $name, float $x, float $y, float $w, float $h, array $items, string $selected = ''): static— создаёт раскрывающийся список, выбранный элемент которого задаётся вselected.checkBox(string $name, float $x, float $y, float $size, bool $checked = false): static— создаёт флажок, состояние которого задаётся вchecked.flattenForms(): static— запекает значение каждого поля в статическое содержимое страницы и удаляет AcroForm. При отсутствии полей не выполняет никаких действий. Внутренне делегирует работуNextPDF\Form\FormFlattener.
NextPDF\Core\Concerns\HasOutput:
save(string $path): void— формирует и записывает PDF. ВыбрасываетNextPDF\Exception\InvalidConfigException, если путь вывода является потоковой оболочкой, содержит нулевой байт или указывает на несуществующий родительский каталог.
Средство проверки в примерах ниже — это ваш код приложения, а не символ core. В core нет API проверки значений, поэтому шаг проверки здесь задан явно.
Пример кода — быстрый старт
Заголовок раздела «Пример кода — быстрый старт»Этот минимальный процесс проверяет карту значений, создаёт на её основе три поля, затем сводит и сохраняет одну заблокированную копию.
<?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";Пример кода — промышленный вариант
Заголовок раздела «Пример кода — промышленный вариант»Промышленный процесс отделяет проверку от отрисовки. Типизированный FieldRuleSet проверяет карту значений один раз и возвращает проверенный набор данных. Один вспомогательный метод renderForm() создаёт поля и запускается дважды: со сведением для заблокированной копии и без него для редактируемой. Обе копии создаются из одних и тех же проверенных значений, поэтому интерактивное состояние переносится в обе. Обвязка считывает пути вывода из NEXTPDF_COOKBOOK_LOCKED_OUTPUT и 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);}Ожидаемый вывод:
Wrote locked and editable registration copiesЗаблокированная копия открывается без интерактивных полей: значения представлены статической графикой. Редактируемая копия открывается с теми же предварительно заполненными значениями, и каждое поле остаётся редактируемым. Обе отражают единый проверенный набор данных.
Граничные случаи и подводные камни
Заголовок раздела «Граничные случаи и подводные камни»- Проверяйте перед созданием, а не после. Методы создания записывают ваше значение в
Vбез изменений. Ни один обработчик в core не отклоняет некорректное значение на этапеsave(), поэтому значение, которое прошло мимо проверки, запекается в сведённую копию без возможности восстановления. flattenForms()работает по принципу “всё или ничего”. Он сводит каждое поле в документе. Чтобы оставить некоторые поля редактируемыми, отрисуйте второй документ без вызова сведения, как это делает промышленный пример, — не ждите переключателя для отдельных полей в API core.- Порядок вызовов. Сначала создайте все поля, затем вызовите
flattenForms(), а потомsave(). ВызовflattenForms()при отсутствии полей безопасен и не выполняет никаких действий; вызов послеsave()не влияет на уже записанные байты. - Имена полей должны быть уникальными. Два поля с одинаковым именем становятся в соответствующих стандарту программах просмотра одним логическим полем с общим значением. Проверяйте имена на уникальность, когда они формируются на основе данных.
- Истинность флажка при сведении. Сведённый флажок рисует галочку, когда его значение равно
Yes,On,1илиtrue; пустое значение илиOffрисует только рамку. Передавайте вcheckBox()настоящее булево значение, чтобы отрисованное состояние совпадало с вашим проверенным значением. - Поля подписи никогда не сводятся. Поверхность поля
/Sig— это внешний вид, формируемый из данных подписи, а не значение, которое можно повторно отрисовать, поэтому средство сведения его пропускает. Сводите перед подписанием, но никогда после. - Редактируемая копия по-прежнему остаётся редактируемой. Сохранение состояния в редактируемой копии означает, что получатель может его изменить. Рассматривайте заблокированную копию как авторитетную запись, а редактируемую копию — как рабочий черновик.
Производительность
Заголовок раздела «Производительность»Проверка линейна по числу полей и выполняется один раз на набор данных. Каждая отрисовка создаёт для каждого поля одну аннотацию-виджет и внешний вид. Шаг сведения добавляет для каждого поля ограниченный блок содержимого. Двойная отрисовка набора данных примерно удваивает затраты на документ, но всё равно укладывается в бюджет 1500 мс / 64 МБ для форм из нескольких сотен полей. Если вам нужен только один результат, отрисуйте один раз и пропустите второй проход.
Примечания по безопасности
Заголовок раздела «Примечания по безопасности»- Проверяйте недоверенный ввод на границе. Формат адреса электронной почты, принадлежность к допустимым вариантам и наличие обязательных полей обеспечиваются в коде приложения, потому что core этого не делает. Экранируйте или нормализуйте любое значение из недоверенного ввода до того, как оно попадёт в поле: core записывает его в документ без изменений.
- Сведение — это не управление доступом. Сведённое значение нельзя редактировать в обычной программе просмотра, но оно остаётся видимым в содержимом страницы и извлекаемым любым текстовым инструментом. Не рассматривайте сведение как редактирование с удалением данных или как защиту конфиденциальных значений.
- Редактируемая копия несёт те же данные. Распространяйте её только среди сторон, которым разрешено видеть и изменять эти значения. Когда содержимое конфиденциально, сочетайте любую из копий с Шифрованием с разрешениями и учитывайте описанную там оговорку о зависимости от программы просмотра: биты разрешений не обеспечивают ограничений на чтение.
- Завершайте работу с ошибкой при сбое. При сбое проверки или вывода промышленный пример завершается с ненулевым кодом вместо записи частичного или недопустимого документа. Никогда не подавляйте эти исключения.
Соответствие стандартам
Заголовок раздела «Соответствие стандартам»| Утверждение | Спецификация | Пункт | Идентификатор ссылки (reference_id) |
|---|---|---|---|
| Сведённая форма — это неинтерактивное (статическое) представление полей. | ISO 32000-2 | 12.7 |
NextPDF формирует статическую структуру, описанную в цитируемом пункте; он не заявляет о полном соответствии ISO 32000-2. Правила проверки в этом рецепте — политика приложения, а не требование стандарта к соответствию.
Смотрите также
Заголовок раздела «Смотрите также»- Создание и предварительное заполнение PDF-формы — создание полей и задание их начальных значений.
- Сведение полей формы — сведение всего документа, на котором основан этот рецепт.
- Обработка ошибок с помощью иерархии исключений NextPDF — перехват сбоев с нужной степенью детализации.
- Шифрование с разрешениями — добавление конфиденциальности для конфиденциальных данных формы.
- Модуль форм — справочник по полям формы.