Files
2026-09-16 23:20:08 +07:00

150 lines
5.8 KiB
PHP

<?php
namespace App\Exceptions;
use App\Helpers\Response;
use PDOException;
use Throwable;
/**
* Class Handler
* Enterprise Global Exception & Error Handler (OWASP Information Leakage Guard)
*
* @package App\Exceptions
*/
class Handler
{
/**
* Register Global Exception and Error Handlers
*/
public static function register(): void
{
set_exception_handler([self::class, 'handleException']);
set_error_handler([self::class, 'handleError']);
register_shutdown_function([self::class, 'handleShutdown']);
}
/**
* Handle uncaught exceptions
*/
public static function handleException(Throwable $e): void
{
$status = 500;
$message = "Internal Server Error";
$details = [];
if ($e instanceof PDOException) {
$status = 500;
$message = "Database Query Error";
// ในโหมด Debug จะแสดง Error จริง ถ้า Production ซ่อนรายละเอียดป้องกัน SQLi enumeration
$details = getenv('APP_DEBUG') === 'true' ? ['pdo_error' => $e->getMessage()] : ['info' => 'Please check database logs'];
error_log("PDOException: " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
} elseif (is_int($e->getCode()) && $e->getCode() >= 400 && $e->getCode() <= 599) {
$status = $e->getCode();
$message = $e->getMessage();
} else {
$message = $e->getMessage() ?: 'Unexpected Error Occurred';
error_log("Exception: " . $message . " in " . $e->getFile() . ":" . $e->getLine());
}
if (getenv('APP_DEBUG') === 'true') {
$details['file'] = $e->getFile();
$details['line'] = $e->getLine();
$details['trace'] = explode("\n", $e->getTraceAsString());
}
if (self::isApiRequest()) {
Response::error($message, $status, $details);
} else {
self::renderHtmlError($status, $message, $details, $e);
}
}
/**
* Handle PHP Warnings/Notices as Exceptions
*
* @param int $level
* @param string $message
* @param string $file
* @param int $line
* @return bool
*/
public static function handleError(int $level, string $message, string $file, int $line): bool
{
if (!(error_reporting() & $level)) {
return false;
}
error_log("PHP Error [{$level}]: {$message} in {$file}:{$line}");
return true;
}
/**
* Handle Fatal Errors on shutdown
*/
public static function handleShutdown(): void
{
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
error_log("Fatal Shutdown Error: {$error['message']} in {$error['file']}:{$error['line']}");
if (self::isApiRequest()) {
Response::error("Fatal Server Error: {$error['message']}", 500);
} else {
self::renderHtmlError(500, "Fatal PHP Error: " . $error['message'], ['file' => $error['file'], 'line' => $error['line']]);
}
}
}
private static function isApiRequest(): bool
{
$uri = (string)($_SERVER['REQUEST_URI'] ?? '');
$accept = (string)($_SERVER['HTTP_ACCEPT'] ?? '');
return (strpos($uri, '/api/') !== false) ||
(strpos($uri, 'api=') !== false) ||
(strpos($uri, 'endpoint=') !== false) ||
(strpos($accept, 'application/json') !== false) ||
isset($_GET['api']) || isset($_GET['endpoint']);
}
/**
* Render HTML Error Page
*
* @param int $status
* @param string $message
* @param array<string, mixed> $details
* @param Throwable|null $e
*/
private static function renderHtmlError(int $status, string $message, array $details = [], ?Throwable $e = null): void
{
http_response_code($status);
$debugHtml = '';
if ((getenv('APP_DEBUG') === 'true' || getenv('APP_ENV') === 'local') && ($details || $e)) {
$errText = $e ? htmlspecialchars($e->getMessage()) : htmlspecialchars(json_encode($details, JSON_UNESCAPED_UNICODE));
$errFile = $e ? htmlspecialchars($e->getFile() . ':' . $e->getLine()) : '';
$debugHtml = "<div class='mt-4 p-4 bg-slate-950/80 rounded-xl text-left border border-rose-500/30 overflow-auto max-h-60 text-xs font-mono text-rose-300'>
<p class='font-bold underline mb-1'>Debug Information (APP_DEBUG=true):</p>
<p class='mb-2'><strong>Message:</strong> {$errText}</p>
" . ($errFile ? "<p class='mb-2'><strong>Location:</strong> {$errFile}</p>" : "") . "
</div>";
}
echo "<!DOCTYPE html>
<html lang='th'>
<head>
<meta charset='UTF-8'>
<title>Error {$status} - TTMQMS</title>
<script src='https://cdn.tailwindcss.com'></script>
</head>
<body class='bg-slate-900 text-slate-100 flex items-center justify-center min-h-screen font-sans p-4'>
<div class='bg-slate-800 border border-slate-700 rounded-2xl p-8 max-w-xl w-full text-center shadow-2xl'>
<div class='text-6xl mb-4'>⚠️</div>
<h1 class='text-4xl font-bold text-rose-500 mb-2'>{$status}</h1>
<p class='text-lg text-slate-300 mb-4'>{$message}</p>
{$debugHtml}
<div class='mt-6'>
<a href='" . (defined('BASE_URL') ? BASE_URL : '') . "/' class='bg-emerald-600 hover:bg-emerald-500 text-white font-medium px-6 py-2.5 rounded-xl transition inline-block'>กลับสู่หน้าหลัก / รีเฟรช</a>
</div>
</div>
</body>
</html>";
exit();
}
}