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

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

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

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

Глава 60 · После подключения Firebird и параметризованного SQL

Firebird: перевод между складами, откат и сохранение контекста транзакции

Две стороны перевода, отмена при отсутствующем получателе и обе retaining-границы: четыре реальных сценария с состояниями до/после и пять полных карточек.

Как не потерять товар между двумя UPDATE

Три коробки переезжают с одного склада на другой. В учёте нужно уменьшить один остаток и увеличить другой; половина операции недопустима. Здесь мы увидим успешные 7/5 вместо 10/2, восстановление 10/2 после отказа, а затем две разные границы retaining: сохранить первую партию или отменить черновик перед исправлением.

Одна накладная — один из двух исходов
До операции: склад 1 — 10; склад 2 — 2.
Транзакция tr объединяет списание 3 со склада 1 и приход 3 на склад 2.

↓ Проверяем оба изменения. Дальше выбирается только одна ветвь ↓

Ветвь «Успех»

Оба склада найдены, оба изменения выполнены.

↓ ibase_commit(tr)

Сохранено: склад 1 — 7; склад 2 — 5.

Ветвь «Отказ»

Получатель не найден после списания. Всю операцию нужно отменить.

↓ ibase_rollback(tr)

Восстановлено: склад 1 — 10; склад 2 — 2.

Это альтернативы: после успешного commit нельзя отменить уже подтверждённую операцию последующим rollback.

Транзакцию можно представить как общую папку ещё не утверждённых изменений. commit утверждает всю папку, rollback отменяет её содержимое. Это лишь модель границы: движок не копирует буквально всю таблицу в папку. Изоляция определяет, какие версии данных видят параллельные операции; блокировки и конфликты не исчезают от одной команды begin. В этой главе сначала изучается сохранность одной операции, затем продолжение контекста; многопользовательская проверка конфликтов — отдельное развитие.

Перед началом нужны соединение, запрос, результат и явный commit после подготовки данных и значения отдельно от SQL. Название расширения остаётся interbase, но примеры проверяются с отдельным поддерживаемым драйвером FirebirdSQL 3.0.1, PHP 8.2.35 и Firebird 5.0.4. Старые страницы PHP Manual относятся к встроенному расширению до PHP 7.4; современные Firebird и InterBase не считаются одним продуктом.

Что требуетсяВыбор
Сохранить две стороны одной операцииibase_trans → оба UPDATE → ibase_commit
Первый шаг прошёл, второй нарушил правилоibase_rollback всей операции
Принять независимую партию и продолжить контекстibase_commit_ret; в конце обычное завершение
Отменить текущую партию и продолжить контекстibase_rollback_ret; в конце обычное завершение

Соединение, транзакция и кто отвечает за её завершение

Одна запись таблицы представляет остаток одного склада. warehouse_id — первичный ключ; qty — целое количество с запретом отрицательных значений. Оба склада находятся в одной таблице одной учебной базы. Случайное имя таблицы принадлежит только этому запуску. Встроенный lab.php обеспечивает соединение, диагностические проверки и удаление собственной таблицы; он не управляет явной бизнес-транзакцией вместо программы.

$tr = ibase_trans(IBASE_WRITE | IBASE_CONCURRENCY | IBASE_NOWAIT, $db);
// tr — отдельная транзакция, db — соединение.
// ibase_query($tr, ...) выполняется в tr.
// ibase_query($db, ...) выбирает default-транзакцию db, а не tr.
// ibase_commit($tr) и ibase_commit($db) также выбирают разные контексты.

Новую синтаксическую конструкцию try/finally используем, чтобы незавершённая операция не осталась открытой после исключения. В try находятся рабочие шаги; finally выполняется при выходе. После успешного обычного commit/rollback tr становится null, и finally не пытается завершить его второй раз. Функция moveStock154 не открывает свою транзакцию: ей передаётся уже выбранный tr. Так одна функция не подтверждает изменения раньше владельца операции.

У setup есть короткая отдельная default-транзакция для создания исходных 10/2. Она заканчивается до начала tr. После окончательного завершения tr контрольный SELECT начинает новое чтение через db. Если бы default-снимок был открыт раньше и оставлен незавершённым, он мог бы показать старые значения; это не доказательство, что commit не работал. Все выборки этой главы ограничены двумя известными id, освобождаются помощником чтения и не сортируются повторно в PHP.

Переместить три единицы между складами одной операцией

На первом складе 10 коробок, на втором 2. Нужно переместить 3: итог должен быть 7 и 5, общий остаток — 12. Списание и приход составляют одну операцию. Если подтвердить только списание, коробки потеряются в учёте; если только приход — появятся лишние. Одна явная транзакция объединяет оба изменения.

Путь конкретных значений
  1. Таблица: склад 1 = 10, склад 2 = 2
  2. ibase_trans → отдельный ресурс tr для операции
  3. UPDATE через tr: 10−3=7; проверено affected_rows=1
  4. Второй UPDATE через тот же tr: 2+3=5
  5. ibase_commit(tr) → новый SELECT показывает 7 и 5, сумма 12

Сначала один шаг

$tr = ibase_trans(IBASE_WRITE | IBASE_CONCURRENCY | IBASE_NOWAIT, $db);
if ($tr === false) { throw new RuntimeException("Транзакция не начата"); }
// Два UPDATE должны получить $tr, а не $db.
// Только после успешных двух шагов: ibase_commit($tr).

Транзакция — граница согласованной работы: либо сохраняются оба изменения, либо оба отменяются. db обозначает соединение, tr — конкретную транзакцию внутри него. Ресурс не является номером склада. Знак | объединяет независимые настройки: WRITE разрешает изменение, CONCURRENCY выбирает снимок, NOWAIT не оставляет учебную команду ждать при конфликте. moveStock154 — наша функция из полного кода, не функция расширения: она проверяет вход, выполняет два параметризованных UPDATE и проверяет число затронутых строк. Решение commit/rollback остаётся у вызывающего кода.

Ожидаемый полезный результат

НаблюдениеРезультат
Перемещено единиц3
Склад 1: было → стало10 → 7
Склад 2: было → стало2 → 5
Общий остаток12
Подтверждение операциида
Строк изменено списанием / приходом1 / 1
Переместить три единицы между складами одной операциейСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f154Config();$db=f154Connect($config);$runtime=f154Runtime($db,$config);

    function startStock154($db) {
        $call=f154Capture(static fn()=>ibase_trans(IBASE_WRITE|IBASE_CONCURRENCY|IBASE_NOWAIT,$db));
        if ($call['value']===false || $call['warning']) {f154Failure('Транзакция не начата',$call);}
        return $call['value'];
    }
    function moveStock154($tr,string $q,int $from,int $to,int $amount): array {
        if ($amount<=0 || $from===$to) {throw new InvalidArgumentException('Нужны разные склады и положительное количество.');}
        f154Query($tr,"UPDATE $q SET qty=qty-? WHERE warehouse_id=? AND qty>=?",[$amount,$from,$amount]);
        $debited=ibase_affected_rows($tr);
        if ($debited!==1) {throw new DomainException('Склад-отправитель не найден или товара недостаточно.');}
        f154Query($tr,"UPDATE $q SET qty=qty+? WHERE warehouse_id=?",[$amount,$to]);
        $credited=ibase_affected_rows($tr);
        if ($credited!==1) {throw new DomainException('Склад-получатель не найден.');}
        // The caller commits or rolls back both statements together.
        return ['debited'=>$debited,'credited'=>$credited];
    }
    function readStock154($context,string $q): array {
        return f154Read($context,"SELECT warehouse_id AS \"warehouse_id\",qty AS \"qty\" FROM $q WHERE warehouse_id IN (1,2) ORDER BY warehouse_id");
    }

    $name=f154OwnTable($db,$owned,'stock','(warehouse_id INTEGER NOT NULL PRIMARY KEY,qty INTEGER NOT NULL CHECK(qty>=0))');
    $q=f154Qualified($name);
    f154Command($db,"INSERT INTO $q(warehouse_id,qty) VALUES(?,?)",[1,10]);
    f154Command($db,"INSERT INTO $q(warehouse_id,qty) VALUES(?,?)",[2,2]);
    f154Commit($db); // Close the setup's default transaction before explicit business work.

    $tr=startStock154($db);
    try {
        $counts=moveStock154($tr,$q,1,2,3);
        $inside=readStock154($tr,$q);
        $committed=ibase_commit($tr);
        if ($committed!==true) {f154Failure('Перемещение не подтверждено');}
        $tr=null;

    } finally {
        if (is_resource($tr)) {
            $rollback=ibase_rollback($tr);$tr=null;
            if ($rollback!==true) {f154Failure('Незавершённую транзакцию не удалось откатить');}
        }
    }

    $saved=readStock154($db,$q); // A fresh default read transaction after explicit commit.
    $rows=[['Перемещено единиц',3],['Склад 1: было → стало','10 → '.$saved[0]['qty']],['Склад 2: было → стало','2 → '.$saved[1]['qty']],
        ['Общий остаток',$saved[0]['qty']+$saved[1]['qty']],['Подтверждение операции',$committed],['Строк изменено списанием / приходом',$counts['debited'].' / '.$counts['credited']]];
    $checks=['both_updates_one_row'=>$counts===['debited'=>1,'credited'=>1],'inside_transfer'=>array_column($inside,'qty')===[7,5],
        'commit_confirmed'=>$committed===true,'fresh_read_committed'=>array_column($saved,'qty')===[7,5],'total_preserved'=>array_sum(array_column($saved,'qty'))===12];
    $observations=['before'=>[10,2],'inside_transaction'=>$inside,'committed_rows'=>$saved,'affected'=>$counts];

} finally {
    if (is_resource($db)) {try {$removed=f154Cleanup($db,$owned);} finally {ibase_close($db);}}
}

f154Report('fb154-transfer-commit','Переместить три единицы между складами одной операцией',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Перемещено единиц — 3
Склад 1: было → стало — 10 → 7
Склад 2: было → стало — 2 → 5
Общий остаток — 12
Подтверждение операции — да
Строк изменено списанием / приходом — 1 / 1

Самостоятельный пример содержит lab.php и README.txt, создаёт одну собственную таблицу с двумя складами, выполняет реальные транзакции, сохраняет HTML/JSON-отчёт и удаляет только свою учебную таблицу. Явная транзакция всегда завершается до общей очистки.

Фактический HTML-результат · Данные результата JSON.

Ошибки и границы

Перед началом записи проверены положительное количество и разные склады; условие qty>=? стоит непосредственно в UPDATE, а не только в прежнем SELECT. Это не демонстрация всех конфликтов одновременных переводов: при конфликте нужно завершить неудачную операцию, при необходимости повторить её целиком с новым контекстом и ограничением попыток. Не держите транзакцию, пока человек заполняет форму. После обычного commit ресурс tr больше не используют; переменная обнуляется. Обычная транзакция соединения после подготовки данных уже завершена, поэтому контрольный SELECT действительно начинает новое чтение.

Долгоживущий PHP-скрипт пытался настроить default-транзакцию через ibase_trans и столкнулся с путаницей между ними. Исходная ситуация на Stack Overflow. Наш код сохраняет явный ресурс tr и передаёт его в оба UPDATE и commit. db и tr не подменяют друг друга. Исторический сбой старого PHP не объявляется поведением текущего драйвера; долгую задачу делят на завершённые операции.

Измените условие задачи

Измените перемещение на 6 единиц. Как проверить, что сумма не изменилась?

Как проверить решение

Остатки станут 4 и 8, сумма останется 12. Обе команды должны изменить ровно одну строку. При попытке переместить 11 первая команда не найдёт подходящую строку; функция откажет, а finally откатит операцию.

Отменить списание, если склад-получатель не найден

В документе перемещения ошибочно указан склад 99. Списание с первого склада уже выполнилось: внутри операции осталось 7. Второй UPDATE синтаксически успешен, но не находит получателя. Приложение считает это отказом операции, делает rollback, и свежий запрос снова показывает 10 и 2. Это реальная бизнес-проверка, а не искусственно неправильный SQL.

Путь конкретных значений
  1. Старт: 10 и 2
  2. Первый UPDATE через tr → временно 7 и 2
  3. Второй UPDATE для warehouse_id=99 → affected_rows=0
  4. DomainException означает отказ всего перемещения
  5. ibase_rollback(tr) → после нового чтения снова 10 и 2

Сначала один шаг

ibase_query($tr, "UPDATE STOCK SET qty = qty + ? WHERE warehouse_id = ?", 3, 99);
$affected = ibase_affected_rows($tr);
if ($affected !== 1) {
    // Не подтверждать ранее выполненное списание.
    ibase_rollback($tr);
}

Ноль изменённых строк не является ошибкой синтаксиса SQL. В interbase такой UPDATE может вернуть true; поэтому результат query нельзя использовать как количество строк. Проверка affected_rows выполняется сразу, в том же tr. DomainException — стандартное исключение PHP для нарушения правила задачи; оно здесь переводит «получателя нет» в общую ветку отмены. rollback отменяет все незавершённые изменения текущей транзакции, включая уже успешное списание. Массив inside сохраняется лишь как свидетельство промежуточного состояния и не должен показываться как подтверждённый остаток.

Ожидаемый полезный результат

НаблюдениеРезультат
Причина отказаСклад-получатель не найден.
Внутри операции после списания7
Откат подтверждёнда
Склад 1 после отката10
Склад 2 после отката2
Общий остаток12
Отменить списание, если склад-получатель не найденСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f154Config();$db=f154Connect($config);$runtime=f154Runtime($db,$config);

    function startStock154($db) {
        $call=f154Capture(static fn()=>ibase_trans(IBASE_WRITE|IBASE_CONCURRENCY|IBASE_NOWAIT,$db));
        if ($call['value']===false || $call['warning']) {f154Failure('Транзакция не начата',$call);}
        return $call['value'];
    }
    function moveStock154($tr,string $q,int $from,int $to,int $amount): array {
        if ($amount<=0 || $from===$to) {throw new InvalidArgumentException('Нужны разные склады и положительное количество.');}
        f154Query($tr,"UPDATE $q SET qty=qty-? WHERE warehouse_id=? AND qty>=?",[$amount,$from,$amount]);
        $debited=ibase_affected_rows($tr);
        if ($debited!==1) {throw new DomainException('Склад-отправитель не найден или товара недостаточно.');}
        f154Query($tr,"UPDATE $q SET qty=qty+? WHERE warehouse_id=?",[$amount,$to]);
        $credited=ibase_affected_rows($tr);
        if ($credited!==1) {throw new DomainException('Склад-получатель не найден.');}
        // The caller commits or rolls back both statements together.
        return ['debited'=>$debited,'credited'=>$credited];
    }
    function readStock154($context,string $q): array {
        return f154Read($context,"SELECT warehouse_id AS \"warehouse_id\",qty AS \"qty\" FROM $q WHERE warehouse_id IN (1,2) ORDER BY warehouse_id");
    }

    $name=f154OwnTable($db,$owned,'stock','(warehouse_id INTEGER NOT NULL PRIMARY KEY,qty INTEGER NOT NULL CHECK(qty>=0))');
    $q=f154Qualified($name);
    f154Command($db,"INSERT INTO $q(warehouse_id,qty) VALUES(?,?)",[1,10]);
    f154Command($db,"INSERT INTO $q(warehouse_id,qty) VALUES(?,?)",[2,2]);
    f154Commit($db); // Close the setup's default transaction before explicit business work.

    $tr=startStock154($db);$reason='';$rolled=false;$inside=[];
    try {
        try {
            moveStock154($tr,$q,1,99,3);
            throw new RuntimeException('Контрольная ошибка: отсутствующий склад неожиданно найден.');
        } catch (DomainException $error) {
            $reason=$error->getMessage();
            $inside=readStock154($tr,$q);
            $rolled=ibase_rollback($tr);
            if ($rolled!==true) {f154Failure('Перемещение не удалось отменить');}
            $tr=null;
        }

    } finally {
        if (is_resource($tr)) {
            $rollback=ibase_rollback($tr);$tr=null;
            if ($rollback!==true) {f154Failure('Незавершённую транзакцию не удалось откатить');}
        }
    }

    $saved=readStock154($db,$q);
    $rows=[['Причина отказа',$reason],['Внутри операции после списания',$inside[0]['qty']],['Откат подтверждён',$rolled],
        ['Склад 1 после отката',$saved[0]['qty']],['Склад 2 после отката',$saved[1]['qty']],['Общий остаток',array_sum(array_column($saved,'qty'))]];
    $checks=['recipient_failure'=>$reason==='Склад-получатель не найден.','first_update_was_pending'=>array_column($inside,'qty')===[7,2],
        'rollback_confirmed'=>$rolled===true,'original_rows_restored'=>array_column($saved,'qty')===[10,2],'total_preserved'=>array_sum(array_column($saved,'qty'))===12];
    $observations=['requested'=>['from'=>1,'to'=>99,'amount'=>3],'before'=>[10,2],'pending_before_rollback'=>$inside,'after_rollback'=>$saved,'reason'=>$reason];

} finally {
    if (is_resource($db)) {try {$removed=f154Cleanup($db,$owned);} finally {ibase_close($db);}}
}

f154Report('fb154-transfer-rollback','Отменить списание, если склад-получатель не найден',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Причина отказа — Склад-получатель не найден.
Внутри операции после списания — 7
Откат подтверждён — да
Склад 1 после отката — 10
Склад 2 после отката — 2
Общий остаток — 12

Самостоятельный пример содержит lab.php и README.txt, создаёт одну собственную таблицу с двумя складами, выполняет реальные транзакции, сохраняет HTML/JSON-отчёт и удаляет только свою учебную таблицу. Явная транзакция всегда завершается до общей очистки.

Фактический HTML-результат · Данные результата JSON.

Ошибки и границы

Отказ SQL, отсутствие получателя и отказ фиксации — разные причины, но каждая требует завершить начатую операцию. Нельзя ловить все исключения и затем безусловно выполнять commit. Отправленное письмо, HTTP-запрос или запись в обычный файл не откатываются транзакцией Firebird; внешнее действие планируют после подтверждения или через отдельный надёжный механизм. После уже успешного commit обычный rollback не возвращает историю назад: понадобятся новое компенсирующее действие либо восстановление. Диагностика SQL не выводится посетителю без фильтрации.

Разработчик спрашивал, можно ли откатить уже подтверждённую транзакцию и восстановить удалённые данные. Исходная ситуация на Stack Overflow. Отмена действует до подтверждения текущей работы. Две стороны одного перевода подтверждаются вместе; retaining-пример отдельно показывает, что последующий rollback не отменяет уже подтверждённую первую партию. Восстановление удалённых данных — отдельная задача резервных копий или компенсирующих действий.

Измените условие задачи

Что произойдёт, если получатель существует, но запросили 20 единиц со склада с остатком 10?

Как проверить решение

Первая команда с qty>=20 затронет 0 строк. Получатель не будет увеличен, всё перемещение откатится. Подтверждённые остатки останутся 10 и 2; сообщение будет о нехватке товара или отсутствии отправителя.

Подтвердить первую партию и отменить только следующую

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

Путь конкретных значений
  1. 10 / 2 → первый документ на 3 → 7 / 5
  2. ibase_commit_ret(tr) подтверждает первую партию
  3. Тот же tr принимает второй документ на 2 → 5 / 7
  4. ibase_rollback(tr) завершает контекст и отменяет вторую партию
  5. Новый SELECT → 7 / 5; первая партия не откатилась

Сначала один шаг

if (ibase_commit_ret($tr) !== true) {
    throw new RuntimeException("Первая партия не подтверждена");
}
// $tr остаётся для следующих команд.
// Последующий rollback отменит только работу после этой фиксации.

Суффикс ret означает retaining: сохраняется контекст для продолжения работы, а не право отменить уже подтверждённую партию. По назначению это отличается от savepoint: точка сохранения не делает предыдущие изменения окончательно подтверждёнными. Обычный commit закрывает транзакцию; commit_ret даёт продолжить с тем же ресурсом. Каждый логически самостоятельный документ должен быть завершён целиком до такой границы — нельзя поставить commit_ret между списанием и приходом одного перемещения.

Ожидаемый полезный результат

НаблюдениеРезультат
Первая партия, единиц3
После первой партии7 / 5
commit_ret выполненда
После второй, ещё не подтверждённой партии5 / 7
После окончательного rollback7 / 5
Первая партия сохраниласьда
Подтвердить первую партию и отменить только следующуюСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f154Config();$db=f154Connect($config);$runtime=f154Runtime($db,$config);

    function startStock154($db) {
        $call=f154Capture(static fn()=>ibase_trans(IBASE_WRITE|IBASE_CONCURRENCY|IBASE_NOWAIT,$db));
        if ($call['value']===false || $call['warning']) {f154Failure('Транзакция не начата',$call);}
        return $call['value'];
    }
    function moveStock154($tr,string $q,int $from,int $to,int $amount): array {
        if ($amount<=0 || $from===$to) {throw new InvalidArgumentException('Нужны разные склады и положительное количество.');}
        f154Query($tr,"UPDATE $q SET qty=qty-? WHERE warehouse_id=? AND qty>=?",[$amount,$from,$amount]);
        $debited=ibase_affected_rows($tr);
        if ($debited!==1) {throw new DomainException('Склад-отправитель не найден или товара недостаточно.');}
        f154Query($tr,"UPDATE $q SET qty=qty+? WHERE warehouse_id=?",[$amount,$to]);
        $credited=ibase_affected_rows($tr);
        if ($credited!==1) {throw new DomainException('Склад-получатель не найден.');}
        // The caller commits or rolls back both statements together.
        return ['debited'=>$debited,'credited'=>$credited];
    }
    function readStock154($context,string $q): array {
        return f154Read($context,"SELECT warehouse_id AS \"warehouse_id\",qty AS \"qty\" FROM $q WHERE warehouse_id IN (1,2) ORDER BY warehouse_id");
    }

    $name=f154OwnTable($db,$owned,'stock','(warehouse_id INTEGER NOT NULL PRIMARY KEY,qty INTEGER NOT NULL CHECK(qty>=0))');
    $q=f154Qualified($name);
    f154Command($db,"INSERT INTO $q(warehouse_id,qty) VALUES(?,?)",[1,10]);
    f154Command($db,"INSERT INTO $q(warehouse_id,qty) VALUES(?,?)",[2,2]);
    f154Commit($db); // Close the setup's default transaction before explicit business work.

    $tr=startStock154($db);
    try {
        $firstCounts=moveStock154($tr,$q,1,2,3);
        $first=readStock154($tr,$q);
        $retained=ibase_commit_ret($tr);
        if ($retained!==true) {f154Failure('Первая партия не подтверждена');}
        $secondCounts=moveStock154($tr,$q,1,2,2);
        $second=readStock154($tr,$q);
        $rolled=ibase_rollback($tr);
        if ($rolled!==true) {f154Failure('Вторая партия не отменена');}
        $tr=null;

    } finally {
        if (is_resource($tr)) {
            $rollback=ibase_rollback($tr);$tr=null;
            if ($rollback!==true) {f154Failure('Незавершённую транзакцию не удалось откатить');}
        }
    }

    $saved=readStock154($db,$q);
    $rows=[['Первая партия, единиц',3],['После первой партии',implode(' / ',array_column($first,'qty'))],['commit_ret выполнен',$retained],
        ['После второй, ещё не подтверждённой партии',implode(' / ',array_column($second,'qty'))],['После окончательного rollback',implode(' / ',array_column($saved,'qty'))],['Первая партия сохранилась',array_column($saved,'qty')===[7,5]]];
    $checks=['first_batch_correct'=>array_column($first,'qty')===[7,5],'retained_commit'=>$retained===true,
        'same_context_accepts_next_batch'=>$secondCounts===['debited'=>1,'credited'=>1]&&array_column($second,'qty')===[5,7],
        'final_rollback'=>$rolled===true,'committed_batch_survives'=>array_column($saved,'qty')===[7,5],'total_preserved'=>array_sum(array_column($saved,'qty'))===12];
    $observations=['initial'=>[10,2],'first_batch'=>$first,'second_pending_batch'=>$second,'after_final_rollback'=>$saved,'first_affected'=>$firstCounts,'second_affected'=>$secondCounts];

} finally {
    if (is_resource($db)) {try {$removed=f154Cleanup($db,$owned);} finally {ibase_close($db);}}
}

f154Report('fb154-commit-retaining','Подтвердить первую партию и отменить только следующую',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Первая партия, единиц — 3
После первой партии — 7 / 5
commit_ret выполнен — да
После второй, ещё не подтверждённой партии — 5 / 7
После окончательного rollback — 7 / 5
Первая партия сохранилась — да

Самостоятельный пример содержит lab.php и README.txt, создаёт одну собственную таблицу с двумя складами, выполняет реальные транзакции, сохраняет HTML/JSON-отчёт и удаляет только свою учебную таблицу. Явная транзакция всегда завершается до общей очистки.

Фактический HTML-результат · Данные результата JSON.

Ошибки и границы

При SNAPSHOT/CONCURRENCY retaining не обещает свежий снимок изменений других транзакций. В нашем примере проверяется сохранность собственных двух партий, а не видимость параллельных клиентов. Для нового снимка завершите текущую транзакцию обычным commit/rollback и начните новую. Бесконечный retaining в долгоживущем сервисе может удерживать старые версии и мешать их уборке; для нового простого приложения обычно понятнее короткая обычная транзакция на документ. Этот пример обязательно заканчивает контекст обычным rollback.

Автор сравнивал противоречащие советы о бесконечных commit retaining и производительности Firebird/InterBase. Исходная ситуация на Stack Overflow. Сохраняемый контекст явно отделён от новой транзакции. Показываем два ограниченных этапа и окончательный rollback, не бесконечный цикл. Поведение и стоимость старых версий InterBase не переносятся на Firebird; видимость и уборка старых версий требуют обычных завершений.

Измените условие задачи

Замените заключительный rollback на обычный commit. Каким должен быть результат и что теперь нельзя отменить?

Как проверить решение

Сохранятся обе партии: 5 и 7. Последующий rollback новой транзакции не отменит ни первую, ни вторую. Если два документа должны быть отменяемы вместе, не подтверждайте первый через commit_ret между ними.

Отклонить черновую партию и сохранить исправленную

В существующем пакетном обработчике обнаружена неверная партия на 3 единицы. Нужно отменить её и обработать исправленный документ на 2. rollback_ret сохраняет контекст, но убирает все текущие неподтверждённые изменения. В итоге сохраняется только исправленная партия: остатки 8 и 4. Это короткая последовательность внутри программы, а не транзакция, оставленная открытой на время ручного редактирования формы.

Путь конкретных значений
  1. 10 / 2 → черновая партия на 3 → 7 / 5
  2. ibase_rollback_ret(tr) → черновик отменён, снова 10 / 2
  3. Тот же tr: исправленная партия на 2 → 8 / 4
  4. ibase_commit(tr) завершает и подтверждает исправление
  5. Новый SELECT показывает только 8 / 4

Сначала один шаг

if (ibase_rollback_ret($tr) !== true) {
    throw new RuntimeException("Черновик не отменён");
}
// Следующие команды используют тот же $tr.
// В конце нужен обычный commit или rollback.

rollback_ret не выбирает «последний UPDATE». Он отменяет текущую незавершённую работу целиком, сохраняя контекст транзакции. В примере поэтому исчезают обе стороны чернового перевода. Если перед ним был commit_ret, уже подтверждённая работа остаётся. Для отмены только части внутри ещё не подтверждённой операции существуют SQL savepoints, это другой инструмент. Сохранение контекста также не означает обновление чужих данных в снимке.

Ожидаемый полезный результат

НаблюдениеРезультат
Черновое перемещение на 37 / 5
rollback_ret выполненда
После отмены черновика10 / 2
Принято перемещение на 28 / 4
После окончательного commit8 / 4
Общий остаток12
Отклонить черновую партию и сохранить исправленнуюСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f154Config();$db=f154Connect($config);$runtime=f154Runtime($db,$config);

    function startStock154($db) {
        $call=f154Capture(static fn()=>ibase_trans(IBASE_WRITE|IBASE_CONCURRENCY|IBASE_NOWAIT,$db));
        if ($call['value']===false || $call['warning']) {f154Failure('Транзакция не начата',$call);}
        return $call['value'];
    }
    function moveStock154($tr,string $q,int $from,int $to,int $amount): array {
        if ($amount<=0 || $from===$to) {throw new InvalidArgumentException('Нужны разные склады и положительное количество.');}
        f154Query($tr,"UPDATE $q SET qty=qty-? WHERE warehouse_id=? AND qty>=?",[$amount,$from,$amount]);
        $debited=ibase_affected_rows($tr);
        if ($debited!==1) {throw new DomainException('Склад-отправитель не найден или товара недостаточно.');}
        f154Query($tr,"UPDATE $q SET qty=qty+? WHERE warehouse_id=?",[$amount,$to]);
        $credited=ibase_affected_rows($tr);
        if ($credited!==1) {throw new DomainException('Склад-получатель не найден.');}
        // The caller commits or rolls back both statements together.
        return ['debited'=>$debited,'credited'=>$credited];
    }
    function readStock154($context,string $q): array {
        return f154Read($context,"SELECT warehouse_id AS \"warehouse_id\",qty AS \"qty\" FROM $q WHERE warehouse_id IN (1,2) ORDER BY warehouse_id");
    }

    $name=f154OwnTable($db,$owned,'stock','(warehouse_id INTEGER NOT NULL PRIMARY KEY,qty INTEGER NOT NULL CHECK(qty>=0))');
    $q=f154Qualified($name);
    f154Command($db,"INSERT INTO $q(warehouse_id,qty) VALUES(?,?)",[1,10]);
    f154Command($db,"INSERT INTO $q(warehouse_id,qty) VALUES(?,?)",[2,2]);
    f154Commit($db); // Close the setup's default transaction before explicit business work.

    $tr=startStock154($db);
    try {
        moveStock154($tr,$q,1,2,3);
        $draft=readStock154($tr,$q);
        $retained=ibase_rollback_ret($tr);
        if ($retained!==true) {f154Failure('Черновая партия не отменена');}
        $reset=readStock154($tr,$q);
        $acceptedCounts=moveStock154($tr,$q,1,2,2);
        $accepted=readStock154($tr,$q);
        $committed=ibase_commit($tr);
        if ($committed!==true) {f154Failure('Исправленная партия не подтверждена');}
        $tr=null;

    } finally {
        if (is_resource($tr)) {
            $rollback=ibase_rollback($tr);$tr=null;
            if ($rollback!==true) {f154Failure('Незавершённую транзакцию не удалось откатить');}
        }
    }

    $saved=readStock154($db,$q);
    $rows=[['Черновое перемещение на 3',implode(' / ',array_column($draft,'qty'))],['rollback_ret выполнен',$retained],['После отмены черновика',implode(' / ',array_column($reset,'qty'))],
        ['Принято перемещение на 2',implode(' / ',array_column($accepted,'qty'))],['После окончательного commit',implode(' / ',array_column($saved,'qty'))],['Общий остаток',array_sum(array_column($saved,'qty'))]];
    $checks=['draft_applied'=>array_column($draft,'qty')===[7,5],'retained_rollback'=>$retained===true,
        'draft_fully_discarded'=>array_column($reset,'qty')===[10,2],'same_context_reused'=>$acceptedCounts===['debited'=>1,'credited'=>1]&&array_column($accepted,'qty')===[8,4],
        'final_commit'=>$committed===true,'only_corrected_batch_saved'=>array_column($saved,'qty')===[8,4],'total_preserved'=>array_sum(array_column($saved,'qty'))===12];
    $observations=['before'=>[10,2],'rejected_draft'=>$draft,'after_rollback_ret'=>$reset,'accepted_batch'=>$accepted,'committed_rows'=>$saved];

} finally {
    if (is_resource($db)) {try {$removed=f154Cleanup($db,$owned);} finally {ibase_close($db);}}
}

f154Report('fb154-rollback-retaining','Отклонить черновую партию и сохранить исправленную',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Черновое перемещение на 3 — 7 / 5
rollback_ret выполнен — да
После отмены черновика — 10 / 2
Принято перемещение на 2 — 8 / 4
После окончательного commit — 8 / 4
Общий остаток — 12

Самостоятельный пример содержит lab.php и README.txt, создаёт одну собственную таблицу с двумя складами, выполняет реальные транзакции, сохраняет HTML/JSON-отчёт и удаляет только свою учебную таблицу. Явная транзакция всегда завершается до общей очистки.

Фактический HTML-результат · Данные результата JSON.

Ошибки и границы

retaining нужен при осознанном продолжении существующего контекста, а не как универсальная обработка любой ошибки. После конфликта со stale snapshot повтор в том же контексте может снова не увидеть нужные изменения; новой попытке бизнес-операции обычно нужна новая транзакция. Обычный окончательный commit/rollback всё равно обязателен. Никакое retaining не отменит PHP-массивы, отправленный HTTP-запрос или изменения вне этой транзакции.

Измените условие задачи

После rollback_ret отмените также исправленную партию обычным rollback. Каков итог таблицы?

Как проверить решение

Вернутся исходные 10 и 2. Ни черновая, ни исправленная партия не были подтверждены. Если вместо первого rollback_ret стоял commit_ret, черновая партия уже сохранилась бы и итог был бы другим.

Пять функций управления границами

НужноКарточка
Начать явную транзакцию с выбранными настройками.ibase_trans
Подтвердить всю текущую работу и закончить транзакцию.ibase_commit
Отменить неподтверждённую работу и закончить транзакцию.ibase_rollback
Подтвердить текущую партию, сохранив контекст.ibase_commit_ret
Отменить текущую партию, сохранив контекст.ibase_rollback_ret

От правильных остатков к обработке отказов

Проверенный остаток должен отвечать выбранной границе: 7/5 после обычной фиксации; 10/2 после отмены неудачного перевода; 7/5 после принятой первой и отменённой второй партии; 8/4 после отменённого черновика и принятого исправления. Успешное SQL-выражение, подтверждённая бизнес-операция и свежесть снимка — разные вещи.

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

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

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

ibase_transНачать явную транзакцию с выбранными настройками.

Исторический PHP 5 / PHP 7 < 7.4; проверенный отдельный драйвер FirebirdSQL 3.0.1 · interbase

ibase_trans(int $trans_args = ..., resource $link_identifier = ..., ...): resource|false; также ibase_trans(resource $link_identifier = ...)

Начинает явную транзакцию, в которую приложение направляет все части одной операции. Возвращённый ресурс tr связывает списание и приход. Первый ibase_trans не возвращает уже существующую default-транзакцию соединения: это отдельный контекст.

Форма / параметрКонтракт
ibase_trans($flags, $db)Явное соединение и битовая маска настроек. Рекомендуемая читаемая форма в этих примерах.
ibase_trans($db)Новое явное выполнение со стандартными настройками. Не означает получить default-транзакцию.
ibase_trans() / ibase_trans($flags)Последнее открытое соединение. В коде с несколькими соединениями укажите db явно.
$flagsint: по одному совместимому варианту из каждой группы; объединять |. Наличие константы не оправдывает произвольные противоречащие комбинации.
$dbДействующий ресурс соединения. Не ресурс результата и не уже закрытая транзакция.
resource / falseРесурс новой транзакции либо отказ; сохраняйте ibase_errcode/ibase_errmsg сразу. Ошибочный PHP-тип может дать отдельную ошибку PHP.
ФлагНазначение
IBASE_READТранзакция для чтения обычных постоянных таблиц; соответствует READ ONLY. Не заменяет права пользователя. У Firebird есть отдельные правила для глобальных временных таблиц.
IBASE_WRITEЧтение и изменение; READ WRITE. Выбирайте один режим доступа, не объединяйте READ и WRITE.
IBASE_CONCURRENCYSNAPSHOT: согласованный снимок подтверждённых данных на момент начала плюс собственные изменения. Новые чужие commit не обновляют этот снимок.
IBASE_CONSISTENCYSNAPSHOT TABLE STABILITY: более строгая изоляция с ограничениями параллельного доступа к таблицам; не стандартный выбор для обычного каталога.
IBASE_COMMITTEDREAD COMMITTED: чтение подтверждённых данных; детали видимости и конфликтов зависят от режима record version и версии/настройки Firebird. Не читать чужие неподтверждённые записи.
IBASE_REC_VERSIONМодификатор COMMITTED: в классическом режиме позволяет читать доступную подтверждённую версию при незавершённом изменении. Не снимок всей транзакции.
IBASE_REC_NO_VERSIONДругой модификатор COMMITTED: в классическом режиме при конкурирующей незавершённой версии важны WAIT/NOWAIT. Не объединяйте с REC_VERSION.
IBASE_WAITОжидание разрешения конфликтующей операции. Этот флаг не задаёт длительность тайм-аута.
IBASE_NOWAITОтказ без ожидания конфликтующей записи. Не отключает блокировки и не означает «игнорировать чужие изменения».
IBASE_DEFAULTСтандартный блок параметров драйвера/клиента. Это не выбор уже открытой default-транзакции и не autocommit после каждой команды.

Все девять флагов со страницы PHP Manual сохранены выше, IBASE_DEFAULT дополнительно объясняет стандартный режим. Параметры применяются к следующему за ними соединению. Исходник 3.0.1 передаёт классические TPB-параметры; Firebird 4/5 при настройке ReadConsistency может преобразовать старые READ COMMITTED RECORD_VERSION/NO_RECORD_VERSION в READ CONSISTENCY. Не обещайте одинаковые конфликты для любого сервера только по написанию константы. В наших четырёх сценариях явно выбран CONCURRENCY; проверка конкурентного доступа — отдельная задача.

Перемещение между складами

$flags = IBASE_WRITE | IBASE_CONCURRENCY | IBASE_NOWAIT;
$tr = ibase_trans($flags, $db);
if ($tr === false) { throw new RuntimeException("Операция не началась"); }
// Оба UPDATE выполняются с $tr.
// Полный сценарий обязательно заканчивает tr через commit/rollback.

Полное перемещение 3 единиц даёт 7 и 5. Общая функция изменения не делает скрытый commit: вызывающий код управляет всей бизнес-операцией.

Отчёт, которому нужен один согласованный снимок

$tr = ibase_trans(IBASE_READ | IBASE_CONCURRENCY | IBASE_NOWAIT, $db);
if ($tr === false) { throw new RuntimeException("Отчёт не начат"); }
// SELECT только нужных итогов и строк выполняются через $tr.
// Освободить результаты, затем проверить ibase_commit($tr).

Это контекстный каркас отчёта: отдельный режим READ выражает отсутствие записи. Для реального отчёта нужны свои SELECT; чтение также имеет срок жизни и требует завершения. Не держите снимок, пока посетитель рассматривает открытую страницу.

Несколько соединений и двухфазная фиксация

$tr = ibase_trans($flagsA, $dbA, $flagsB, $dbB);
// Контекстный пример: оба соединения должны уже существовать.
$result = ibase_query($dbA, $tr, "SELECT id FROM DOCUMENTS WHERE id = ?", 71);
// Для второй базы также передать её db и этот tr; в конце общий commit/rollback.

Полная форма принимает повторяющиеся группы настроек и соединений; последняя маска применяется к следующим ссылкам до новой маски. Для нескольких баз драйвер использует распределённую транзакцию и двухфазную фиксацию. Это не позволяет JOIN таблиц из разных баз в одном запросе: query/prepare должны получить и соединение, и транзакцию. При сбое связи между фазами возможны limbo-транзакции, требующие восстановления администратором; это не обещание отсутствия неопределённого состояния при аварии. Многобазовый вариант не запускался в этой локальной главе.

Неоконченные явные транзакции автоматически откатываются при завершении скрипта, но это аварийный рубеж, а не основной алгоритм. Явно завершайте каждый путь. Default-транзакция соединения имеет отдельные правила; вызов commit(db) не является commit(tr). Следующий шаг — общий откат при неудаче второго действия.

ibase_commitПодтвердить всю текущую работу и закончить транзакцию.

Исторический PHP 5 / PHP 7 < 7.4; проверенный отдельный драйвер FirebirdSQL 3.0.1 · interbase

ibase_commit(resource $link_or_trans_identifier = null): bool

Подтверждает все изменения выбранной транзакции и завершает её. «SQL выполнился» ещё не равно «операция сохранена»: сообщение об успехе выдаётся после успешного commit. Для уже прочитанных массивов PHP commit не является командой обновления значений.

Аргумент / результатЗначение
Без аргументаDefault-транзакция последнего/default-соединения. Не все явные транзакции приложения.
$db — соединениеDefault-транзакция этого соединения. Явная tr, созданная через ibase_trans, этим не выбирается.
$tr — транзакцияИменно переданная явная транзакция. В самостоятельных сценариях используется этот вариант.
trueОперация завершения успешна. Текущую явную транзакцию после этого не используют.
falseОтказ; сразу сохранить диагностику, не сообщать пользователю об успешном сохранении/отмене. Неверный или закрытый PHP-ресурс может привести к ошибке PHP.

В официальной сигнатуре аргумент помечен как необязательный с null; для проверенной сборки 3.0.1 безопасный документированный способ выбора по умолчанию — пропустить аргумент. Парсер исходника принимает необязательный ресурс, поэтому явный null не следует считать проверенной заменой отсутствия аргумента.

Две стороны одного перемещения

// До этой строки оба UPDATE выполнены через один $tr.
if (ibase_commit($tr) !== true) {
    $error = ibase_errcode(); // Сохранить до следующего обращения к драйверу.
    throw new RuntimeException("Перемещение не подтверждено");
}
$tr = null;
// Теперь можно показать подтверждённый результат.

Проверяемый сценарий заканчивается остатками 7 и 5. В полном коде finally пытается откатить незавершённую операцию при ошибке; обычный commit не ставится между списанием и приходом.

Завершить исправленную партию после rollback_ret

// Черновик уже отменён через rollback_ret; новая партия выполнена.
if (ibase_commit($tr) !== true) { throw new RuntimeException("Исправление не сохранено"); }
$tr = null;
// Старый ресурс нельзя использовать для следующего документа.

В сценарии исправления сохраняются только 8 и 4. Это отличается от commit_ret, после которого ресурс контекста остаётся пригодным для продолжения. Обычный commit нужен и для завершения читающего отчёта: отсутствие UPDATE не делает его бессрочным.

При подтверждённом сервером отказе обработайте диагностику и завершите работу корректно. При потере связи в момент фиксации клиент может не знать, успел ли сервер сохранить операцию: слепой повтор перевода опасен. Для рабочего сервиса нужен идентификатор операции и проверяемое правило повторов. Уже успешный commit нельзя отменить вызовом rollback новой транзакции. Альтернатива — ibase_commit_ret только когда намеренно сохраняется контекст.

ibase_rollbackОтменить неподтверждённую работу и закончить транзакцию.

Исторический PHP 5 / PHP 7 < 7.4; проверенный отдельный драйвер FirebirdSQL 3.0.1 · interbase

ibase_rollback(resource $link_or_trans_identifier = null): bool

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

Аргумент / результатЗначение
Без аргументаDefault-транзакция последнего/default-соединения. Не все явные транзакции приложения.
$db — соединениеDefault-транзакция этого соединения. Явная tr, созданная через ibase_trans, этим не выбирается.
$tr — транзакцияИменно переданная явная транзакция. В самостоятельных сценариях используется этот вариант.
trueОперация завершения успешна. Текущую явную транзакцию после этого не используют.
falseОтказ; сразу сохранить диагностику, не сообщать пользователю об успешном сохранении/отмене. Неверный или закрытый PHP-ресурс может привести к ошибке PHP.

В официальной сигнатуре аргумент помечен как необязательный с null; для проверенной сборки 3.0.1 безопасный документированный способ выбора по умолчанию — пропустить аргумент. Парсер исходника принимает необязательный ресурс, поэтому явный null не следует считать проверенной заменой отсутствия аргумента.

Получатель не найден после успешного списания

if ($credited !== 1) {
    if (ibase_rollback($tr) !== true) { throw new RuntimeException("Отмена не подтверждена"); }
    $tr = null;
    // Сообщить: перемещение не выполнено; не выдавать промежуточный остаток.
}

Полный пример сначала видит 7 и 2 внутри tr, затем 10 и 2 после rollback. Откат определяется не только SQL-ошибкой: отсутствие получателя нарушает правило операции, хотя UPDATE выполнен корректно.

Отменить последнюю партию после commit_ret

// Первая партия уже подтверждена через commit_ret.
// Вторая партия ещё не подтверждена.
if (ibase_rollback($tr) !== true) { throw new RuntimeException("Вторая партия не отменена"); }
$tr = null;
// Первая партия останется; это не откат всей истории.

Именно это проверяет сценарий двух партий: после временных 5 и 7 остаются подтверждённые 7 и 5. Для отмены части незавершённой операции используйте savepoint, а для продолжения контекста после отмены — ibase_rollback_ret.

Откат базы не отменяет внешний HTTP-запрос, письмо или обычный файл. SQL-описание ROLLBACK как безусловной отмены не отменяет возможный отказ клиентского API из-за недействительного handle или потери соединения: проверка bool нужна. Сохраняйте первоначальную причину отказа до попытки rollback, которая сама меняет диагностику. Нельзя «исправить» неопределённый результат сетевого commit простым обещанием, что следующий rollback всё вернул.

ibase_commit_retПодтвердить текущую партию, сохранив контекст.

Исторический PHP 5 / PHP 7 < 7.4; проверенный отдельный драйвер FirebirdSQL 3.0.1 · interbase

ibase_commit_ret(resource $link_or_trans_identifier = null): bool

ibase_commit_ret подтверждает текущую работу, сохраняя контекст транзакции. Это специализированный инструмент продолжения: последующие запросы используют тот же tr. Он не является обычным завершением и не заменяет точку сохранения внутри ещё не подтверждённой операции.

Аргумент / результатЗначение
Без аргументаDefault-транзакция последнего/default-соединения. Не все явные транзакции приложения.
$db — соединениеDefault-транзакция этого соединения. Явная tr, созданная через ibase_trans, этим не выбирается.
$tr — транзакцияИменно переданная явная транзакция. В самостоятельных сценариях используется этот вариант.
trueОперация завершения успешна. Контекст сохраняется для продолжения.
falseОтказ; сразу сохранить диагностику, не сообщать пользователю об успешном сохранении/отмене. Неверный или закрытый PHP-ресурс может привести к ошибке PHP.

В официальной сигнатуре аргумент помечен как необязательный с null; для проверенной сборки 3.0.1 безопасный документированный способ выбора по умолчанию — пропустить аргумент. Парсер исходника принимает необязательный ресурс, поэтому явный null не следует считать проверенной заменой отсутствия аргумента.

Принять документ и отклонить следующий

if (ibase_commit_ret($tr) !== true) { throw new RuntimeException("Документ не подтверждён"); }
// Выполнить следующий самостоятельный документ через тот же $tr.
// Если он отклонён: обычный ibase_rollback($tr), затем $tr = null.

В полной программе первая партия даёт 7/5, вторая временно 5/7, окончательный rollback оставляет 7/5. Первая фиксация настоящая: последующая отмена не возвращает исходные 10/2.

Завершить небольшой пакет принятых документов

// Первый документ уже принят; это явная граница независимой партии.
if (ibase_commit_ret($tr) !== true) { throw new RuntimeException("Первая партия не сохранена"); }
// Выполнить второй документ, проверить оба его изменения.
if (ibase_commit($tr) !== true) { throw new RuntimeException("Вторая партия не сохранена"); }
$tr = null;

Это контекстное изменение полного сценария: вместо финального rollback стоит обычный commit. Ожидаются 5 и 7. Если весь пакет должен быть неделимым, промежуточный commit_ret использовать нельзя: его граница разрушает общую отменяемость.

В SNAPSHOT и SNAPSHOT TABLE STABILITY retaining сохраняет видимость снимка: новые подтверждения других транзакций не становятся автоматически видимыми. Для новой картины данных нужны обычное завершение и новая транзакция. Курсоры/подготовленные команды могут сохранять контекст согласно API, но это не требование держать все результаты открытыми: в наших программах чтение заканчивается и результаты освобождаются перед границами. Многократный retaining без обычного окончания способен удерживать старые версии записей и увеличивать стоимость уборки/чтения. Для простого сайта предпочитайте короткую транзакцию на завершённую задачу; не оставляйте её между HTTP-запросами.

Не используйте retaining как универсальный повтор после конфликта: старый снимок может быть причиной повторного отказа. Ошибку самой функции проверяют по false/диагностике, и только успех позволяет считать текущую работу подтверждённой или отменённой. Сохранение ресурса не означает сохранение всех PHP-переменных или откат внешних действий.

ibase_rollback_retОтменить текущую партию, сохранив контекст.

Исторический PHP 5 / PHP 7 < 7.4; проверенный отдельный драйвер FirebirdSQL 3.0.1 · interbase

ibase_rollback_ret(resource $link_or_trans_identifier = null): bool

ibase_rollback_ret отменяет текущую работу, сохраняя контекст транзакции. Это специализированный инструмент продолжения: последующие запросы используют тот же tr. Он не является обычным завершением и не заменяет точку сохранения внутри ещё не подтверждённой операции.

Аргумент / результатЗначение
Без аргументаDefault-транзакция последнего/default-соединения. Не все явные транзакции приложения.
$db — соединениеDefault-транзакция этого соединения. Явная tr, созданная через ibase_trans, этим не выбирается.
$tr — транзакцияИменно переданная явная транзакция. В самостоятельных сценариях используется этот вариант.
trueОперация завершения успешна. Контекст сохраняется для продолжения.
falseОтказ; сразу сохранить диагностику, не сообщать пользователю об успешном сохранении/отмене. Неверный или закрытый PHP-ресурс может привести к ошибке PHP.

В официальной сигнатуре аргумент помечен как необязательный с null; для проверенной сборки 3.0.1 безопасный документированный способ выбора по умолчанию — пропустить аргумент. Парсер исходника принимает необязательный ресурс, поэтому явный null не следует считать проверенной заменой отсутствия аргумента.

Заменить отклонённый черновик исправленным

if (ibase_rollback_ret($tr) !== true) { throw new RuntimeException("Черновик не отменён"); }
// Выполнить исправленную партию через тот же $tr.
// Затем обычный ibase_commit($tr) и $tr = null.

Полный пример возвращает черновые 7/5 к 10/2, затем сохраняет только исправленные 8/4. Контекст сохранён, но оба изменения черновика отменены.

Отклонить текущую партию после уже принятой

// Раньше: ibase_commit_ret($tr) подтвердил независимую партию.
// Затем следующая неподтверждённая партия оказалась неверной.
if (ibase_rollback_ret($tr) !== true) { throw new RuntimeException("Текущая партия не отменена"); }
// Подтверждённая раньше партия остаётся; дальнейшие команды ещё возможны.
// Всё равно закончить контекст обычным commit/rollback.

Это составной контекстный вариант двух известных границ, не ещё один выполненный MAIN. Если первая партия дала 7/5, а текущая — 5/7, rollback_ret оставит 7/5 внутри продолжаемого контекста. Он не выбирает отдельный последний запрос и не возвращает состояние до прежнего commit_ret.

В SNAPSHOT и SNAPSHOT TABLE STABILITY retaining сохраняет видимость снимка: новые подтверждения других транзакций не становятся автоматически видимыми. Для новой картины данных нужны обычное завершение и новая транзакция. Курсоры/подготовленные команды могут сохранять контекст согласно API, но это не требование держать все результаты открытыми: в наших программах чтение заканчивается и результаты освобождаются перед границами. Многократный retaining без обычного окончания способен удерживать старые версии записей и увеличивать стоимость уборки/чтения. Для простого сайта предпочитайте короткую транзакцию на завершённую задачу; не оставляйте её между HTTP-запросами.

Не используйте retaining как универсальный повтор после конфликта: старый снимок может быть причиной повторного отказа. Ошибку самой функции проверяют по false/диагностике, и только успех позволяет считать текущую работу подтверждённой или отменённой. Сохранение ресурса не означает сохранение всех PHP-переменных или откат внешних действий.

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

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

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

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

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