Работа с базой через PDO без обработки ошибок — это риск: при сбое пользователь увидит либо пустую страницу, либо «500 Internal Server Error», а разработчик не поймёт, в чём дело. Разберём, как сделать так, чтобы приложение тихо логировало проблему, а пользователю не показывало технические детали.
Главное правило: не показывать пользователю SQL‑ошибки
SQL‑ошибки содержат имена таблиц, колонок, иногда части логики. Их нельзя выводить в браузер. Пользователь должен видеть нейтральное сообщение вроде «Не удалось загрузить данные. Попробуйте позже». А всё техническое — только в лог.
Включаем правильный режим ошибок в PDO
Самый надёжный вариант — PDO::ERRMODE_EXCEPTION. Тогда любая ошибка базы сразу превращается в исключение, которое можно перехватить.
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8mb4'; $user = 'user'; $pass = 'pass'; $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; try { $pdo = new PDO($dsn, $user, $pass, $options); } catch (PDOException $e) { // Логируем ошибку, пользователю — ничего не показываем error_log('DB connection failed: ' . $e->getMessage()); die('Не удалось подключиться к базе данных.'); }
Почему именно ERRMODE_EXCEPTION:
- Проще контролировать поток: всё падает в
catch, нет разбросанного кода с проверками. - Удобно оборачивать отдельные запросы в свои блоки
try/catch, если нужно по‑разному реагировать на разные ошибки.
Как правильно логировать ошибки
В лог нужно писать не только сообщение, но и контекст: тип операции, параметры (без чувствительных данных), стек.
Пример простой функции логирования:
function logDbError(string $context, \Throwable $e): void { $message = sprintf( '[DB] %s | Message: %s | Code: %s | Trace: %s', $context, $e->getMessage(), $e->getCode(), $e->getTraceAsString() ); error_log($message); }
Теперь используем её при запросах:
try { $stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id'); $stmt->execute(['id' => $userId]); $user = $stmt->fetch(); } catch (PDOException $e) { logDbError('Fetch user by ID', $e); // Для пользователя — нейтральное сообщение http_response_code(500); echo 'Не удалось загрузить профиль. Попробуйте позже.'; }
Что важно:
- Не логируйте пароли, токены, персональные данные.
- В продакшене лучше писать в файл или в систему сбора логов (Monolog, Graylog, Sentry), а не просто в
error_log.
Частые сценарии и как их обрабатывать
1. Ошибка подключения к БД
Уже показан выше: лог + короткое сообщение пользователю. Если это критично (например, главная страница), можно показать заглушку и отправить алерт администратору.
2. Ошибка запроса (синтаксис, несуществующая таблица и т.п.)
Это почти всегда проблема кода, а не данных. Лог с контекстом поможет быстро найти баг.
try { $stmt = $pdo->query('SELECT * FROM non_existent_table'); } catch (PDOException $e) { logDbError('Query error in dashboard stats', $e); http_response_code(500); echo 'Ошибка загрузки статистики.'; }
3. Нарушение уникальности (дубликат, уникальный индекс)
Тут ошибка — это часть бизнес‑логики. Например, пользователь пытается зарегистрироваться с уже занятым email.
try { $stmt = $pdo->prepare('INSERT INTO users (email, password_hash) VALUES (:email, :pass)'); $stmt->execute([ 'email' => $email, 'pass' => $passwordHash, ]); } catch (PDOException $e) { if ($e->getCode() === '23000') { // нарушение уникальности logDbError('Duplicate email on registration', $e); http_response_code(409); echo 'Такой email уже зарегистрирован.'; return; } // Остальные ошибки — как обычно logDbError('Registration DB error', $e); http_response_code(500); echo 'Произошла ошибка при регистрации.'; }
4. Транзакции и частичный откат
Если вы используете транзакции, ошибки внутри них нужно обрабатывать так, чтобы не оставить базу в несогласованном состоянии.
$pdo->beginTransaction(); try { $stmt1 = $pdo->prepare('UPDATE accounts SET balance = balance - :amount WHERE id = :from'); $stmt1->execute(['amount' => $amount, 'from' => $fromId]); $stmt2 = $pdo->prepare('UPDATE accounts SET balance = balance + :amount WHERE id = :to'); $stmt2->execute(['amount' => $amount, 'to' => $toId]); $pdo->commit(); } catch (PDOException $e) { $pdo->rollBack(); logDbError('Transfer failed', $e); http_response_code(500); echo 'Не удалось выполнить перевод. Попробуйте позже.'; }
Практические советы под ваши задачи (админки, статьи, каталоги)
- В админке при сохранении статьи или товара ошибка БД — это критичная ситуация. Логируйте с контекстом (ID статьи/товара, тип операции) и покажите пользователю: «Не удалось сохранить. Проверьте соединение или обратитесь к администратору».
- На публичной странице каталога ошибка выборки товаров — не должна ломать всю страницу. Можно показать пустой список или заглушку «Товары не найдены» и залогировать проблему.
- При массовых операциях (импорт, пересчёт цен) оборачивайте каждую итерацию или блок в
try/catchи продолжайте обработку, пропуская сбойные строки. Так сайт не «падает», а только часть данных не обрабатывается.
Чего избегать
- Выводить
$e->getMessage()пользователю. Это раскрытие внутренней структуры. - Игнорировать исключения. Если не ловить
PDOException, приложение может вести себя непредсказуемо. - Логировать чувствительные данные. Никогда не пишите в лог пароли, токены, полные данные карт.
- Думать, что
ERRMODE_WARNINGилиERRMODE_SILENT— это проще. В итоге вы теряете контроль над ошибками и усложняете отладку.
Минимальный шаблон для проекта
class Db { private PDO $pdo; public function __construct() { $dsn = 'mysql:host=localhost;dbname=test;charset=utf8mb4'; $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; try { $this->pdo = new PDO($dsn, $_ENV['DB_USER'], $_ENV['DB_PASS'], $options); } catch (PDOException $e) { logDbError('DB init', $e); die('Сервис временно недоступен.'); } } public function getPdo(): PDO { return $this->pdo; } } function logDbError(string $context, \Throwable $e): void { error_log(sprintf( '[DB] %s | %s | %s', $context, $e->getMessage(), $e->getTraceAsString() )); }
Дальше все запросы делаются через $db->getPdo() и оборачиваются в try/catch с вызовом logDbError.
