Added strict typing with parameter types, return types, and property types across all core classes: - helpers: Database, ErrorHandler, CacheHelper - models: TicketModel, UserModel, WorkflowModel, TemplateModel, UserPreferencesModel - middleware: RateLimitMiddleware, CsrfMiddleware, SecurityHeadersMiddleware Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
264 lines
7.6 KiB
PHP
264 lines
7.6 KiB
PHP
<?php
|
|
/**
|
|
* Centralized Error Handler
|
|
*
|
|
* Provides consistent error handling, logging, and response formatting
|
|
* across the application.
|
|
*/
|
|
class ErrorHandler {
|
|
private static ?string $logFile = null;
|
|
private static bool $initialized = false;
|
|
|
|
/**
|
|
* Initialize error handling
|
|
*
|
|
* @param bool $displayErrors Whether to display errors (false in production)
|
|
*/
|
|
public static function init(bool $displayErrors = false): void {
|
|
if (self::$initialized) {
|
|
return;
|
|
}
|
|
|
|
// Set error reporting
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', $displayErrors ? '1' : '0');
|
|
ini_set('log_errors', '1');
|
|
|
|
// Set up log file
|
|
self::$logFile = sys_get_temp_dir() . '/tinker_tickets_errors.log';
|
|
ini_set('error_log', self::$logFile);
|
|
|
|
// Register handlers
|
|
set_error_handler([self::class, 'handleError']);
|
|
set_exception_handler([self::class, 'handleException']);
|
|
register_shutdown_function([self::class, 'handleShutdown']);
|
|
|
|
self::$initialized = true;
|
|
}
|
|
|
|
/**
|
|
* Handle PHP errors
|
|
*
|
|
* @param int $errno Error level
|
|
* @param string $errstr Error message
|
|
* @param string $errfile File where error occurred
|
|
* @param int $errline Line number
|
|
* @return bool
|
|
*/
|
|
public static function handleError(int $errno, string $errstr, string $errfile, int $errline): bool {
|
|
// Don't handle suppressed errors
|
|
if (!(error_reporting() & $errno)) {
|
|
return false;
|
|
}
|
|
|
|
$errorType = self::getErrorTypeName($errno);
|
|
$message = "$errorType: $errstr in $errfile on line $errline";
|
|
|
|
self::log($message, $errno);
|
|
|
|
// For fatal errors, throw exception
|
|
if (in_array($errno, [E_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR])) {
|
|
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Handle uncaught exceptions
|
|
*
|
|
* @param Throwable $exception
|
|
*/
|
|
public static function handleException(Throwable $exception): void {
|
|
$message = sprintf(
|
|
"Uncaught %s: %s in %s on line %d\nStack trace:\n%s",
|
|
get_class($exception),
|
|
$exception->getMessage(),
|
|
$exception->getFile(),
|
|
$exception->getLine(),
|
|
$exception->getTraceAsString()
|
|
);
|
|
|
|
self::log($message, E_ERROR);
|
|
|
|
// Send error response if headers not sent
|
|
if (!headers_sent()) {
|
|
self::sendErrorResponse(
|
|
'An unexpected error occurred',
|
|
500,
|
|
$exception
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle fatal errors on shutdown
|
|
*/
|
|
public static function handleShutdown(): void {
|
|
$error = error_get_last();
|
|
|
|
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
|
|
$message = sprintf(
|
|
"Fatal Error: %s in %s on line %d",
|
|
$error['message'],
|
|
$error['file'],
|
|
$error['line']
|
|
);
|
|
|
|
self::log($message, E_ERROR);
|
|
|
|
if (!headers_sent()) {
|
|
self::sendErrorResponse('A fatal error occurred', 500);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log an error message
|
|
*
|
|
* @param string $message Error message
|
|
* @param int $level Error level
|
|
* @param array $context Additional context
|
|
*/
|
|
public static function log(string $message, int $level = E_USER_NOTICE, array $context = []): void {
|
|
$timestamp = date('Y-m-d H:i:s');
|
|
$levelName = self::getErrorTypeName($level);
|
|
|
|
$logMessage = "[$timestamp] [$levelName] $message";
|
|
|
|
if (!empty($context)) {
|
|
$logMessage .= " | Context: " . json_encode($context);
|
|
}
|
|
|
|
error_log($logMessage);
|
|
}
|
|
|
|
/**
|
|
* Send a JSON error response
|
|
*
|
|
* @param string $message User-facing error message
|
|
* @param int $httpCode HTTP status code
|
|
* @param Throwable|null $exception Original exception (for debug info)
|
|
*/
|
|
public static function sendErrorResponse(string $message, int $httpCode = 500, ?Throwable $exception = null): void {
|
|
http_response_code($httpCode);
|
|
|
|
if (!headers_sent()) {
|
|
header('Content-Type: application/json');
|
|
}
|
|
|
|
$response = [
|
|
'success' => false,
|
|
'error' => $message
|
|
];
|
|
|
|
// Add debug info in development (check for debug mode)
|
|
if (isset($GLOBALS['config']['DEBUG']) && $GLOBALS['config']['DEBUG'] && $exception) {
|
|
$response['debug'] = [
|
|
'type' => get_class($exception),
|
|
'message' => $exception->getMessage(),
|
|
'file' => $exception->getFile(),
|
|
'line' => $exception->getLine()
|
|
];
|
|
}
|
|
|
|
echo json_encode($response);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Send a validation error response
|
|
*
|
|
* @param array $errors Array of validation errors
|
|
* @param string $message Overall error message
|
|
*/
|
|
public static function sendValidationError(array $errors, string $message = 'Validation failed'): void {
|
|
http_response_code(422);
|
|
|
|
if (!headers_sent()) {
|
|
header('Content-Type: application/json');
|
|
}
|
|
|
|
echo json_encode([
|
|
'success' => false,
|
|
'error' => $message,
|
|
'validation_errors' => $errors
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Send a not found error response
|
|
*
|
|
* @param string $message Error message
|
|
*/
|
|
public static function sendNotFoundError(string $message = 'Resource not found'): void {
|
|
self::sendErrorResponse($message, 404);
|
|
}
|
|
|
|
/**
|
|
* Send an unauthorized error response
|
|
*
|
|
* @param string $message Error message
|
|
*/
|
|
public static function sendUnauthorizedError(string $message = 'Authentication required'): void {
|
|
self::sendErrorResponse($message, 401);
|
|
}
|
|
|
|
/**
|
|
* Send a forbidden error response
|
|
*
|
|
* @param string $message Error message
|
|
*/
|
|
public static function sendForbiddenError(string $message = 'Access denied'): void {
|
|
self::sendErrorResponse($message, 403);
|
|
}
|
|
|
|
/**
|
|
* Get error type name from error number
|
|
*
|
|
* @param int $errno Error number
|
|
* @return string Error type name
|
|
*/
|
|
private static function getErrorTypeName(int $errno): string {
|
|
$types = [
|
|
E_ERROR => 'ERROR',
|
|
E_WARNING => 'WARNING',
|
|
E_PARSE => 'PARSE',
|
|
E_NOTICE => 'NOTICE',
|
|
E_CORE_ERROR => 'CORE_ERROR',
|
|
E_CORE_WARNING => 'CORE_WARNING',
|
|
E_COMPILE_ERROR => 'COMPILE_ERROR',
|
|
E_COMPILE_WARNING => 'COMPILE_WARNING',
|
|
E_USER_ERROR => 'USER_ERROR',
|
|
E_USER_WARNING => 'USER_WARNING',
|
|
E_USER_NOTICE => 'USER_NOTICE',
|
|
E_STRICT => 'STRICT',
|
|
E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR',
|
|
E_DEPRECATED => 'DEPRECATED',
|
|
E_USER_DEPRECATED => 'USER_DEPRECATED',
|
|
];
|
|
|
|
return $types[$errno] ?? 'UNKNOWN';
|
|
}
|
|
|
|
/**
|
|
* Get recent error log entries
|
|
*
|
|
* @param int $lines Number of lines to return
|
|
* @return array Log entries
|
|
*/
|
|
public static function getRecentErrors(int $lines = 50): array {
|
|
if (self::$logFile === null || !file_exists(self::$logFile)) {
|
|
return [];
|
|
}
|
|
|
|
$file = file(self::$logFile);
|
|
if ($file === false) {
|
|
return [];
|
|
}
|
|
|
|
return array_slice($file, -$lines);
|
|
}
|
|
}
|