validateCsrf()) { http_response_code(403); if ($this->isJsonRequest()) { $this->json(['error' => 'CSRF token invalid'], 403); } else { echo 'CSRF token invalid'; } exit; } } private function isJsonRequest(): bool { $accept = $_SERVER['HTTP_ACCEPT'] ?? ''; $contentType = $_SERVER['CONTENT_TYPE'] ?? ''; return str_contains($accept, 'application/json') || str_contains($contentType, 'application/json'); } protected function view(string $name, array $data = []): void { extract($data); $file = VIEW_PATH . '/' . str_replace('.', '/', $name) . '.php'; if (file_exists($file)) { require $file; } else { throw new \Exception("View not found: {$name}"); } } protected function json(mixed $data, int $status = 200): void { http_response_code($status); header('Content-Type: application/json'); echo json_encode($data, JSON_UNESCAPED_UNICODE); } protected function redirect(string $url): void { header("Location: {$url}"); exit; } /** * @return array */ protected function getJsonInput(): array { $input = file_get_contents('php://input'); if ($input === false || $input === '') { return []; } $decoded = json_decode($input, true); if (!is_array($decoded)) { return []; } return $decoded; } protected function jsonError(string $message, int $status = 500): void { $this->json(['success' => false, 'error' => $message], $status); } }