viewsPath = dirname(__DIR__, 2) . '/views/'; $this->security = $security; } public function setLayout(string $layout): self { $this->layout = $layout; return $this; } public function assign(string $key, mixed $value): self { $this->data[$key] = $value; return $this; } public function assignMultiple(array $data): self { $this->data = array_merge($this->data, $data); return $this; } public function render(string $view, array $data = []): string { $data = array_merge($this->data, $data); $content = $this->renderView($view, $data); if ($this->layout !== null) { $data['content'] = $content; $content = $this->renderView('layouts/' . $this->layout, $data); } return $content; } public function renderView(string $view, array $data = []): string { $viewFile = $this->viewsPath . str_replace('.', '/', $view) . '.php'; if (!file_exists($viewFile)) { throw new \RuntimeException("View not found: {$view}"); } extract($data, EXTR_SKIP); ob_start(); include $viewFile; return ob_get_clean(); } public function renderPartial(string $partial, array $data = []): string { return $this->renderView('partials/' . $partial, $data); } public function display(string $view, array $data = []): void { echo $this->render($view, $data); } public function escape(string $value): string { return $this->security->purify($value); } public function csrfToken(): string { return $this->security->generateCSRFToken(); } public function csrfField(): string { $token = $this->csrfToken(); return ''; } public function redirect(string $url, int $statusCode = 302): never { http_response_code($statusCode); header("Location: {$url}"); exit; } public function json(mixed $data, int $statusCode = 200): never { http_response_code($statusCode); header('Content-Type: application/json; charset=utf-8'); echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); exit; } public function error(int $statusCode, string $message = ''): never { http_response_code($statusCode); if (str_starts_with($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json')) { $this->json(['error' => true, 'message' => $message ?: $this->getStatusMessage($statusCode)], $statusCode); } $this->display("errors.{$statusCode}", [ 'code' => $statusCode, 'message' => $message ?: $this->getStatusMessage($statusCode), ]); exit; } private function getStatusMessage(int $code): string { return match($code) { 400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 429 => 'Too Many Requests', 500 => 'Internal Server Error', 503 => 'Service Unavailable', default => 'Error', }; } }