1. php
  2. /basics
  3. /error-handling

PHP Error Handling and Debugging

Introduction to Error Handling

Error handling is a critical aspect of PHP development that determines how gracefully your application responds to unexpected situations. Proper error handling not only prevents crashes and security vulnerabilities but also provides valuable feedback for debugging and improves the overall user experience.

Understanding PHP's error handling mechanisms is essential for building robust, production-ready applications that can handle edge cases, provide meaningful feedback to users, and assist developers in troubleshooting issues efficiently.

Why Error Handling Matters

Application Stability: Unhandled errors can crash your application or leave it in an inconsistent state. Proper error handling ensures your application continues to function even when unexpected situations occur.

Security: Poor error handling can expose sensitive information like database credentials, file paths, or internal application structure to attackers. Proper error handling prevents information disclosure vulnerabilities.

User Experience: Well-handled errors provide users with clear, actionable feedback instead of cryptic error messages or blank pages, maintaining trust and usability.

Debugging and Maintenance: Comprehensive error logging and handling makes it easier to identify, reproduce, and fix problems in production environments.

Compliance: Many regulatory frameworks require proper error handling and logging for audit trails and incident response.

Types of Errors in PHP

Understanding the different types of errors in PHP is crucial for implementing appropriate handling strategies:

Fatal Errors: Stop script execution immediately. These include syntax errors, calling undefined functions, or running out of memory. Fatal errors cannot be recovered from within the script itself.

Parse Errors: Occur when PHP cannot parse your code due to syntax mistakes. These prevent the script from running at all. Common causes include missing semicolons, unmatched brackets, or invalid syntax.

Warnings: Non-fatal errors that don't stop execution but indicate potential problems. Common examples include using undefined variables or deprecated functions. While the script continues, the results may be unpredictable.

Notices: Minor issues that don't affect functionality but suggest improvements. Examples include accessing undefined array indices or using deprecated features. These help maintain code quality and prevent future issues.

Exceptions: Object-oriented error handling mechanism that allows for structured error management and recovery. Unlike traditional errors, exceptions can be caught and handled programmatically.

Error Reporting Levels

PHP provides granular control over which errors are reported through error reporting levels. Understanding these levels helps you configure appropriate error handling for different environments:

  • E_ERROR: Fatal runtime errors that halt script execution
  • E_WARNING: Runtime warnings (non-fatal) that indicate potential issues
  • E_PARSE: Compile-time parse errors preventing script execution
  • E_NOTICE: Runtime notices suggesting code improvements
  • E_STRICT: Code suggestions for best practices and forward compatibility
  • E_DEPRECATED: Warnings about deprecated functionality that will be removed
  • E_ALL: All errors and warnings (recommended for development)

You can combine these levels using bitwise operators to create custom error reporting configurations that suit your needs.

Basic Error Handling

Error Reporting Configuration

Proper error reporting configuration is the foundation of effective error handling. The configuration should differ between development and production environments to balance debugging needs with security concerns.

<?php
/**
 * PHP Error Reporting Configuration
 * 
 * Proper error reporting configuration is crucial for both
 * development and production environments, but with different
 * approaches for each.
 */

/**
 * Development Environment Configuration
 * 
 * In development, you want to see all errors to catch issues early
 * and ensure code quality.
 */
function configureErrorsForDevelopment(): void
{
    // Report all errors
    error_reporting(E_ALL);
    
    // Display errors on screen
    ini_set('display_errors', 1);
    ini_set('display_startup_errors', 1);
    
    // Log errors to file as well
    ini_set('log_errors', 1);
    ini_set('error_log', 'logs/php_errors.log');
    
    // Show detailed error information
    ini_set('html_errors', 1);
    ini_set('docref_root', 'http://www.php.net/');
    
    // Track errors in variables
    ini_set('track_errors', 1);
}

Development Configuration Explained:

The development configuration prioritizes visibility and detail:

  • error_reporting(E_ALL): Reports all error types, ensuring nothing is missed during development
  • display_errors: Shows errors directly in the browser/output, enabling immediate feedback
  • display_startup_errors: Catches errors that occur during PHP's startup sequence
  • log_errors: Creates a permanent record for later analysis
  • html_errors: Formats errors with HTML for better readability in browsers
  • docref_root: Adds links to PHP documentation for each error, speeding up problem resolution
/**
 * Production Environment Configuration
 * 
 * In production, you want to log errors without exposing
 * sensitive information to users.
 */
function configureErrorsForProduction(): void
{
    // Report all errors internally
    error_reporting(E_ALL);
    
    // Don't display errors to users
    ini_set('display_errors', 0);
    ini_set('display_startup_errors', 0);
    
    // Log all errors
    ini_set('log_errors', 1);
    ini_set('error_log', '/var/log/php/error.log');
    
    // Don't expose PHP version
    ini_set('expose_php', 0);
    
    // Limit error message details
    ini_set('html_errors', 0);
}

Production Configuration Explained:

The production configuration balances security with debugging capabilities:

  • Still reports all errors: error_reporting(E_ALL) ensures all issues are detected
  • Hides errors from users: Prevents information disclosure and maintains professional appearance
  • Centralized logging: Errors go to a secure log file for administrator review
  • Security hardening: Disables PHP version exposure and HTML formatting
  • Consistent error handling: All errors are logged uniformly for easier analysis
/**
 * Conditional configuration based on environment
 */
function configureErrors(): void
{
    $environment = $_ENV['APP_ENV'] ?? 'production';
    
    switch ($environment) {
        case 'development':
        case 'dev':
        case 'local':
            configureErrorsForDevelopment();
            break;
            
        case 'staging':
            // Staging might want a hybrid approach
            error_reporting(E_ALL);
            ini_set('display_errors', 0);
            ini_set('log_errors', 1);
            ini_set('error_log', '/var/log/php/staging_errors.log');
            break;
            
        default:
            configureErrorsForProduction();
    }
}

Environment-Based Configuration:

This pattern allows you to:

  • Automatically adapt: Configuration changes based on deployment environment
  • Staging flexibility: Test production-like settings while maintaining debugging capabilities
  • Safe defaults: Unknown environments default to production settings for security
  • Easy management: Single configuration point for all environments
/**
 * Runtime error reporting control
 * 
 * You can also control error reporting programmatically
 * during script execution.
 */
function demonstrateErrorReporting(): void
{
    // Save current error reporting level
    $oldLevel = error_reporting();
    
    // Temporarily suppress all errors
    error_reporting(0);
    
    // Code that might generate warnings you want to suppress
    $result = @file_get_contents('nonexistent_file.txt');
    
    // Restore original error reporting
    error_reporting($oldLevel);
    
    // Or report only fatal errors and warnings
    error_reporting(E_ERROR | E_WARNING);
    
    // Back to all errors
    error_reporting(E_ALL);
}
?>

Runtime Error Control Techniques:

Sometimes you need to temporarily adjust error reporting:

  • Saving/Restoring Levels: Preserves the original configuration while making temporary changes
  • Error Suppression: The @ operator suppresses errors for a single operation (use sparingly!)
  • Selective Reporting: Bitwise operators allow fine-grained control over which errors to report
  • Context-Specific Settings: Different parts of your application may need different error reporting

Important considerations:

  • Avoid using @ operator in production code - it makes debugging difficult
  • Always restore original error reporting levels after temporary changes
  • Consider using try-catch blocks instead of error suppression
  • Log suppressed errors for later analysis

Custom Error Handlers

Custom error handlers give you complete control over how your application responds to errors. They allow you to implement sophisticated error tracking, custom logging formats, and user-friendly error messages.

<?php
/**
 * Custom Error Handling System
 * 
 * Custom error handlers provide fine-grained control over
 * how your application responds to and logs different types
 * of errors.
 */

class ErrorHandler
{
    private string $logFile;
    private bool $displayErrors;
    private array $errorCounts = [];
    
    public function __construct(string $logFile = 'php_errors.log', bool $displayErrors = false)
    {
        $this->logFile = $logFile;
        $this->displayErrors = $displayErrors;
        
        // Register custom error handler
        set_error_handler([$this, 'handleError']);
        set_exception_handler([$this, 'handleException']);
        register_shutdown_function([$this, 'handleFatalError']);
    }
}

**Custom Error Handler Architecture**:

This class demonstrates a comprehensive error handling system:
- **Centralized Management**: All error handling logic in one place
- **Flexible Configuration**: Can be adapted for different environments
- **Error Statistics**: Tracks error frequency for monitoring
- **Multiple Handler Types**: Handles regular errors, exceptions, and fatal errors

The three registration functions serve different purposes:
- **`set_error_handler()`**: Catches standard PHP errors (warnings, notices)
- **`set_exception_handler()`**: Catches uncaught exceptions
- **`register_shutdown_function()`**: Catches fatal errors that can't be handled normally

```php
    /**
     * Handle PHP errors (warnings, notices, etc.)
     */
    public function handleError(int $severity, string $message, string $file, int $line): bool
    {
        // Don't handle errors that are suppressed with @
        if (!(error_reporting() & $severity)) {
            return false;
        }
        
        $errorType = $this->getErrorType($severity);
        $this->errorCounts[$errorType] = ($this->errorCounts[$errorType] ?? 0) + 1;
        
        $errorInfo = [
            'type' => $errorType,
            'message' => $message,
            'file' => $file,
            'line' => $line,
            'timestamp' => date('Y-m-d H:i:s'),
            'url' => $_SERVER['REQUEST_URI'] ?? 'CLI',
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'CLI',
            'ip' => $_SERVER['REMOTE_ADDR'] ?? 'localhost'
        ];
        
        // Log the error
        $this->logError($errorInfo);
        
        // Display error if enabled
        if ($this->displayErrors) {
            $this->displayError($errorInfo);
        }
        
        // Don't execute PHP's internal error handler
        return true;
    }
}

**Error Handler Method Explained**:

This method showcases best practices for error handling:
- **Respects Error Suppression**: Honors the `@` operator when used
- **Collects Context**: Gathers comprehensive information about the error environment
- **Tracks Statistics**: Counts errors by type for monitoring
- **Flexible Output**: Can both log and display errors based on configuration

The collected context information is invaluable for debugging:
- **Timestamp**: When the error occurred
- **URL**: Which page/endpoint triggered the error
- **User Agent**: What browser/client was used
- **IP Address**: Where the request came from (useful for tracking patterns)

```php
    /**
     * Handle uncaught exceptions
     */
    public function handleException(Throwable $exception): void
    {
        $errorInfo = [
            'type' => 'EXCEPTION',
            'message' => $exception->getMessage(),
            'file' => $exception->getFile(),
            'line' => $exception->getLine(),
            'trace' => $exception->getTraceAsString(),
            'timestamp' => date('Y-m-d H:i:s'),
            'url' => $_SERVER['REQUEST_URI'] ?? 'CLI',
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'CLI',
            'ip' => $_SERVER['REMOTE_ADDR'] ?? 'localhost'
        ];
        
        $this->logError($errorInfo);
        
        if ($this->displayErrors) {
            $this->displayError($errorInfo);
        } else {
            // Show generic error page in production
            $this->showErrorPage();
        }
    }
}

**Exception Handler Features**:

Exception handling provides additional capabilities:
- **Stack Traces**: Full execution path leading to the exception
- **Type Safety**: Uses `Throwable` to catch both exceptions and errors
- **User-Friendly Errors**: Shows generic error page in production
- **Complete Context**: Captures all relevant debugging information

The distinction between development and production behavior is crucial:
- Development: Shows full error details for debugging
- Production: Shows generic error page to maintain security and professionalism

```php
    /**
     * Handle fatal errors
     */
    public function handleFatalError(): void
    {
        $error = error_get_last();
        
        if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
            $errorInfo = [
                'type' => 'FATAL',
                'message' => $error['message'],
                'file' => $error['file'],
                'line' => $error['line'],
                'timestamp' => date('Y-m-d H:i:s'),
                'url' => $_SERVER['REQUEST_URI'] ?? 'CLI',
                'memory_usage' => memory_get_peak_usage(true),
                'memory_limit' => ini_get('memory_limit')
            ];
            
            $this->logError($errorInfo);
            
            if (!$this->displayErrors) {
                $this->showErrorPage();
            }
        }
    }
}

**Fatal Error Handling Insights**:

Fatal errors require special handling because:
- **Script Termination**: They stop execution immediately
- **Limited Recovery**: Can only log and display, not recover
- **Memory Information**: Often caused by memory issues, so tracking usage helps
- **Shutdown Function**: Only way to catch these errors

The additional memory information helps diagnose common fatal error causes:
- Out of memory errors
- Infinite recursion
- Large data structure problems

## Exception Handling

### Understanding Exceptions

Exceptions represent a modern, object-oriented approach to error handling in PHP. Unlike traditional errors, exceptions can be caught and handled, allowing your application to recover gracefully from error conditions. Understanding when and how to use exceptions is crucial for building robust applications.

```php
<?php
/**
 * PHP Exception Handling Fundamentals
 * 
 * Exceptions provide a structured way to handle errors in PHP,
 * allowing for better error recovery and cleaner code organization.
 */

/**
 * Basic exception handling concepts
 */
function demonstrateBasicExceptions(): void
{
    try {
        // Code that might throw an exception
        $result = riskyOperation();
        echo "Operation succeeded: $result\n";
        
    } catch (InvalidArgumentException $e) {
        // Handle specific exception type
        echo "Invalid argument: " . $e->getMessage() . "\n";
        
    } catch (RuntimeException $e) {
        // Handle another specific type
        echo "Runtime error: " . $e->getMessage() . "\n";
        
    } catch (Exception $e) {
        // Handle any other exception
        echo "General error: " . $e->getMessage() . "\n";
        
    } finally {
        // Code that always executes
        echo "Cleanup operations\n";
    }
}

Try-Catch-Finally Structure Explained:

The try-catch-finally structure is the foundation of exception handling:

Try Block: Contains code that might throw exceptions. If an exception occurs, execution immediately jumps to the appropriate catch block.

Catch Blocks: Handle specific exception types. Order matters - more specific exceptions should come before general ones. Each catch block can:

  • Access the exception object for details
  • Log the error
  • Attempt recovery
  • Re-throw the exception
  • Convert to a different exception type

Finally Block: Always executes, regardless of whether an exception occurred. Perfect for:

  • Closing resources (files, database connections)
  • Releasing locks
  • Cleanup operations
  • Resetting state
/**
 * Example function that demonstrates different exception scenarios
 */
function riskyOperation($input = null)
{
    if ($input === null) {
        throw new InvalidArgumentException('Input cannot be null');
    }
    
    if (!is_numeric($input)) {
        throw new InvalidArgumentException('Input must be numeric');
    }
    
    if ($input < 0) {
        throw new RuntimeException('Input cannot be negative');
    }
    
    // Simulate some processing
    if (rand(1, 10) > 8) {
        throw new RuntimeException('Random failure occurred');
    }
    
    return $input * 2;
}

Exception Best Practices Demonstrated:

This function shows several important patterns:

Validate Early: Check inputs at the beginning and throw appropriate exceptions immediately. This follows the "fail fast" principle.

Use Specific Exception Types:

  • InvalidArgumentException for input validation failures
  • RuntimeException for operational failures
  • This allows callers to handle different error types appropriately

Descriptive Messages: Exception messages should clearly explain what went wrong, making debugging easier.

Consistent Error Handling: All error paths throw exceptions rather than returning error codes or false values.

/**
 * Exception chaining and nested exceptions
 */
function demonstrateExceptionChaining(): void
{
    try {
        try {
            throw new RuntimeException('Original error');
        } catch (RuntimeException $e) {
            // Chain exceptions to preserve error context
            throw new Exception('Wrapped error', 0, $e);
        }
    } catch (Exception $e) {
        echo "Current error: " . $e->getMessage() . "\n";
        
        $previous = $e->getPrevious();
        if ($previous) {
            echo "Previous error: " . $previous->getMessage() . "\n";
        }
    }
}

Exception Chaining Benefits:

Exception chaining preserves the full error context:

  • Root Cause Preservation: The original exception is maintained in the chain
  • Layered Error Handling: Each layer can add its own context
  • Better Debugging: Full error history is available for troubleshooting
  • Abstraction: Lower-level exceptions can be wrapped in higher-level ones

Common use cases:

  • Converting database exceptions to application exceptions
  • Adding business context to technical errors
  • Creating audit trails of error handling
/**
 * Re-throwing exceptions for different handling levels
 */
function demonstrateRethrowingExceptions(): void
{
    try {
        lowLevelFunction();
    } catch (DatabaseException $e) {
        // Log the database error
        error_log('Database error: ' . $e->getMessage());
        
        // Re-throw as a more general application error
        throw new ApplicationException('Service temporarily unavailable', 0, $e);
    }
}

function lowLevelFunction(): void
{
    // Simulate database error
    throw new DatabaseException('Connection timeout');
}

// Custom exception classes
class ApplicationException extends Exception {}
class DatabaseException extends Exception {}

Re-throwing Strategy:

Re-throwing exceptions allows for multi-layered error handling:

Layer-Specific Handling: Each layer handles what it can and passes up what it can't:

  • Data layer: Logs technical database errors
  • Business layer: Converts to business exceptions
  • Presentation layer: Shows user-friendly messages

Information Hiding: Technical details are logged but not exposed to users, maintaining security while preserving debugging information.

Graceful Degradation: Each layer can attempt recovery before passing the error up.

Custom Exception Classes

Custom exception classes allow you to create a structured error handling system that's specific to your application's needs. They provide better organization, more context, and cleaner error handling code.

<?php
/**
 * Custom Exception Classes for Application-Specific Error Handling
 * 
 * Creating custom exception classes helps organize error handling
 * and provides more specific context for different types of errors.
 */

/**
 * Base application exception
 */
abstract class AppException extends Exception
{
    protected array $context = [];
    protected string $userMessage = 'An error occurred';
    
    public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null, array $context = [])
    {
        parent::__construct($message, $code, $previous);
        $this->context = $context;
    }
    
    /**
     * Get additional context about the error
     */
    public function getContext(): array
    {
        return $this->context;
    }
    
    /**
     * Get user-friendly error message
     */
    public function getUserMessage(): string
    {
        return $this->userMessage;
    }
    
    /**
     * Get error details for logging
     */
    public function getErrorDetails(): array
    {
        return [
            'exception_class' => get_class($this),
            'message' => $this->getMessage(),
            'user_message' => $this->getUserMessage(),
            'code' => $this->getCode(),
            'file' => $this->getFile(),
            'line' => $this->getLine(),
            'context' => $this->getContext(),
            'trace' => $this->getTraceAsString()
        ];
    }
}

Base Exception Class Design:

This base class demonstrates several important patterns:

Separation of Concerns:

  • message: Technical details for developers
  • userMessage: Safe, friendly message for end users
  • context: Additional debugging information

Rich Error Information: The getErrorDetails() method provides comprehensive information for logging and debugging, making troubleshooting easier.

Extensibility: Abstract class allows all application exceptions to share common functionality while adding their own specific behaviors.

Type Safety: Using custom exceptions allows for more precise catch blocks and better IDE support.

/**
 * Validation-related exceptions
 */
class ValidationException extends AppException
{
    private array $errors = [];
    protected string $userMessage = 'Please check your input and try again';
    
    public function __construct(array $errors = [], string $message = 'Validation failed', int $code = 422)
    {
        $this->errors = $errors;
        $context = ['validation_errors' => $errors];
        
        parent::__construct($message, $code, null, $context);
    }
    
    public function getValidationErrors(): array
    {
        return $this->errors;
    }
    
    public function hasErrors(): bool
    {
        return !empty($this->errors);
    }
    
    public function getErrorsForField(string $field): array
    {
        return $this->errors[$field] ?? [];
    }
}

Validation Exception Features:

This specialized exception demonstrates domain-specific error handling:

Structured Error Storage: Validation errors are stored in a structured format, making it easy to:

  • Display field-specific errors in forms
  • Generate API error responses
  • Log detailed validation failures

HTTP Status Code: Uses 422 (Unprocessable Entity) by default, following REST conventions.

Helper Methods: Provides convenient methods for checking and retrieving errors, improving code readability.

/**
 * Database-related exceptions
 */
class DatabaseException extends AppException
{
    protected string $userMessage = 'Database operation failed';
    
    public static function connectionFailed(string $details = ''): self
    {
        return new self(
            'Database connection failed' . ($details ? ": $details" : ''),
            1001,
            null,
            ['type' => 'connection_failure', 'details' => $details]
        );
    }
    
    public static function queryFailed(string $query, string $error): self
    {
        return new self(
            "Query failed: $error",
            1002,
            null,
            ['type' => 'query_failure', 'query' => $query, 'error' => $error]
        );
    }
    
    public static function transactionFailed(string $reason): self
    {
        return new self(
            "Transaction failed: $reason",
            1003,
            null,
            ['type' => 'transaction_failure', 'reason' => $reason]
        );
    }
}

Factory Method Pattern:

Using static factory methods provides several advantages:

Consistent Creation: Ensures exceptions are created with all necessary information in a consistent format.

Semantic Clarity: Method names clearly indicate the error type: connectionFailed() vs new DatabaseException('connection failed').

Encapsulation: Internal error codes and context structure are hidden from calling code.

Type Hinting: IDEs can provide better autocomplete and type checking for specific error scenarios.

/**
 * Authentication and authorization exceptions
 */
class AuthenticationException extends AppException
{
    protected string $userMessage = 'Authentication required';
    
    public static function invalidCredentials(): self
    {
        return new self(
            'Invalid username or password',
            2001,
            null,
            ['type' => 'invalid_credentials']
        );
    }
    
    public static function accountLocked(int $minutesRemaining): self
    {
        $instance = new self(
            'Account is temporarily locked',
            2002,
            null,
            ['type' => 'account_locked', 'minutes_remaining' => $minutesRemaining]
        );
        
        $instance->userMessage = "Account locked. Try again in $minutesRemaining minutes.";
        return $instance;
    }
    
    public static function sessionExpired(): self
    {
        return new self(
            'Session has expired',
            2003,
            null,
            ['type' => 'session_expired']
        );
    }
}

Security-Aware Exception Design:

Authentication exceptions demonstrate security best practices:

Generic User Messages: For invalidCredentials(), the user message doesn't reveal whether the username or password was wrong, preventing username enumeration attacks.

Contextual Information: The accountLocked() method provides helpful information (time remaining) without revealing why the account was locked.

Consistent Error Codes: Each authentication failure type has a unique code, enabling proper monitoring and alerting.

Logging and Debugging

Comprehensive Logging System

<?php
/**
 * Advanced Logging System for PHP Applications
 * 
 * Logging is crucial for debugging, monitoring, and maintaining
 * applications in production environments.
 */

enum LogLevel: string
{
    case EMERGENCY = 'emergency';
    case ALERT = 'alert';
    case CRITICAL = 'critical';
    case ERROR = 'error';
    case WARNING = 'warning';
    case NOTICE = 'notice';
    case INFO = 'info';
    case DEBUG = 'debug';
    
    public function getNumericValue(): int
    {
        return match($this) {
            self::EMERGENCY => 0,
            self::ALERT => 1,
            self::CRITICAL => 2,
            self::ERROR => 3,
            self::WARNING => 4,
            self::NOTICE => 5,
            self::INFO => 6,
            self::DEBUG => 7
        };
    }
}

interface LoggerInterface
{
    public function log(LogLevel $level, string $message, array $context = []): void;
    public function emergency(string $message, array $context = []): void;
    public function alert(string $message, array $context = []): void;
    public function critical(string $message, array $context = []): void;
    public function error(string $message, array $context = []): void;
    public function warning(string $message, array $context = []): void;
    public function notice(string $message, array $context = []): void;
    public function info(string $message, array $context = []): void;
    public function debug(string $message, array $context = []): void;
}

class Logger implements LoggerInterface
{
    private string $logDir;
    private LogLevel $minLevel;
    private array $handlers = [];
    private array $processors = [];
    
    public function __construct(string $logDir = 'logs', LogLevel $minLevel = LogLevel::INFO)
    {
        $this->logDir = rtrim($logDir, '/');
        $this->minLevel = $minLevel;
        
        // Create log directory if it doesn't exist
        if (!is_dir($this->logDir)) {
            mkdir($this->logDir, 0755, true);
        }
        
        // Add default file handler
        $this->addHandler(new FileLogHandler($this->logDir . '/app.log'));
    }
    
    public function addHandler(LogHandlerInterface $handler): void
    {
        $this->handlers[] = $handler;
    }
    
    public function addProcessor(callable $processor): void
    {
        $this->processors[] = $processor;
    }
    
    public function log(LogLevel $level, string $message, array $context = []): void
    {
        // Check if we should log this level
        if ($level->getNumericValue() > $this->minLevel->getNumericValue()) {
            return;
        }
        
        // Create log record
        $record = [
            'timestamp' => microtime(true),
            'datetime' => date('Y-m-d H:i:s'),
            'level' => $level,
            'message' => $this->interpolate($message, $context),
            'context' => $context,
            'extra' => []
        ];
        
        // Apply processors
        foreach ($this->processors as $processor) {
            $record = $processor($record);
        }
        
        // Send to handlers
        foreach ($this->handlers as $handler) {
            $handler->handle($record);
        }
    }
    
    /**
     * Interpolate context values into the message placeholders
     */
    private function interpolate(string $message, array $context): string
    {
        $replace = [];
        
        foreach ($context as $key => $val) {
            if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) {
                $replace['{' . $key . '}'] = $val;
            }
        }
        
        return strtr($message, $replace);
    }
    
    public function emergency(string $message, array $context = []): void
    {
        $this->log(LogLevel::EMERGENCY, $message, $context);
    }
    
    public function alert(string $message, array $context = []): void
    {
        $this->log(LogLevel::ALERT, $message, $context);
    }
    
    public function critical(string $message, array $context = []): void
    {
        $this->log(LogLevel::CRITICAL, $message, $context);
    }
    
    public function error(string $message, array $context = []): void
    {
        $this->log(LogLevel::ERROR, $message, $context);
    }
    
    public function warning(string $message, array $context = []): void
    {
        $this->log(LogLevel::WARNING, $message, $context);
    }
    
    public function notice(string $message, array $context = []): void
    {
        $this->log(LogLevel::NOTICE, $message, $context);
    }
    
    public function info(string $message, array $context = []): void
    {
        $this->log(LogLevel::INFO, $message, $context);
    }
    
    public function debug(string $message, array $context = []): void
    {
        $this->log(LogLevel::DEBUG, $message, $context);
    }
}

interface LogHandlerInterface
{
    public function handle(array $record): void;
}

class FileLogHandler implements LogHandlerInterface
{
    private string $filename;
    private string $format;
    
    public function __construct(string $filename, string $format = null)
    {
        $this->filename = $filename;
        $this->format = $format ?? "[{datetime}] {level}: {message} {context}\n";
    }
    
    public function handle(array $record): void
    {
        $formatted = $this->format($record);
        
        // Ensure directory exists
        $dir = dirname($this->filename);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        
        file_put_contents($this->filename, $formatted, FILE_APPEND | LOCK_EX);
    }
    
    private function format(array $record): string
    {
        $output = $this->format;
        
        $replacements = [
            '{datetime}' => $record['datetime'],
            '{level}' => strtoupper($record['level']->value),
            '{message}' => $record['message'],
            '{context}' => !empty($record['context']) ? json_encode($record['context']) : '',
        ];
        
        return strtr($output, $replacements);
    }
}

class DatabaseLogHandler implements LogHandlerInterface
{
    private PDO $pdo;
    private string $table;
    
    public function __construct(PDO $pdo, string $table = 'logs')
    {
        $this->pdo = $pdo;
        $this->table = $table;
    }
    
    public function handle(array $record): void
    {
        $sql = "INSERT INTO {$this->table} (level, message, context, created_at) VALUES (?, ?, ?, ?)";
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            $record['level']->value,
            $record['message'],
            json_encode($record['context']),
            $record['datetime']
        ]);
    }
}

// Processors add additional context to log records
function addRequestInfoProcessor(array $record): array
{
    $record['extra']['request'] = [
        'method' => $_SERVER['REQUEST_METHOD'] ?? 'CLI',
        'uri' => $_SERVER['REQUEST_URI'] ?? 'CLI',
        'ip' => $_SERVER['REMOTE_ADDR'] ?? 'localhost',
        'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'CLI'
    ];
    
    return $record;
}

function addMemoryUsageProcessor(array $record): array
{
    $record['extra']['memory'] = [
        'usage' => memory_get_usage(true),
        'peak' => memory_get_peak_usage(true)
    ];
    
    return $record;
}

// Usage example
$logger = new Logger('logs', LogLevel::DEBUG);

// Add processors for additional context
$logger->addProcessor('addRequestInfoProcessor');
$logger->addProcessor('addMemoryUsageProcessor');

// Add database handler (optional)
// $logger->addHandler(new DatabaseLogHandler($pdo));

// Log different types of events
$logger->info('Application started');
$logger->debug('Debug information', ['user_id' => 123]);
$logger->warning('This is a warning message');
$logger->error('An error occurred', ['error_code' => 500, 'details' => 'Database connection failed']);
$logger->critical('Critical system error', ['system' => 'payment', 'amount' => 1000]);

// Log with message interpolation
$logger->info('User {username} logged in from {ip}', [
    'username' => 'john_doe',
    'ip' => '192.168.1.1',
    'user_id' => 123
]);
?>

For more PHP development fundamentals:

Summary

Effective error handling and debugging are fundamental to building robust PHP applications:

Error Configuration: Properly configure error reporting for development and production environments with appropriate visibility and logging.

Custom Error Handlers: Implement custom error handlers to provide consistent error processing, logging, and user feedback across your application.

Exception Handling: Use structured exception handling with try-catch blocks and custom exception classes for better error organization and recovery.

Comprehensive Logging: Implement detailed logging systems that capture context, support multiple output formats, and provide useful debugging information.

Production Considerations: Never expose sensitive error details to users in production, but ensure comprehensive logging for debugging and monitoring.

Best Practices: Always log errors with sufficient context, use appropriate error levels, implement graceful degradation, and provide meaningful user feedback.

Mastering error handling and debugging techniques enables you to build applications that fail gracefully, provide excellent user experiences, and are easy to maintain and troubleshoot in production environments.