Valide valores de campos de formulário e achate preservando o estado interativo
Visão geral
Seção intitulada “Visão geral”Um pipeline de formulário em produção raramente achata dados brutos de entrada. Primeiro, você valida cada valor. Depois, decide quais campos se tornam permanentes e quais permanecem editáveis. O core do NextPDF oferece dois blocos de construção para esse trabalho: o trait de criação HasFormFields, que grava o valor de um campo no documento conforme você o cria, e flattenForms(), que incorpora cada campo aos gráficos estáticos da página e remove o formulário interativo.
Esta receita conecta esses dois blocos com uma etapa de validação na camada da aplicação, que o core deixa deliberadamente sob sua responsabilidade. Você vai:
- Validar um mapa de valores de acordo com regras por campo antes de criar qualquer campo, para que um valor inválido nunca chegue ao documento.
- Construir um conjunto de dados validado e emiti-lo duas vezes — uma vez achatado (uma cópia bloqueada, somente leitura) e uma vez interativo (uma cópia editável) — para que o mesmo estado de campo seja levado às duas saídas.
Pré-requisitos: uma instalação funcional do core do NextPDF (composer require nextpdf/core), além da leitura de Crie e preencha previamente um formulário PDF e Achate campos de formulário, cujos mecanismos de criação de campos e achatamento são combinados nesta receita.
Limite de escopo. O
flattenForms()do core é uma operação de documento inteiro: ele achata todos os campos ou nenhum deles. A API de formulário do core não expõe nenhum interruptor público de achatamento por campo nem um validador de valores embutido. Portanto, “achatar alguns, manter outros editáveis” acontece na camada da aplicação: valide uma vez e renderize o mesmo conjunto de dados validado em dois documentos. Esta receita documenta esse padrão; ela não inventa um método de core por campo.
Instalação
Seção intitulada “Instalação”composer require nextpdf/coreNão é preciso instalar nenhuma extensão adicional. Tanto o trait de criação de formulários quanto o achatador vêm no core.
Visão conceitual
Seção intitulada “Visão conceitual”Um campo de formulário Acrobat (AcroForm) armazena o valor atual na entrada V do respectivo dicionário de campo. O flattenForms() lê o valor V de cada campo e o renderiza no fluxo de conteúdo da página correspondente — campos de texto se tornam texto BT ... Tj ... ET, caixas de seleção e botões de opção se tornam traçados desenhados, e campos de escolha renderizam o item selecionado. Em seguida, ele remove a entrada /AcroForm do catálogo. O resultado é um formulário não interativo: uma representação estática dos campos, exibida de forma idêntica em qualquer leitor, sem exigir recursos de preenchimento de formulário (ISO 32000-2 12.7).
Dois fatos moldam o padrão de produção:
-
O core não valida os valores dos campos. Cada método de criação (
textField(),comboBox(),checkBox()e os demais) grava o valor que você passar diretamente emV. O formato de e-mail, a pertinência a opções permitidas e a presença de campos obrigatórios são responsabilidades da aplicação. Valide antes de criar e interrompa o fluxo imediatamente em caso de violação, em vez de emitir um documento com um valor inválido incorporado. -
O achatamento é irreversível e abrange o documento inteiro. Depois de chamar
flattenForms()esave(), os campos são gráficos estáticos. Para manter também uma cópia editável, você não reverte o achatamento — você renderiza o conjunto de dados validado uma segunda vez sem chamarflattenForms(). As duas cópias partem dos mesmos valores validados, de modo que a cópia bloqueada e a cópia editável carregam um estado de campo idêntico.
O perfil de reprodutibilidade é structural: cada documento carrega um array /ID no trailer que um pós-processamento normaliza antes de duas execuções serem comparadas.
Superfície da API
Seção intitulada “Superfície da API”NextPDF\Core\Document (via NextPDF\Core\Concerns\HasFormFields):
textField(string $name, float $x, float $y, float $w, float $h, string $default = '', array $options = []): static— cria um campo de texto com o valor emdefault.comboBox(string $name, float $x, float $y, float $w, float $h, array $items, string $selected = ''): static— cria uma lista suspensa com o item selecionado emselected.checkBox(string $name, float $x, float $y, float $size, bool $checked = false): static— cria uma caixa de seleção com o estado emchecked.flattenForms(): static— incorpora o valor de cada campo ao conteúdo estático da página e remove o AcroForm. É uma operação sem efeito quando não há campos. Internamente, delega paraNextPDF\Form\FormFlattener.
NextPDF\Core\Concerns\HasOutput:
save(string $path): void— constrói e grava o PDF. LançaNextPDF\Exception\InvalidConfigExceptionquando o caminho de saída é um stream wrapper, contém um byte nulo ou nomeia um diretório pai inexistente.
O validador nos exemplos abaixo é código da sua aplicação, não um símbolo do core. O core não tem nenhuma API de validação de valores, e é por isso que a etapa de validação aparece explicitamente aqui.
Exemplo de código — Início rápido
Seção intitulada “Exemplo de código — Início rápido”Este fluxo mínimo valida um mapa de valores, cria três campos a partir dele e, em seguida, achata e salva uma cópia bloqueada.
<?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";Exemplo de código — Produção
Seção intitulada “Exemplo de código — Produção”O fluxo de produção separa a validação da renderização. Um FieldRuleSet tipado valida o mapa de valores uma vez e retorna um conjunto de dados validado. Um único auxiliar renderForm() cria os campos e é executado duas vezes — com achatamento para a cópia bloqueada e sem ele para a cópia editável. As duas cópias vêm dos mesmos valores validados, de modo que o estado interativo é preservado entre elas. O script de exemplo lê os caminhos de saída de NEXTPDF_COOKBOOK_LOCKED_OUTPUT e 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);}Saída esperada:
Wrote locked and editable registration copiesA cópia bloqueada abre sem campos interativos — os valores são gráficos estáticos. A cópia editável abre com os mesmos valores previamente preenchidos, e cada campo permanece editável. As duas refletem o mesmo conjunto de dados validado.
Casos extremos e armadilhas
Seção intitulada “Casos extremos e armadilhas”- Valide antes de criar, não depois. Os métodos de criação gravam o valor em
Vliteralmente. Nenhuma validação do core rejeita um valor malformado no momento desave(), portanto um valor que pulou a validação é incorporado à cópia achatada sem caminho de recuperação. - O
flattenForms()é tudo ou nada. Ele achata todos os campos do documento. Para manter alguns campos editáveis, renderize um segundo documento sem a chamada de achatamento, como faz o exemplo de produção — não espere um interruptor por campo na API do core. - Ordem de chamada. Crie todos os campos primeiro, depois chame
flattenForms()e, então,save(). ChamarflattenForms()sem campos é uma operação segura e sem efeito; chamá-lo apóssave()não tem efeito sobre os bytes já gravados. - Os nomes dos campos devem ser únicos. Dois campos que compartilham um nome se tornam um único campo lógico com um valor compartilhado em leitores em conformidade. Valide a unicidade dos nomes quando eles forem derivados de dados.
- Estado da caixa de seleção ao achatar. Uma caixa de seleção achatada desenha a marca de seleção quando o valor é
Yes,On,1outrue; um valor vazio ouOffdesenha apenas a caixa. Passe um booleano real paracheckBox()para que o estado renderizado corresponda ao valor validado. - Campos de assinatura nunca são achatados. A aparência de um campo
/Sigé produzida a partir da carga útil da assinatura, não de um valor re-renderizável, por isso o achatador o ignora. Achate antes de assinar, nunca depois. - A cópia editável continua editável. Preservar o estado na cópia editável significa que o destinatário pode alterá-lo. Trate a cópia bloqueada como o registro autoritativo e a cópia editável como um rascunho de trabalho.
Desempenho
Seção intitulada “Desempenho”A validação é linear em relação ao número de campos e é executada uma vez por conjunto de dados. Cada renderização cria uma anotação de widget, além de uma aparência, por campo. A etapa de achatamento anexa um bloco de conteúdo limitado por campo. Renderizar o conjunto de dados duas vezes praticamente dobra o custo por documento, que ainda assim permanece bem dentro de um orçamento de 1500 ms / 64 MB para formulários com algumas centenas de campos. Se você precisar de apenas uma saída, renderize uma vez e pule a segunda passagem.
Notas de segurança
Seção intitulada “Notas de segurança”- Valide a entrada não confiável no limite. O formato de e-mail, a pertinência a opções permitidas e a presença de campos obrigatórios são impostos no código da aplicação, porque o core não os impõe. Faça o escape ou normalize qualquer valor derivado de entrada não confiável antes que ele chegue a um campo, porque o core o grava literalmente no documento.
- O achatamento não é controle de acesso. Um valor achatado não é editável em um leitor comum, mas permanece visível no conteúdo da página e extraível com qualquer ferramenta de texto. Não trate o achatamento como redação (remoção) nem como proteção de valores sensíveis.
- A cópia editável carrega os mesmos dados. Distribua-a apenas às partes autorizadas a ver e alterar esses valores. Quando o conteúdo for sensível, combine qualquer uma das cópias com Criptografe com permissões e observe a ressalva de cooperação do leitor descrita ali: os bits de permissão não impõem restrições de leitura.
- Falhe de forma fechada. O exemplo de produção sai com código diferente de zero em caso de falha de validação ou de saída, em vez de gravar um documento parcial ou inválido. Nunca engula essas exceções.
Conformidade
Seção intitulada “Conformidade”| Declaração | Especificação | Cláusula | reference_id |
|---|---|---|---|
| Um formulário achatado é uma representação não interativa (estática) dos campos. | ISO 32000-2 | 12.7 |
O NextPDF produz a estrutura estática descrita na cláusula citada; ele não declara conformidade geral com a ISO 32000-2. As regras de validação desta receita são política da aplicação, não um requisito de conformidade do padrão.
Veja também
Seção intitulada “Veja também”- Crie e preencha previamente um formulário PDF — crie os campos e defina os valores iniciais.
- Achate campos de formulário — o achatamento de documento inteiro sobre o qual esta receita se baseia.
- Trate erros com a hierarquia de exceções do NextPDF — capture falhas na granularidade certa.
- Criptografe com permissões — adicione confidencialidade quando os dados do formulário são sensíveis.
- Módulo de formulário — a referência de campos de formulário.