Обработка ошибок в 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.