<?php
/**
* ErrorHandler class for comprehensive error handling and logging
* Provides structured error responses, logging, and validation
*/
class ErrorHandler {
private $logFile;
private $logLevel;
private $enableLogging;
// Error codes for different types of errors
const ERROR_CODES = [
// Input validation errors (400-499)
'INVALID_INPUT' => 'INVALID_INPUT',
'MISSING_REQUIRED_FIELD' => 'MISSING_REQUIRED_FIELD',
'INVALID_JSON' => 'INVALID_JSON',
'INVALID_PARAMETER' => 'INVALID_PARAMETER',
'VALIDATION_FAILED' => 'VALIDATION_FAILED',
'INVALID_FILE_TYPE' => 'INVALID_FILE_TYPE',
'FILE_TOO_LARGE' => 'FILE_TOO_LARGE',
'INVALID_IMAGE' => 'INVALID_IMAGE',
// Resource not found errors (404)
'ALBUM_NOT_FOUND' => 'ALBUM_NOT_FOUND',
'PHOTO_NOT_FOUND' => 'PHOTO_NOT_FOUND',
'TAG_NOT_FOUND' => 'TAG_NOT_FOUND',
'RESOURCE_NOT_FOUND' => 'RESOURCE_NOT_FOUND',
// Authentication/Authorization errors (401-403)
'UNAUTHORIZED' => 'UNAUTHORIZED',
'FORBIDDEN' => 'FORBIDDEN',
'INVALID_TOKEN' => 'INVALID_TOKEN',
// Method not allowed (405)
'METHOD_NOT_ALLOWED' => 'METHOD_NOT_ALLOWED',
// Conflict errors (409)
'RESOURCE_EXISTS' => 'RESOURCE_EXISTS',
'DUPLICATE_ENTRY' => 'DUPLICATE_ENTRY',
// Server errors (500-599)
'INTERNAL_ERROR' => 'INTERNAL_ERROR',
'DATABASE_ERROR' => 'DATABASE_ERROR',
'FILE_SYSTEM_ERROR' => 'FILE_SYSTEM_ERROR',
'IMAGE_PROCESSING_ERROR' => 'IMAGE_PROCESSING_ERROR',
'STORAGE_ERROR' => 'STORAGE_ERROR',
'CONFIGURATION_ERROR' => 'CONFIGURATION_ERROR',
// Service unavailable (503)
'SERVICE_UNAVAILABLE' => 'SERVICE_UNAVAILABLE',
'STORAGE_FULL' => 'STORAGE_FULL',
];
// HTTP status codes mapping
const HTTP_STATUS_CODES = [
'INVALID_INPUT' => 400,
'MISSING_REQUIRED_FIELD' => 400,
'INVALID_JSON' => 400,
'INVALID_PARAMETER' => 400,
'VALIDATION_FAILED' => 400,
'INVALID_FILE_TYPE' => 400,
'FILE_TOO_LARGE' => 413,
'INVALID_IMAGE' => 400,
'ALBUM_NOT_FOUND' => 404,
'PHOTO_NOT_FOUND' => 404,
'TAG_NOT_FOUND' => 404,
'RESOURCE_NOT_FOUND' => 404,
'UNAUTHORIZED' => 401,
'FORBIDDEN' => 403,
'INVALID_TOKEN' => 401,
'METHOD_NOT_ALLOWED' => 405,
'RESOURCE_EXISTS' => 409,
'DUPLICATE_ENTRY' => 409,
'INTERNAL_ERROR' => 500,
'DATABASE_ERROR' => 500,
'FILE_SYSTEM_ERROR' => 500,
'IMAGE_PROCESSING_ERROR' => 500,
'STORAGE_ERROR' => 500,
'CONFIGURATION_ERROR' => 500,
'SERVICE_UNAVAILABLE' => 503,
'STORAGE_FULL' => 507,
];
// Log levels
const LOG_LEVELS = [
'DEBUG' => 0,
'INFO' => 1,
'WARNING' => 2,
'ERROR' => 3,
'CRITICAL' => 4
];
/**
* Constructor
* @param string $logFile Path to log file
* @param string $logLevel Minimum log level to record
* @param bool $enableLogging Whether to enable logging
*/
public function __construct($logFile = 'logs/error.log', $logLevel = 'ERROR', $enableLogging = true) {
$this->logFile = $logFile;
$this->logLevel = $logLevel;
$this->enableLogging = $enableLogging;
// Ensure log directory exists
$logDir = dirname($this->logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
}
/**
* Create structured error response
* @param string $message Error message
* @param string $errorCode Error code constant
* @param array $details Additional error details
* @param array $context Additional context information
* @return array Error response structure
*/
public function createErrorResponse($message, $errorCode = 'INTERNAL_ERROR', $details = [], $context = []) {
// Get HTTP status code
$httpCode = self::HTTP_STATUS_CODES[$errorCode] ?? 500;
// Log the error
$this->logError($message, $errorCode, $details, $context, $httpCode);
// Create response structure
$response = [
'success' => false,
'error' => [
'message' => $message,
'code' => $errorCode,
'http_status' => $httpCode,
'timestamp' => date('c')
]
];
// Add details if provided
if (!empty($details)) {
$response['error']['details'] = $details;
}
// Add request ID for tracking
$response['error']['request_id'] = $this->generateRequestId();
return $response;
}
/**
* Create success response
* @param array $data Response data
* @param int $httpCode HTTP status code
* @param string $message Optional success message
* @return array Success response structure
*/
public function createSuccessResponse($data, $httpCode = 200, $message = null) {
$response = [
'success' => true,
'data' => $data,
'meta' => [
'http_status' => $httpCode,
'timestamp' => date('c'),
'request_id' => $this->generateRequestId()
]
];
if ($message) {
$response['message'] = $message;
}
// Log successful operations for audit trail
$this->logInfo('API Success', [
'http_status' => $httpCode,
'data_keys' => array_keys($data),
'message' => $message
]);
return $response;
}
/**
* Log error with context
* @param string $message Error message
* @param string $errorCode Error code
* @param array $details Error details
* @param array $context Context information
* @param int $httpCode HTTP status code
*/
public function logError($message, $errorCode = 'INTERNAL_ERROR', $details = [], $context = [], $httpCode = 500) {
if (!$this->enableLogging) {
return;
}
$logLevel = $httpCode >= 500 ? 'ERROR' : 'WARNING';
$logData = [
'level' => $logLevel,
'message' => $message,
'error_code' => $errorCode,
'http_status' => $httpCode,
'timestamp' => date('c'),
'request_id' => $this->generateRequestId(),
'request_uri' => $_SERVER['REQUEST_URI'] ?? '',
'request_method' => $_SERVER['REQUEST_METHOD'] ?? '',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'ip_address' => $this->getClientIpAddress(),
];
if (!empty($details)) {
$logData['details'] = $details;
}
if (!empty($context)) {
$logData['context'] = $context;
}
$this->writeLog($logData, $logLevel);
}
/**
* Log informational message
* @param string $message Log message
* @param array $context Context data
*/
public function logInfo($message, $context = []) {
if (!$this->enableLogging) {
return;
}
$logData = [
'level' => 'INFO',
'message' => $message,
'timestamp' => date('c'),
'request_id' => $this->generateRequestId(),
'request_uri' => $_SERVER['REQUEST_URI'] ?? '',
'request_method' => $_SERVER['REQUEST_METHOD'] ?? '',
];
if (!empty($context)) {
$logData['context'] = $context;
}
$this->writeLog($logData, 'INFO');
}
/**
* Log warning message
* @param string $message Warning message
* @param array $context Context data
*/
public function logWarning($message, $context = []) {
if (!$this->enableLogging) {
return;
}
$logData = [
'level' => 'WARNING',
'message' => $message,
'timestamp' => date('c'),
'request_id' => $this->generateRequestId(),
'request_uri' => $_SERVER['REQUEST_URI'] ?? '',
'request_method' => $_SERVER['REQUEST_METHOD'] ?? '',
];
if (!empty($context)) {
$logData['context'] = $context;
}
$this->writeLog($logData, 'WARNING');
}
/**
* Write log entry to file
* @param array $logData Log data
* @param string $level Log level
*/
private function writeLog($logData, $level) {
// Check if we should log this level
if (self::LOG_LEVELS[$level] < self::LOG_LEVELS[$this->logLevel]) {
return;
}
$logEntry = json_encode($logData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL;
// Write to log file with error handling
if (file_put_contents($this->logFile, $logEntry, FILE_APPEND | LOCK_EX) === false) {
// Fallback to error_log if file writing fails
error_log('ErrorHandler: Failed to write to log file. Original message: ' . $logData['message']);
}
}
/**
* Generate unique request ID for tracking
* @return string Request ID
*/
private function generateRequestId() {
static $requestId = null;
if ($requestId === null) {
$requestId = uniqid('req_', true);
}
return $requestId;
}
/**
* Get client IP address
* @return string IP address
*/
private function getClientIpAddress() {
$ipKeys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'];
foreach ($ipKeys as $key) {
if (!empty($_SERVER[$key])) {
$ip = $_SERVER[$key];
// Handle comma-separated IPs (X-Forwarded-For)
if (strpos($ip, ',') !== false) {
$ip = trim(explode(',', $ip)[0]);
}
return $ip;
}
}
return 'unknown';
}
/**
* Validate required fields in input data
* @param array $data Input data
* @param array $requiredFields Required field names
* @return array|null Returns error response if validation fails, null if valid
*/
public function validateRequiredFields($data, $requiredFields) {
$missingFields = [];
foreach ($requiredFields as $field) {
if (!isset($data[$field]) || (is_string($data[$field]) && trim($data[$field]) === '')) {
$missingFields[] = $field;
}
}
if (!empty($missingFields)) {
return $this->createErrorResponse(
'Missing required fields: ' . implode(', ', $missingFields),
'MISSING_REQUIRED_FIELD',
['missing_fields' => $missingFields]
);
}
return null;
}
/**
* Validate JSON input
* @param string $jsonString JSON string to validate
* @return array|null Returns error response if validation fails, null if valid
*/
public function validateJsonInput($jsonString) {
if (empty($jsonString)) {
return $this->createErrorResponse(
'Request body is empty',
'INVALID_INPUT'
);
}
$data = json_decode($jsonString, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return $this->createErrorResponse(
'Invalid JSON input: ' . json_last_error_msg(),
'INVALID_JSON',
['json_error' => json_last_error_msg()]
);
}
if (!is_array($data)) {
return $this->createErrorResponse(
'Request body must be a JSON object',
'INVALID_INPUT'
);
}
return null;
}
/**
* Validate string length
* @param string $value String value
* @param string $fieldName Field name for error message
* @param int $minLength Minimum length
* @param int $maxLength Maximum length
* @return array|null Returns error response if validation fails, null if valid
*/
public function validateStringLength($value, $fieldName, $minLength = 0, $maxLength = null) {
$length = strlen($value);
if ($length < $minLength) {
return $this->createErrorResponse(
"{$fieldName} must be at least {$minLength} characters long",
'VALIDATION_FAILED',
['field' => $fieldName, 'min_length' => $minLength, 'actual_length' => $length]
);
}
if ($maxLength !== null && $length > $maxLength) {
return $this->createErrorResponse(
"{$fieldName} cannot exceed {$maxLength} characters",
'VALIDATION_FAILED',
['field' => $fieldName, 'max_length' => $maxLength, 'actual_length' => $length]
);
}
return null;
}
/**
* Validate numeric value
* @param mixed $value Value to validate
* @param string $fieldName Field name for error message
* @param int|float $min Minimum value
* @param int|float $max Maximum value
* @return array|null Returns error response if validation fails, null if valid
*/
public function validateNumeric($value, $fieldName, $min = null, $max = null) {
if (!is_numeric($value)) {
return $this->createErrorResponse(
"{$fieldName} must be a numeric value",
'VALIDATION_FAILED',
['field' => $fieldName, 'value' => $value]
);
}
$numValue = (float)$value;
if ($min !== null && $numValue < $min) {
return $this->createErrorResponse(
"{$fieldName} must be at least {$min}",
'VALIDATION_FAILED',
['field' => $fieldName, 'min' => $min, 'value' => $numValue]
);
}
if ($max !== null && $numValue > $max) {
return $this->createErrorResponse(
"{$fieldName} cannot exceed {$max}",
'VALIDATION_FAILED',
['field' => $fieldName, 'max' => $max, 'value' => $numValue]
);
}
return null;
}
/**
* Validate array values
* @param mixed $value Value to validate
* @param string $fieldName Field name for error message
* @param array $allowedValues Allowed values
* @return array|null Returns error response if validation fails, null if valid
*/
public function validateAllowedValues($value, $fieldName, $allowedValues) {
if (!in_array($value, $allowedValues, true)) {
return $this->createErrorResponse(
"{$fieldName} must be one of: " . implode(', ', $allowedValues),
'VALIDATION_FAILED',
['field' => $fieldName, 'value' => $value, 'allowed_values' => $allowedValues]
);
}
return null;
}
/**
* Handle exceptions and convert to error response
* @param Exception $exception Exception to handle
* @param string $context Context where exception occurred
* @return array Error response
*/
public function handleException($exception, $context = '') {
$message = $exception->getMessage();
$errorCode = 'INTERNAL_ERROR';
// Map specific exception types to error codes
if (strpos($message, 'not found') !== false || strpos($message, 'does not exist') !== false) {
$errorCode = 'RESOURCE_NOT_FOUND';
} elseif (strpos($message, 'already exists') !== false || strpos($message, 'duplicate') !== false) {
$errorCode = 'DUPLICATE_ENTRY';
} elseif (strpos($message, 'invalid') !== false || strpos($message, 'validation') !== false) {
$errorCode = 'VALIDATION_FAILED';
} elseif (strpos($message, 'permission') !== false || strpos($message, 'access') !== false) {
$errorCode = 'FORBIDDEN';
} elseif (strpos($message, 'storage') !== false || strpos($message, 'disk') !== false) {
$errorCode = 'STORAGE_ERROR';
} elseif (strpos($message, 'image') !== false || strpos($message, 'conversion') !== false) {
$errorCode = 'IMAGE_PROCESSING_ERROR';
} elseif (strpos($message, 'database') !== false || strpos($message, 'data') !== false) {
$errorCode = 'DATABASE_ERROR';
} elseif (strpos($message, 'file') !== false || strpos($message, 'directory') !== false) {
$errorCode = 'FILE_SYSTEM_ERROR';
}
$details = [
'exception_type' => get_class($exception),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString()
];
if (!empty($context)) {
$details['context'] = $context;
}
return $this->createErrorResponse($message, $errorCode, $details);
}
}