Глава 3.3: PDO — подключение, prepared statements, fetchAll/fetch, обработка ошибок, транзакции
🎯 Что ты узнаешь
После этой главы ты будешь:
- Понимать, что такое PDO и почему это стандарт работы с БД в PHP
- Уметь безопасно подключаться к базе данных
- Знать, как защититься от SQL-инъекций через prepared statements
- Работать с разными способами получения данных
- Правильно обрабатывать ошибки
- Использовать транзакции для атомарных операций
📖 Теория
Что такое PDO?
PDO (PHP Data Objects) — это универсальный интерфейс для работы с базами данных в PHP. Это не отдельная база данных, а прослойка между твоим кодом и БД.
Почему PDO, а не mysqli или mysql_*?
// ❌ ПЛОХО: старый способ (mysql_*)
// Устарел, удалён из PHP 7.0, небезопасен
$conn = mysql_connect('localhost', 'user', 'pass');
$result = mysql_query("SELECT * FROM users WHERE id = " . $_GET['id']); // SQL-инъекция!
// ⚠️ СРЕДНЕ: mysqli
// Работает только с MySQL, не универсален
$mysqli = new mysqli('localhost', 'user', 'pass', 'database');
$stmt = $mysqli->prepare("SELECT * FROM users WHERE id = ?");
// ✅ ХОРОШО: PDO
// Универсален, современен, объектно-ориентирован
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");Преимущества PDO:
- Поддержка 12+ типов БД (MySQL, PostgreSQL, SQLite, Oracle...)
- Единый API — сменил БД? Код почти не меняется
- Prepared statements "из коробки"
- Объектно-ориентированный подход
- Гибкая обработка ошибок
🔌 Подключение к базе данных
Базовое подключение
<?php
// DSN (Data Source Name) — строка подключения
// Формат: драйвер:параметр1=значение1;параметр2=значение2
$dsn = 'mysql:host=localhost;dbname=shop;charset=utf8mb4';
$username = 'root';
$password = 'secret';
try {
$pdo = new PDO($dsn, $username, $password);
echo "Подключение успешно!";
} catch (PDOException $e) {
die("Ошибка подключения: " . $e->getMessage());
}Опции подключения
<?php
$options = [
// Режим обработки ошибок: выбрасывать исключения
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
// Режим получения данных: ассоциативные массивы
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
// Отключить эмуляцию prepared statements (безопаснее)
PDO::ATTR_EMULATE_PREPARES => false,
// Постоянное подключение (переиспользование)
PDO::ATTR_PERSISTENT => false, // обычно false для веба
];
$pdo = new PDO($dsn, $username, $password, $options);Важно про charset:
// ✅ ПРАВИЛЬНО: указываем charset в DSN
$dsn = 'mysql:host=localhost;dbname=shop;charset=utf8mb4';
// ❌ НЕПРАВИЛЬНО: через SET NAMES (небезопасно с prepared statements)
$pdo->exec("SET NAMES utf8mb4"); // не делай так!Правильная структура подключения
<?php
// config/database.php
function getDbConnection(): PDO
{
static $pdo = null;
if ($pdo === null) {
$config = [
'host' => 'localhost',
'dbname' => 'shop',
'charset' => 'utf8mb4',
];
$dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset={$config['charset']}";
$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, 'root', 'secret', $options);
} catch (PDOException $e) {
// В продакшене НЕ показывай детали ошибки пользователю!
error_log($e->getMessage());
die("Ошибка подключения к базе данных");
}
}
return $pdo;
}🛡️ Prepared Statements — защита от SQL-инъекций
Что такое SQL-инъекция?
// ❌ ОПАСНЫЙ КОД
$id = $_GET['id']; // Пользователь передал: "1 OR 1=1"
$sql = "SELECT * FROM users WHERE id = $id";
// Реальный запрос: SELECT * FROM users WHERE id = 1 OR 1=1
// Результат: получили ВСЕХ пользователей!
// Ещё хуже:
$id = $_GET['id']; // Пользователь передал: "1; DROP TABLE users--"
// Реальный запрос: SELECT * FROM users WHERE id = 1; DROP TABLE users--
// Результат: таблица удалена! 💀Как работают Prepared Statements
Prepared statements — это разделение структуры SQL-запроса и данных. Сервер БД сначала разбирает структуру запроса, а потом подставляет данные как значения, а не как код.
// ✅ БЕЗОПАСНЫЙ КОД
$id = $_GET['id']; // Любое значение!
// 1. Подготовка запроса (с плейсхолдерами)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
// 2. Выполнение с данными
$stmt->execute([$id]);
// Даже если $id = "1 OR 1=1", БД воспримет это как строку "1 OR 1=1"
// и не найдёт такого ID. Инъекция невозможна!Два типа плейсхолдеров
1. Позиционные плейсхолдеры (?)
// Порядок важен!
$stmt = $pdo->prepare("
SELECT * FROM products
WHERE category = ? AND price < ? AND in_stock = ?
");
$stmt->execute(['electronics', 1000, true]);
// ↑ первый ? ↑ второй ? ↑ третий ?2. Именованные плейсхолдеры (:name)
// Порядок не важен, более читаемо
$stmt = $pdo->prepare("
SELECT * FROM products
WHERE category = :category AND price < :max_price AND in_stock = :available
");
$stmt->execute([
':category' => 'electronics',
':max_price' => 1000,
':available' => true,
]);
// Можно без двоеточия в ключах
$stmt->execute([
'category' => 'electronics',
'max_price' => 1000,
'available' => true,
]);Рекомендация: используй именованные плейсхолдеры для сложных запросов — код становится самодокументируемым.
Что МОЖНО и что НЕЛЬЗЯ через плейсхолдеры
// ✅ МОЖНО: значения
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = ?");
$stmt->execute(['John']);
// ✅ МОЖНО: числа, булевы значения
$stmt = $pdo->prepare("SELECT * FROM products WHERE price > ? AND active = ?");
$stmt->execute([100, true]);
// ❌ НЕЛЬЗЯ: названия таблиц
$table = 'users';
$stmt = $pdo->prepare("SELECT * FROM ?"); // НЕ СРАБОТАЕТ!
// Решение: whitelist
$allowed_tables = ['users', 'products'];
if (in_array($table, $allowed_tables)) {
$stmt = $pdo->query("SELECT * FROM $table");
}
// ❌ НЕЛЬЗЯ: названия колонок
$column = 'email';
$stmt = $pdo->prepare("SELECT ? FROM users"); // НЕ СРАБОТАЕТ!
// Решение: whitelist
$allowed_columns = ['id', 'name', 'email'];
if (in_array($column, $allowed_columns)) {
$stmt = $pdo->query("SELECT $column FROM users");
}
// ❌ НЕЛЬЗЯ: операторы
$operator = '>';
$stmt = $pdo->prepare("SELECT * FROM products WHERE price ? 100"); // НЕ СРАБОТАЕТ!📥 Получение данных: fetch, fetchAll, fetchColumn
fetchAll() — все строки сразу
$stmt = $pdo->query("SELECT * FROM users");
$users = $stmt->fetchAll();
// Результат: массив массивов
// [
// ['id' => 1, 'name' => 'John', 'email' => 'john@example.com'],
// ['id' => 2, 'name' => 'Jane', 'email' => 'jane@example.com'],
// ]
foreach ($users as $user) {
echo $user['name'] . "<br>";
}fetch() — по одной строке
$stmt = $pdo->query("SELECT * FROM users");
while ($user = $stmt->fetch()) {
echo $user['name'] . "<br>";
}
// Полезно для больших результатов — не грузит всё в память сразуfetchColumn() — одно значение
// Получить количество
$stmt = $pdo->query("SELECT COUNT(*) FROM users");
$count = $stmt->fetchColumn();
echo "Всего пользователей: $count";
// Получить одно конкретное значение
$stmt = $pdo->prepare("SELECT email FROM users WHERE id = ?");
$stmt->execute([5]);
$email = $stmt->fetchColumn();
echo "Email: $email";Режимы получения данных (Fetch Modes)
// PDO::FETCH_ASSOC — ассоциативный массив (по умолчанию, если установлено)
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// ['id' => 1, 'name' => 'John']
// PDO::FETCH_NUM — индексированный массив
$user = $stmt->fetch(PDO::FETCH_NUM);
// [0 => 1, 1 => 'John']
// PDO::FETCH_BOTH — оба варианта (по умолчанию, если не установлено)
$user = $stmt->fetch(PDO::FETCH_BOTH);
// ['id' => 1, 0 => 1, 'name' => 'John', 1 => 'John']
// PDO::FETCH_OBJ — объект
$user = $stmt->fetch(PDO::FETCH_OBJ);
// stdClass Object { id: 1, name: "John" }
echo $user->name;
// PDO::FETCH_CLASS — собственный класс
class User {
public $id;
public $name;
public $email;
}
$stmt->setFetchMode(PDO::FETCH_CLASS, 'User');
$user = $stmt->fetch();
// Объект класса UserПолезные методы для результатов
// Получить все значения одной колонки
$stmt = $pdo->query("SELECT name FROM users");
$names = $stmt->fetchAll(PDO::FETCH_COLUMN);
// ['John', 'Jane', 'Bob']
// Получить пары ключ-значение
$stmt = $pdo->query("SELECT id, name FROM users");
$users = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
// [1 => 'John', 2 => 'Jane', 3 => 'Bob']
// Группировка по первой колонке
$stmt = $pdo->query("SELECT category, name, price FROM products");
$products = $stmt->fetchAll(PDO::FETCH_GROUP);
// [
// 'electronics' => [
// ['name' => 'Laptop', 'price' => 1000],
// ['name' => 'Phone', 'price' => 500],
// ],
// 'books' => [...]
// ]⚠️ Обработка ошибок
Три режима обработки ошибок
// 1. PDO::ERRMODE_SILENT (по умолчанию)
// Тихо игнорирует ошибки, нужно проверять вручную
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$stmt = $pdo->query("INVALID SQL");
if (!$stmt) {
print_r($pdo->errorInfo());
}
// 2. PDO::ERRMODE_WARNING
// Выводит PHP Warning
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
$stmt = $pdo->query("INVALID SQL"); // Warning: ...
// 3. PDO::ERRMODE_EXCEPTION (рекомендуется!)
// Выбрасывает исключение PDOException
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
try {
$stmt = $pdo->query("INVALID SQL");
} catch (PDOException $e) {
echo "Ошибка: " . $e->getMessage();
}Правильная обработка ошибок
function createUser(PDO $pdo, string $email, string $password): bool
{
try {
$stmt = $pdo->prepare("
INSERT INTO users (email, password, created_at)
VALUES (:email, :password, NOW())
");
$stmt->execute([
'email' => $email,
'password' => password_hash($password, PASSWORD_DEFAULT),
]);
return true;
} catch (PDOException $e) {
// Проверяем код ошибки
if ($e->getCode() == 23000) { // Duplicate entry
error_log("Duplicate email: $email");
return false;
}
// Логируем неизвестную ошибку
error_log("Database error: " . $e->getMessage());
throw $e; // Прокидываем дальше
}
}Получение информации об ошибке
try {
$pdo->exec("INVALID SQL");
} catch (PDOException $e) {
echo $e->getMessage(); // Текст ошибки
echo $e->getCode(); // Код ошибки (например, 23000)
echo $e->getFile(); // Файл, где произошла ошибка
echo $e->getLine(); // Строка
// Детальная информация
print_r($pdo->errorInfo());
// [
// 0 => "42S02", // SQLSTATE код
// 1 => 1146, // Код ошибки драйвера
// 2 => "Table 'test.no...", // Сообщение
// ]
}🔄 Транзакции
Что такое транзакция?
Транзакция — это группа операций с БД, которые либо все выполняются, либо все отменяются. Это гарантирует целостность данных.
Пример: перевод денег между счетами:
- Списать деньги со счёта A
- Зачислить деньги на счёт B
Что если после шага 1 произойдёт ошибка? Деньги спишутся, но не зачислятся! Транзакция решает эту проблему.
ACID принципы
- Atomicity (Атомарность): всё или ничего
- Consistency (Согласованность): данные остаются валидными
- Isolation (Изолированность): транзакции не мешают друг другу
- Durability (Долговечность): результат сохраняется навсегда
Базовое использование
try {
// Начать транзакцию
$pdo->beginTransaction();
// Операция 1: списать деньги
$stmt = $pdo->prepare("
UPDATE accounts
SET balance = balance - :amount
WHERE id = :from_account
");
$stmt->execute(['amount' => 100, 'from_account' => 1]);
// Операция 2: зачислить деньги
$stmt = $pdo->prepare("
UPDATE accounts
SET balance = balance + :amount
WHERE id = :to_account
");
$stmt->execute(['amount' => 100, 'to_account' => 2]);
// Всё успешно — зафиксировать изменения
$pdo->commit();
} catch (Exception $e) {
// Ошибка — откатить все изменения
$pdo->rollBack();
echo "Ошибка перевода: " . $e->getMessage();
}Проверка статуса транзакции
if ($pdo->inTransaction()) {
echo "Транзакция активна";
}
// Вложенные транзакции НЕ поддерживаются
$pdo->beginTransaction();
$pdo->beginTransaction(); // Ошибка!Реальный пример: создание заказа
function createOrder(PDO $pdo, int $userId, array $items): int
{
try {
$pdo->beginTransaction();
// 1. Создать заказ
$stmt = $pdo->prepare("
INSERT INTO orders (user_id, total, created_at)
VALUES (:user_id, :total, NOW())
");
$total = array_sum(array_column($items, 'price'));
$stmt->execute(['user_id' => $userId, 'total' => $total]);
$orderId = $pdo->lastInsertId();
// 2. Добавить товары в заказ
$stmt = $pdo->prepare("
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (:order_id, :product_id, :quantity, :price)
");
foreach ($items as $item) {
$stmt->execute([
'order_id' => $orderId,
'product_id' => $item['product_id'],
'quantity' => $item['quantity'],
'price' => $item['price'],
]);
}
// 3. Уменьшить остатки товаров
$stmt = $pdo->prepare("
UPDATE products
SET stock = stock - :quantity
WHERE id = :product_id AND stock >= :quantity
");
foreach ($items as $item) {
$stmt->execute([
'quantity' => $item['quantity'],
'product_id' => $item['product_id'],
]);
// Проверка: товар был обновлён?
if ($stmt->rowCount() === 0) {
throw new Exception("Недостаточно товара {$item['product_id']}");
}
}
$pdo->commit();
return $orderId;
} catch (Exception $e) {
$pdo->rollBack();
error_log("Order creation failed: " . $e->getMessage());
throw $e;
}
}📝 Практические примеры
CRUD операции с PDO
class UserRepository
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
// CREATE
public function create(string $name, string $email, string $password): int
{
$stmt = $this->pdo->prepare("
INSERT INTO users (name, email, password, created_at)
VALUES (:name, :email, :password, NOW())
");
$stmt->execute([
'name' => $name,
'email' => $email,
'password' => password_hash($password, PASSWORD_DEFAULT),
]);
return (int) $this->pdo->lastInsertId();
}
// READ (один)
public function find(int $id): ?array
{
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch();
return $user ?: null;
}
// READ (все)
public function all(): array
{
$stmt = $this->pdo->query("SELECT * FROM users ORDER BY created_at DESC");
return $stmt->fetchAll();
}
// UPDATE
public function update(int $id, array $data): bool
{
$fields = [];
$values = [];
foreach ($data as $key => $value) {
$fields[] = "$key = :$key";
$values[$key] = $value;
}
$values['id'] = $id;
$sql = "UPDATE users SET " . implode(', ', $fields) . " WHERE id = :id";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($values);
return $stmt->rowCount() > 0;
}
// DELETE
public function delete(int $id): bool
{
$stmt = $this->pdo->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([$id]);
return $stmt->rowCount() > 0;
}
// Поиск по email
public function findByEmail(string $email): ?array
{
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
return $user ?: null;
}
}Использование
require_once 'config/database.php';
$pdo = getDbConnection();
$userRepo = new UserRepository($pdo);
// Создание
$userId = $userRepo->create('John Doe', 'john@example.com', 'secret123');
echo "Создан пользователь с ID: $userId\n";
// Чтение
$user = $userRepo->find($userId);
echo "Пользователь: {$user['name']}\n";
// Обновление
$userRepo->update($userId, ['name' => 'John Smith']);
// Все пользователи
$users = $userRepo->all();
foreach ($users as $user) {
echo "{$user['name']} - {$user['email']}\n";
}
// Удаление
$userRepo->delete($userId);🎓 Упражнения
Упражнение 1: Безопасный поиск
Создай функцию поиска товаров с защитой от SQL-инъекций:
function searchProducts(PDO $pdo, string $query, float $minPrice, float $maxPrice): array
{
// Твой код здесь
// Должен искать товары по названию (LIKE) и диапазону цен
// Использовать prepared statements
}
// Тест
$products = searchProducts($pdo, 'laptop', 500, 2000);Решение
function searchProducts(PDO $pdo, string $query, float $minPrice, float $maxPrice): array
{
$stmt = $pdo->prepare("
SELECT * FROM products
WHERE name LIKE :query
AND price BETWEEN :min_price AND :max_price
ORDER BY price ASC
");
$stmt->execute([
'query' => "%$query%",
'min_price' => $minPrice,
'max_price' => $maxPrice,
]);
return $stmt->fetchAll();
}Упражнение 2: Система регистрации
Создай функцию регистрации пользователя с проверкой на дубликаты:
function registerUser(PDO $pdo, string $email, string $password, string $name): array
{
// Вернуть ['success' => true, 'user_id' => 123]
// или ['success' => false, 'error' => 'Email already exists']
}Решение
function registerUser(PDO $pdo, string $email, string $password, string $name): array
{
try {
// Проверка на существование
$stmt = $pdo->prepare("SELECT id FROM users WHERE email = ?");
$stmt->execute([$email]);
if ($stmt->fetch()) {
return ['success' => false, 'error' => 'Email already exists'];
}
// Регистрация
$stmt = $pdo->prepare("
INSERT INTO users (email, password, name, created_at)
VALUES (:email, :password, :name, NOW())
");
$stmt->execute([
'email' => $email,
'password' => password_hash($password, PASSWORD_DEFAULT),
'name' => $name,
]);
return [
'success' => true,
'user_id' => (int) $pdo->lastInsertId(),
];
} catch (PDOException $e) {
return ['success' => false, 'error' => 'Database error'];
}
}Упражнение 3: Транзакция переноса товара
Создай функцию переноса товара между складами с использованием транзакции:
function transferStock(
PDO $pdo,
int $productId,
int $fromWarehouse,
int $toWarehouse,
int $quantity
): bool {
// Должна:
// 1. Проверить наличие товара на складе-источнике
// 2. Уменьшить количество на складе-источнике
// 3. Увеличить количество на складе-назначении
// 4. Использовать транзакцию
// 5. Вернуть true при успехе, false при ошибке
}Решение
function transferStock(
PDO $pdo,
int $productId,
int $fromWarehouse,
int $toWarehouse,
int $quantity
): bool {
try {
$pdo->beginTransaction();
// Проверка наличия
$stmt = $pdo->prepare("
SELECT quantity FROM warehouse_stock
WHERE product_id = ? AND warehouse_id = ?
");
$stmt->execute([$productId, $fromWarehouse]);
$stock = $stmt->fetchColumn();
if ($stock < $quantity) {
throw new Exception('Insufficient stock');
}
// Уменьшить на складе-источнике
$stmt = $pdo->prepare("
UPDATE warehouse_stock
SET quantity = quantity - :quantity
WHERE product_id = :product_id AND warehouse_id = :warehouse_id
");
$stmt->execute([
'quantity' => $quantity,
'product_id' => $productId,
'warehouse_id' => $fromWarehouse,
]);
// Увеличить на складе-назначении
$stmt = $pdo->prepare("
INSERT INTO warehouse_stock (product_id, warehouse_id, quantity)
VALUES (:product_id, :warehouse_id, :quantity)
ON DUPLICATE KEY UPDATE quantity = quantity + :quantity
");
$stmt->execute([
'product_id' => $productId,
'warehouse_id' => $toWarehouse,
'quantity' => $quantity,
]);
$pdo->commit();
return true;
} catch (Exception $e) {
$pdo->rollBack();
error_log($e->getMessage());
return false;
}
}🚀 Практическое задание
Создай систему корзины покупок с использованием PDO:
Требования:
- Таблицы:
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL DEFAULT 0
);
CREATE TABLE carts (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE cart_items (
id INT PRIMARY KEY AUTO_INCREMENT,
cart_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
FOREIGN KEY (cart_id) REFERENCES carts(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id)
);- Функции для реализации:
class ShoppingCart
{
private PDO $pdo;
// Добавить товар в корзину (или увеличить количество)
public function addProduct(int $cartId, int $productId, int $quantity = 1): bool
// Удалить товар из корзины
public function removeProduct(int $cartId, int $productId): bool
// Получить содержимое корзины с деталями товаров
public function getItems(int $cartId): array
// Получить общую стоимость корзины
public function getTotal(int $cartId): float
// Оформить заказ (использовать транзакцию!)
// - Создать заказ
// - Перенести товары из корзины в заказ
// - Уменьшить остатки товаров
// - Очистить корзину
public function checkout(int $cartId): int // возвращает ID заказа
}- Обязательно:
- Все запросы через prepared statements
- Транзакция для checkout
- Проверка наличия товара на складе
- Обработка ошибок через try-catch
- Валидация данных
📚 Частые ошибки
❌ Ошибка 1: Забыли execute()
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
// $stmt->execute([1]); // забыли!
$user = $stmt->fetch(); // NULL, данных нет❌ Ошибка 2: Смешивание типов плейсхолдеров
// НЕПРАВИЛЬНО: нельзя смешивать ? и :name
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ? AND name = :name");
// Используй что-то одно!❌ Ошибка 3: Плейсхолдеры для идентификаторов
// НЕПРАВИЛЬНО: названия таблиц и колонок нельзя
$table = 'users';
$stmt = $pdo->prepare("SELECT * FROM ?"); // НЕ СРАБОТАЕТ
// ПРАВИЛЬНО: whitelist
$allowed = ['users', 'products'];
if (in_array($table, $allowed)) {
$stmt = $pdo->query("SELECT * FROM $table");
}❌ Ошибка 4: Не откатил транзакцию при ошибке
// ПЛОХО
$pdo->beginTransaction();
try {
// операции...
$pdo->commit();
} catch (Exception $e) {
// забыли rollBack()!
echo $e->getMessage();
}
// ХОРОШО
try {
$pdo->beginTransaction();
// операции...
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
echo $e->getMessage();
}❌ Ошибка 5: LIKE с плейсхолдером
// НЕПРАВИЛЬНО
$stmt = $pdo->prepare("SELECT * FROM users WHERE name LIKE '%?%'");
$stmt->execute(['John']); // НЕ СРАБОТАЕТ, ? в кавычках
// ПРАВИЛЬНО
$stmt = $pdo->prepare("SELECT * FROM users WHERE name LIKE ?");
$stmt->execute(["%John%"]); // % в самих данных🎯 Чек-лист: Что ты должен знать
После этой главы ты должен уметь:
- [ ] Подключаться к БД через PDO с правильными опциями
- [ ] Понимать разницу между query() и prepare()
- [ ] Использовать позиционные и именованные плейсхолдеры
- [ ] Объяснить, как prepared statements защищают от SQL-инъекций
- [ ] Работать с fetch(), fetchAll(), fetchColumn()
- [ ] Настраивать режимы получения данных (FETCH_*)
- [ ] Обрабатывать ошибки через PDOException
- [ ] Использовать транзакции для атомарных операций
- [ ] Получать ID последней вставленной записи
- [ ] Проверять количество затронутых строк
🔍 Вопросы для самопроверки
- В чём разница между
$pdo->query()и$pdo->prepare()? - Почему нельзя использовать плейсхолдеры для названий таблиц?
- Когда использовать
fetch()вместоfetchAll()? - Что произойдёт, если не вызвать
rollBack()при ошибке в транзакции? - Как получить количество строк, которые были изменены запросом UPDATE?
Ответы
query()выполняет запрос сразу (для статических запросов),prepare()создаёт подготовленное выражение для многократного использования с разными параметрами.Плейсхолдеры работают только для значений, не для структуры SQL. БД должна знать структуру запроса до подстановки параметров.
fetch()получает по одной строке, экономит память для больших результатов.fetchAll()загружает всё сразу — удобнее, но требует больше памяти.Изменения останутся "подвешенными" до конца соединения или явного commit/rollback. В MySQL с InnoDB изменения откатятся автоматически при закрытии соединения, но полагаться на это — плохая практика.
$stmt->rowCount()после execute().
🎊 Итоги
PDO — это твой главный инструмент для работы с базами данных в PHP. Запомни золотое правило:
ВСЕГДА используй prepared statements для пользовательских данных!
Это не просто "хорошая практика" — это обязательное требование безопасности.
В следующей главе мы посмотрим на паттерны работы с БД — Repository, Active Record, Query Builder — и ты поймёшь, как структурировать код для работы с базой данных на профессиональном уровне.
Удачи! 🚀