Обработка MySQL ошибок в PHP коде
Основные подходы к обработке ошибок MySQL в PHP
Как сделать обработку ошибок MySQL централизованной и предсказуемой?
Наиболее эффективным решением считается использование расширения PDO с режимом исключений. При таком подходе каждая ошибка запроса или соединения превращается в исключение, которое можно перехватить в блоке try-catch. Это избавляет от необходимости проверять результат каждой функции вручную.
Пример настройки PDO с исключениями:
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8';
$user = 'root';
$pass = '';
try {
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
// Ошибка соединения обрабатывается здесь
echo 'Не удалось соединиться: ' . $e->getMessage();
}
После этой настройки любые неудачные запросы (например, синтаксическая ошибка SQL или нарушение ограничений) будут автоматически вызывать исключение PDOException. Это упрощает отладку, особенно на этапе разработки.
Типичная проблема: если забыть установить PDO::ATTR_ERRMODE, PDO по умолчанию работает в «тихом» режиме - ошибки не выводятся и не выбрасываются. Тогда разработчик может не заметить проблему до тех пор, пока данные не окажутся некорректными.
Решение: всегда явно задавать нужный режим. Для продакшена можно использовать PDO::ERRMODE_WARNING (только предупреждения) или комбинировать с логированием, но на разработке рекомендуется ERRMODE_EXCEPTION.
Как обрабатывать ошибки MySQL с помощью mysqli и исключений?
Расширение mysqli также может выбрасывать исключения, если включить строгий режим отчёта об ошибках. Это делается одной директивой:
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$conn = new mysqli('localhost', 'root', '', 'test');
После этого любая ошибка (включая проблемы соединения) вызывает исключение mysqli_sql_exception. Поведение становится похожим на PDO.
Проблема: если добавить эту строку внутри уже существующего проекта, могут неожиданно «полететь» участки кода, где ошибки раньше игнорировались.
Решение: оборачивать все вызовы mysqli в try-catch, либо постепенно внедрять новый режим на новых модулях.
Как включить вывод ошибок MySQL на этапе разработки?
На локальной машине полезно видеть все ошибки прямо на экране. Для этого в PHP настраиваются директивы display_errors и error_reporting. Пример для php.ini:
display_errors = On
error_reporting = E_ALL
В коде это можно сделать функциями:
ini_set('display_errors', 1);
ini_set('error_reporting', E_ALL);
Тогда любые предупреждения MySQL будут отображаться в браузере.
Опасность: включение display_errors на боевом сервере раскрывает детали соединения с БД.
Решение: оставлять display_errors = Off на продакшене, а для отладки использовать логирование.
Как скрыть ошибки MySQL от пользователя, но записать их в лог?
На боевом сервере ошибки следует логировать, не показывая пользователю. В php.ini настраиваются:
display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log
В коде можно перехватывать исключения PDO и записывать их в собственный лог с дополнительной информацией:
try {
$pdo->query('SELECT * FROM non_existent_table');
} catch (PDOException $e) {
error_log('MySQL query failed: ' . $e->getMessage() . ' [SQL: ' . $sql . ']');
// Пользователю выводится общее сообщение
echo 'Извините, произошла внутренняя ошибка.';
}
Такой подход защищает данные и помогает администратору анализировать сбои.
Проблема: если лог-файл не создан или недоступен для записи, ошибка не сохранится.
Решение: проверять права на каталог лога, использовать syslog или внешние сервисы (Sentry, Graylog).
Как обработать конкретный код ошибки MySQL (например, дублирование ключа 1062)?
В некоторых сценариях требуется различать типы ошибок. Например, при вставке данных возможен дубликат первичного ключа. В PDO код ошибки можно получить через $e->getCode() или $e->errorInfo[1]:
try {
$pdo->exec("INSERT INTO users (id, name) VALUES (1, 'test')");
} catch (PDOException $e) {
if ($e->getCode() == '23000' && $e->errorInfo[1] == 1062) {
echo 'Такая запись уже существует. Попробуйте другое значение.';
} else {
throw $e; // перебросить другие ошибки
}
}
Это позволяет дать пользователю понятное сообщение, не раскрывая структуру БД.
Сложность: коды ошибок MySQL различаются в зависимости от версии и драйвера.
Решение: сверяться с документацией MySQL и тестировать на целевой версии.
Расширенные примеры отладки MySQL ошибок в PHP
Пример 1: PDO с разными режимами ошибок (сравнение)
Рассмотрим три режима: ERRMODE_SILENT (по умолчанию), ERRMODE_WARNING и ERRMODE_EXCEPTION. Код и результат для ошибочного запроса:
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8';
$pdo = new PDO($dsn, 'root', '');
// 1. SILENT
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$result = $pdo->query('SELECT invalid_column FROM users');
if (!$result) {
echo 'Ошибка в тихом режиме: ' . $pdo->errorCode() . "\n";
} // Вывод: Ошибка в тихом режиме: 42S22
// 2. WARNING
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
$result = $pdo->query('SELECT invalid_column FROM users');
// Вывод предупреждения на экран (если display_errors включен)
// Warning: PDO::query(): SQLSTATE[42S22]: ...
// 3. EXCEPTION
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
try {
$pdo->query('SELECT invalid_column FROM users');
} catch (PDOException $e) {
echo 'Исключение: ' . $e->getMessage() . "\n";
} // Вывод: Исключение: SQLSTATE[42S22]: Column not found: ...
Ошибка в тихом режиме: 42S22 Исключение: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'invalid_column' in 'field list'
Для продакшена часто выбирают ERRMODE_WARNING с логированием или ERRMODE_EXCEPTION с глобальным обработчиком.
Пример 2: mysqli с подготовленными запросами и детальным отловом ошибок
Использование mysqli_report с исключениями позволяет перехватывать ошибки даже в подготовленных запросах, которые могут вернуть false при неудачной подготовке.
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$conn = new mysqli('localhost', 'root', '', 'test');
try {
$stmt = $conn->prepare('SELECT * FROM users WHERE email = ?');
$stmt->bind_param('s', $email);
$email = 'test@example.com';
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
print_r($row);
}
} catch (mysqli_sql_exception $e) {
echo 'Ошибка MySQL: ' . $e->getMessage() . ' (код ' . $e->getCode() . ')';
}
// Если таблица 'users' не существует, будет выброшено исключение с сообщением
// 'Table 'test.users' doesn't exist'
Ошибка MySQL: Table 'test.users' doesn't exist (код 1146)
Примечание: код ошибки (1146) можно использовать для выборочной обработки.
Пример 3: Логирование ошибок с дополнительным контекстом (файл, строка, SQL)
Для детального логирования полезно передавать в лог не только сообщение, но и текст запроса, вызвавшего ошибку.
function logSqlError(PDOException $e, $sql) {
$message = sprintf(
"[%s] SQL Error: %s in file %s on line %d\nQuery: %s\n",
date('Y-m-d H:i:s'),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$sql
);
error_log($message, 3, '/var/log/mysql_errors.log');
}
$sql = "INSERT INTO logs (data) VALUES ('test')";
try {
$pdo->exec($sql);
} catch (PDOException $e) {
logSqlError($e, $sql);
echo 'Операция не выполнена. Пожалуйста, свяжитесь с администратором.';
}
(Файл /var/log/mysql_errors.log появится с записью типа)
[2025-03-18 12:30:00] SQL Error: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'test' for key 'data' in file /var/www/html/index.php on line 15
Query: INSERT INTO logs (data) VALUES ('test')
Такой лог включает время, исходный код и параметры запроса, что ускоряет поиск причины.
Пример 4: Глобальный обработчик ошибок для PDO и MySQL
Можно установить пользовательский обработчик исключений, который перехватывает все PDOException и отправляет их в единый лог-сервис.
set_exception_handler(function (Throwable $e) {
if ($e instanceof PDOException) {
// Запись в файл
error_log('PDO Exception: ' . $e->getMessage());
// Отправка в Sentry (пример)
// \Sentry\captureException($e);
// Вывод пользователю
echo 'Произошла ошибка базы данных. Мы уже работаем над её исправлением.';
} else {
// Для других исключений - стандартное поведение
throw $e;
}
});
// Теперь любой необработанный PDOException попадёт в этот обработчик
$pdo->query('SELECT * FROM nonexistent');
(На экране отобразится сообщение:) Произошла ошибка базы данных. Мы уже работаем над её исправлением.
Этот подход позволяет централизованно управлять всеми ошибками MySQL, не прописывая try-catch в каждом месте.
Пример 5: Обработка ошибок при работе с кодировкой (charset)
Если не указать кодировку соединения, возможны ошибки неверного сравнения строк или искажения данных. Пример ошибки и её исправления:
// Ошибочное соединение (без charset)
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Запрос с русскими символами может привести к ошибке или неправильному результату
try {
$pdo->exec("INSERT INTO users (name) VALUES ('Иван')");
} catch (PDOException $e) {
echo 'Ошибка вставки: ' . $e->getMessage() . "\n";
}
// Возможная ошибка: 'Incorrect string value' (SQLSTATE[22007])
// Исправление - указать charset в DSN
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8mb4';
$pdo = new PDO($dsn, 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("INSERT INTO users (name) VALUES ('Иван')");
(При первом варианте - сообщение об ошибке, при втором - успешная вставка) Ошибка вставки: SQLSTATE[22007]: Invalid datetime format: 1366 Incorrect string value: '\xD0\xB8...'
Рекомендуется всегда указывать utf8mb4 для полной поддержки Unicode.