db = Database::getInstance(); } protected function view(string $view, array $data = [], ?string $layout = 'main'): void { extract($data); $viewFile = APP_ROOT . '/app/views/' . str_replace('.', '/', $view) . '.php'; if (!file_exists($viewFile)) { die("View not found: $viewFile"); } if ($layout) { $layoutFile = APP_ROOT . '/app/views/layouts/' . $layout . '.php'; if (file_exists($layoutFile)) { // Capture view content ob_start(); require $viewFile; $content = ob_get_clean(); require $layoutFile; return; } } require $viewFile; } protected function json(mixed $data, int $code = 200): void { http_response_code($code); header('Content-Type: application/json; charset=utf-8'); echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } protected function redirect(string $url): void { // Flush session ke disk sebelum redirect agar data tidak hilang if (session_status() === PHP_SESSION_ACTIVE) { session_write_close(); } header("Location: $url"); exit; } protected function back(): void { $referer = $_SERVER['HTTP_REFERER'] ?? APP_URL; $this->redirect($referer); } protected function csrf(): string { if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; } protected function verifyCsrf(): bool { // Pastikan session aktif sebelum membaca token Auth::init(); $token = $_POST['_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; return hash_equals($_SESSION['csrf_token'] ?? '', $token); } protected function input(string $key, mixed $default = null): mixed { return $_POST[$key] ?? $_GET[$key] ?? $default; } protected function sanitize(string $value): string { return htmlspecialchars(trim($value), ENT_QUOTES, 'UTF-8'); } protected function flash(string $type, string $message, array $extra = []): void { $_SESSION['flash'] = ['type' => $type, 'message' => $message] + $extra; } protected function getFlash(): ?array { if (isset($_SESSION['flash'])) { $flash = $_SESSION['flash']; unset($_SESSION['flash']); return $flash; } return null; } protected function uploadFile(array $file, string $dir = 'uploads'): ?string { if ($file['error'] !== UPLOAD_ERR_OK) return null; $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); $allow = ['jpg', 'jpeg', 'png', 'webp']; if (!in_array($ext, $allow)) return null; $maxSize = 5 * 1024 * 1024; // 5MB if ($file['size'] > $maxSize) return null; $filename = uniqid('img_', true) . '.' . $ext; $destDir = APP_ROOT . '/public/images/' . $dir . '/'; if (!is_dir($destDir)) mkdir($destDir, 0755, true); if (move_uploaded_file($file['tmp_name'], $destDir . $filename)) { return $filename; } return null; } protected function paginate(int $total, int $perPage = 20): array { $page = max(1, (int)($_GET['page'] ?? 1)); $offset = ($page - 1) * $perPage; $pages = (int)ceil($total / $perPage); return compact('page', 'offset', 'perPage', 'pages', 'total'); } }