'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); } }