Gere um sumário com base na estrutura do documento em tempo de execução
Visão geral
Seção intitulada “Visão geral”O conteúdo pode tomar forma em tempo de execução: capítulos vindos de um banco de dados, seções vindas da resposta de uma Application Programming Interface (API) ou títulos gerados por um laço que você não consegue conhecer de antemão. Você precisa que o outline do documento e o sumário clicável correspondam exatamente a esse conteúdo, sem manter uma segunda lista manual que possa ficar fora de sincronia.
Esta receita constrói o outline dinamicamente. À medida que você escreve cada título, lê o cursor ao vivo e a página a partir do engine com getPage(), getY() e getNumPages() e, em seguida, passa esses valores para bookmark(). O bookmark fica vinculado à posição lida naquele momento; assim, o outline acompanha o conteúdo mesmo quando as quebras de página caem em pontos inesperados. No fim, addTOC() renderiza uma página de sumário real a partir das mesmas entradas.
Pré-requisitos: uma instalação do Core (composer require nextpdf/core:^3) e conteúdo cuja estrutura de títulos você descobre enquanto escreve, não antes.
Esta página cobre o padrão dinâmico, orientado pela posição. Para o caso estático, em que você conhece todos os títulos e níveis de antemão, leia primeiro Adicione bookmarks e um sumário. Esta receita usa a mesma superfície de bookmark() e addTOC() e não repete esses fundamentos.
Instalação
Seção intitulada “Instalação”composer require nextpdf/core:^3Você não precisa de nenhuma extensão opcional. A superfície de navegação (bookmark(), addTOC()) e os acessadores de posição (getPage(), getY(), getNumPages()) estão estáveis desde a 1.2.0 e funcionam em toda a matriz de backport da 8.1 à 8.4.
Visão conceitual
Seção intitulada “Visão conceitual”Um sumário dinâmico tem duas partes que precisam permanecer consistentes:
- O outline (também chamado de bookmarks): a árvore que o leitor vê na barra lateral de navegação, em que cada entrada leva a uma posição no documento.
- O sumário renderizado: uma página gerada que lista as mesmas entradas com seus números de página.
O NextPDF mantém os dois sincronizados por meio de uma única chamada. bookmark($title, $level, $y) adiciona um item de outline e uma entrada de sumário, ambos vinculados à página atual e à posição vertical atual. Você não mantém duas listas.
A parte dinâmica está em de onde a posição vem. Uma receita estática passa títulos literais na ordem do código-fonte. Aqui, você escreve um título e, em seguida, pergunta imediatamente ao engine onde o cursor parou:
getPage()retorna o índice baseado em zero da página ativa. Antes de a primeira página ser adicionada, ele retorna-1.getNumPages()retorna a contagem total de páginas, incluindo a página ativa que ainda não foi descarregada.getY()retorna o cursor vertical atual em unidades de usuário, medido como a distância a partir do topo da página.getX(),getPageHeight()egetMargins()completam o quadro quando você precisa decidir se um título e sua primeira linha de texto do corpo cabem juntos.
Leia esses valores e, em seguida, chame bookmark(). A quebra de página automática pode mover o cursor para uma nova página entre dois títulos; por isso, ler a posição novamente mantém o destino do outline na página correta.
Um único ponto de ordenação rege todo o padrão: chame bookmark() no ponto exato em que você quer o destino, imediatamente antes de renderizar o texto do título. Se você escrever o título primeiro e fizer o bookmark depois, o getY() registrado ficará logo abaixo do título.
Superfície da API
Seção intitulada “Superfície da API”Esta receita depende destes métodos de \NextPDF\Core\Document:
bookmark(string $title, int $level = 0, float $y = -1): static- adiciona um item de outline e uma entrada de sumário em$level, vinculados à página atual. Com$y = -1, o destino é o Y atual do cursor; passe um Y não negativo para fixar um destino preciso.addTOC(int $pageIndex = 0, string $title = ''): static- renderiza uma página de sumário a partir das entradas acumuladas e a insere em$pageIndex. Retorna sem inserir uma página quando nenhum bookmark existe.getPage(): int- índice baseado em zero da página ativa (-1antes da primeira página).getNumPages(): int- contagem total de páginas, incluindo a página ativa ainda não descarregada.getY(): float- Y atual do cursor em unidades de usuário (distância a partir do topo da página).getX(): float- X atual do cursor em unidades de usuário.getPageHeight(): float- altura da página atual em unidades de usuário.getMargins(): \NextPDF\ValueObjects\Margin- as margens ativas (top,right,bottom,left).setY(float $y): static- move o cursor para um Y explícito.setAutoPageBreak(bool $enabled, float $margin = 20): static- controla a quebra de página automática e seu limite de margem inferior.
Exemplo de código — Início rápido
Seção intitulada “Exemplo de código — Início rápido”Este exemplo escreve três seções a partir de uma lista em tempo de execução. Cada iteração lê a página ao vivo com getPage() antes de fazer o bookmark; assim, o destino do outline permanece correto após uma quebra de página automática.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
/** @var list<array{title: string, body: string}> $sections */$sections = [ ['title' => 'Origins', 'body' => 'Runtime content for the first section.'], ['title' => 'Method', 'body' => 'Runtime content for the second section.'], ['title' => 'Results', 'body' => 'Runtime content for the third section.'],];
$doc = Document::createStandalone();$doc->addPage();
foreach ($sections as $section) { // Read the live page back, then bookmark BEFORE rendering the heading, // so the destination points at the heading, not below it. $pageIndex = $doc->getPage(); $doc->bookmark($section['title'], level: 0);
$doc->setFont('helvetica', 'B', 16); $doc->cell(0, 10, $section['title'], newLine: true); $doc->setFont('helvetica', '', 11); $doc->multiCell(0, 7, $section['body']); $doc->ln(6);
echo "Bookmarked '{$section['title']}' on page index {$pageIndex}\n";}
// Splice the rendered table of contents in as the first page.$doc->addTOC(pageIndex: 0, title: 'Contents');
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/dynamic-toc.pdf');Saída esperada no terminal, com uma linha por seção:
Bookmarked 'Origins' on page index 0Bookmarked 'Method' on page index 0Bookmarked 'Results' on page index 0Exemplo de código — Produção
Seção intitulada “Exemplo de código — Produção”Esta versão constrói um outline de dois níveis (capítulos e seções) a partir de uma estrutura aninhada em tempo de execução. Ela mantém um título junto da primeira linha do corpo ao ler a posição antes de escrever, e envolve a geração em blocos try/catch para as exceções mais específicas do NextPDF. PageLayoutException cobre uma falha do lado da geração, como exceder o teto de páginas. save() lança InvalidConfigException quando o caminho de saída não é gravável ou seguro.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Exception\InvalidConfigException;use NextPDF\Exception\PageLayoutException;
/** * Render a report whose chapter and section structure is known only at runtime, * building the outline and table of contents from the live cursor position. * * @param list<array{title: string, sections: list<array{title: string, body: string}>}> $chapters * * @throws PageLayoutException When page generation exceeds an engine limit. * @throws InvalidConfigException When the output path cannot be written. */function renderDynamicToc(array $chapters, string $outputPath): void{ $doc = Document::createStandalone(); $doc->setTitle('Runtime Report'); $doc->setPrintHeader(false); $doc->setPrintFooter(false); // A 25 mm bottom threshold so a heading does not strand at the page foot. $doc->setAutoPageBreak(true, margin: 25); $doc->addPage();
foreach ($chapters as $chapter) { // Reserve space so the chapter heading and its first section start // together: if less than 40 user units remain, break first. $remaining = $doc->getPageHeight() - $doc->getMargins()->bottom - $doc->getY(); if ($remaining < 40.0) { $doc->addPage(); }
// Bookmark at the destination point, before the heading is drawn. $doc->bookmark($chapter['title'], level: 0); $doc->setFont('helvetica', 'B', 18); $doc->cell(0, 12, $chapter['title'], newLine: true); $doc->ln(3);
foreach ($chapter['sections'] as $section) { $doc->bookmark($section['title'], level: 1); $doc->setFont('helvetica', 'B', 13); $doc->cell(0, 9, $section['title'], newLine: true); $doc->setFont('helvetica', '', 11); $doc->multiCell(0, 7, $section['body']); $doc->ln(5); } }
// Render the table of contents only when at least one bookmark exists. // addTOC() is a no-op when the entry list is empty, so an empty report // produces no contents page rather than a blank one. $doc->addTOC(pageIndex: 0, title: 'Table of Contents');
$doc->save($outputPath);}
/** @var list<array{title: string, sections: list<array{title: string, body: string}>}> $chapters */$chapters = [ [ 'title' => 'Chapter 1: Overview', 'sections' => [ ['title' => 'Scope', 'body' => 'Runtime body text for the scope section.'], ['title' => 'Audience', 'body' => 'Runtime body text for the audience section.'], ], ], [ 'title' => 'Chapter 2: Detail', 'sections' => [ ['title' => 'Inputs', 'body' => 'Runtime body text for the inputs section.'], ], ],];
$output = getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/dynamic-toc.pdf';
try { renderDynamicToc($chapters, $output); echo "Wrote {$output}\n";} catch (PageLayoutException $e) { // A structural limit was hit during generation; surface the page context. fwrite(STDERR, 'Layout failure while building the report: ' . $e->getMessage() . "\n"); exit(1);} catch (InvalidConfigException $e) { // The output path was rejected (stream wrapper, missing directory, or // a null byte). Report it without leaking the resolved path to a client. fwrite(STDERR, 'Output path rejected: ' . $e->getMessage() . "\n"); exit(1);}Casos extremos e pegadinhas
Seção intitulada “Casos extremos e pegadinhas”getPage()retorna-1antes da primeira página. Adicione a primeira página antes de ler a posição ou chamarbookmark(). Os exemplos adicionam uma página previamente.- Faça o bookmark antes do título, não depois.
bookmark()com$y = -1registra ogetY()atual. Chame-o imediatamente antes de renderizar o título para que o destino caia no título, não na linha abaixo dele. - As quebras de página automáticas movem o destino. Quando
setAutoPageBreak()está ativado, uma chamada decell()oumultiCell()pode descarregar para uma nova página. LeiagetPage()novamente na próxima iteração em vez de armazená-lo em cache. O destino acompanha o conteúdo porquebookmark()lê a posição ao vivo todas as vezes. - Reserve espaço para um título e sua primeira linha juntos. Um título que cabe no pé da página enquanto o corpo passa para a página seguinte fica difícil de ler. O exemplo de produção calcula a altura restante a partir de
getPageHeight(),getMargins()->bottomegetY()e, em seguida, força umaddPage()antecipado quando resta menos que um limite. addTOC()em um documento vazio não faz nada. Se nenhuma chamada debookmark()foi executada,addTOC()retorna sem inserir uma página. Proteger o relatório contra entrada vazia, portanto, não é obrigatório, embora valha a pena saber que a página de sumário não aparecerá.- O sumário é renderizado uma única vez, na posição em que você o insere.
addTOC(pageIndex: 0)insere o sumário como a primeira página. Os números de página nas entradas renderizadas usam a página registrada de cada entrada; portanto, insira o sumário depois que toda chamada debookmark()tiver sido executada. - Saltos de nível parecem malformados. Aumente
$levelem no máximo um entre bookmarks sucessivos. Saltar do nível 0 para o nível 2 sem um nível 1 intermediário produz uma hierarquia que alguns leitores renderizam incorretamente.
Desempenho
Seção intitulada “Desempenho”Cada chamada de bookmark() acrescenta um item de outline e uma entrada de sumário em tempo O(1), e cada leitura de posição (getPage(), getY(), getNumPages()) é um acesso a campo em tempo constante no contexto de renderização, sem percorrer estruturas. A árvore do outline e a página de sumário são materializadas uma única vez cada: em addTOC() e em save(), respectivamente. Um relatório com centenas de títulos permanece bem dentro de um orçamento de 2000 ms / 64 MB. A geração roda em processo, sem navegador headless e sem chamada de rede.
Notas de segurança
Seção intitulada “Notas de segurança”Os títulos dos bookmarks e a página de sumário renderizam os valores que você passa para bookmark(). Quando esses títulos carregarem dados de tempo de execução, como o nome de um capítulo vindo de uma linha de banco de dados ou um campo de API, limite o comprimento e sanitize a string antes que ela chegue a bookmark(), exatamente como você faria com qualquer valor exibido no leitor. Não construa títulos a partir de entrada de requisição não validada.
O engine valida o caminho de saída passado para save(): ele rejeita stream wrappers (scheme://) e bytes nulos embutidos, e resolve o diretório pai para bloquear path traversal, lançando InvalidConfigException em qualquer uma dessas condições. Mantenha essa validação funcionando ao passar um caminho que você controla; nunca entregue a save() um nome de arquivo bruto fornecido pelo cliente. Quando você reportar uma InvalidConfigException a um chamador, registre o detalhe no lado do servidor e retorne uma mensagem genérica em vez do caminho resolvido.
Conformidade
Seção intitulada “Conformidade”Esta receita não faz nenhuma afirmação própria de conformidade ISO 32000-2. A semântica do outline e do sumário, incluindo o outline do documento como uma árvore de itens de outline e destinos associados a esses itens, está descrita em Adicione bookmarks e um sumário, que traz as citações de cláusulas pertinentes. O padrão dinâmico aqui muda apenas de onde vem a posição de destino, não a estrutura que é escrita.
Perfil de reprodutibilidade - estrutural. O /ID do trailer e os átomos de data variam a cada save; uma comparação estrutural remove esses valores. Esta página documenta como o NextPDF produz o outline e o sumário a partir do cursor ao vivo; ela não faz uma afirmação genérica de conformidade com padrões.
Veja também
Seção intitulada “Veja também”- Adicione bookmarks e um sumário - a contraparte estática desta receita
- Módulo de navegação
- Concern HasPages - a superfície de página e posição
- Construa um documento de várias páginas
- Cabeçalhos e rodapés