→ TfeController → tfe view * /apropos → AboutController → about view * /licence → LicenceController → licence view * /media.php → MediaController (direct output) * /live-reload → LiveReloadController (direct output) * /partage/ → share-link flow * /maintenance.php → static maintenance page */ class Dispatcher { private const ROUTES = [ '' => ['controller' => 'HomeController', 'action' => 'handle', 'view' => 'public/home'], '/' => ['controller' => 'HomeController', 'action' => 'handle', 'view' => 'public/home'], '/index.php' => ['controller' => 'HomeController', 'action' => 'handle', 'view' => 'public/home'], '/search' => ['controller' => 'SearchController', 'action' => 'handleSearch', 'view' => 'public/search'], '/search.php' => ['controller' => 'SearchController', 'action' => 'handleSearch', 'view' => 'public/search'], '/repertoire' => ['controller' => 'SearchController', 'action' => 'handleRepertoire', 'view' => 'public/repertoire'], '/repertoire.php' => ['controller' => 'SearchController', 'action' => 'handleRepertoire', 'view' => 'public/repertoire'], '/tfe' => ['controller' => 'TfeController', 'action' => 'handle', 'view' => 'public/tfe'], '/tfe.php' => ['controller' => 'TfeController', 'action' => 'handle', 'view' => 'public/tfe'], '/apropos' => ['controller' => 'AboutController', 'action' => 'handle', 'view' => 'public/about'], '/apropos.php' => ['controller' => 'AboutController', 'action' => 'handle', 'view' => 'public/about'], '/licence' => ['controller' => 'LicenceController', 'action' => 'handle', 'view' => 'public/licence'], '/licence.php' => ['controller' => 'LicenceController', 'action' => 'handle', 'view' => 'public/licence'], ]; private string $path; private array $queryParams; public function __construct() { $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $this->path = $uri; $this->queryParams = $_GET; } /** * Resolve the URI to a route, instantiate the controller, * execute the action, and render the view. */ public function dispatch(): void { // 1. Direct-response endpoints (render their own output) $direct = $this->matchDirect(); if ($direct) { $direct(); return; } // 2. Routed pages (controller + view) $route = $this->matchRoute(); if (!$route) { http_response_code(404); echo '

404 — Page non trouvée

'; return; } // 3. Load controller $ctrlClass = $route['controller']; require_once APP_ROOT . '/src/Controllers/' . $ctrlClass . '.php'; $controller = $ctrlClass::create(); $vars = $controller->{$route['action']}(); // 4. Render view $this->render($route['view'], $vars); } /** * Match endpoints that render their own response (no view layer). */ private function matchDirect(): ?callable { $path = $this->path; // /live-reload if ($path === '/live-reload' || $path === '/live-reload.php') { return function() { require_once APP_ROOT . '/src/Controllers/LiveReloadController.php'; $controller = new LiveReloadController(APP_ROOT); $result = $controller->handle(); header('Content-Type: application/json'); echo json_encode($result['body']); }; } // /media.php if ($path === '/media' || $path === '/media.php') { return function() { require_once APP_ROOT . '/src/Controllers/MediaController.php'; $controller = new MediaController(); $controller->handle(); }; } // /maintenance.php if ($path === '/maintenance' || $path === '/maintenance.php') { return function() { require APP_ROOT . '/public/maintenance.php'; }; } // /repertoire/student-preview (HTMX popover) if ($path === '/repertoire/student-preview') { return function() { require_once APP_ROOT . '/src/Database.php'; require_once APP_ROOT . '/src/RateLimit.php'; require_once APP_ROOT . '/src/Controllers/SearchController.php'; $controller = SearchController::create(); $controller->handleStudentPreview(); }; } // /partage/* if (preg_match('#^/partage(/.*)?$#', $path)) { return function() { require APP_ROOT . '/public/partage/index.php'; }; } return null; } /** * Match the current path against the static route table. * Supports exact match and prefix-based (for /tfe?id=). */ private function matchRoute(): ?array { $path = $this->path; // Exact match first if (isset(self::ROUTES[$path])) { return self::ROUTES[$path]; } // /tfe?id= pattern (TfeController handles the id param internally) if (preg_match('#^/tfe$#', $path) && isset($_GET['id'])) { return self::ROUTES['/tfe']; } return null; } /** * Render a view template wrapped in the full page layout. * Includes head.php, header.php, the view, and footer.php. */ private function render(string $view, array $vars): void { $viewPath = APP_ROOT . '/templates/' . $view . '.php'; if (!file_exists($viewPath)) { http_response_code(500); echo "View not found: {$viewPath}"; return; } extract($vars); include APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; include $viewPath; include APP_ROOT . '/templates/footer.php'; } }