Przejdź do głównej zawartości

Walidacja wartości pól formularza i spłaszczanie z zachowaniem stanu pól

W produkcyjnym potoku formularzy rzadko spłaszcza się surowe dane wejściowe. Najpierw sprawdzasz poprawność każdej wartości. Następnie decydujesz, które pola staną się trwałe, a które pozostaną edytowalne. Rdzeń NextPDF udostępnia do tego dwa elementy składowe: cechę HasFormFields służącą do tworzenia formularzy, która zapisuje wartość pola do dokumentu w trakcie jego tworzenia, oraz funkcję flattenForms(), która utrwala każde pole w postaci statycznej grafiki strony i usuwa interaktywny formularz.

Ten przepis łączy te dwa elementy przez krok walidacji w warstwie aplikacji, który rdzeń celowo pozostawia po stronie aplikacji. Wykonasz następujące czynności:

  • Sprawdź mapę wartości zgodnie z regułami dla poszczególnych pól, zanim utworzysz jakiekolwiek pole, tak aby nieprawidłowa wartość nigdy nie trafiła do dokumentu.
  • Zbuduj jeden zweryfikowany zestaw danych, a następnie wygeneruj z niego dwa wyniki — raz jako spłaszczony (zablokowana kopia tylko do odczytu), a raz jako interaktywny (kopia edytowalna) — tak aby ten sam stan pól przeniósł się na oba wyniki.

Wymagania wstępne: działająca instalacja rdzenia NextPDF (composer require nextpdf/core) oraz znajomość przewodników Tworzenie i wstępne wypełnianie formularza PDF i Spłaszczanie pól formularza. Ten przepis łączy opisaną w nich mechanikę tworzenia pól i spłaszczania.

Granica zakresu. Funkcja rdzenia flattenForms() działa na całym dokumencie: spłaszcza wszystkie pola albo żadne. Interfejs formularzy rdzenia nie udostępnia publicznego przełącznika spłaszczania dla poszczególnych pól ani wbudowanego walidatora wartości. Dlatego „spłaszcz niektóre, pozostaw inne edytowalne” trzeba zrealizować w warstwie aplikacji: sprawdź poprawność raz, a następnie wyrenderuj ten sam zweryfikowany zestaw danych do dwóch dokumentów. Ten przepis dokumentuje ten wzorzec; nie wprowadza metody rdzenia działającej na poszczególnych polach.

Okno terminala
composer require nextpdf/core

Nie potrzebujesz dodatkowego rozszerzenia. Cecha formularzy i mechanizm spłaszczania są dostarczane w rdzeniu.

Pole formularza Acrobat (AcroForm) przechowuje bieżącą wartość we wpisie V swojego słownika pola. flattenForms() odczytuje wartość V każdego pola i renderuje ją w strumieniu treści strony, do której należy pole — pola tekstowe stają się tekstem BT ... Tj ... ET, pola wyboru i przyciski opcji stają się narysowanymi ścieżkami, a pola listy renderują wybraną pozycję. Następnie usuwa wpis katalogu /AcroForm. Efektem jest formularz nieinteraktywny: statyczna reprezentacja pól, która wyświetla się identycznie w każdym czytniku, bez wymogu obsługi wypełniania formularza (ISO 32000-2 12.7).

Wzorzec produkcyjny kształtują dwa fakty:

  1. Rdzeń nie sprawdza poprawności wartości pól. Każda metoda tworząca pole (textField(), comboBox(), checkBox() i pozostałe) zapisuje przekazaną wartość bezpośrednio do V. Format adresu e-mail, przynależność do dozwolonych opcji oraz obecność pól wymaganych to kwestie aplikacji. Sprawdzaj poprawność przed utworzeniem pól i przerywaj natychmiast w razie naruszenia, zamiast generować dokument z utrwaloną błędną wartością.

  2. Spłaszczanie jest nieodwracalne i obejmuje cały dokument. Po wywołaniu flattenForms() i save() pola są statyczną grafiką. Aby zachować również kopię edytowalną, nie próbujesz cofać spłaszczania — renderujesz zweryfikowany zestaw danych po raz drugi, bez wywoływania flattenForms(). Obie kopie powstają z tych samych zweryfikowanych wartości, więc kopia zablokowana i kopia edytowalna niosą identyczny stan pól.

Profil odtwarzalności to structural: każdy dokument niesie w zwiastunie tablicę /ID, którą krok końcowy normalizuje przed porównaniem dwóch przebiegów.

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

  • textField(string $name, float $x, float $y, float $w, float $h, string $default = '', array $options = []): static — tworzy pole tekstowe z wartością w default.
  • comboBox(string $name, float $x, float $y, float $w, float $h, array $items, string $selected = ''): static — tworzy listę rozwijaną z wybraną pozycją w selected.
  • checkBox(string $name, float $x, float $y, float $size, bool $checked = false): static — tworzy pole wyboru ze stanem w checked.
  • flattenForms(): static — utrwala wartość każdego pola w statycznej treści strony i usuwa AcroForm. Jest operacją pustą, gdy nie istnieją żadne pola. Wewnętrznie deleguje do NextPDF\Form\FormFlattener.

NextPDF\Core\Concerns\HasOutput:

  • save(string $path): void — buduje i zapisuje plik PDF. Zgłasza NextPDF\Exception\InvalidConfigException, gdy ścieżka wyjściowa jest opakowaniem strumienia, zawiera bajt null lub wskazuje nieistniejący katalog nadrzędny.

Walidator w poniższych przykładach jest częścią kodu aplikacji, a nie symbolem rdzenia. Rdzeń nie ma interfejsu walidacji wartości, dlatego krok walidacji jest tutaj jawny.

Ten minimalny przepływ sprawdza poprawność mapy wartości, tworzy z niej trzy pola, a następnie spłaszcza i zapisuje jedną zablokowaną kopię.

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

Przepływ produkcyjny oddziela walidację od renderowania. Typowany FieldRuleSet sprawdza poprawność mapy wartości raz i zwraca zweryfikowany zestaw danych. Jeden pomocnik renderForm() tworzy pola i jest wywoływany dwukrotnie — ze spłaszczaniem dla kopii zablokowanej i bez niego dla kopii edytowalnej. Obie kopie pochodzą z tych samych zweryfikowanych wartości, więc stan pól pozostaje między nimi spójny. Przykładowy kod odczytuje ścieżki wyjściowe z NEXTPDF_COOKBOOK_LOCKED_OUTPUT i 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);
}

Oczekiwane wyjście:

Wrote locked and editable registration copies

Kopia zablokowana otwiera się bez pól interaktywnych — wartości są statyczną grafiką. Kopia edytowalna otwiera się z tymi samymi wstępnie wypełnionymi wartościami, a każde pole pozostaje edytowalne. Obie odzwierciedlają jeden zweryfikowany zestaw danych.

  • Sprawdzaj poprawność przed utworzeniem pól, a nie po. Metody tworzące pola zapisują przekazaną wartość do V dosłownie. Żaden mechanizm rdzenia nie odrzuca nieprawidłowej wartości w momencie save(), więc wartość, która ominęła walidację, zostaje utrwalona w spłaszczonej kopii bez możliwości jej odzyskania.
  • flattenForms() działa na zasadzie wszystko albo nic. Spłaszcza każde pole w dokumencie. Aby zachować niektóre pola edytowalne, wyrenderuj drugi dokument bez wywołania spłaszczania, tak jak robi to przykład produkcyjny — nie oczekuj przełącznika dla poszczególnych pól w interfejsie rdzenia.
  • Kolejność wywołań. Najpierw utwórz wszystkie pola, następnie wywołaj flattenForms(), a potem save(). Wywołanie flattenForms() bez pól jest bezpieczną operacją pustą; wywołanie tej funkcji po save() nie ma wpływu na już zapisane bajty.
  • Nazwy pól muszą być unikalne. Dwa pola o tej samej nazwie stają się jednym logicznym polem o wspólnej wartości w zgodnych czytnikach. Sprawdzaj nazwy pod kątem unikalności, gdy są generowane na podstawie danych.
  • Logika prawdziwości pola wyboru przy spłaszczaniu. Spłaszczone pole wyboru rysuje swój znacznik, gdy jego wartością jest Yes, On, 1 lub true; pusta wartość lub Off rysuje tylko ramkę. Przekaż rzeczywistą wartość logiczną do checkBox(), aby wyrenderowany stan odpowiadał zweryfikowanej wartości.
  • Pola podpisu nigdy nie są spłaszczane. Wygląd pola /Sig powstaje z ładunku podpisu, a nie z wartości możliwej do ponownego wyrenderowania, więc mechanizm spłaszczania pomija takie pole. Spłaszczaj przed podpisaniem, nigdy po.
  • Kopia edytowalna nadal jest edytowalna. Zachowanie stanu w kopii edytowalnej oznacza, że odbiorca może go zmienić. Traktuj kopię zablokowaną jako miarodajny zapis, a kopię edytowalną jako roboczy szkic.

Walidacja jest liniowa względem liczby pól i wykonuje się raz na zestaw danych. Każde renderowanie tworzy jedną adnotację widżetu oraz jeden wygląd dla każdego pola. Krok spłaszczania dodaje blok treści o ograniczonym rozmiarze dla każdego pola. Renderowanie zestawu danych dwukrotnie mniej więcej podwaja koszt względem pojedynczego dokumentu, co i tak mieści się z zapasem w budżecie 1500 ms / 64 MB dla formularzy liczących kilkaset pól. Jeśli potrzebujesz tylko jednego wyniku, wyrenderuj raz i pomiń drugi przebieg.

  • Sprawdzaj poprawność niezaufanych danych wejściowych na granicy. Format adresu e-mail, przynależność do dozwolonych opcji oraz obecność pól wymaganych są egzekwowane w kodzie aplikacji, ponieważ rdzeń ich nie egzekwuje. Przekształć lub znormalizuj każdą wartość pochodzącą z niezaufanych danych wejściowych, zanim trafi do pola, ponieważ rdzeń zapisuje ją do dokumentu dosłownie.
  • Spłaszczanie nie jest kontrolą dostępu. Spłaszczona wartość nie jest edytowalna w zwykłym czytniku, ale pozostaje widoczna w treści strony i możliwa do wyodrębnienia za pomocą dowolnego narzędzia tekstowego. Nie traktuj spłaszczania jako redakcji ani jako ochrony wartości wrażliwych.
  • Kopia edytowalna niesie te same dane. Rozpowszechniaj ją tylko wśród stron uprawnionych do oglądania i zmieniania tych wartości. Gdy treść jest wrażliwa, dla każdej kopii zastosuj Szyfrowanie z uprawnieniami i zwróć uwagę na opisane tam zastrzeżenie dotyczące współpracy czytnika: bity uprawnień nie egzekwują ograniczeń odczytu.
  • Działaj zachowawczo (fail closed). Przykład produkcyjny kończy działanie z niezerowym kodem w razie niepowodzenia walidacji lub zapisu, zamiast zapisywać częściowy lub nieprawidłowy dokument. Nigdy nie ignoruj tych wyjątków.
StwierdzenieSpecyfikacjaKlauzulareference_id
Spłaszczony formularz to nieinteraktywna (statyczna) reprezentacja pól.ISO 32000-212.7

NextPDF wytwarza statyczną strukturę opisaną przez przytoczoną klauzulę; nie deklaruje pełnej zgodności z ISO 32000-2. Reguły walidacji w tym przepisie to polityka aplikacji, a nie wymóg zgodności ze standardem.