Pular para o conteúdo

Uma API que se recusa a adivinhar

Spec: ISO/IEC 25010 Spec: ISO 32000-2 Evidence: Code-backed

O NextPDF faz você dizer exatamente o que quer. Quando a intenção altera os bytes — um nível de assinatura, um destino de saída, um alvo de conformidade —, ela vira um argumento explícito obrigatório, não algo que o motor infere a partir do contexto.

Esta página mostra essa postura no próprio código-fonte do motor: nas assinaturas dos métodos, nos argumentos nomeados e nos pontos em que uma entrada ambígua é rejeitada antes que qualquer byte seja produzido.

Um palpite é uma decisão tomada em seu nome sem aviso. Em um campo de texto, isso é levemente irritante. Em um PDF, é um defeito latente, porque o que você entrega costuma ser um artefato jurídico ou de arquivamento cuja correção será verificada depois por outra pessoa com um validador.

Considere uma assinatura. O digest dela é calculado sobre um intervalo de bytes declarado que exclui deliberadamente o próprio valor da assinatura ( Spec: ISO 32000-2, §12.8 ). Uma API que “ajuda” em silêncio — reescrevendo a estrutura, inferindo um nível, preenchendo um espaço reservado — não ajudou. Ela alterou os bytes que uma assinatura deveria proteger. O palpite que parece amigável no ponto de chamada vira um incidente de produção semanas depois. É a mesma linha de código.

  • Se uma escolha altera a saída e não tem um padrão seguro, o NextPDF a transforma em argumento obrigatório, não em algo inferido.
  • Argumentos opcionais com leitura ambígua são nomeados, de modo que o ponto de chamada declare a intenção (newLine: true, não um true nu).
  • Entradas que poderiam ser inseguras são validadas antes da renderização e rejeitadas com uma exceção tipada que nomeia a causa.
  • Uma instância de documento é de uso único: é construída, emitida e descartada. Não existe reset(), então não há “será que isto está sendo reutilizado?” para adivinhar.
  • O motor nunca emite um artefato de aparência plausível no lugar daquele que você pediu. Em vez disso, ele se recusa.

O mecanismo é simples, e esse é justamente o ponto. Ele combina sistema de tipos, argumentos nomeados, enums em vez de strings mágicas e um pequeno número de cláusulas de guarda deliberadas colocadas antes da saída.

A tabela compara algumas entradas ambíguas. Para cada uma, mostra o que uma biblioteca que “ajuda” inferiria e o que o NextPDF faz no lugar. Toda coluna do NextPDF descreve um comportamento citado a partir do código-fonte mostrado mais adiante nesta página.

Entrada ambíguaO que uma biblioteca que adivinha fazO que o NextPDF faz
Uma string de orientação como "portait"Usa um padrão e renderiza mesmo assimaddPage() recebe o enum Orientation, não uma string — um erro de digitação é um erro de tipo, não um padrão silencioso
Um true nu no fim da chamada a cell()Escolhe a posição booleana que supõe que você quis indicarO booleano é nomeado no ponto de chamada (newLine: true); um literal sem nome é justamente o cheiro que a API remove
Um wrapper php:// ou um caminho de travessia passado a save()”Faz o melhor que pode” e grava em algum lugarRejeitado antes de o PDF ser construído, com uma InvalidConfigException tipada que nomeia a chave, o valor e o tipo esperado
setSignature() e em seguida save() enquanto o assinador de alto nível não está conectadoEmite um arquivo não assinado que o chamador acredita estar assinadoLança NotImplementedException antes de produzir bytes, nomeando o caminho suportado
Reutilizar uma instância de Document para uma segunda renderizaçãoAdivinha se o estado residual ainda se aplicaSem reset() e sem caminho de reutilização — uma instância nova por requisição via DocumentFactory, então não há estado residual a adivinhar

A intenção é um argumento obrigatório. O contrato central, PdfDocumentInterface, recebe geometria e alinhamento como objetos de valor tipados e enums, não primitivos soltos:

public function addPage(
?PageSize $size = null,
Orientation $orientation = Orientation::Portrait,
): static;
public function cell(
float $width,
float $height,
string $text = '',
bool|string $border = false,
bool $newLine = false,
Alignment $align = Alignment::Left,
bool $fill = false,
): static;

Orientation e Alignment são enums, então a chamada não pode passar "portait" e fazer isso significar “padrão” em silêncio. Quando existe um padrão, ele é seguro (retrato, à esquerda, sem borda), não um palpite sobre o que você provavelmente queria.

Booleanos ambíguos são nomeados no ponto de chamada. Ao longo dos exemplos que servem como a referência de fato da API, a mesma forma se repete:

$document->cell(0, 15, 'Hello, NextPDF!', newLine: true);
$document->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);
$pdf = $document->output(dest: OutputDestination::String);

newLine: true é inequívoco. Um true nu no fim não seria. O nível da assinatura é SignatureLevel::PAdES_B_B, um caso de enum — nunca uma string que o motor precise interpretar. O destino de saída é OutputDestination::String, então “me dê os bytes, sem cabeçalhos HTTP, sem arquivo” está declarado. Não é inferido com base em o nome de um arquivo ter sido passado ou não.

Entradas inseguras são rejeitadas antes de um byte ser gravado. save() valida o caminho de destino antes de construir o PDF:

public function save(string $path): void
{
// Reject stream wrappers and null bytes
if (\str_contains($path, "\0") || \preg_match('#^[a-zA-Z]+://#', $path)) {
throw new InvalidConfigException(
configKey: 'output_path',
givenValue: $path,
expectedType: 'valid_path',
);
}
// Resolve the parent directory to prevent path traversal
$dir = \dirname($path);
$realDir = \realpath($dir);
if ($realDir === false) {
throw new InvalidConfigException(
configKey: 'output_path',
givenValue: $dir,
expectedType: 'existing_directory',
);
}
// ... only now is the PDF built and written atomically
}

O motor não “faz o melhor que pode” com um wrapper php:// ou um caminho de travessia. Ele se recusa, e a exceção nomeia a chave, o valor e o esperado.

O motor se recusa em vez de emitir um artefato enganoso. A forma mais forte de se recusar a adivinhar é não produzir saída alguma quando essa saída seria falsa. Quando uma assinatura de alto nível está configurada, mas a costura do escritor que de fato assinaria não está conectada, o caminho de construção lança antes de produzir bytes, em vez de emitir um arquivo não assinado que o chamador acreditaria estar assinado:

if ($this->padesOrchestrator !== null) {
throw new NotImplementedException(
feature: 'Document::setSignature()->save()/output()/getPdfData()',
followUp: 'The high-level PAdES writer seam is not yet wired ... '
. 'Produce a signed PDF via the direct two-phase '
. 'PadesOrchestrator::signDocument() then finalizeSignature() '
. 'buffer API ...',
);
}

Um PDF não assinado que parece assinado é exatamente o tipo de artefato errado, mas plausível, que este princípio existe para evitar. A mesma postura aparece no caminho estrito de CSS. Um desvio de especificação não registrado lança uma StrictModeViolation no ponto de detecção, em vez de renderizar uma aproximação e deixar o desvio passar despercebido.

O uso único elimina toda uma classe de palpites. Um Document é descartável — construído, emitido e descartado. Não existe reset() nem caminho de reutilização. Um worker de longa duração cria uma instância nova por requisição através do DocumentFactory. O motor nunca precisa adivinhar se o estado residual de um documento anterior ainda é significativo, porque, por construção, esse estado não existe.

Esta página é Evidence: Code-backed : toda forma acima é citada a partir do próprio código-fonte do motor e de seus exemplos, não parafraseada com base na intenção.

  • As assinaturas tipadas, portadoras de enums, são o contrato público em PdfDocumentInterface. O estilo de chamada com argumentos nomeados é a forma usada de modo consistente em todos os exemplos canônicos que atuam como a referência de fato da API.
  • A validação do caminho antes da renderização, com sua InvalidConfigException tipada, e a guarda de recusa antes da emissão, NotImplementedException, são citadas literalmente a partir do caminho de saída da fachada do documento.
  • A âncora de padrões é Spec: ISO/IEC 25010, §3.32 — proteção contra erro do usuário, a propriedade de qualidade que uma API que se recusa a adivinhar existe para satisfazer no ponto de chamada. A segunda âncora é Spec: ISO 32000-2, §12.8 , e é por isso que adivinhar em torno de um documento assinado nunca é inofensivo. O digest cobre um intervalo de bytes declarado que exclui o valor da assinatura, então qualquer reescrita silenciosa o invalida.

Veja um programa pequeno e completo. Toda linha que poderia ser ambígua declara sua intenção. A única entrada insegura é recusada antes que qualquer trabalho seja feito.

<?php
declare(strict_types=1);
use NextPDF\Contracts\OutputDestination;
use NextPDF\Core\Document;
use NextPDF\Exception\InvalidConfigException;
use NextPDF\ValueObjects\PageSize;
use NextPDF\Contracts\Orientation;
$document = Document::createStandalone();
$document->setTitle('Quarterly Report');
// Intent is explicit: a typed page size and an Orientation enum case,
// not a string the engine has to interpret.
$document->addPage(PageSize::a4(), Orientation::Landscape);
$document->setFont('helvetica', 'B', 16);
// Ambiguous boolean is named, so the call reads as intent.
$document->cell(0, 12, 'Quarterly Report', newLine: true);
try {
// Unsafe path is rejected before a byte is built.
$document->save('php://output/report.pdf');
} catch (InvalidConfigException $e) {
// "Invalid configuration for key "output_path": expected valid_path, ..."
error_log($e->getMessage());
// The String destination is explicit: bytes only, no HTTP headers,
// no file side effect. Nothing is inferred from a missing filename.
$bytes = $document->output(dest: OutputDestination::String);
}

Não há caminho em que este programa faça a coisa errada em silêncio. Ele declara a intenção e prossegue, ou nomeia o problema e para.

A objeção frequente é “isso é só verbosidade”. Não é verbosidade. É a ausência de padrões ocultos. Um true nu é mais curto que newLine: true exatamente na medida da clareza que ele remove. O motor troca alguns caracteres no ponto de chamada pela eliminação de uma categoria de bug — aquela em que o código compila, executa, produz um arquivo e está errado.

Um equívoco relacionado é achar que falhar rápido significa “lança muito”. No uso normal, o NextPDF não lança nada. Entradas válidas fluem normalmente. As guardas disparam apenas em entradas que são genuinamente ambíguas ou inseguras — precisamente as entradas sobre as quais você quer saber de imediato, não aquelas que você quer que sejam adivinhadas.

Recusar-se a adivinhar aplica-se a intenção e segurança, não a toda conveniência. O NextPDF ainda tem padrões seguros: orientação retrato, alinhamento à esquerda, sem borda. O princípio é oferecer um padrão somente onde ele é seguro e não traz surpresas, nunca onde a inferência errada produz um documento errado.

Esta página demonstra o princípio na superfície central da API pública (a fachada do documento, seu contrato e o caminho de saída). Os subsistemas têm seus próprios pontos de entrada, e cada um documenta o próprio comportamento de validação. As formas citadas aqui estão atualizadas no momento desta revisão. Elas ilustram o padrão; não são um catálogo exaustivo de todas as guardas do motor.

As guardas de falha rápida descritas são guardas de correção e segurança. Por si só, não constituem uma fronteira de segurança. A validação de entrada é uma camada. A filosofia de design e a documentação de segurança descrevem a postura mais ampla.

  • Com respaldo no código (nível de evidência) — uma página cujas afirmações são verificadas contra o próprio código-fonte do motor ou contra um exemplo executável, citado em vez de parafraseado.
  • Falhar rápido — rejeitar uma entrada inválida no ponto mais inicial possível, com uma causa clara, em vez de prosseguir e falhar de forma obscura mais tarde.
  • Argumento nomeado — uma sintaxe de ponto de chamada do PHP (newLine: true) que vincula um valor a um parâmetro pelo nome, tornando autoexplicativo um literal de outra forma ambíguo.
  • Ciclo de vida de uso único — o contrato descartável de Document: instanciar, escrever, salvar, descartar. Sem reset(), sem reutilização. Os workers criam uma instância nova por requisição através do DocumentFactory.
  • PAdES — PDF Advanced Electronic Signatures, a família de perfis ETSI para assinatura de PDF. Expandido no primeiro uso; coberto em profundidade nas páginas de assinatura.