0) { ob_end_clean(); } } // Set HTTP 500 if no error status has been set yet. $currentStatus = http_response_code(); if ($currentStatus === false || $currentStatus < 400) { http_response_code(500); } // Collect diagnostic data. $data = ErrorDataCollector::collect($exception); // Log to file (always, in both dev and prod). ErrorLogger::log($exception, $data); // Bail out if headers were already sent (cannot render a page). if (headers_sent()) { self::$handling = false; return; } // API requests get a JSON response, not HTML. if (self::isApiRequest()) { self::sendApiError($data['request']['request_id'] ?? null); self::$handling = false; return; } // Render HTML error page. if (self::shouldShowDebugPage()) { echo ErrorRenderer::renderDev($data); } else { $requestId = (string) ($data['request']['request_id'] ?? 'unknown'); echo ErrorRenderer::renderProd($requestId); } } catch (\Throwable $inner) { self::renderMinimalFallback($exception); } self::$handling = false; } /** * Converts PHP errors into ErrorException. * Respects the error_reporting() bitmask (honors @ suppression operator). */ public static function handleError(int $severity, string $message, string $file, int $line): true { if (!(error_reporting() & $severity)) { // Error was suppressed with @. return true; } throw new \ErrorException($message, 0, $severity, $file, $line); } /** * Catches fatal errors that bypass set_error_handler (E_PARSE, E_COMPILE_ERROR, etc.). */ public static function handleShutdown(): void { self::$reservedMemory = null; $error = error_get_last(); if (!is_array($error)) { return; } $fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR]; if (!in_array($error['type'], $fatalTypes, true)) { return; } // Only act if we haven't already handled this (e.g. via set_exception_handler). if (self::$handling) { return; } $exception = new \ErrorException( $error['message'], 0, $error['type'], $error['file'], $error['line'], ); self::handleException($exception); } private static function isApiRequest(): bool { if (defined('MINTY_API_REQUEST')) { return true; } $uri = $_SERVER['REQUEST_URI'] ?? ''; $path = ltrim(parse_url($uri, PHP_URL_PATH) ?? '', '/'); return str_starts_with($path, 'api/'); } private static function shouldShowDebugPage(): bool { // Check the Debugger flag, which mirrors APP_DEBUG. if (class_exists(Debugger::class, false) && Debugger::$enabled) { return true; } return false; } private static function sendApiError(?string $requestId): void { header('Content-Type: application/json; charset=utf-8'); $payload = [ 'ok' => false, 'error_code' => 'internal_error', ]; if ($requestId !== null) { $payload['request_id'] = $requestId; } echo json_encode($payload, JSON_UNESCAPED_SLASHES); } private static function renderMinimalFallback(\Throwable $exception): void { // Last-resort plain-text response when everything else is broken. $requestId = 'unknown'; try { $requestId = RequestContext::currentId() ?? RequestContext::id(); } catch (\Throwable) { } if (!headers_sent()) { http_response_code(500); header('Content-Type: text/plain; charset=utf-8'); } echo "500 Internal Server Error\nRequest ID: {$requestId}\n"; // In debug mode, also show the exception for developers. if (class_exists(Debugger::class, false) && Debugger::$enabled) { echo "\n" . $exception->getMessage() . "\n" . $exception->getTraceAsString() . "\n"; } } private static function isPhpUnitRuntime(): bool { return defined('PHPUNIT_COMPOSER_INSTALL') || defined('__PHPUNIT_PHAR__'); } }