debug = $debug; $this->registerDefaultHandlers(); } /** * register registers a custom error handler for a specific status code */ public function register(int $status, callable $handler): void { $this->handlers[$status] = $handler; } /** * setDefaultHandler sets the default error handler for unregistered status codes */ public function setDefaultHandler(callable $handler): void { $this->defaultHandler = $handler; } /** * handle handles an error with the appropriate handler */ public function handle(Context $context, int $status, string $message = '', ?\Exception $exception = null): void { if (isset($this->handlers[$status])) { $handler = $this->handlers[$status]; $handler($context, $status, $message, $exception); return; } if ($this->defaultHandler) { ($this->defaultHandler)($context, $status, $message, $exception); return; } $this->renderDefaultError($context, $status, $message, $exception); } /** * handleException handles uncaught exceptions */ public function handleException(Context $context, \Exception $exception): void { $status = $this->getStatusFromException($exception); $message = $this->debug ? $exception->getMessage() : $this->getDefaultMessage($status); if ($this->debug) { error_log($exception->getMessage() . "\n" . $exception->getTraceAsString()); } $this->handle($context, $status, $message, $exception); } /** * getStatusFromException determines HTTP status from exception type */ private function getStatusFromException(\Exception $exception): int { if ($exception instanceof HttpException) { return $exception->getStatusCode(); } return match(get_class($exception)) { 'InvalidArgumentException' => 400, 'UnauthorizedException' => 401, 'ForbiddenException' => 403, 'NotFoundException' => 404, 'MethodNotAllowedException' => 405, 'ValidationException' => 422, default => 500 }; } /** * registerDefaultHandlers registers built-in error handlers */ private function registerDefaultHandlers(): void { // 404 Not Found $this->register(404, function(Context $context, int $status, string $message) { $accept = $context->request->header('accept') ?? ''; if (str_contains($accept, 'application/json')) { $context->json(['error' => $message ?: 'Not Found'], 404); } else { $html = $this->render404Page($message); $context->html($html, 404); } }); // 500 Internal Server Error $this->register(500, function(Context $context, int $status, string $message, ?\Exception $exception) { $accept = $context->request->header('accept') ?? ''; if (str_contains($accept, 'application/json')) { $response = ['error' => $message ?: 'Internal Server Error']; if ($this->debug && $exception) { $response['trace'] = $exception->getTrace(); } $context->json($response, 500); } else { $html = $this->render500Page($message, $exception); $context->html($html, 500); } }); } /** * renderDefaultError renders a generic error response */ private function renderDefaultError(Context $context, int $status, string $message, ?\Exception $exception): void { $message = $message ?: $this->getDefaultMessage($status); $accept = $context->request->header('accept') ?? ''; if (str_contains($accept, 'application/json')) { $response = ['error' => $message]; if ($this->debug && $exception) { $response['trace'] = $exception->getTrace(); } $context->json($response, $status); } else { $html = $this->renderErrorPage($status, $message, $exception); $context->html($html, $status); } } /** * getDefaultMessage gets default message for status code */ private function getDefaultMessage(int $status): string { return match($status) { 400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 422 => 'Unprocessable Entity', 429 => 'Too Many Requests', 500 => 'Internal Server Error', 502 => 'Bad Gateway', 503 => 'Service Unavailable', default => 'Error' }; } /** * render404Page renders a 404 error page */ private function render404Page(string $message): string { $message = htmlspecialchars($message ?: 'Not Found'); return <<
Exception: {$exceptionClass}
File: {$file}:{$line}
{$trace}
HTML;
}
return <<
Something went wrong on our end. Please try again later.
{$debugInfo}Exception: {$exceptionClass}
File: {$file}:{$line}
{$trace}
HTML;
}
return <<