Formuliervelden valideren en afvlakken met behoud van de interactieve status
In het kort
Sectie met titel “In het kort”Een productiepijplijn voor formulieren vlakt ruwe invoer zelden direct af. Eerst valideert u elke waarde. Vervolgens bepaalt u welke velden worden vastgezet en welke bewerkbaar blijven. NextPDF core biedt u twee bouwstenen voor dit werk: de authoring-trait HasFormFields, die de veldwaarde in het document schrijft wanneer u het veld aanmaakt, en flattenForms(), die elk veld inbrandt in statische pagina-graphics en het interactieve formulier verwijdert.
Dit recipe verbindt die twee bouwstenen met een validatiestap op applicatieniveau, die core bewust aan u overlaat. U gaat:
- Een waardenmap valideren aan de hand van regels per veld voordat u een veld wegschrijft, zodat een ongeldige waarde nooit in het document terechtkomt.
- Eén gevalideerde dataset bouwen en deze vervolgens twee keer renderen — eenmaal afgevlakt (een vergrendelde, alleen-lezen kopie) en eenmaal interactief (een bewerkbare kopie) — zodat dezelfde veldstatus in beide uitvoerbestanden behouden blijft.
Vereisten: een werkende NextPDF core-installatie (composer require nextpdf/core) en kennis van Een PDF-formulier bouwen en vooraf invullen en Formuliervelden afvlakken, waarvan dit recipe de mechanismen voor het schrijven en afvlakken van velden combineert.
Reikwijdte. De
flattenForms()-bewerking in core werkt op het hele document: deze vlakt elk veld af of geen enkel veld. De form-API van core heeft geen publieke afvlakschakelaar per veld en geen ingebouwde waardevalidator. Daarom gebeurt „sommige velden afvlakken en andere bewerkbaar houden” op de applicatielaag: valideer eenmaal en render vervolgens dezelfde gevalideerde dataset in twee documenten. Dit recipe documenteert dat patroon; het verzint geen core-methode per veld.
Installeren
Sectie met titel “Installeren”composer require nextpdf/coreU hebt geen extra extensie nodig. Core levert zowel de trait voor het schrijven van formulieren als de flattener.
Conceptueel overzicht
Sectie met titel “Conceptueel overzicht”Een Acrobat-formulierveld (AcroForm) slaat zijn huidige waarde op in het V-element van zijn velddictionary. flattenForms() leest de V-waarde van elk veld en geeft die weer in de contentstream van de bijbehorende pagina — tekstvelden worden BT ... Tj ... ET-tekst, selectievakjes en keuzerondjes worden getekende paden, en keuzevelden geven hun geselecteerde item weer. Daarna verwijdert het het /AcroForm-element uit de catalogus. Het resultaat is een niet-interactief formulier: een statische weergave van de velden die in elke reader identiek wordt weergegeven, zonder dat invulfunctionaliteit nodig is (ISO 32000-2 12.7).
Twee feiten bepalen het productiepatroon:
-
Core valideert geen veldwaarden. Elke authoring-methode (
textField(),comboBox(),checkBox(), en de overige) schrijft elke waarde die u doorgeeft rechtstreeks inV. Het e-mailformaat, de vraag of een waarde tot de toegestane opties behoort en de aanwezigheid van verplichte velden zijn verantwoordelijkheden van applicatiecode. Valideer voordat u schrijft en laat de verwerking snel falen bij een overtreding, in plaats van een document te produceren waarin een onjuiste waarde is ingebakken. -
Afvlakken is onomkeerbaar en geldt voor het hele document. Nadat u
flattenForms()ensave()hebt aangeroepen, zijn de velden statische graphics. Om ook een bewerkbare kopie te behouden, draait u het afvlakken niet terug — u rendert de gevalideerde dataset een tweede keer zonderflattenForms()aan te roepen. Beide kopieën beginnen met dezelfde gevalideerde waarden, zodat de vergrendelde kopie en de bewerkbare kopie een identieke veldstatus hebben.
Het reproduceerbaarheidsprofiel is structural: elk document heeft een trailer-/ID-array die in een nabewerking wordt genormaliseerd voordat twee runs worden vergeleken.
API-oppervlak
Sectie met titel “API-oppervlak”NextPDF\Core\Document (via NextPDF\Core\Concerns\HasFormFields):
textField(string $name, float $x, float $y, float $w, float $h, string $default = '', array $options = []): static— schrijft een tekstveld met de waarde uitdefault.comboBox(string $name, float $x, float $y, float $w, float $h, array $items, string $selected = ''): static— schrijft een vervolgkeuzelijst met het geselecteerde item uitselected.checkBox(string $name, float $x, float $y, float $size, bool $checked = false): static— schrijft een selectievakje met de status uitchecked.flattenForms(): static— brandt de waarde van elk veld in als statische pagina-inhoud en verwijdert de AcroForm. Een no-op als er geen velden zijn. Delegeert intern naarNextPDF\Form\FormFlattener.
NextPDF\Core\Concerns\HasOutput:
save(string $path): void— bouwt en schrijft de PDF. WerptNextPDF\Exception\InvalidConfigExceptionwanneer het uitvoerpad een stream wrapper is, een null-byte bevat of verwijst naar een niet-bestaande bovenliggende map.
De validator in de onderstaande voorbeelden is applicatiecode die u zelf beheert, geen core-symbool. Core heeft geen API voor waardevalidatie; daarom is de validatiestap hier expliciet.
Codevoorbeeld — snelstart
Sectie met titel “Codevoorbeeld — snelstart”Deze minimale flow valideert een waardenmap, schrijft op basis daarvan drie velden, vlakt vervolgens één vergrendelde kopie af en slaat die op.
<?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";Codevoorbeeld — productie
Sectie met titel “Codevoorbeeld — productie”De productieflow scheidt validatie van rendering. Een getypeerde FieldRuleSet valideert de waardenmap eenmaal en retourneert een gevalideerde dataset. Eén renderForm()-hulpfunctie schrijft de velden en wordt twee keer aangeroepen — met afvlakken voor de vergrendelde kopie en zonder afvlakken voor de bewerkbare kopie. Beide kopieën zijn gebaseerd op dezelfde gevalideerde waarden, zodat de interactieve status in beide behouden blijft. De harness leest uitvoerpaden uit NEXTPDF_COOKBOOK_LOCKED_OUTPUT en 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);}Verwachte uitvoer:
Wrote locked and editable registration copiesDe vergrendelde kopie wordt geopend zonder interactieve velden — de waarden zijn statische graphics. De bewerkbare kopie wordt geopend met dezelfde vooraf ingevulde waarden en elk veld blijft bewerkbaar. Beide geven dezelfde gevalideerde dataset weer.
Randgevallen en valkuilen
Sectie met titel “Randgevallen en valkuilen”- Valideer voordat u schrijft, niet erna. De authoring-methoden schrijven uw waarde letterlijk in
V. Geen enkele core-hook weigert een misvormde waarde op het moment vansave(), dus een niet-gevalideerde waarde wordt in de afgevlakte kopie ingebakken zonder herstelmogelijkheid. flattenForms()is alles-of-niets. Het vlakt elk veld in het document af. Om sommige velden bewerkbaar te houden, rendert u een tweede document zonder de afvlakaanroep, zoals het productievoorbeeld doet — verwacht geen schakelaar per veld in de core-API.- Aanroepvolgorde. Schrijf eerst elk veld, roep dan
flattenForms()aan en vervolgenssave().flattenForms()aanroepen zonder velden is een veilige no-op; het nasave()aanroepen heeft geen effect op de reeds geschreven bytes. - Veldnamen moeten uniek zijn. Twee velden met dezelfde naam worden in conforme readers één logisch veld met een gedeelde waarde. Valideer namen op uniciteit wanneer ze uit data komen.
- Booleanwaarde van selectievakjes bij afvlakken. Een afgevlakt selectievakje tekent zijn vinkje wanneer de waarde
Yes,On,1oftrueis; een lege ofOff-waarde tekent alleen het vakje. Geef een echte boolean door aancheckBox()zodat de weergegeven status overeenkomt met uw gevalideerde waarde. - Handtekeningvelden worden nooit afgevlakt. De weergave van een
/Sig-veld wordt geproduceerd uit zijn handtekeningpayload; het is geen waarde die opnieuw kan worden weergegeven, dus de flattener slaat het over. Vlak af voordat u ondertekent, nooit erna. - De bewerkbare kopie blijft bewerkbaar. Het behouden van de status in de bewerkbare kopie betekent dat de ontvanger die kopie kan wijzigen. Beschouw de vergrendelde kopie als de gezaghebbende vastlegging en de bewerkbare kopie als een werkversie.
Prestaties
Sectie met titel “Prestaties”Validatie is lineair in het aantal velden en draait eenmaal per dataset. Elke renderstap schrijft per veld één widget-annotatie plus een appearance. De afvlakstap voegt per veld een begrensd contentblok toe. De dataset twee keer renderen verdubbelt ruwweg de kosten per document, die voor formulieren met enkele honderden velden ruim binnen een budget van 1500 ms / 64 MB blijven. Als u maar één uitvoerbestand nodig hebt, render dan eenmaal en sla de tweede doorloop over.
Beveiligingsopmerkingen
Sectie met titel “Beveiligingsopmerkingen”- Valideer niet-vertrouwde invoer aan de grens. Het e-mailformaat, de vraag of een waarde tot de toegestane opties behoort en de aanwezigheid van verplichte velden worden afgedwongen in applicatiecode, omdat core ze niet afdwingt. Escape of normaliseer elke waarde uit niet-vertrouwde invoer voordat die een veld bereikt, omdat core deze letterlijk in het document schrijft.
- Afvlakken is geen toegangscontrole. Een afgevlakte waarde is niet bewerkbaar in een normale reader, maar blijft zichtbaar in de pagina-inhoud en is extraheerbaar met elke teksttool. Beschouw afvlakken niet als redactie of als bescherming van gevoelige waarden.
- De bewerkbare kopie draagt dezelfde gegevens. Distribueer deze alleen naar partijen die die waarden mogen zien en wijzigen. Wanneer de inhoud gevoelig is, combineert u een van beide kopieën met Versleutelen met permissies, en houd rekening met het daar beschreven voorbehoud rond reader-coöperatie: permissiebits dwingen geen leesbeperkingen af.
- Faal gesloten. Het productievoorbeeld eindigt met een niet-nul exitcode bij een validatie- of uitvoerfout in plaats van een gedeeltelijk of ongeldig document te schrijven. Slik deze excepties nooit in.
Conformiteit
Sectie met titel “Conformiteit”| Bewering | Specificatie | Clausule | reference_id |
|---|---|---|---|
| Een afgevlakt formulier is een niet-interactieve (statische) weergave van de velden. | ISO 32000-2 | 12.7 |
NextPDF produceert de statische structuur die in de geciteerde clausule wordt beschreven; het claimt geen volledige ISO 32000-2-conformiteit. De validatieregels in dit recipe zijn applicatiebeleid, geen conformiteitsvereiste van de standaard.
Zie ook
Sectie met titel “Zie ook”- Een PDF-formulier bouwen en vooraf invullen — velden schrijven en hun beginwaarden instellen.
- Formuliervelden afvlakken — het afvlakken van het hele document waarop dit recipe voortbouwt.
- Fouten afhandelen met de NextPDF-exceptiehiërarchie — fouten op het juiste niveau opvangen.
- Versleutelen met permissies — vertrouwelijkheid toevoegen wanneer de formuliergegevens gevoelig zijn.
- Form-module — de formulierveldreferentie.