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 << 404 - Not Found

404

{$message}

The page you are looking for could not be found.

Go to Homepage
HTML; } /** * render500Page renders a 500 error page */ private function render500Page(string $message, ?\Exception $exception): string { $message = htmlspecialchars($message ?: 'Internal Server Error'); $debugInfo = ''; if ($this->debug && $exception) { $exceptionClass = get_class($exception); $file = htmlspecialchars($exception->getFile()); $line = $exception->getLine(); $trace = htmlspecialchars($exception->getTraceAsString()); $debugInfo = <<

Debug Information

Exception: {$exceptionClass}

File: {$file}:{$line}

{$trace}
HTML; } return << 500 - Internal Server Error

500

{$message}

Something went wrong on our end. Please try again later.

{$debugInfo}
HTML; } /** * renderErrorPage renders a generic error page */ private function renderErrorPage(int $status, string $message, ?\Exception $exception): string { $message = htmlspecialchars($message); $debugInfo = ''; if ($this->debug && $exception) { $exceptionClass = get_class($exception); $file = htmlspecialchars($exception->getFile()); $line = $exception->getLine(); $trace = htmlspecialchars($exception->getTraceAsString()); $debugInfo = <<

Debug Information

Exception: {$exceptionClass}

File: {$file}:{$line}

{$trace}
HTML; } return << {$status} - {$message}

{$status}

{$message}

{$debugInfo}
HTML; } } /** * HttpException base class for HTTP exceptions */ class HttpException extends \Exception { protected int $statusCode; public function __construct(int $statusCode, string $message = '', int $code = 0, \Exception $previous = null) { $this->statusCode = $statusCode; parent::__construct($message, $code, $previous); } public function getStatusCode(): int { return $this->statusCode; } } /** * ValidationException for validation errors */ class ValidationException extends HttpException { private array $errors; public function __construct(array $errors, string $message = 'Validation failed') { $this->errors = $errors; parent::__construct(422, $message); } public function getErrors(): array { return $this->errors; } }