Walidacja wartości pól formularza i spłaszczanie z zachowaniem stanu pól
W skrócie
Dział zatytułowany „W skrócie”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.
Instalacja
Dział zatytułowany „Instalacja”composer require nextpdf/coreNie potrzebujesz dodatkowego rozszerzenia. Cecha formularzy i mechanizm spłaszczania są dostarczane w rdzeniu.
Przegląd koncepcyjny
Dział zatytułowany „Przegląd koncepcyjny”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:
-
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 doV. 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ą. -
Spłaszczanie jest nieodwracalne i obejmuje cały dokument. Po wywołaniu
flattenForms()isave()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ływaniaflattenForms(). 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.
Powierzchnia API
Dział zatytułowany „Powierzchnia API”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ą wdefault.comboBox(string $name, float $x, float $y, float $w, float $h, array $items, string $selected = ''): static— tworzy listę rozwijaną z wybraną pozycją wselected.checkBox(string $name, float $x, float $y, float $size, bool $checked = false): static— tworzy pole wyboru ze stanem wchecked.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 doNextPDF\Form\FormFlattener.
NextPDF\Core\Concerns\HasOutput:
save(string $path): void— buduje i zapisuje plik PDF. ZgłaszaNextPDF\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.
Przykład kodu — szybki start
Dział zatytułowany „Przykład kodu — szybki start”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', '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";Przykład kodu — wersja produkcyjna
Dział zatytułowany „Przykład kodu — wersja produkcyjna”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', '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 copiesKopia 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.
Przypadki brzegowe i pułapki
Dział zatytułowany „Przypadki brzegowe i pułapki”- Sprawdzaj poprawność przed utworzeniem pól, a nie po. Metody tworzące pola zapisują przekazaną wartość do
Vdosłownie. Żaden mechanizm rdzenia nie odrzuca nieprawidłowej wartości w momenciesave(), 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 potemsave(). WywołanieflattenForms()bez pól jest bezpieczną operacją pustą; wywołanie tej funkcji posave()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,1lubtrue; pusta wartość lubOffrysuje tylko ramkę. Przekaż rzeczywistą wartość logiczną docheckBox(), aby wyrenderowany stan odpowiadał zweryfikowanej wartości. - Pola podpisu nigdy nie są spłaszczane. Wygląd pola
/Sigpowstaje 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.
Wydajność
Dział zatytułowany „Wydajność”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.
Uwagi dotyczące bezpieczeństwa
Dział zatytułowany „Uwagi dotyczące bezpieczeństwa”- 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.
Zgodność
Dział zatytułowany „Zgodność”| Stwierdzenie | Specyfikacja | Klauzula | reference_id |
|---|---|---|---|
| Spłaszczony formularz to nieinteraktywna (statyczna) reprezentacja pól. | ISO 32000-2 | 12.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.
Zobacz także
Dział zatytułowany „Zobacz także”- Tworzenie i wstępne wypełnianie formularza PDF — tworzenie pól i ustawianie ich wartości początkowych.
- Spłaszczanie pól formularza — spłaszczanie całego dokumentu, na którym opiera się ten przepis.
- Obsługa błędów za pomocą hierarchii wyjątków NextPDF — przechwytywanie niepowodzeń na właściwym poziomie szczegółowości.
- Szyfrowanie z uprawnieniami — zapewnienie poufności, gdy dane formularza są wrażliwe.
- Moduł formularzy — dokumentacja referencyjna pól formularza.