nhance/app/Controllers/Docs/DocsController.php
2026-05-18 12:28:19 +05:30

584 lines
31 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Controllers\Docs;
use App\Controllers\BaseController;
use CodeIgniter\Exceptions\PageNotFoundException;
/**
* DocsController
* app/Controllers/Docs/DocsController.php
*
* Handles all developer documentation pages.
* Each page only needs to define its own content — layout partials are assembled here.
*
* Route setup (app/Config/Routes.php):
* $routes->get('docs', 'Docs\DocsController::index');
* $routes->get('docs/(:segment)', 'Docs\DocsController::page/$1');
*/
class DocsController extends BaseController
{
// ─── APP-LEVEL DEFAULTS ───────────────────────────────────────────────────
// Change these once here; they propagate to every page automatically.
protected string $appName = 'Nhance PAM';
protected string $appVersion = 'v1.1.0';
// ─── SIDEBAR NAV ──────────────────────────────────────────────────────────
// Add / remove pages here. 'id' must match the key used in page configs below.
protected array $nav = [
[
'label' => 'Getting Started',
'items' => [
['id' => 'introduction', 'label' => 'Introduction', 'url' => 'docs/introduction'],
['id' => 'installation', 'label' => 'Installation', 'url' => 'docs/installation'],
['id' => 'configuration', 'label' => 'Configuration', 'url' => 'docs/configuration'],
['id' => 'env-setup', 'label' => 'Environment Setup', 'url' => 'docs/env-setup'],
],
],
[
'label' => 'Architecture',
'items' => [
['id' => 'project-structure', 'label' => 'Project Structure', 'url' => 'docs/project-structure'],
['id' => 'routing', 'label' => 'Routing', 'url' => 'docs/routing'],
['id' => 'controllers', 'label' => 'Controllers', 'url' => 'docs/controllers'],
['id' => 'models', 'label' => 'Models', 'url' => 'docs/models'],
['id' => 'services', 'label' => 'Services', 'url' => 'docs/services'],
['id' => 'helpers', 'label' => 'Helpers', 'url' => 'docs/helpers'],
],
],
[
'label' => 'Features',
'items' => [
['id' => 'authentication', 'label' => 'Authentication', 'url' => 'docs/authentication'],
['id' => 'acl', 'label' => 'ACL / Access Control','url' => 'docs/acl'],
['id' => 'input-security', 'label' => 'Input Security Guard','url' => 'docs/input-security'],
['id' => 'file-uploads', 'label' => 'File Upload Guard', 'url' => 'docs/file-uploads'],
['id' => 'background-jobs', 'label' => 'Background Jobs', 'url' => 'docs/background-jobs'],
['id' => 'visit-onboard', 'label' => 'Visit onboard', 'url' => 'docs/visit-onboard'],
['id' => 'visit-offboard', 'label' => 'Visit offboard', 'url' => 'docs/visit-offboard'],
['id' => 'notifications', 'label' => 'Email / Notifications', 'url' => 'docs/notifications'],
['id' => 'api-rate-limiter', 'label' => 'API Rate Limiter', 'url' => 'docs/api-rate-limiter'],
['id' => 'tpa-recon', 'label' => 'TPA Recon', 'url' => 'docs/tpa-recon'],
['id' => 'eb-rack-rate-config', 'label' => 'EB rack rate config', 'url' => 'docs/eb-rack-rate-config'],
['id' => 'eb-rack-rate-calculation', 'label' => 'EB rack rate calculation', 'url' => 'docs/eb-rack-rate-calculation'],
],
],
[
'label' => 'API Reference',
'items' => [
['id' => 'endpoints', 'label' => 'Endpoints', 'url' => 'docs/endpoints'],
['id' => 'request-response', 'label' => 'Request / Response', 'url' => 'docs/request-response'],
['id' => 'error-codes', 'label' => 'Error Codes', 'url' => 'docs/error-codes'],
],
],
[
'label' => 'DevOps',
'items' => [
['id' => 'deployment', 'label' => 'Deployment', 'url' => 'docs/deployment'],
['id' => 'cicd', 'label' => 'CI/CD Pipeline', 'url' => 'docs/cicd'],
['id' => 's3-cloudfront', 'label' => 'S3 & CloudFront', 'url' => 'docs/s3-cloudfront'],
],
],
[
'label' => 'Reference',
'items' => [
['id' => 'changelog', 'label' => 'Changelog', 'url' => 'docs/changelog'],
['id' => 'contributing', 'label' => 'Contributing', 'url' => 'docs/contributing'],
],
],
];
// ─── PAGE REGISTRY ────────────────────────────────────────────────────────
// One entry per docs page.
// 'view' — path inside app/Views/ (without .php)
// Other keys are passed straight to the partials — add/remove as needed.
protected array $pages = [
'introduction' => [
'view' => 'docs/introduction',
'title' => 'Introduction',
'breadcrumb' => 'Getting Started',
'last_updated' => 'May 2025',
'author' => 'Core Team',
'read_time' => '3 min read',
'toc' => [
['label' => 'What is MyApp?', 'href' => '#what-is-myapp'],
['label' => 'Tech stack', 'href' => '#tech-stack'],
['label' => 'Conventions', 'href' => '#conventions'],
],
'prev' => null,
'next' => ['label' => 'Installation', 'url' => 'docs/installation'],
],
'installation' => [
'view' => 'docs/installation',
'title' => 'Installation',
'breadcrumb' => 'Getting Started',
'last_updated' => 'May 2025',
'author' => 'Core Team',
'read_time' => '5 min read',
'toc' => [
['label' => 'Requirements', 'href' => '#requirements'],
['label' => 'Steps', 'href' => '#steps'],
['label' => 'Clone repo', 'href' => '#clone', 'level' => 'h3'],
['label' => 'Run migrations', 'href' => '#migrate', 'level' => 'h3'],
['label' => 'Configuration', 'href' => '#configuration'],
['label' => 'Sample flowchart', 'href' => '#sample-flowchart'],
],
'prev' => ['label' => 'Introduction', 'url' => 'docs/introduction'],
'next' => ['label' => 'Configuration', 'url' => 'docs/configuration'],
],
'configuration' => [
'view' => 'docs/configuration',
'title' => 'Configuration',
'breadcrumb' => 'Getting Started',
'last_updated' => 'May 2025',
'author' => 'Core Team',
'read_time' => '4 min read',
'toc' => [
['label' => 'Environment file', 'href' => '#env-file'],
['label' => 'Database', 'href' => '#database'],
['label' => 'Mail', 'href' => '#mail'],
['label' => 'File storage', 'href' => '#storage'],
],
'prev' => ['label' => 'Installation', 'url' => 'docs/installation'],
'next' => ['label' => 'Environment Setup','url' => 'docs/env-setup'],
],
'acl' => [
'view' => 'docs/acl',
'title' => 'ACL / Access Control',
'breadcrumb' => 'Features',
'last_updated' => 'May 2026',
'author' => 'Core Team',
'read_time' => '9 min read',
'toc' => [
['label' => 'Overview', 'href' => '#overview'],
['label' => 'Where it is wired', 'href' => '#where-it-is-wired'],
['label' => 'Rule format', 'href' => '#rule-format'],
['label' => 'Matching behavior', 'href' => '#matching-behavior'],
['label' => 'Auth context', 'href' => '#auth-context'],
['label' => 'Allow and deny flow', 'href' => '#allow-and-deny-flow'],
['label' => 'Developer steps', 'href' => '#developer-steps'],
['label' => 'Examples', 'href' => '#examples'],
['label' => 'Do and dont', 'href' => '#do-and-dont'],
['label' => 'Common pitfalls', 'href' => '#common-pitfalls'],
],
'prev' => null,
'next' => ['label' => 'Input Security Guard', 'url' => 'docs/input-security'],
],
'input-security' => [
'view' => 'docs/input-security',
'title' => 'Input Security Guard',
'breadcrumb' => 'Features',
'last_updated' => 'May 2026',
'author' => 'Core Team',
'read_time' => '8 min read',
'toc' => [
['label' => 'Overview', 'href' => '#overview'],
['label' => 'Where it runs', 'href' => '#where-it-runs'],
['label' => 'What it checks', 'href' => '#what-it-checks'],
['label' => 'Canonicalization', 'href' => '#canonicalization'],
['label' => 'Block behavior', 'href' => '#block-behavior'],
['label' => 'Filter exceptions', 'href' => '#filter-exceptions'],
['label' => 'Developer steps', 'href' => '#developer-steps'],
['label' => 'Common pitfalls', 'href' => '#common-pitfalls'],
],
'prev' => ['label' => 'ACL / Access Control', 'url' => 'docs/acl'],
'next' => ['label' => 'File Upload Guard', 'url' => 'docs/file-uploads'],
],
'file-uploads' => [
'view' => 'docs/file-uploads',
'title' => 'File Upload Guard',
'breadcrumb' => 'Features',
'last_updated' => 'May 2026',
'author' => 'Core Team',
'read_time' => '8 min read',
'toc' => [
['label' => 'Overview', 'href' => '#overview'],
['label' => 'When it runs', 'href' => '#when-it-runs'],
['label' => 'Allowed file types', 'href' => '#allowed-file-types'],
['label' => 'Blocked extensions', 'href' => '#blocked-extensions'],
['label' => 'Validation flow', 'href' => '#validation-flow'],
['label' => 'Magic bytes check', 'href' => '#magic-bytes-check'],
['label' => 'Route coverage', 'href' => '#route-coverage'],
['label' => 'Blocked response', 'href' => '#blocked-response'],
['label' => 'Operational notes', 'href' => '#operational-notes'],
],
'prev' => ['label' => 'Input Security Guard', 'url' => 'docs/input-security'],
'next' => ['label' => 'Background Jobs', 'url' => 'docs/background-jobs'],
],
'background-jobs' => [
'view' => 'docs/background-jobs',
'title' => 'Background Jobs',
'breadcrumb' => 'Features',
'last_updated' => 'May 2026',
'author' => 'Core Team',
'read_time' => '8 min read',
'toc' => [
['label' => 'Overview', 'href' => '#overview'],
['label' => 'Queueing jobs', 'href' => '#queueing-jobs'],
['label' => 'Worker lifecycle', 'href' => '#worker-lifecycle'],
['label' => 'Handler registry', 'href' => '#handler-registry'],
['label' => 'Sample handlers', 'href' => '#sample-handlers'],
['label' => 'Status lifecycle', 'href' => '#status-lifecycle'],
['label' => 'Failure behavior', 'href' => '#failure-behavior'],
['label' => 'Running via CLI', 'href' => '#running-via-cli'],
['label' => 'Live runner script', 'href' => '#live-runner-script'],
['label' => 'Systemd service', 'href' => '#systemd-service'],
['label' => 'Permissions setup', 'href' => '#permissions-setup'],
['label' => 'Checking status', 'href' => '#checking-status'],
['label' => 'Adding a new handler','href' => '#adding-a-new-handler'],
],
'prev' => ['label' => 'File Upload Guard','url' => 'docs/file-uploads'],
'next' => ['label' => 'Visit onboard', 'url' => 'docs/visit-onboard'],
],
'visit-onboard' => [
'view' => 'docs/visit-onboard',
'title' => 'Visit onboard',
'breadcrumb' => 'Features',
'last_updated' => 'May 2026',
'author' => 'Core Team',
'read_time' => '8 min read',
'toc' => [
['label' => 'Overview', 'href' => '#overview'],
['label' => 'Prerequisites', 'href' => '#prerequisites'],
['label' => 'Entry points', 'href' => '#entry-points'],
['label' => 'Eligibility rules', 'href' => '#eligibility-rules'],
['label' => 'Async flow', 'href' => '#async-flow'],
['label' => 'Family payload', 'href' => '#family-payload'],
['label' => 'API integration', 'href' => '#api-integration'],
['label' => 'Response examples', 'href' => '#response-examples'],
['label' => 'Database updates', 'href' => '#database-updates'],
['label' => 'Failure behavior', 'href' => '#failure-behavior'],
['label' => 'Developer steps', 'href' => '#developer-steps'],
['label' => 'Common pitfalls', 'href' => '#common-pitfalls'],
],
'prev' => ['label' => 'Background Jobs', 'url' => 'docs/background-jobs'],
'next' => ['label' => 'Visit offboard', 'url' => 'docs/visit-offboard'],
],
'visit-offboard' => [
'view' => 'docs/visit-offboard',
'title' => 'Visit offboard',
'breadcrumb' => 'Features',
'last_updated' => 'May 2026',
'author' => 'Core Team',
'read_time' => '6 min read',
'toc' => [
['label' => 'Overview', 'href' => '#overview'],
['label' => 'When it is queued', 'href' => '#when-it-is-queued'],
['label' => 'visitOffBoard()', 'href' => '#visit-off-board-api'],
['label' => 'updateVisitoffboardStatus()', 'href' => '#update-status'],
['label' => 'Manual test route', 'href' => '#manual-test-route'],
['label' => 'Developer steps', 'href' => '#developer-steps'],
['label' => 'Common pitfalls', 'href' => '#common-pitfalls'],
],
'prev' => ['label' => 'Visit onboard', 'url' => 'docs/visit-onboard'],
'next' => ['label' => 'Deployment', 'url' => 'docs/deployment'],
],
'deployment' => [
'view' => 'docs/deployment',
'title' => 'Deployment',
'breadcrumb' => 'DevOps',
'last_updated' => 'May 2026',
'author' => 'Core Team',
'read_time' => '8 min read',
'toc' => [
['label' => 'Overview', 'href' => '#overview'],
['label' => 'Dev cPanel flow', 'href' => '#dev-cpanel-flow', 'tag' => 'DEV'],
['label' => 'cPanel deploy scripts', 'href' => '#cpanel-deploy-scripts', 'tag' => 'DEV'],
['label' => 'Branch promotion flow', 'href' => '#branch-promotion-flow', 'tag' => 'UAT/LIVE'],
['label' => 'Auto merge script', 'href' => '#auto-merge-script', 'tag' => 'UAT/LIVE'],
['label' => 'Script usage', 'href' => '#script-usage', 'tag' => 'UAT/LIVE'],
['label' => 'Log output', 'href' => '#log-output', 'tag' => 'UAT/LIVE'],
['label' => 'Operational notes', 'href' => '#operational-notes', 'tag' => 'UAT/LIVE'],
['label' => 'Code move scripts', 'href' => '#code-move-scripts', 'tag' => 'UAT/LIVE'],
['label' => 'Manual fallback', 'href' => '#manual-fallback', 'tag' => 'DEV'],
],
'prev' => ['label' => 'Visit offboard', 'url' => 'docs/visit-offboard'],
'next' => ['label' => 'CI/CD Pipeline', 'url' => 'docs/cicd'],
],
'cicd' => [
'view' => 'docs/cicd',
'title' => 'CI/CD Pipeline',
'breadcrumb' => 'DevOps',
'last_updated' => 'May 2026',
'author' => 'Core Team',
'read_time' => '4 min read',
'toc' => [
['label' => 'Overview', 'href' => '#overview'],
['label' => 'Dev pipeline', 'href' => '#dev-pipeline', 'tag' => 'DEV'],
['label' => 'UAT and live flow', 'href' => '#uat-live-flow', 'tag' => 'UAT/LIVE'],
['label' => 'Related docs', 'href' => '#related-docs'],
],
'prev' => ['label' => 'Deployment', 'url' => 'docs/deployment'],
'next' => ['label' => 'S3 & CloudFront', 'url' => 'docs/s3-cloudfront'],
],
's3-cloudfront' => [
'view' => 'docs/s3-cloudfront',
'title' => 'S3 & CloudFront',
'breadcrumb' => 'DevOps',
'last_updated' => 'May 2026',
'author' => 'Core Team',
'read_time' => '7 min read',
'toc' => [
['label' => 'Overview', 'href' => '#overview'],
['label' => 'Prerequisites', 'href' => '#prerequisites'],
['label' => 'Selection flow', 'href' => '#selection-flow'],
['label' => 'Deployment steps', 'href' => '#deployment-steps'],
['label' => 'Invalidation polling', 'href' => '#invalidation-polling'],
['label' => 'Operational notes', 'href' => '#operational-notes'],
['label' => 'Full script', 'href' => '#full-script'],
],
'prev' => ['label' => 'CI/CD Pipeline', 'url' => 'docs/cicd'],
'next' => ['label' => 'Changelog', 'url' => 'docs/changelog'],
],
'api-rate-limiter' => [
'view' => 'docs/api-rate-limiter',
'title' => 'API Rate Limiter',
'breadcrumb' => 'Features',
'last_updated' => 'May 2026',
'author' => 'Core Team',
'read_time' => '10 min read',
'toc' => [
['label' => 'Overview', 'href' => '#overview'],
['label' => 'Core service', 'href' => '#core-service'],
['label' => 'Auth API filter', 'href' => '#auth-api-filter'],
['label' => 'JWT API filter', 'href' => '#jwt-api-filter'],
['label' => 'Configuration', 'href' => '#configuration'],
['label' => 'Change block counts and durations', 'href' => '#change-block-count-duration'],
['label' => 'Manual unblock samples', 'href' => '#manual-unblock-samples'],
['label' => 'Cache TTL and auto-release', 'href' => '#cache-ttl-auto-release'],
['label' => 'DB reconciliation (cron)', 'href' => '#db-reconciliation-cron'],
['label' => 'HTTP responses', 'href' => '#http-responses'],
['label' => 'Wiring routes', 'href' => '#wiring-routes'],
['label' => 'Blocked list URL', 'href' => '#blocked-list-url'],
['label' => 'Smoke test command', 'href' => '#smoke-test-command'],
['label' => 'Operational notes', 'href' => '#operational-notes'],
],
'prev' => ['label' => 'Email / Notifications', 'url' => 'docs/notifications'],
'next' => ['label' => 'TPA Recon', 'url' => 'docs/tpa-recon'],
],
'tpa-recon' => [
'view' => 'docs/tpa-recon',
'title' => 'TPA Recon',
'breadcrumb' => 'Features',
'last_updated' => 'May 2026',
'author' => 'Core Team',
'read_time' => '18 min read',
'toc' => [
['label' => 'What this is', 'href' => '#what-this-is'],
['label' => 'Glossary', 'href' => '#glossary'],
['label' => 'Key files and routes', 'href' => '#key-files-routes'],
['label' => 'End-to-end flow', 'href' => '#end-to-end-flow'],
['label' => 'Variation report', 'href' => '#variation-report'],
['label' => 'Classifying rec_type', 'href' => '#classifying-rec-type'],
['label' => 'Linking ref column', 'href' => '#linking-ref-column'],
['label' => 'Proceed next step', 'href' => '#proceed-next-step'],
['label' => 'Not in Nhance → inception', 'href' => '#not-in-nhance-inception'],
['label' => 'Need to review → DB sync', 'href' => '#need-to-review-sync'],
['label' => 'Deletion initialization', 'href' => '#deletion-initialization'],
['label' => 'Background jobs chain', 'href' => '#background-jobs-chain'],
['label' => 'New developer checklist', 'href' => '#new-developer-checklist'],
],
'prev' => ['label' => 'API Rate Limiter', 'url' => 'docs/api-rate-limiter'],
'next' => ['label' => 'EB rack rate config', 'url' => 'docs/eb-rack-rate-config'],
],
'eb-rack-rate-config' => [
'view' => 'docs/eb-rack-rate-config',
'title' => 'EB rack rate config',
'breadcrumb' => 'Features',
'last_updated' => 'May 2026',
'author' => 'Core Team',
'read_time' => '20 min read',
'toc' => [
['label' => 'Purpose', 'href' => '#purpose'],
['label' => 'Premium calculation (all)', 'href' => '#premium-calculation-modes'],
['label' => 'Applicable family members', 'href' => '#applicable-family-members'],
['label' => 'Member radios (Yes / No / …)', 'href' => '#applicable-members-radio-meanings'],
['label' => 'Rack matching vs family', 'href' => '#applicable-members-matching'],
['label' => 'Rack vs family flowchart', 'href' => '#rack-rate-family-matching-flowchart'],
['label' => 'How to configure a rack rate', 'href' => '#how-to-configure-rack-rate'],
['label' => 'Where it appears in the UI', 'href' => '#ui-entry'],
['label' => 'Load grid data (backend)', 'href' => '#getpolicy-grid-data'],
['label' => 'Save rack rate (backend)', 'href' => '#create-premium'],
['label' => 'Frontend flow', 'href' => '#frontend-flow'],
['label' => 'Grid IDs and storage', 'href' => '#grid-ids-storage'],
['label' => 'Grid types (113)', 'href' => '#grid-types-1-13'],
['label' => 'Excel paste path', 'href' => '#excel-paste'],
['label' => 'Related routes', 'href' => '#related-routes'],
],
'prev' => ['label' => 'TPA Recon', 'url' => 'docs/tpa-recon'],
'next' => ['label' => 'EB rack rate calculation', 'url' => 'docs/eb-rack-rate-calculation'],
],
'eb-rack-rate-calculation' => [
'view' => 'docs/eb-rack-rate-calculation',
'title' => 'EB rack rate calculation',
'breadcrumb' => 'Features',
'last_updated' => 'May 2026',
'author' => 'Core Team',
'read_time' => '18 min read',
'toc' => [
['label' => 'Scope and entry point', 'href' => '#scope'],
['label' => 'Data loaded before premium', 'href' => '#inputs'],
['label' => 'Excel row shape (numeric columns)', 'href' => '#excel-columns'],
['label' => 'Flow: employeesOnboardPreprocess', 'href' => '#flow-preprocess'],
['label' => 'Flow: calculate_premium_new', 'href' => '#flow-calculate-premium'],
['label' => 'group_slab_rates_basedon_name', 'href' => '#flow-group-slabs'],
['label' => 'get_familiy_composition', 'href' => '#flow-family-composition'],
['label' => 'compare_incoming…_slab', 'href' => '#flow-compare-slab'],
['label' => 'get_applicable_familiy_members', 'href' => '#flow-applicable-members'],
['label' => 'Per-member transform and gate', 'href' => '#flow-per-member'],
['label' => 'premium_calculation_manager', 'href' => '#flow-premium-manager'],
['label' => 'Dependent addition extras', 'href' => '#dependent-addition'],
['label' => 'Failure: zero successful families', 'href' => '#failure-modes'],
['label' => 'Related', 'href' => '#related'],
],
'prev' => ['label' => 'EB rack rate config', 'url' => 'docs/eb-rack-rate-config'],
'next' => ['label' => 'Endpoints', 'url' => 'docs/endpoints'],
],
// ── Add more pages here following the same pattern ──────────────────
];
// =========================================================================
// PUBLIC ROUTES
// =========================================================================
/**
* GET /docs → redirect to first page (introduction)
*/
public function index(): \CodeIgniter\HTTP\RedirectResponse
{
return redirect()->to(base_url('docs/introduction'));
}
/**
* GET /docs/(:segment)
*
* Looks up the slug in $pages, assembles layout partials, and returns HTML.
* The individual view only contains its own content — no layout boilerplate.
*/
public function page(string $slug): string
{
// 1. Resolve page config
$config = $this->getPageConfig($slug);
// 2. Render the inner content view (must exist)
$content = $this->renderContentView($config['view'], $config);
// 3. Assemble full layout and return
return $this->renderDocPage($config, $content);
}
// =========================================================================
// CORE LAYOUT ASSEMBLER
// =========================================================================
/**
* Wraps any content string with the full docs layout (header, sidebar,
* main open/close, footer) and returns the complete HTML page.
*
* @param array $config Page config entry from $this->pages
* @param string $content Rendered HTML from the content-only view
*/
protected function renderDocPage(array $config, string $content): string
{
$sharedLayout = [
'app_name' => $this->appName,
'app_version' => $this->appVersion,
'nav' => $this->nav, // sidebar needs the full nav
];
$header = view('docs/partials/docs_header', array_merge($sharedLayout, [
'doc_title' => $config['title'],
]));
$sidebar = view('docs/partials/docs_sidebar', array_merge($sharedLayout, [
'active_page' => $config['id'],
]));
$mainOpen = view('docs/partials/docs_main_open', [
'doc_title' => $config['title'],
'breadcrumb' => $config['breadcrumb'] ?? '',
'last_updated' => $config['last_updated'] ?? '',
'author' => $config['author'] ?? '',
'read_time' => $config['read_time'] ?? '',
'toc' => $config['toc'] ?? [],
]);
$mainClose = view('docs/partials/docs_main_close', [
'toc' => $config['toc'] ?? [],
'prev_label' => $config['prev']['label'] ?? '',
'prev_url' => $config['prev']['url'] ?? '',
'next_label' => $config['next']['label'] ?? '',
'next_url' => $config['next']['url'] ?? '',
]);
$footer = view('docs/partials/docs_footer', [
'app_name' => $this->appName,
]);
return $header . $sidebar . $mainOpen . $content . $mainClose . $footer;
}
// =========================================================================
// HELPERS
// =========================================================================
/**
* Resolve a URL slug to its page config array.
* Injects 'id' (the slug) so partials can use it without repeating it.
*
* @throws PageNotFoundException
*/
protected function getPageConfig(string $slug): array
{
if (! array_key_exists($slug, $this->pages)) {
throw new PageNotFoundException("Docs page not found: {$slug}");
}
return array_merge($this->pages[$slug], ['id' => $slug]);
}
/**
* Render the content-only view file.
* Passes the full config as view data so the content view
* can access $title, $toc, etc. if needed.
*
* @throws PageNotFoundException if the view file does not exist on disk
*/
protected function renderContentView(string $viewPath, array $data = []): string
{
$fullPath = APPPATH . 'Views/' . $viewPath . '.php';
if (! file_exists($fullPath)) {
throw new PageNotFoundException("Docs view file not found: {$viewPath}.php");
}
// Remove 'view' key so it doesn't shadow anything inside the view
unset($data['view']);
return view($viewPath, $data);
}
}