Een inhoudsopgave genereren vanuit de documentstructuur tijdens runtime
In het kort
Sectie met titel “In het kort”Uw inhoud kan tijdens runtime vorm krijgen: hoofdstukken uit een database, secties uit een Application Programming Interface (API)-respons of koppen uit een lus die u niet vooraf kunt kennen. U wilt dat de documentoverzichtsstructuur en de aanklikbare inhoudsopgave precies aansluiten op die inhoud, zonder een tweede, handmatig bijgehouden lijst die uit de pas kan gaan lopen.
Dit recipe bouwt de overzichtsstructuur dynamisch op. Telkens wanneer u een kop schrijft, leest u de actuele cursor- en paginapositie van de engine met getPage(), getY() en getNumPages(), en geeft u die waarden vervolgens door aan bookmark(). De bladwijzer wordt gebonden aan de positie die u op dat moment uitleest, zodat de overzichtsstructuur de inhoud volgt, zelfs wanneer pagina-einden op onverwachte plaatsen vallen. Aan het einde rendert addTOC() een echte inhoudsopgavepagina op basis van dezelfde vermeldingen.
Vereisten: een Core-installatie (composer require nextpdf/core:^3) en inhoud waarvan u de kopstructuur pas tijdens het schrijven ontdekt, niet vooraf.
Deze pagina behandelt het dynamische, positiegestuurde patroon. Lees voor het statische geval, waarin u elke kop en het bijbehorende niveau vooraf kent, eerst Bladwijzers en een inhoudsopgave toevoegen. Dit recipe gebruikt dezelfde bookmark()- en addTOC()-interface en herhaalt die basis niet.
Installeren
Sectie met titel “Installeren”composer require nextpdf/core:^3U hebt geen optionele extensie nodig. De navigatie-interface (bookmark(), addTOC()) en de positie-accessors (getPage(), getY(), getNumPages()) zijn stabiel sinds 1.2.0 en werken op de volledige backport-matrix van 8.1 tot en met 8.4.
Conceptueel overzicht
Sectie met titel “Conceptueel overzicht”Een dynamische inhoudsopgave bestaat uit twee delen die op elkaar moeten aansluiten:
- De overzichtsstructuur (ook wel bladwijzers genoemd): de boomstructuur die de lezer in de navigatiezijbalk ziet, waarbij elke vermelding naar een positie in het document springt.
- De weergegeven inhoudsopgave: een gegenereerde pagina die dezelfde vermeldingen met hun paginanummers opsomt.
NextPDF houdt beide synchroon met één enkele aanroep. bookmark($title, $level, $y) voegt één overzichtsitem en één inhoudsopgavevermelding toe, beide gebonden aan de huidige pagina en de huidige verticale positie. U hoeft geen twee lijsten bij te houden.
Het dynamische deel is waar de positie vandaan komt. Een statisch recipe geeft vaste koppen door in de volgorde van de broncode. Hier schrijft u een kop en vraagt u de engine vervolgens meteen waar de cursor terechtkwam:
getPage()retourneert de nulgebaseerde index van de actieve pagina. Voordat de eerste pagina is toegevoegd, retourneert het-1.getNumPages()retourneert het totale aantal pagina’s, inclusief de actieve pagina die nog niet is afgerond.getY()retourneert de huidige verticale cursor in gebruikerseenheden, gemeten als de afstand vanaf de bovenkant van de pagina.getX(),getPageHeight()engetMargins()geven de aanvullende positiegegevens wanneer u moet bepalen of een kop en de eerste regel van de bijbehorende bodytekst samen passen.
Lees die waarden uit en roep vervolgens bookmark() aan. Een automatisch pagina-einde kan de cursor tussen twee koppen naar een nieuwe pagina verplaatsen, dus door de positie terug te lezen blijft de overzichtsbestemming op de juiste pagina.
Eén punt in de volgorde bepaalt het hele patroon: roep bookmark() aan precies op het punt waar u de bestemming wilt, namelijk onmiddellijk voordat u de koptekst rendert. Als u eerst de kop schrijft en pas daarna een bladwijzer plaatst, ligt de vastgelegde getY() direct onder de kop.
API-interface
Sectie met titel “API-interface”Dit recipe maakt gebruik van deze methoden van \NextPDF\Core\Document:
bookmark(string $title, int $level = 0, float $y = -1): static- voegt op$leveleen overzichtsitem en een inhoudsopgavevermelding toe, gebonden aan de huidige pagina. Met$y = -1is de bestemming de huidige cursor-Y; geef een niet-negatieve Y door om een precieze bestemming vast te pinnen.addTOC(int $pageIndex = 0, string $title = ''): static- rendert een inhoudsopgavepagina op basis van de verzamelde vermeldingen en voegt deze in op$pageIndex. Keert terug zonder een pagina in te voegen wanneer er geen bladwijzers zijn.getPage(): int- nulgebaseerde index van de actieve pagina (-1vóór de eerste pagina).getNumPages(): int- totale paginatelling, inclusief de actieve, nog niet doorgeschreven pagina.getY(): float- huidige cursor-Y in gebruikerseenheden (afstand vanaf de bovenkant van de pagina).getX(): float- huidige cursor-X in gebruikerseenheden.getPageHeight(): float- hoogte van de huidige pagina in gebruikerseenheden.getMargins(): \NextPDF\ValueObjects\Margin- de actieve marges (top,right,bottom,left).setY(float $y): static- verplaatst de cursor naar een expliciete Y.setAutoPageBreak(bool $enabled, float $margin = 20): static- beheert het automatische pagina-einde en de bijbehorende drempel voor de ondermarge.
Codevoorbeeld — Snelstart
Sectie met titel “Codevoorbeeld — Snelstart”Dit voorbeeld schrijft drie secties uit een runtime-lijst. Elke iteratie leest met getPage() de actuele pagina uit vóór het plaatsen van de bladwijzer, zodat de overzichtsbestemming correct blijft na een automatisch pagina-einde.
<?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');Verwachte terminaluitvoer, met één regel per sectie:
Bookmarked 'Origins' on page index 0Bookmarked 'Method' on page index 0Bookmarked 'Results' on page index 0Codevoorbeeld — Productie
Sectie met titel “Codevoorbeeld — Productie”Deze versie bouwt een overzichtsstructuur met twee niveaus (hoofdstukken en secties) op uit een geneste runtime-structuur. De kop blijft bij de eerste regel van de bijbehorende bodytekst doordat de positie wordt uitgelezen voordat er wordt geschreven, en de generatie wordt omhuld met try/catch-blokken voor de meest specifieke NextPDF-uitzonderingen. PageLayoutException dekt fouten tijdens de generatie af, zoals het overschrijden van het paginaplafond. save() genereert InvalidConfigException voor een niet-beschrijfbaar of onveilig uitvoerpad.
<?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);}Randgevallen en valkuilen
Sectie met titel “Randgevallen en valkuilen”getPage()retourneert-1vóór de eerste pagina. Voeg de eerste pagina toe voordat u de positie uitleest ofbookmark()aanroept. De voorbeelden voegen vooraf een pagina toe.- Plaats de bladwijzer vóór de kop, niet erna.
bookmark()met$y = -1legt de huidigegetY()vast. Roep deze methode onmiddellijk aan voordat u de kop rendert, zodat de bestemming op de kop terechtkomt en niet op de regel eronder. - Automatische pagina-einden verplaatsen de bestemming. Wanneer
setAutoPageBreak()aanstaat, kan een aanroep vancell()ofmultiCell()op een nieuwe pagina terechtkomen. LeesgetPage()bij de volgende iteratie opnieuw uit in plaats van het in de cache te bewaren. De bestemming volgt de inhoud doordatbookmark()telkens de actuele positie uitleest. - Reserveer ruimte voor een kop en de bijbehorende eerste regel samen. Een kop die onder aan de pagina past terwijl de bijbehorende bodytekst naar de volgende pagina doorloopt, leest slecht. Het productievoorbeeld berekent de resterende hoogte uit
getPageHeight(),getMargins()->bottomengetY(), en forceert vervolgens een vroegtijdigeaddPage()wanneer er minder dan een drempelwaarde overblijft. addTOC()doet niets bij een leeg document. Als er geenbookmark()-aanroep is uitgevoerd, keertaddTOC()terug zonder een pagina in te voegen. Het beveiligen van het rapport tegen lege invoer is daarom niet vereist, al is het goed om te weten dat de inhoudsopgavepagina dan niet verschijnt.- De inhoudsopgave wordt eenmaal gerenderd, op de positie waar u deze invoegt.
addTOC(pageIndex: 0)voegt de inhoudsopgave in als de eerste pagina. Paginanummers in de weergegeven vermeldingen gebruiken de vastgelegde pagina van elke vermelding, dus voeg de inhoudsopgave pas in nadat elkebookmark()-aanroep is uitgevoerd. - Het overslaan van niveaus oogt onjuist genest. Verhoog
$levelmet hooguit één tussen opeenvolgende bladwijzers. Een sprong van niveau 0 naar niveau 2 zonder een tussenliggend niveau 1 levert een hiërarchie op die sommige readers onjuist weergeven.
Prestaties
Sectie met titel “Prestaties”Elke aanroep van bookmark() voegt één overzichtsitem en één inhoudsopgavevermelding toe in O(1)-tijd, en elke positielezing (getPage(), getY(), getNumPages()) is een veldtoegang met constante tijd op de renderingcontext, zonder traversal. De overzichtsboom en de inhoudsopgavepagina worden elk eenmaal gematerialiseerd: respectievelijk bij addTOC() en bij save(). Een rapport met honderden koppen blijft ruim binnen een budget van 2000 ms / 64 MB. De generatie verloopt in-process, zonder headless browser en zonder netwerkaanroep.
Beveiligingsopmerkingen
Sectie met titel “Beveiligingsopmerkingen”Bladwijzertitels en de inhoudsopgavepagina geven de waarden weer die u doorgeeft aan bookmark(). Wanneer die titels runtime-gegevens bevatten, zoals een hoofdstuknaam uit een databaserij of een API-veld, begrens dan de lengte en saneer de tekenreeks voordat deze bookmark() bereikt, precies zoals u dat zou doen met elke waarde die in de reader wordt weergegeven. Bouw geen titels op uit niet-gevalideerde requestinvoer.
De engine valideert het uitvoerpad dat aan save() wordt doorgegeven: het weigert stream-wrappers (scheme://) en ingebedde null-bytes, en het resolveert de bovenliggende map om path traversal te blokkeren, waarbij het InvalidConfigException genereert bij elk van deze voorwaarden. Houd die validatie effectief door een pad door te geven dat u beheert; geef save() nooit een onbewerkte, door de client aangeleverde bestandsnaam. Wanneer u een InvalidConfigException aan een aanroeper rapporteert, log dan de details aan de serverzijde en retourneer een algemene melding in plaats van het geresolveerde pad.
Conformiteit
Sectie met titel “Conformiteit”Dit recipe doet geen eigen ISO 32000-2-conformiteitsclaim. De semantiek van de overzichtsstructuur en de inhoudsopgave, waaronder de documentoverzichtsstructuur als een boom van overzichtsitems en de bij die items behorende bestemmingen, wordt beschreven in Bladwijzers en een inhoudsopgave toevoegen, waarin de relevante clausuleverwijzingen staan. Het dynamische patroon hier verandert alleen waar de bestemmingspositie vandaan komt, niet de structuur die wordt geschreven.
Reproduceerbaarheidsprofiel - structureel. De trailer-/ID en datumatomen variëren per save; een structurele vergelijking verwijdert die waarden. Deze pagina documenteert hoe NextPDF de overzichtsstructuur en inhoudsopgave uit de actuele cursor produceert; deze pagina doet geen algemene claim over conformiteit met de standaarden.
Zie ook
Sectie met titel “Zie ook”- Bladwijzers en een inhoudsopgave toevoegen - de statische tegenhanger van dit recipe
- Navigatiemodule
- HasPages-concern - de pagina- en positie-interface
- Een document met meerdere pagina’s bouwen
- Kop- en voetteksten