Обработка ошибок в PHP: try/catch и продвинутые техники

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Обработка ошибок в PHP: от try/catch к промышленным решениям

Каждый разработчик PHP сталкивается с ошибками. Базовый try/catch — это лишь вершина айсберга. В этой статье мы разберем продвинутые техники обработки ошибок, которые превратят хрупкий код в надежную систему, способную корректно реагировать на любые сбои.

1. Глубокое погружение в try/catch: блок finally и множественные catch

Стандартный try/catch — это основа, но многие забывают о блоке finally, который выполняется всегда, независимо от того, было ли исключение. Это идеальное место для закрытия ресурсов (файлов, соединений с БД).

<?php
function processFile(string $filename): void {
    $handle = fopen($filename, 'r');
    if (!$handle) {
        throw new \RuntimeException("Не удалось открыть файл: $filename");
    }
    
    try {
        // Обработка файла
        while (($line = fgets($handle)) !== false) {
            // ... логика обработки
        }
    } catch (\RuntimeException $e) {
        // Специфичная обработка ошибок ввода-вывода
        error_log("Ошибка чтения файла: " . $e->getMessage());
        throw $e; // Проброс наверх
    } catch (\Exception $e) {
        // Обработка любых других исключений
        error_log("Неизвестная ошибка: " . $e->getMessage());
        throw $e;
    } finally {
        fclose($handle); // Всегда закрываем файл
    }
}
?>

Важно: Порядок catch блоков имеет значение. Более специфичные исключения (например, \RuntimeException) должны идти раньше общих (\Exception). Иначе специфичный блок никогда не выполнится.

2. Пользовательские классы исключений: семантика и иерархия

Встроенные исключения (PDOException, RuntimeException) часто недостаточно информативны. Создайте свою иерархию исключений, чтобы разделять ошибки по типам и упрощать их обработку.

<?php
// Базовое исключение приложения
class AppException extends \Exception {}

// Ошибки валидации
class ValidationException extends AppException {
    private array $errors;
    
    public function __construct(array $errors, string $message = "Ошибка валидации") {
        parent::__construct($message, 422);
        $this->errors = $errors;
    }
    
    public function getErrors(): array {
        return $this->errors;
    }
}

// Ошибки доступа
class AccessDeniedException extends AppException {
    public function __construct(string $message = "Доступ запрещен") {
        parent::__construct($message, 403);
    }
}

// Пример использования
function registerUser(array $data): void {
    $errors = [];
    
    if (empty($data['email'])) {
        $errors['email'] = 'Email обязателен';
    }
    if (empty($data['password'])) {
        $errors['password'] = 'Пароль обязателен';
    }
    
    if (!empty($errors)) {
        throw new ValidationException($errors);
    }
    
    // Логика регистрации...
}

try {
    registerUser(['email' => '']);
} catch (ValidationException $e) {
    // Специфичная обработка: возвращаем JSON с ошибками
    http_response_code(422);
    echo json_encode(['errors' => $e->getErrors()]);
} catch (AccessDeniedException $e) {
    http_response_code(403);
    echo json_encode(['error' => $e->getMessage()]);
} catch (AppException $e) {
    // Общая обработка для всех остальных ошибок приложения
    http_response_code(500);
    echo json_encode(['error' => 'Внутренняя ошибка сервера']);
}
?>

Совет: Используйте коды HTTP (400, 403, 422, 500) как коды исключений — это упрощает интеграцию с REST API.

3. Глобальный обработчик ошибок: ErrorHandler и ShutdownHandler

Не все ошибки можно поймать через try/catch. Фатальные ошибки (E_ERROR), предупреждения (E_WARNING) и ошибки парсинга требуют глобального подхода.

<?php
// 1. Устанавливаем пользовательский обработчик ошибок
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): bool {
    // Преобразуем "старые" ошибки в исключения
    if (!(error_reporting() & $errno)) {
        // Ошибка не входит в текущий уровень error_reporting
        return false;
    }
    
    throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
});

// 2. Устанавливаем обработчик для фатальных ошибок
register_shutdown_function(function (): void {
    $error = error_get_last();
    
    if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
        // Логируем фатальную ошибку
        $logMessage = sprintf(
            "[FATAL] %s in %s on line %d",
            $error['message'],
            $error['file'],
            $error['line']
        );
        error_log($logMessage);
        
        // Отправляем 500 ошибку
        http_response_code(500);
        if (php_sapi_name() !== 'cli') {
            // Для веб-запросов отдаем чистую страницу
            echo "<h1>500 Internal Server Error</h1>";
        }
        
        // Завершаем скрипт
        exit(1);
    }
});

// 3. Устанавливаем глобальный обработчик непойманных исключений
set_exception_handler(function (\Throwable $e): void {
    $logMessage = sprintf(
        "[UNCAUGHT EXCEPTION] %s: %s in %s on line %d\nStack trace:\n%s",
        get_class($e),
        $e->getMessage(),
        $e->getFile(),
        $e->getLine(),
        $e->getTraceAsString()
    );
    error_log($logMessage);
    
    http_response_code(500);
    echo "<h1>Произошла критическая ошибка</h1>";
    echo "<p>Пожалуйста, попробуйте позже.</p>";
});

// Пример: теперь даже предупреждения можно ловить через try/catch
try {
    $result = 1 / 0; // Вызовет DivisionByZeroError (в PHP 8) или Warning (в PHP 7)
} catch (\DivisionByZeroError | \ErrorException $e) {
    echo "Поймали ошибку: " . $e->getMessage();
}
?>

Важно: Всегда устанавливайте error_reporting(E_ALL) для отлова всех типов ошибок. В production используйте display_errors = Off, а в dev — On.

4.

Блоги

Рекомендаций по книгам