إنشاء جدول محتويات من بنية المستند أثناء وقت التشغيل
لمحة سريعة
قسم بعنوان «لمحة سريعة»قد يتشكّل محتواك أثناء وقت التشغيل: فصول من قاعدة بيانات، أو أقسام من استجابة واجهة برمجة تطبيقات (API)، أو عناوين من حلقة لا يمكنك معرفتها مسبقًا. تحتاج إلى أن يطابق مخطط المستند وجدول المحتويات القابل للنقر ذلك المحتوى تمامًا، دون الاحتفاظ بقائمة ثانية مكتوبة يدويًا قد تخرج عن التزامن.
تبني هذه الوصفة المخطط ديناميكيًا. عند كتابة كل عنوان، اقرأ موضع المؤشر الحي والصفحة من المحرك عبر getPage() وgetY() وgetNumPages()، ثم مرّر تلك القيم إلى bookmark(). ترتبط العلامة المرجعية بالموضع الذي قرأته في تلك اللحظة، فيتبع المخطط المحتوى حتى عندما تظهر فواصل الصفحات في أماكن غير متوقعة. في النهاية، يُصيِّر addTOC() صفحة جدول محتويات حقيقية من المدخلات نفسها.
المتطلبات المسبقة: تثبيت Core (composer require nextpdf/core:^3) ومحتوى لا تتضح بنية عناوينه إلا أثناء الكتابة، لا قبلها.
تغطي هذه الصفحة النمط الديناميكي المُوجَّه بالموضع. أما في الحالة الساكنة، حيث تعرف كل عنوان ومستواه مسبقًا، فاقرأ أولًا إضافة علامات مرجعية وجدول محتويات. تستخدم هذه الوصفة الواجهة نفسها bookmark() وaddTOC() ولا تكرّر تلك الأساسيات.
التثبيت
قسم بعنوان «التثبيت»composer require nextpdf/core:^3لا تحتاج إلى أي امتداد اختياري. ظلّت واجهة التنقل (bookmark() وaddTOC()) ومُوصِّلات الموضع (getPage() وgetY() وgetNumPages()) مستقرة منذ 1.2.0 وتعمل عبر مصفوفة التوافق من 8.1 حتى 8.4.
نظرة مفاهيمية
قسم بعنوان «نظرة مفاهيمية»لجدول المحتويات الديناميكي جزآن يجب أن يتفقا:
- المخطط (يُسمّى أيضًا العلامات المرجعية): الشجرة التي يراها القارئ في الشريط الجانبي للتنقل، حيث تقفز كل مدخلة إلى موضع في المستند.
- جدول المحتويات المُصيَّر: صفحة مُولَّدة تسرد المدخلات نفسها مع أرقام صفحاتها.
يبقي NextPDF كليهما متزامنًا عبر استدعاء واحد. يضيف bookmark($title, $level, $y) عنصر مخطط واحدًا و مدخلة جدول محتويات واحدة، ويربط كليهما بالصفحة الحالية والموضع الرأسي الحالي. لا تحتفظ بقائمتين.
الجزء الديناميكي هو من أين يأتي الموضع. تمرّر الوصفة الساكنة عناوين حرفية بترتيب المصدر. أما هنا، فتكتب عنوانًا، ثم تسأل المحرك فورًا عن المكان الذي استقر فيه المؤشر:
- يُرجع
getPage()الفهرس المبني على الصفر للصفحة النشطة. قبل إضافة الصفحة الأولى يُرجع-1. - يُرجع
getNumPages()إجمالي عدد الصفحات، بما في ذلك الصفحة النشطة التي لم تُفرَّغ بعد. - يُرجع
getY()المؤشر الرأسي الحالي بوحدات المستخدم، مقيسًا بوصفه المسافة من أعلى الصفحة. - تُكمل
getX()وgetPageHeight()وgetMargins()الصورة عندما تحتاج إلى تحديد ما إذا كان العنوان وأول سطر من نص المتن يتلاءمان معًا.
اقرأ تلك القيم، ثم استدعِ bookmark(). يمكن لفصل الصفحات التلقائي أن ينقل المؤشر إلى صفحة جديدة بين عنوانين، لذا فإن قراءة الموضع مرة أخرى تُبقي وجهة المخطط على الصفحة الصحيحة.
يعتمد النمط كله على قاعدة واحدة في ترتيب الاستدعاءات: استدعِ bookmark() عند النقطة الدقيقة التي تريد أن تكون الوجهة عندها، أي مباشرة قبل تصيير نص العنوان. إذا كتبت العنوان أولًا ووضعت العلامة المرجعية بعده، فإن قيمة getY() المسجَّلة تقع مباشرة أسفل العنوان.
واجهة API
قسم بعنوان «واجهة API»تعتمد هذه الوصفة على طرائق \NextPDF\Core\Document هذه:
bookmark(string $title, int $level = 0, float $y = -1): static- يضيف عنصر مخطط ومدخلة جدول محتويات عند$level، ويربطهما بالصفحة الحالية. مع$y = -1تكون الوجهة عند Y المؤشر الحالي؛ مرّر قيمة Y غير سالبة لتثبيت وجهة دقيقة.addTOC(int $pageIndex = 0, string $title = ''): static- يُصيِّر صفحة جدول محتويات من المدخلات المتراكمة ويدرجها عند$pageIndex. يعود دون إدراج صفحة إذا لم توجد أي علامة مرجعية.getPage(): int- الفهرس المبني على الصفر للصفحة النشطة (-1قبل الصفحة الأولى).getNumPages(): int- إجمالي عدد الصفحات، بما في ذلك الصفحة النشطة غير المُفرَّغة.getY(): float- Y المؤشر الحالي بوحدات المستخدم (المسافة من أعلى الصفحة).getX(): float- X المؤشر الحالي بوحدات المستخدم.getPageHeight(): float- ارتفاع الصفحة الحالية بوحدات المستخدم.getMargins(): \NextPDF\ValueObjects\Margin- الهوامش النشطة (topوrightوbottomوleft).setY(float $y): static- ينقل المؤشر إلى قيمة Y صريحة.setAutoPageBreak(bool $enabled, float $margin = 20): static- يتحكم في فصل الصفحات التلقائي وعتبة الهامش السفلي له.
عينة الشيفرة — بداية سريعة
قسم بعنوان «عينة الشيفرة — بداية سريعة»تكتب هذه العينة ثلاثة أقسام من قائمة أثناء وقت التشغيل. يقرأ كل تكرار الصفحة الحية عبر getPage() قبل وضع العلامة المرجعية، كي تبقى وجهة المخطط صحيحة بعد حدوث فصل صفحة تلقائي.
<?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');مخرجات الطرفية المتوقعة، بسطر واحد لكل قسم:
Bookmarked 'Origins' on page index 0Bookmarked 'Method' on page index 0Bookmarked 'Results' on page index 0عينة الشيفرة — للإنتاج
قسم بعنوان «عينة الشيفرة — للإنتاج»يبني هذا الإصدار مخططًا من مستويين (فصول وأقسام) من بنية متداخلة في وقت التشغيل. يُبقي العنوان مع أول سطر من المتن عبر قراءة الموضع قبل الكتابة، ويغلِّف التوليد في كتل try/catch لأكثر استثناءات NextPDF تحديدًا. يغطي PageLayoutException فشلًا في التوليد، مثل تجاوز سقف الصفحات. يرفع save() الاستثناء InvalidConfigException عند استخدام مسار إخراج غير قابل للكتابة أو غير آمن.
<?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);}حالات حدّية ومزالق
قسم بعنوان «حالات حدّية ومزالق»- يُرجع
getPage()القيمة-1قبل الصفحة الأولى. أضف الصفحة الأولى قبل أن تقرأ الموضع أو تستدعيbookmark(). تضيف العينات صفحة في البداية. - ضع العلامة المرجعية قبل العنوان، لا بعده. يسجّل
bookmark()مع$y = -1قيمةgetY()الحالية. استدعِه مباشرة قبل تصيير العنوان كي تستقر الوجهة على العنوان، لا على السطر الذي تحته. - فواصل الصفحات التلقائية تنقل الوجهة. عندما يكون
setAutoPageBreak()مفعّلًا، يمكن لاستدعاءcell()أوmultiCell()أن ينتقل إلى صفحة جديدة. اقرأgetPage()مرة أخرى في التكرار التالي بدلًا من تخزينه مؤقتًا. تتبع الوجهة المحتوى لأنbookmark()يقرأ الموضع الحي في كل مرة. - احجز مساحة للعنوان وأول سطر له معًا. العنوان الذي يتلاءم عند أسفل الصفحة بينما ينتقل متنه إلى الصفحة التالية يضعف قابلية القراءة. تحسب عينة الإنتاج الارتفاع المتبقي من
getPageHeight()وgetMargins()->bottomوgetY()، ثم تفرضaddPage()مبكرًا عندما يتبقّى أقل من العتبة. addTOC()على مستند فارغ لا يفعل شيئًا. إذا لم يُشغَّل أي استدعاءbookmark()، يعودaddTOC()دون إدراج صفحة. لذا فإن حماية التقرير من المدخلات الفارغة غير مطلوبة، وإن كان من المفيد معرفة أن صفحة المحتويات لن تظهر.- جدول المحتويات يُصيَّر مرة واحدة، عند الموضع الذي تدرجه فيه. يُدرج
addTOC(pageIndex: 0)المحتويات بوصفها الصفحة الأولى. تستخدم أرقام الصفحات في المدخلات المُصيَّرة الصفحة المسجَّلة لكل مدخلة، لذا أدرج المحتويات بعد أن يكون كل استدعاءbookmark()قد شُغِّل. - يبدو تخطّي المستويات مشوّهًا. زِد
$levelبمقدار واحد على الأكثر بين العلامات المرجعية المتتالية. القفز من المستوى 0 إلى المستوى 2 دون مستوى 1 وسيط يُنتج تسلسلًا هرميًا يُصيِّره بعض القرّاء بشكل غير صحيح.
الأداء
قسم بعنوان «الأداء»يُلحق كل استدعاء bookmark() عنصر مخطط واحدًا ومدخلة جدول محتويات واحدة بزمن O(1)، وكل قراءة موضع (getPage() وgetY() وgetNumPages()) هي قراءة حقل بزمن ثابت على سياق التصيير، دون اجتياز. تُنشأ شجرة المخطط وصفحة المحتويات مرة واحدة: عند addTOC() وعند save() على التوالي. يبقى تقرير يضم مئات العناوين ضمن ميزانية 2000 ms / 64 MB بأريحية. يجري التوليد داخل العملية، دون متصفح بلا واجهة ودون أي استدعاء شبكي.
ملاحظات أمنية
قسم بعنوان «ملاحظات أمنية»تُصيِّر عناوين العلامات المرجعية وصفحة المحتويات القيم التي تمرّرها إلى bookmark(). عندما تحمل تلك العناوين بيانات وقت التشغيل، مثل اسم فصل من صف قاعدة بيانات أو حقل API، حدِّد طول السلسلة وطهِّرها قبل أن تصل إلى bookmark()، تمامًا كما تفعل مع أي قيمة تُعرض في القارئ. لا تبنِ العناوين من مدخلات طلب غير مُتحقَّق منها.
يتحقق المحرك من مسار الإخراج المُمرَّر إلى save(): فيرفض مغلِّفات الدفق (scheme://) والبايتات الصفرية المضمّنة، ويحلّ الدليل الأب لمنع اجتياز المسار، رافعًا InvalidConfigException عند تحقق أي من هذه الشروط. أبقِ ذلك التحقق فعّالًا بتمرير مسار تتحكم فيه؛ لا تسلّم save() اسم ملف خامًا مُورَّدًا من العميل أبدًا. عندما تُبلِّغ مستدعيًا عن InvalidConfigException، سجِّل التفاصيل من جانب الخادم وأرجِع رسالة عامة بدلًا من المسار بعد حله.
المطابقة
قسم بعنوان «المطابقة»لا تؤكد هذه الوصفة أي ادعاء مطابقة لـ ISO 32000-2 خاص بها. يرد وصف دلالات المخطط وجدول المحتويات، بما في ذلك مخطط المستند بوصفه شجرة من عناصر المخطط والوجهات المرتبطة بتلك العناصر، في إضافة علامات مرجعية وجدول محتويات، التي تتضمن استشهادات البنود ذات الصلة. النمط الديناميكي هنا يغيّر فقط من أين يأتي موضع الوجهة، لا البنية التي تُكتب.
ملف إمكانية إعادة الإنتاج - بنيوي. تتباين قيم /ID في التذييل والتاريخ في كل حفظ؛ تزيل المقارنة البنيوية تلك القيم. توثّق هذه الصفحة كيف يُنتج NextPDF المخطط والمحتويات من المؤشر الحي؛ وهي لا تؤكد ادعاء مطابقة شاملًا للمعايير.
انظر أيضًا
قسم بعنوان «انظر أيضًا»- إضافة علامات مرجعية وجدول محتويات - النظير الساكن لهذه الوصفة
- وحدة التنقل
- سمة HasPages - واجهة الصفحة والموضع
- بناء مستند متعدد الصفحات
- الرؤوس والتذييلات