$error], $extra); self::send($body, $status, $error); } public static function unauthorized(): never { self::error('unauthorized', 401); } public static function forbidden(string $detail = 'forbidden'): never { self::error($detail, 403); } public static function notFound(string $detail = 'not_found'): never { self::error($detail, 404); } public static function methodNotAllowed(): never { self::error('method_not_allowed', 405); } public static function validationError(array $errors): never { self::error('validation_error', 422, ['errors' => $errors]); } public static function tooManyRequests(int $retryAfter = 60): never { self::send( ['error' => 'rate_limit_exceeded'], 429, 'rate_limit_exceeded', ['Retry-After: ' . $retryAfter] ); } /** * Transform a service result (['ok' => bool, ...] pattern) into an API response. */ public static function fromServiceResult(array $result, int $successStatus = 200): never { if ($result['ok'] ?? false) { $data = $result; unset($data['ok']); self::success($data, $successStatus); } $status = (int) ($result['status'] ?? 0); if ($status < 400) { $status = 422; } $error = (string) ($result['error'] ?? 'operation_failed'); $errors = $result['errors'] ?? []; $extra = []; if ($errors) { $extra['errors'] = $errors; } self::error($error, $status, $extra); } /** * Read JSON request body into an array. */ public static function readJsonBody(): array { $raw = file_get_contents('php://input'); if ($raw === false || $raw === '') { return []; } $data = json_decode($raw, true); return is_array($data) ? $data : []; } /** * Require specific HTTP method(s). */ public static function requireMethod(string ...$methods): void { $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')); if (!in_array($method, $methods, true)) { self::methodNotAllowed(); } } /** * Require API authentication. */ public static function requireAuth(): void { if (!ApiAuth::isAuthenticated()) { self::unauthorized(); } } /** * Require a specific permission key. */ public static function requirePermission(string $key): void { self::requireAuth(); if (!ApiAuth::hasPermission($key)) { self::forbidden(); } } private static function send(?array $body, int $status, ?string $errorCode = null, array $headers = []): never { http_response_code($status); foreach ($headers as $headerLine) { header($headerLine); } if ($body === null) { ApiAuditService::finish($status, $errorCode); die(); } $json = json_encode($body, JSON_UNESCAPED_UNICODE); if (!is_string($json)) { $status = 500; http_response_code($status); $errorCode = $errorCode ?: 'serialization_error'; $json = json_encode(['error' => 'serialization_error'], JSON_UNESCAPED_UNICODE) ?: '{"error":"serialization_error"}'; } header('Content-Type: application/json; charset=utf-8'); ApiAuditService::finish($status, $errorCode); die($json); } }