Настрой PDO так, как это делают в продакшене, структурно устрани SQL-инъекции и сделай записи надёжными с помощью транзакций.
Открыть этот урок в KodokonPDO - это единый интерфейс доступа к базам данных: MySQL, PostgreSQL или SQLite управляются одним и тем же API. Наивное подключение от продакшен-подключения отделяют три настройки: ERRMODE_EXCEPTION, чтобы каждая SQL-ошибка поднимала исключение, а не падала молча, FETCH_ASSOC по умолчанию для чистых результатов и EMULATE_PREPARES в false, чтобы подготовленные запросы действительно выполнялись сервером. Добавь charset=utf8mb4 прямо в DSN - это единственное надёжное место, где его стоит объявлять.
<?php
declare(strict_types=1);
$dsn = 'mysql:host=localhost;dbname=shop'
. ';charset=utf8mb4';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$pdo = new PDO($dsn, 'app_user', 'secret', $options);Подготовленный запрос - твоя единственная структурная защита от SQL-инъекций. Принцип такой: структура запроса с плейсхолдерами :email или ? уходит на сервер первой; данные следуют отдельно и никогда не трактуются как SQL. Склеивать переменную с запросом, пусть даже «экранированную», остаётся профессиональной ошибкой. Именованные плейсхолдеры делают код читаемым; для коротких запросов достаточно позиционных.
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
PDO::ATTR_ERRMODE,
PDO::ERRMODE_EXCEPTION,
);
$pdo->exec(
'CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL
)'
);
$insert = $pdo->prepare(
'INSERT INTO users (email) VALUES (:email)'
);
$insert->execute(['email' => 'lea@example.com']);
$query = $pdo->prepare(
'SELECT id, email FROM users
WHERE email = :email'
);
$query->execute(['email' => 'lea@example.com']);
var_dump($query->fetch(PDO::FETCH_ASSOC));Транзакция гарантирует атомарность: либо все записи проходят, либо ни одна. Канонический паттерн: beginTransaction(), работа внутри try, commit() в конце блока, rollBack() в catch - и затем ты пробрасываешь исключение дальше, потому что спрятать сбой хуже самого сбоя. Держи транзакции короткими: любая блокировка, удерживаемая во время сетевого вызова или долгой обработки, ухудшает конкурентность во всём приложении.
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
PDO::ATTR_ERRMODE,
PDO::ERRMODE_EXCEPTION,
);
$pdo->exec(
'CREATE TABLE accounts (
id INTEGER PRIMARY KEY,
balance INTEGER NOT NULL
)'
);
$pdo->exec(
'INSERT INTO accounts (balance)
VALUES (100), (20)'
);
$pdo->beginTransaction();
try {
$debit = $pdo->prepare(
'UPDATE accounts
SET balance = balance - :n
WHERE id = :id'
);
$debit->execute(['n' => 50, 'id' => 1]);
$credit = $pdo->prepare(
'UPDATE accounts
SET balance = balance + :n
WHERE id = :id'
);
$credit->execute(['n' => 50, 'id' => 2]);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}
echo 'Transfer done';:param в подготовленном запросе?catch в каноническом паттерне транзакции?commit(), чтобы сохранить то, что успело пройтиrollBack(), а затем пробросить исключение дальше