Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 33 · Практика

Транзакции, гонки и идемпотентность заказа

Последний товар, двойной платёж, откат и outbox: надёжность при параллельных запросах.

Транзакция: несколько изменений должны завершиться вместе

При переводе баллов нужно уменьшить один баланс и увеличить другой. Если вторая операция не выполнится, первую нельзя оставить единственным результатом. Транзакция объединяет изменения: commit фиксирует, rollBack отменяет.

Что уже пригодится. первая выборка PDO, исключения

Группа изменений
  1. beginTransaction: начать
  2. Выполнить необходимые записи
  3. Успех → commit
  4. Ошибка → rollBack

Это схема возможных исходов, не указание выполнить commit и rollBack подряд. Транзакция базы не отменяет уже отправленное письмо или HTTP-запрос.

Наблюдаем отмену измененияСамостоятельный пример
<?php
declare(strict_types=1);

$db = new PDO('sqlite::memory:');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('CREATE TABLE balances (id INTEGER PRIMARY KEY, points INTEGER NOT NULL)');
$db->exec('INSERT INTO balances VALUES (1, 100)');
$db->beginTransaction();
$db->exec('UPDATE balances SET points = 70 WHERE id = 1');
$db->rollBack();
echo $db->query('SELECT points FROM balances WHERE id = 1')->fetchColumn(), "\n";
Ожидаемый результат
100

После UPDATE внутри транзакции значение временно изменилось. rollBack отменил эту запись, и чтение снова возвращает 100. Здесь отмена намеренная; в рабочем сценарии решение принимает обработчик успешного результата или исключения. SQLite в памяти изолирует опыт от реальных данных.

Следующая ступень. Далее добавляем обработку исключений, конкурентные заказы и блокировки. Транзакция сама по себе не делает любой алгоритм защищённым от двух одновременных запросов.

Зачем несколько изменений объединяют

Наглядная модель

Остаток и заказ должны измениться вместе

Два покупателя хотят последний товар. Одного предварительного чтения «остаток = 1» недостаточно.

  1. BEGIN

    Начинаем короткую транзакцию в одной базе. Незавершённые изменения ещё не фиксируем.

  2. Условно уменьшить остаток

    UPDATE ... WHERE stock >= 1. Нет изменённой строки — товара уже не хватило: переходим к откату.

  3. Записать заказ и outbox

    В той же транзакции сохраняем позиции заказа и событие для последующего уведомления.

  4. Завершить транзакцию

    Исход определяет, какие изменения останутся в базе.

    • COMMIT: всё получилось

      База фиксирует изменения. Отдельный worker затем читает outbox и доставляет уведомление.

    • ROLLBACK: произошёл отказ

      Изменения этой транзакции откатываются; незавершённый заказ не выдаётся как успешный.

Транзакция БД не откатывает уже отправленное письмо или запрос банку. Внешние действия требуют отдельного протокола.

Оформление заказа меняет остаток, создаёт заказ и сохраняет его позиции. Если процесс оборвётся посередине, недопустимо получить списанный товар без заказа или заказ без позиций. Транзакция объединяет изменения в одной поддерживающей её базе: либо фиксируются все, либо откатываются. Она не распространяется автоматически на письмо, HTTP-запрос платёжному сервису и файл на диске.

Начало транзакции не делает любую последовательность защищённой от гонки. Два покупателя могут прочитать остаток 1 и оба решить, что товар доступен. Решение — атомарная условная запись либо корректная блокировка с учётом уровня изоляции. Ограничения UNIQUE, FOREIGN KEY и CHECK сохраняют инварианты даже тогда, когда ошибся другой путь приложения. PHP-валидация улучшает сообщения, но база остаётся последней линией целостности.

Транзакция должна быть короткой. Проверку формата, расчёт очевидных значений и внешний HTTP-вызов не стоит выполнять под удерживаемыми блокировками. При этом данные, влияющие на итоговую запись, должны быть актуальны в момент решения: просто вынести чтение остатка наружу нельзя. Разделите подготовку и атомарную фиксацию, а долгие внешние действия оформляйте отдельными этапами.

Оформление заказа
  1. Проверить вход и пользователя
  2. BEGIN
  3. Условно зарезервировать остаток
  4. Создать заказ + событие outbox
  5. COMMIT
  6. Воркер доставляет событие с повторами

Условный UPDATE предотвращает продажу сверх остатка

Один экземпляр и две последовательные попыткиСамостоятельный пример
<?php
declare(strict_types=1);

$db = new PDO('sqlite::memory:',null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
$db->exec('CREATE TABLE stock (sku TEXT PRIMARY KEY, available INTEGER NOT NULL CHECK(available >= 0))');
$db->exec("INSERT INTO stock VALUES ('book',1)");
$db->exec('CREATE TABLE orders (id INTEGER PRIMARY KEY, sku TEXT NOT NULL)');
function reserve(PDO $db,string $sku): void {
    $db->beginTransaction();
    try {
        $change = $db->prepare('UPDATE stock SET available=available-1 WHERE sku=? AND available>=1');
        $change->execute([$sku]);
        if ($change->rowCount() !== 1) throw new DomainException('sold-out');
        $db->prepare('INSERT INTO orders(sku) VALUES (?)')->execute([$sku]);
        $db->commit();
    } catch (Throwable $error) {
        if ($db->inTransaction()) $db->rollBack();
        throw $error;
    }
}
foreach ([1,2] as $attempt) {
    try { reserve($db,'book'); echo "reserved
"; }
    catch (DomainException $error) { echo $error->getMessage()."
"; }
}
echo $db->query('SELECT COUNT(*) FROM orders')->fetchColumn()." order
";
Ожидаемый результат
reserved
sold-out
1 order

SQLite-тест демонстрирует инвариант и ветку отказа. Он не имитирует два процесса одновременно; конкурентный тест выполняют отдельно на целевой СУБД. Проверка rowCount относится к UPDATE, а не SELECT.

Проверяем именно откат

Ошибка уникальности не оставляет половину операцииСамостоятельный пример
<?php
declare(strict_types=1);

$db = new PDO('sqlite::memory:',null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
$db->exec('CREATE TABLE users (email TEXT UNIQUE)');
$db->exec("INSERT INTO users VALUES ('a@example.com')");
try {
    $db->beginTransaction();
    $statement = $db->prepare('INSERT INTO users VALUES (?)');
    $statement->execute(['b@example.com']);
    $statement->execute(['a@example.com']);
    $db->commit();
} catch (PDOException $error) {
    if ($db->inTransaction()) $db->rollBack();
    echo "rolled-back
";
}
echo $db->query('SELECT COUNT(*) FROM users')->fetchColumn()."
";
Ожидаемый результат
rolled-back
1

Вторая вставка нарушает UNIQUE, а первая вставка той же транзакции тоже исчезает. В пользовательский ответ не выводится SQL или текст драйвера.

Поддержка транзакций зависит от драйвера, движка таблиц и операций. В MySQL некоторые DDL-команды выполняют неявный COMMIT; не смешивайте миграцию схемы с обычной транзакцией заказа. PDO не предоставляет универсальные вложенные транзакции: повторный beginTransaction обычно ошибка. Сервисам нужен согласованный владелец транзакции или явные savepoints, зависящие от СУБД.

Дедлок — ожидаемое следствие конкуренции, а не повод бесконечно повторять любую ошибку. Повторяют всю короткую транзакцию только для распознанного временного кода, ограниченное число раз с паузой и общим дедлайном. Одинаковый порядок блокировки нескольких товаров уменьшает риск. Внешние побочные эффекты внутри повторяемого блока недопустимы: письмо может уйти дважды.

Письмо и платёж живут за границей транзакции

Outbox — таблица событий, запись в которую создаётся в той же транзакции, что и заказ. Отдельный воркер читает готовые события и отправляет письмо или сообщение. Если процесс упал после COMMIT, событие осталось. Если упал после отправки, но до отметки delivered, повтор возможен. Поэтому доставка обычно «как минимум один раз», а получатель должен быть идемпотентным.

Для платёжного API применяют ключ идемпотентности, согласованный с провайдером. При таймауте неизвестно, произошёл платёж или нет: повтор с новым ключом способен списать деньги снова. Сначала уточняют состояние по прежнему ключу/идентификатору, затем продолжают протокол. Локальный order.status=pending отражает неопределённость честнее, чем немедленный failed.

Хранилище ключей идемпотентности должно связывать ключ с пользователем, типом операции и отпечатком тела. Тот же ключ с другим заказом — конфликт, а не молчаливое возвращение чужого результата. UNIQUE обеспечивает защиту от двух одновременно пришедших запросов. Проверка «SELECT, затем INSERT если нет» без ограничения оставляет гонку. Срок хранения ключа выбирают по реальному окну повторов и финансовым требованиям.

Изоляция и оптимистическая версия

Уровень изоляции определяет, какие изменения других транзакций видны и какие аномалии допускаются. Название метода beginTransaction не выбирает универсальный уровень для всех драйверов. Read Committed, Repeatable Read и Serializable имеют разные свойства и цену; настройки надо сверять с документацией конкретной СУБД и проверять конкурентными тестами. Повышение изоляции не отменяет необходимость обработать конфликт и повтор.

Для редактирования карточки товара удобна оптимистическая версия. Вместе с формой сервер отдаёт version, а сохранение выполняет UPDATE ... SET version=version+1 WHERE id=:id AND version=:old_version. Ноль изменённых строк означает, что запись уже изменилась или исчезла. Пользователь получает сравнение изменений, а не молчаливую потерю чужого текста. Переданный version не заменяет проверку владельца и CSRF.

Для нескольких товаров блокировки берут в одном стабильном порядке, например по ID. Это снижает вероятность взаимного ожидания. Не показывайте покупателю техническое слово deadlock; после ограниченного безопасного повтора верните понятную ошибку временной недоступности. В журнале оставьте код СУБД, длительность и request_id без чувствительных параметров заказа.

Практика: проверьте решение, а не только успешный путь

Письмо отправили до COMMIT, а сохранение заказа завершилось ошибкой. Какой тест обнаружит проблему и как изменить схему?

Разбор решения

В тесте спровоцировать ошибку последней записи и проверить, что внешняя отправка не произошла. Поместить событие в outbox внутри транзакции, а отправку вынести в воркер. Отдельно проверить повтор обработки одного события и восстановление после падения между отправкой и отметкой.

Резерв и запись заказа: два изменения с одним решением

Заказ не должен появиться без резерва товара, а остаток — уменьшиться без заказа. После отдельных begin/commit/rollback соберём полный сценарий с двумя попытками: заказать 2 штуки и затем 9.

Резерв и запись заказа: два изменения с одним решением
  1. Молоток 8 → условный UPDATE на 2 → остаток 6
  2. INSERT orders(id1, product_id1, quantity2) → COMMIT
  3. Попытка 9 при остатке 6 → UPDATE не меняет строк → ROLLBACK
  4. SELECT показывает остаток 6 и единственный заказ на 2

WHERE stock >= ? помещает проверку в то же изменение, убирая разрыв между отдельными SELECT и UPDATE. DomainException здесь означает заранее предусмотренный отказ «мало товара», а внешний Throwable — другую ошибку, которую нельзя объявлять обычной нехваткой.

Запись в этом примереКак читать
require __DIR__ . ...Выполняет указанный PHP-файл из папки примера. Точка склеивает путь. Файл включён в архив; он создаёт соединение и исходные данные.
$db->method(...)Стрелка вызывает действие у конкретного соединения, команды или результата. Скобки содержат входные значения; результат можно сохранить через =.
Класс::ИМЯДвойное двоеточие обращается к константе режима либо статическому методу класса; это не выбор колонки SQL.
foreach ($rows as $row)Берёт по одной строке из набора. Блок между { и } повторяется для каждой строки; при пустом наборе не выполняется.
try / catch / finallytry содержит действие, catch обрабатывает отказ, finally выполняет освобождение ресурсов и при успехе, и при исключении.
[...]Квадратные скобки задают массив либо читают его элемент. Ключ имени колонки и номер позиции — разные способы адресации.
Резерв и запись заказа: два изменения с одним решениемСамостоятельный пример
<?php
require __DIR__ . '/catalog.php';
$db->exec('CREATE TABLE orders (id INTEGER PRIMARY KEY, product_id INTEGER NOT NULL, quantity INTEGER NOT NULL CHECK (quantity > 0))');
foreach ([2, 9] as $quantity) {
    $db->beginTransaction();
    try {
        $reserve = $db->prepare('UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?');
        $reserve->execute([$quantity, 1, $quantity]);
        if ($reserve->rowCount() !== 1) { throw new DomainException('Недостаточно товара'); }
        $order = $db->prepare('INSERT INTO orders (product_id, quantity) VALUES (?, ?)');
        $order->execute([1, $quantity]);
        $db->commit();
        echo 'Заказ на ', $quantity, " шт. сохранён
";
    } catch (DomainException $error) {
        $db->rollBack();
        echo "Недостаточно товара: заказ не создан
";
    } catch (Throwable $error) {
        if ($db->inTransaction()) { $db->rollBack(); }
        throw $error;
    }
}
$row = $db->query('SELECT name, stock FROM products WHERE id = 1')->fetch(PDO::FETCH_ASSOC);
echo $row['name'], ' — ', $row['stock'], " шт.
";
foreach ($db->query('SELECT id, quantity FROM orders ORDER BY id') as $order) {
    echo 'Заказ #', $order['id'], ': ', $order['quantity'], " шт.
";
}
Ожидаемый результат
Заказ на 2 шт. сохранён
Недостаточно товара: заказ не создан
Молоток — 6 шт.
Заказ #1: 2 шт.

Все параметры для первого UPDATE — [2,1,2], для второго — [9,1,9]. Вторая вставка orders вообще не выполняется. SELECT после обоих решений подтверждает и товар, и созданный заказ.

Эти же строки в табличном виде. CLI-пример выше печатает текст; таблица помогает увидеть соответствие названия и остатка. Как получить такую таблицу настоящим HTML-обработчиком.

Товар / строкаНаличие
Молоток6 шт.

Проверьте изменение требования. Количество здесь доверенное положительное целое. В HTTP сначала проверяют строковый ввод и право заказа; для повторной отправки нужен ключ идемпотентности. Ошибка записи заказа также должна откатить резерв. Это SQLite-транзакция; DDL вынесен до begin. Задание: заменить 9 на 6 — появится второй заказ, итоговый остаток 0.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

PDO::beginTransactionОбъединить несколько SQL-изменений в одну подтверждаемую или отменяемую работу.

PHP 5.1+; примеры PHP 8.2+ · pdo_sqlite

PDO::beginTransaction(): bool

Связать изменение остатка с исходом заказа. beginTransaction открывает границу: последующие изменения можно подтвердить вместе или отменить.

Данные и действие: PDO::beginTransaction
  1. PDO::beginTransaction()
  2. Мы вычитаем 2 из 8 внутри транзакции. Итоговый SELECT показывает 6 при подтверждении либо 8 при отмене; это наблюдаемая разница в данных.
  3. После выполнения: Резерв подтверждён · Молоток — 6 шт.

В архиве catalog.php открывает отдельную учебную базу и выполняет schema.sql: 1 — Молоток, 8 шт.; 2 — Перчатки, 15 шт.; 3 — Кисть, 0 шт. База находится в памяти и начинает работу с этих данных при каждом запуске. Распакуйте все файлы вместе и запустите php example.php. Полная подготовка и схема.

Запись в этом примереКак читать
$db->method(...)Стрелка вызывает действие у конкретного соединения, команды или результата. Скобки содержат входные значения; результат можно сохранить через =.
Класс::ИМЯДвойное двоеточие обращается к константе режима либо статическому методу класса; это не выбор колонки SQL.
[...]Квадратные скобки задают массив либо читают его элемент. Ключ имени колонки и номер позиции — разные способы адресации.
Связать изменение остатка с исходом заказаСамостоятельный пример
<?php
require __DIR__ . '/catalog.php';
$db->beginTransaction();
$db->exec('UPDATE products SET stock = stock - 2 WHERE id = 1');
$db->commit();
echo "Резерв подтверждён\n";
$statement = $db->query('SELECT name, stock FROM products WHERE id = 1');
$row = $statement->fetch(PDO::FETCH_ASSOC);
echo $row['name'], ' — ', $row['stock'], " шт.\n";
Ожидаемый результат
Резерв подтверждён
Молоток — 6 шт.

Мы вычитаем 2 из 8 внутри транзакции. Итоговый SELECT показывает 6 при подтверждении либо 8 при отмене; это наблюдаемая разница в данных.

Эти же строки в табличном виде. CLI-пример выше печатает текст; таблица помогает увидеть соответствие названия и остатка. Как получить такую таблицу настоящим HTML-обработчиком.

Товар / строкаНаличие
Молоток6 шт.

Если требование изменится. В реальном заказе добавьте запись заказа и условное списание в ту же транзакцию, а при исключении откатывайте только активную. Внешние HTTP-запросы и долгие ожидания не держат внутри транзакции. DDL/implicit commit разных СУБД разобраны в полном контракте ниже.

Полный контракт, ограничения и следующие варианты. Ниже сохранены параметры, возвращаемые значения, ошибки, версии и дополнительные задачи этой операции. Перед каждым более длинным примером сначала прочитайте его подготовку и ход действий.

Объединить несколько SQL-изменений в одну подтверждаемую или отменяемую работу.

$db обозначает соединение, $statement — подготовленную команду и её текущий результат. -> вызывает метод конкретного объекта, :: — статический метод или константу класса. Круглые скобки содержат аргументы; результат справа от = сохраняется в переменной. Это отдельные шаги: выполнить SQL и прочитать полученные строки. Основы: объекты, аргументы и возврат.

АргументДоговор
АргументыНет.

true при начале, false при отказе; уже активная транзакция либо отсутствие поддержки драйвером — PDOException даже при SILENT. Проверка относится к драйверу, не гарантирует транзакционность каждой таблицы (пример: MyISAM). Обычный autocommit меняется до commit/rollBack. DDL в MySQL/Oracle может неявно зафиксировать изменения: не смешивайте миграцию схемы с бизнес-транзакцией. Изоляция и блокировки зависят от СУБД/уровня; транзакция не означает автоматический запрет любых изменений другими клиентами.

Путь данных
  1. beginTransaction открывает границу
  2. UPDATE временно меняет цену 120→999
  3. rollBack отменяет текущую работу
  4. Снова видна цена 120

Как читать этот вариант. Свои незавершённые изменения соединение видит. Откат вернул прежнюю цену. Это управление данными БД: отправленное письмо или HTTP-запрос rollback не отменит.

Начать группу изменений, которую ещё можно отменитьСамостоятельный пример
<?php
$db = new PDO('sqlite::memory:', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$db->exec('CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price INTEGER NOT NULL)');
$db->exec("INSERT INTO products VALUES (1, 'Pen', 120), (2, 'Notebook', 350), (3, 'Box', 200)");
$db->beginTransaction();
$db->exec('UPDATE products SET price = 999 WHERE id = 1');
echo $db->query('SELECT price FROM products WHERE id = 1')->fetchColumn(), "\n";
$db->rollBack();
echo $db->query('SELECT price FROM products WHERE id = 1')->fetchColumn(), "\n";
Ожидаемый результат
999
120

Как читать этот вариант. У PDO нет универсальных вложенных транзакций. Подзадачи должны знать, кто владеет внешней транзакцией; при необходимости изучают SAVEPOINT конкретной СУБД.

Не выдавать вложенный begin за независимую транзакциюСамостоятельный пример
<?php
$db = new PDO('sqlite::memory:', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$db->beginTransaction();
try { $db->beginTransaction(); } catch (PDOException $error) { echo "Уже открыта\n"; }
$db->rollBack();
Ожидаемый результат
Уже открыта

Рядом: PDO::commit(), PDO::rollBack(), PDO::inTransaction().

Дополнительные ранее разобранные сценарии.

Переводит соединение из autocommit в транзакцию и возвращает bool. Если транзакция уже активна или драйвер не поддерживает её, возможно исключение.

Как читать этот вариант. Явно завершаем учебную транзакцию.

Проверить активностьСамостоятельный пример
<?php
declare(strict_types=1);

$db=new PDO('sqlite::memory:',null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
$db->exec('CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, stock INTEGER)');
$db->exec("INSERT INTO products VALUES (1,'Tea',3),(2,'Coffee',0)");
$db->beginTransaction();
echo $db->inTransaction() ? "active\n" : "none\n";
$db->rollBack();
Ожидаемый результат
active

Как читать этот вариант. В рабочем сценарии добавьте try/catch с откатом при любом Throwable.

Объединить два измененияСамостоятельный пример
<?php
declare(strict_types=1);

$db=new PDO('sqlite::memory:',null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
$db->exec('CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, stock INTEGER)');
$db->exec("INSERT INTO products VALUES (1,'Tea',3),(2,'Coffee',0)");
$db->beginTransaction();
$db->exec('UPDATE products SET stock=stock+2 WHERE id=1');
$db->exec('UPDATE products SET stock=stock+1 WHERE id=2');
$db->commit();
echo $db->query('SELECT SUM(stock) FROM products')->fetchColumn()."\n";
Ожидаемый результат
6

Рядом по задаче. commit фиксирует, rollBack отменяет; условный UPDATE и outbox.

PDO::commitПодтвердить изменения открытой транзакции и вернуться в autocommit.

PHP 5.1+; примеры PHP 8.2+ · pdo_sqlite

PDO::commit(): bool

Связать изменение остатка с исходом заказа. commit подтверждает накопленные изменения. После него прежнее состояние не возвращается вызовом rollBack.

Данные и действие: PDO::commit
  1. PDO::commit()
  2. Мы вычитаем 2 из 8 внутри транзакции. Итоговый SELECT показывает 6 при подтверждении либо 8 при отмене; это наблюдаемая разница в данных.
  3. После выполнения: Резерв подтверждён · Молоток — 6 шт.

В архиве catalog.php открывает отдельную учебную базу и выполняет schema.sql: 1 — Молоток, 8 шт.; 2 — Перчатки, 15 шт.; 3 — Кисть, 0 шт. База находится в памяти и начинает работу с этих данных при каждом запуске. Распакуйте все файлы вместе и запустите php example.php. Полная подготовка и схема.

Запись в этом примереКак читать
$db->method(...)Стрелка вызывает действие у конкретного соединения, команды или результата. Скобки содержат входные значения; результат можно сохранить через =.
Класс::ИМЯДвойное двоеточие обращается к константе режима либо статическому методу класса; это не выбор колонки SQL.
[...]Квадратные скобки задают массив либо читают его элемент. Ключ имени колонки и номер позиции — разные способы адресации.
Связать изменение остатка с исходом заказаСамостоятельный пример
<?php
require __DIR__ . '/catalog.php';
$db->beginTransaction();
$db->exec('UPDATE products SET stock = stock - 2 WHERE id = 1');
$db->commit();
echo "Резерв подтверждён\n";
$statement = $db->query('SELECT name, stock FROM products WHERE id = 1');
$row = $statement->fetch(PDO::FETCH_ASSOC);
echo $row['name'], ' — ', $row['stock'], " шт.\n";
Ожидаемый результат
Резерв подтверждён
Молоток — 6 шт.

Мы вычитаем 2 из 8 внутри транзакции. Итоговый SELECT показывает 6 при подтверждении либо 8 при отмене; это наблюдаемая разница в данных.

Эти же строки в табличном виде. CLI-пример выше печатает текст; таблица помогает увидеть соответствие названия и остатка. Как получить такую таблицу настоящим HTML-обработчиком.

Товар / строкаНаличие
Молоток6 шт.

Если требование изменится. В реальном заказе добавьте запись заказа и условное списание в ту же транзакцию, а при исключении откатывайте только активную. Внешние HTTP-запросы и долгие ожидания не держат внутри транзакции. DDL/implicit commit разных СУБД разобраны в полном контракте ниже.

Полный контракт, ограничения и следующие варианты. Ниже сохранены параметры, возвращаемые значения, ошибки, версии и дополнительные задачи этой операции. Перед каждым более длинным примером сначала прочитайте его подготовку и ход действий.

Подтвердить изменения открытой транзакции и вернуться в autocommit.

$db обозначает соединение, $statement — подготовленную команду и её текущий результат. -> вызывает метод конкретного объекта, :: — статический метод или константу класса. Круглые скобки содержат аргументы; результат справа от = сохраняется в переменной. Это отдельные шаги: выполнить SQL и прочитать полученные строки. Основы: объекты, аргументы и возврат.

АргументДоговор
АргументыНет.

true/false; без активной транзакции PDOException независимо от ERRMODE. Сам commit тоже может не удаться, например из-за блокировки/отложенного ограничения или потери соединения. При неясном результате сетевого отказа не повторяйте платёж или INSERT вслепую: нужны идентификатор операции и сверка. Для локального rollback после ошибки сначала проверяют inTransaction; не подавляют исходную ошибку ошибкой очистки.

Путь данных
  1. Несколько SQL-изменений подготовлены
  2. commit просит БД подтвердить их
  3. true → транзакция завершена
  4. Следующие команды снова самостоятельны

Как читать этот вариант. После commit нет активной транзакции, цена сохранена. Последующий rollback не является кнопкой отмены уже подтверждённого заказа.

Зафиксировать согласованное изменениеСамостоятельный пример
<?php
$db = new PDO('sqlite::memory:', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$db->exec('CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price INTEGER NOT NULL)');
$db->exec("INSERT INTO products VALUES (1, 'Pen', 120), (2, 'Notebook', 350), (3, 'Box', 200)");
$db->beginTransaction();
$db->exec('UPDATE products SET price = 130 WHERE id = 1');
$ok = $db->commit();
var_dump($ok, $db->inTransaction());
echo $db->query('SELECT price FROM products WHERE id = 1')->fetchColumn(), "\n";
Ожидаемый результат
bool(true)
bool(false)
130

Как читать этот вариант. Если вспомогательная функция неожиданно сделала commit чужой транзакции, внешнему коду уже нечего фиксировать. Поэтому границы транзакции задают на уровне законченного действия приложения.

Обнаружить ошибку владения транзакциейСамостоятельный пример
<?php
$db = new PDO('sqlite::memory:', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
try { $db->commit(); } catch (PDOException $error) { echo "Нечего фиксировать\n"; }
Ожидаемый результат
Нечего фиксировать

Рядом: PDO::beginTransaction(), PDO::inTransaction().

Дополнительные ранее разобранные сценарии.

Подтверждает изменения и возвращает соединение к autocommit. Возвращает bool; отсутствие активной транзакции или сбой фиксации может вызвать исключение.

Как читать этот вариант. Значение доступно после завершения транзакции.

Зафиксировать изменение остаткаСамостоятельный пример
<?php
declare(strict_types=1);

$db=new PDO('sqlite::memory:',null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
$db->exec('CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, stock INTEGER)');
$db->exec("INSERT INTO products VALUES (1,'Tea',3),(2,'Coffee',0)");
$db->beginTransaction();
$db->exec('UPDATE products SET stock=7 WHERE id=1');
$db->commit();
echo $db->query('SELECT stock FROM products WHERE id=1')->fetchColumn()."\n";
Ожидаемый результат
7

Как читать этот вариант. Даже пустая транзакция должна иметь понятное завершение.

Проверить состояние после фиксацииСамостоятельный пример
<?php
declare(strict_types=1);

$db=new PDO('sqlite::memory:',null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
$db->exec('CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, stock INTEGER)');
$db->exec("INSERT INTO products VALUES (1,'Tea',3),(2,'Coffee',0)");
$db->beginTransaction();
$db->commit();
echo $db->inTransaction() ? "active\n" : "finished\n";
Ожидаемый результат
finished

Рядом по задаче. rollBack для отмены до фиксации; outbox для надёжных внешних реакций.

PDO::rollBackОтменить изменения текущей незавершённой транзакции.

PHP 5.1+; примеры PHP 8.2+ · pdo_sqlite

PDO::rollBack(): bool

Связать изменение остатка с исходом заказа. rollBack отменяет неподтверждённые изменения текущей транзакции.

Данные и действие: PDO::rollBack
  1. PDO::rollBack()
  2. Мы вычитаем 2 из 8 внутри транзакции. Итоговый SELECT показывает 6 при подтверждении либо 8 при отмене; это наблюдаемая разница в данных.
  3. После выполнения: Заказ отменён, остаток восстановлен · Молоток — 8 шт.

В архиве catalog.php открывает отдельную учебную базу и выполняет schema.sql: 1 — Молоток, 8 шт.; 2 — Перчатки, 15 шт.; 3 — Кисть, 0 шт. База находится в памяти и начинает работу с этих данных при каждом запуске. Распакуйте все файлы вместе и запустите php example.php. Полная подготовка и схема.

Запись в этом примереКак читать
$db->method(...)Стрелка вызывает действие у конкретного соединения, команды или результата. Скобки содержат входные значения; результат можно сохранить через =.
Класс::ИМЯДвойное двоеточие обращается к константе режима либо статическому методу класса; это не выбор колонки SQL.
[...]Квадратные скобки задают массив либо читают его элемент. Ключ имени колонки и номер позиции — разные способы адресации.
Связать изменение остатка с исходом заказаСамостоятельный пример
<?php
require __DIR__ . '/catalog.php';
$db->beginTransaction();
$db->exec('UPDATE products SET stock = stock - 2 WHERE id = 1');
$db->rollBack();
echo "Заказ отменён, остаток восстановлен\n";
$statement = $db->query('SELECT name, stock FROM products WHERE id = 1');
$row = $statement->fetch(PDO::FETCH_ASSOC);
echo $row['name'], ' — ', $row['stock'], " шт.\n";
Ожидаемый результат
Заказ отменён, остаток восстановлен
Молоток — 8 шт.

Мы вычитаем 2 из 8 внутри транзакции. Итоговый SELECT показывает 6 при подтверждении либо 8 при отмене; это наблюдаемая разница в данных.

Эти же строки в табличном виде. CLI-пример выше печатает текст; таблица помогает увидеть соответствие названия и остатка. Как получить такую таблицу настоящим HTML-обработчиком.

Товар / строкаНаличие
Молоток8 шт.

Если требование изменится. В реальном заказе добавьте запись заказа и условное списание в ту же транзакцию, а при исключении откатывайте только активную. Внешние HTTP-запросы и долгие ожидания не держат внутри транзакции. DDL/implicit commit разных СУБД разобраны в полном контракте ниже.

Полный контракт, ограничения и следующие варианты. Ниже сохранены параметры, возвращаемые значения, ошибки, версии и дополнительные задачи этой операции. Перед каждым более длинным примером сначала прочитайте его подготовку и ход действий.

Отменить изменения текущей незавершённой транзакции.

$db обозначает соединение, $statement — подготовленную команду и её текущий результат. -> вызывает метод конкретного объекта, :: — статический метод или константу класса. Круглые скобки содержат аргументы; результат справа от = сохраняется в переменной. Это отдельные шаги: выполнить SQL и прочитать полученные строки. Основы: объекты, аргументы и возврат.

АргументДоговор
АргументыНет.

true/false; без активной транзакции исключение независимо от ERRMODE. Возвращает autocommit. Не отменяет внешние эффекты, прошлый commit, изменения нетранзакционных таблиц и уже выполненную DDL с implicit commit. PDO автоматически откатывает незавершённую транзакцию при закрытии, если она начата через beginTransaction; не стройте договор на ручном SQL BEGIN и не оставляйте очистку только завершению процесса.

Путь данных
  1. Первый UPDATE изменил цену
  2. Второй INSERT нарушил уникальность
  3. catch проверяет inTransaction
  4. rollBack возвращает цену до первой команды

Как читать этот вариант. Второй INSERT нарушил уникальность ID. Мы вернули также первое обновление. В реальном приложении после записи безопасной диагностики ошибку передают обработчику либо возвращают явный отказ, а не сообщают успех.

Не оставить половину изменения после ошибкиСамостоятельный пример
<?php
$db = new PDO('sqlite::memory:', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$db->exec('CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price INTEGER NOT NULL)');
$db->exec("INSERT INTO products VALUES (1, 'Pen', 120), (2, 'Notebook', 350), (3, 'Box', 200)");
$db->beginTransaction();
try {
    $db->exec('UPDATE products SET price = 1 WHERE id = 1');
    $db->exec("INSERT INTO products VALUES (1, 'Duplicate', 20)");
    $db->commit();
} catch (PDOException $error) {
    if ($db->inTransaction()) { $db->rollBack(); }
    echo "Отменено\n";
}
echo $db->query('SELECT price FROM products WHERE id = 1')->fetchColumn(), "\n";
Ожидаемый результат
Отменено
120

Как читать этот вариант. Защитная проверка относится к своей операции очистки. Она не разрешает вложенной функции отменять транзакцию, принадлежащую вызывающему коду.

Проверить наличие работы перед отменойСамостоятельный пример
<?php
$db = new PDO('sqlite::memory:', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
if ($db->inTransaction()) { $db->rollBack(); } else { echo "Открытой транзакции нет\n"; }
Ожидаемый результат
Открытой транзакции нет

Рядом: PDO::beginTransaction(), PDO::inTransaction().

Дополнительные ранее разобранные сценарии.

Откатывает поддерживаемые изменения и завершает транзакцию. Возвращает bool; без активной транзакции возможно исключение.

Как читать этот вариант. Исходный остаток восстановлен.

Отмена измененияСамостоятельный пример
<?php
declare(strict_types=1);

$db=new PDO('sqlite::memory:',null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
$db->exec('CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, stock INTEGER)');
$db->exec("INSERT INTO products VALUES (1,'Tea',3),(2,'Coffee',0)");
$db->beginTransaction();
$db->exec('UPDATE products SET stock=99 WHERE id=1');
$db->rollBack();
echo $db->query('SELECT stock FROM products WHERE id=1')->fetchColumn()."\n";
Ожидаемый результат
3

Как читать этот вариант. Удаление не сохранилось после отмены сценария.

Отмена при доменном исключенииСамостоятельный пример
<?php
declare(strict_types=1);

$db=new PDO('sqlite::memory:',null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
$db->exec('CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, stock INTEGER)');
$db->exec("INSERT INTO products VALUES (1,'Tea',3),(2,'Coffee',0)");
try {
    $db->beginTransaction();
    $db->exec('DELETE FROM products WHERE id=2');
    throw new DomainException('cancelled');
} catch (DomainException) { if ($db->inTransaction()) $db->rollBack(); }
echo $db->query('SELECT COUNT(*) FROM products')->fetchColumn()."\n";
Ожидаемый результат
2

Рядом по задаче. commit фиксирует; finally полезен для ресурсов, но выбор commit/rollback должен быть явным.

PDO::inTransactionУзнать, остаётся ли на этом соединении активная транзакция.

PHP 5.1+; примеры PHP 8.2+ · pdo_sqlite

PDO::inTransaction(): bool

Связать изменение остатка с исходом заказа. inTransaction сообщает, есть ли активная транзакция. Само чтение состояния не подтверждает её и не отменяет.

Данные и действие: PDO::inTransaction
  1. PDO::inTransaction()
  2. Мы вычитаем 2 из 8 внутри транзакции. Итоговый SELECT показывает 6 при подтверждении либо 8 при отмене; это наблюдаемая разница в данных.
  3. После выполнения: Резерв ещё не подтверждён · Молоток — 8 шт.

В архиве catalog.php открывает отдельную учебную базу и выполняет schema.sql: 1 — Молоток, 8 шт.; 2 — Перчатки, 15 шт.; 3 — Кисть, 0 шт. База находится в памяти и начинает работу с этих данных при каждом запуске. Распакуйте все файлы вместе и запустите php example.php. Полная подготовка и схема.

Запись в этом примереКак читать
$db->method(...)Стрелка вызывает действие у конкретного соединения, команды или результата. Скобки содержат входные значения; результат можно сохранить через =.
Класс::ИМЯДвойное двоеточие обращается к константе режима либо статическому методу класса; это не выбор колонки SQL.
[...]Квадратные скобки задают массив либо читают его элемент. Ключ имени колонки и номер позиции — разные способы адресации.
Связать изменение остатка с исходом заказаСамостоятельный пример
<?php
require __DIR__ . '/catalog.php';
$db->beginTransaction();
$db->exec('UPDATE products SET stock = stock - 2 WHERE id = 1');
if ($db->inTransaction()) { echo "Резерв ещё не подтверждён\n"; }
$db->rollBack();
$statement = $db->query('SELECT name, stock FROM products WHERE id = 1');
$row = $statement->fetch(PDO::FETCH_ASSOC);
echo $row['name'], ' — ', $row['stock'], " шт.\n";
Ожидаемый результат
Резерв ещё не подтверждён
Молоток — 8 шт.

Мы вычитаем 2 из 8 внутри транзакции. Итоговый SELECT показывает 6 при подтверждении либо 8 при отмене; это наблюдаемая разница в данных.

Эти же строки в табличном виде. CLI-пример выше печатает текст; таблица помогает увидеть соответствие названия и остатка. Как получить такую таблицу настоящим HTML-обработчиком.

Товар / строкаНаличие
Молоток8 шт.

Если требование изменится. В реальном заказе добавьте запись заказа и условное списание в ту же транзакцию, а при исключении откатывайте только активную. Внешние HTTP-запросы и долгие ожидания не держат внутри транзакции. DDL/implicit commit разных СУБД разобраны в полном контракте ниже.

Полный контракт, ограничения и следующие варианты. Ниже сохранены параметры, возвращаемые значения, ошибки, версии и дополнительные задачи этой операции. Перед каждым более длинным примером сначала прочитайте его подготовку и ход действий.

Узнать, остаётся ли на этом соединении активная транзакция.

$db обозначает соединение, $statement — подготовленную команду и её текущий результат. -> вызывает метод конкретного объекта, :: — статический метод или константу класса. Круглые скобки содержат аргументы; результат справа от = сохраняется в переменной. Это отдельные шаги: выполнить SQL и прочитать полученные строки. Основы: объекты, аргументы и возврат.

АргументДоговор
АргументыНет.

true при активной транзакции, false иначе; работает для поддерживающих транзакции драйверов. Это не доказательство того, что конкретная таблица умеет rollback или что вы владеете найденной транзакцией. После SQL-ошибки СУБД могут оставлять разное состояние; проверяйте её договор.

Путь данных
  1. Первое соединение начинает транзакцию
  2. inTransaction первого → true
  3. У второго состояние не изменяется → false

Как читать этот вариант. Метод спрашивает состояние данного драйвера/соединения, а не всего приложения. У второго PDO может быть другая транзакция.

Посмотреть три состояния одного соединенияСамостоятельный пример
<?php
$db = new PDO('sqlite::memory:', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
var_dump($db->inTransaction());
$db->beginTransaction();
var_dump($db->inTransaction());
$db->commit();
var_dump($db->inTransaction());
Ожидаемый результат
bool(false)
bool(true)
bool(false)

Как читать этот вариант. Объекты соединений владеют разным состоянием. Два новых :memory: к тому же имеют разные базы; общий файл не сделал бы транзакции общими.

Не спутать две независимые базы в памятиСамостоятельный пример
<?php
$first = new PDO('sqlite::memory:');
$second = new PDO('sqlite::memory:');
$first->beginTransaction();
var_dump($first->inTransaction(), $second->inTransaction());
$first->rollBack();
Ожидаемый результат
bool(true)
bool(false)

Рядом: PDO::beginTransaction(), PDO::rollBack().

Дополнительные ранее разобранные сценарии.

Возвращает bool о состоянии, которое знает драйвер. Не сообщает уровень изоляции и не доказывает отсутствие блокировок.

Как читать этот вариант. Новое соединение не начинает прикладную транзакцию автоматически.

Начальное состояниеСамостоятельный пример
<?php
declare(strict_types=1);

$db=new PDO('sqlite::memory:',null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
$db->exec('CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, stock INTEGER)');
$db->exec("INSERT INTO products VALUES (1,'Tea',3),(2,'Coffee',0)");
echo $db->inTransaction() ? "active\n" : "autocommit\n";
Ожидаемый результат
autocommit

Как читать этот вариант. Вторая проверка не вызывает ошибочный повторный rollback.

Защитить очистку после отказаСамостоятельный пример
<?php
declare(strict_types=1);

$db=new PDO('sqlite::memory:',null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
$db->exec('CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, stock INTEGER)');
$db->exec("INSERT INTO products VALUES (1,'Tea',3),(2,'Coffee',0)");
$db->beginTransaction();
if ($db->inTransaction()) $db->rollBack();
if ($db->inTransaction()) $db->rollBack();
echo "cleanup finished\n";
Ожидаемый результат
cleanup finished

Рядом по задаче. beginTransaction/commit/rollBack задают жизненный цикл.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.