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

Дерево руководства
Глава 61 · После первых запросов и основ транзакций Firebird
Firebird: результат записи, диагностика ошибок и закрытие соединения
Условное резервирование и ноль строк, мгновенный снимок ошибки, отмена пакета после повторного ключа, явный commit перед close и чтение после нового подключения.
Разделить результат SQL, бизнес-итог и завершение работы
«SQL выполнился», «изменена нужная строка», «пакет подтверждён» и «соединение закрыто» — четыре разных факта. Если их смешать, интерфейс сообщит о несуществующем резерве или сохранит только половину каталога. Разберём каждый факт на собственных маленьких таблицах и увидим итоговые данные, а не только статус вызова.
- query/execute → false или успешное выполнение
- При false → немедленно errcode + errmsg
- При успешной записи → affected_rows нужного контекста
- Пакет успешен → проверенный commit; пакет неудачен → проверенный rollback
- Освободить результаты/подготовки → close соединения
- Читателю → конкретный безопасный результат операции
Начните с соединения, таблицы и первой выборки Firebird, затем с группы записей и транзакций. Здесь понятие «контекст» означает выбранную стандартную или явную транзакцию: её данные и счётчик нельзя подменять соседним соединением. Для параметров и повторяемого SQL есть отдельная глава подготовки и результатов.
Показанные программы используют отдельную учебную базу, PHP 8.2 и расширение interbase 3.0.1. Заголовок PHP Manual «PHP 7 < 7.4» относится к прежнему встроенному расширению; современные архивы используют отдельно установленный драйвер. В README каждого архива описан тот же локальный профиль. Серверные коды в примерах привязаны к точно созданным ошибкам, а не ко всем возможным клиентам и версиям.
Зарезервировать товар и отличить отсутствие остатка от ошибки SQL
На складе три единицы товара. Первый покупатель резервирует две; второму нужны ещё две. Нельзя сообщать обоим «товар зарезервирован» только потому, что SQL выполнился. Один условный UPDATE уменьшит остаток, только если его достаточно. Количество затронутых строк отличит состоявшееся резервирование от отказа по условию.
- STOCK = 3; запрос на 2 единицы
- WHERE ID = 1 AND STOCK >= 2 → условие выполнено
- UPDATE: STOCK = 1; affected_rows = 1; commit
- Повторный запрос на 2: условие уже не выполнено
- UPDATE возвращает true, affected_rows = 0; STOCK остаётся 1
- Приложение сообщает, что второй резерв не создан
Самый короткий шаг
$result = ibase_query($db,
'UPDATE PRODUCTS SET STOCK = STOCK - ? WHERE ID = ? AND STOCK >= ?', 2, 1, 2);
if ($result === false) { throw new RuntimeException('SQL не выполнен'); }
$changed = ibase_affected_rows($db);
// 1 — условие выполнилось; 0 — подходящей строки нет; false — нет корректного счётчика.UPDATE меняет строку в базе. WHERE одновременно проверяет нужный ID и достаточный остаток. Каждый ? получает отдельное значение: вычитаемое количество, ID, минимально допустимый остаток. ibase_affected_rows вызывается сразу после команды в том же контексте. Строгое === false отличает ошибку от допустимого нуля. Затем commit подтверждает конкретный запрос; следующая попытка рассматривается как отдельное действие.
Результат самостоятельного запуска
| Наблюдение | Результат |
|---|---|
| Начальный остаток | 3 |
| Запрошено в каждом резервировании | 2 |
| Первый UPDATE вернул число 1 | да |
| Первый UPDATE: изменено строк | 1 |
| Второй UPDATE вернул true | да |
| Второй UPDATE: изменено строк | 0 |
| Остаток после двух запросов | 1 |
| Второй запрос является ошибкой SQL | нет |
Полный файл, создание таблицы и архив для запуска
<?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);
$items=f154OwnTable($db,$owned,'outcome','(ID INTEGER NOT NULL PRIMARY KEY, STOCK INTEGER NOT NULL CHECK (STOCK >= 0))');
$q=f154Qualified($items);
f154Command($db,"INSERT INTO $q (ID,STOCK) VALUES (?,?)",[1,3]);
f154Commit($db);
$quantity=2;
if ($quantity<=0) {throw new InvalidArgumentException('Количество должно быть положительным');}
$first=f154Query($db,"UPDATE $q SET STOCK=STOCK-? WHERE ID=? AND STOCK>=?",[$quantity,1,$quantity]);
$firstCount=ibase_affected_rows($db);
if ($firstCount===false) {f154Failure('Нет количества строк первого UPDATE');}
if ($firstCount!==1) {throw new RuntimeException('Первое резервирование должно изменить один товар');}
f154Commit($db);
$second=f154Query($db,"UPDATE $q SET STOCK=STOCK-? WHERE ID=? AND STOCK>=?",[$quantity,1,$quantity]);
$secondCount=ibase_affected_rows($db);
if ($secondCount===false) {f154Failure('Нет количества строк второго UPDATE');}
if ($secondCount!==0) {throw new RuntimeException('Недостаточный остаток не должен изменяться');}
f154Commit($db);
$stock=(int)f154Read($db,"SELECT STOCK FROM $q WHERE ID=?",[1])[0]['STOCK'];
$rows=[['Начальный остаток',3],['Запрошено в каждом резервировании',$quantity],
['Первый UPDATE вернул число 1',$first===1],['Первый UPDATE: изменено строк',$firstCount],
['Второй UPDATE вернул true',$second===true],['Второй UPDATE: изменено строк',$secondCount],
['Остаток после двух запросов',$stock],['Второй запрос является ошибкой SQL',$second===false]];
$checks=['first_return'=>$first===1,'first_affected'=>$firstCount===1,'second_success_without_change'=>$second===true,
'second_affected_zero'=>$secondCount===0,'stock_never_negative'=>$stock===1,'positive_quantity'=>$quantity===2];
$observations=['initial_stock'=>3,'quantity'=>2,'first_return'=>$first,'first_affected'=>$firstCount,
'second_return'=>$second,'second_affected'=>$secondCount,'final_stock'=>$stock,
'conditional_update'=>true,'selection_of_all_rows'=>false,'separate_requests_committed'=>2];
} finally {
if (is_resource($db)) {try {$removed=f154Cleanup($db,$owned);} finally {ibase_close($db);}}
}
f154Report('fb154-update-outcome','Зарезервировать товар и отличить отсутствие остатка от ошибки SQL',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Начальный остаток — 3
Запрошено в каждом резервировании — 2
Первый UPDATE вернул число 1 — да
Первый UPDATE: изменено строк — 1
Второй UPDATE вернул true — да
Второй UPDATE: изменено строк — 0
Остаток после двух запросов — 1
Второй запрос является ошибкой SQL — нет
Запустите example.php по README.txt. lab.php содержит открытый код проверки отдельной базы, захвата ожидаемых предупреждений, чтения и вывода отчёта. Создаётся одна собственная таблица; после наблюдений она удаляется. Рядом остаются result.html, result.json и metrics.json.
Открыть наглядный результат · Посмотреть данные и наблюдения JSON.
Границы и ошибки
Нулевое количество означает «ни одна строка не удовлетворила всем условиям», а не обязательно «товар отсутствует»: причина может быть в остатке, статусе или другом условии. Для пояснения интерфейсу допустим отдельный узкий SELECT после отказа; он не должен заменять атомарную проверку внутри UPDATE. Два числа affected_rows не являются испытанием конкурентной нагрузки. При конкуренции возможен конфликт транзакций, его обрабатывают отдельно. Если резерв связан с записью заказа, обе записи должны быть одной явной транзакцией. Повтор HTTP-запроса требует защиты от повторного резервирования на уровне уникального ключа операции.
Вопрос о неработающем UPDATE сначала содержал только сообщение «store issue». После чтения ibase_errmsg автор обнаружил опечатку. Исходный вопрос на Stack Overflow. Разделяем отказ SQL, ноль затронутых строк и подтверждение записи. Отказ от параметров не является исправлением: сохраняем связывание значений и диагностируем точную причину.
Попробуйте изменить условие
Первый покупатель запросил три единицы вместо двух. Как изменятся счётчики и итоговый остаток?
Разбор решения
Первая команда изменит одну строку и оставит STOCK = 0; вторая изменит ноль строк. Проверка STOCK >= ? должна получать то же положительное количество, которое вычитается. Нельзя менять только одно из двух значений.
Сохранить причину ошибки до следующего обращения к драйверу
После переименования поля отчёт перестал открываться. Одного false недостаточно, чтобы понять причину. Намеренно запросим отсутствующее поле NOT_A_FIELD, сразу сохраним код и признаки сообщения, затем исправим SELECT. Будет видно, что новое успешное обращение очистило текущую диагностику драйвера, а наша копия кода сохранилась.
- SELECT NOT_A_FIELD → false и предупреждение PHP
- Сразу: errcode = -206; errmsg содержит имя неизвестного поля
- Сохранить снимок до чтения, rollback, close и любого следующего запроса
- Исправленный SELECT ID, TITLE → строка 1, Кабель
- Текущие errcode и errmsg стали false
- Сохранённый $savedCode остаётся -206; наружу не выводится сырой текст
Самый короткий шаг
$result = ibase_query($db, 'SELECT NOT_A_FIELD FROM PRODUCTS');
if ($result === false) {
$errorCode = ibase_errcode();
$errorMessage = ibase_errmsg();
// Снимок уже сохранён. Теперь можно завершать транзакцию и показывать безопасное сообщение.
}Обе диагностические функции вызываются без аргументов: они читают последнее состояние расширения, а не отдельного соединения. Код — число вроде -206, текст — пояснение причины. Значение false означает отсутствие сохранённой ошибки; это не строка "false" и не текст "0". Чтение errcode и errmsg друг другу не мешает. В полном файле небольшой временный обработчик ловит только ожидаемое PHP-предупреждение и сразу восстанавливает прежний обработчик; он не превращает неизвестные ошибки в успешный результат.
Результат самостоятельного запуска
| Наблюдение | Результат |
|---|---|
| Ошибочный SELECT вернул false | да |
| Сохранённый код ошибки | -206 |
| Сообщение получено сразу | да |
| Сообщение указало NOT_A_FIELD | да |
| После исправленного SELECT код ошибки отсутствует | да |
| После исправленного SELECT сообщение отсутствует | да |
| Исправленный SELECT вернул название | Кабель |
| Сохранённый код после исправления | -206 |
Полный файл, создание таблицы и архив для запуска
<?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);
$items=f154OwnTable($db,$owned,'sql_error','(ID INTEGER NOT NULL PRIMARY KEY, TITLE VARCHAR(40) CHARACTER SET UTF8 NOT NULL)');
$q=f154Qualified($items);
f154Command($db,"INSERT INTO $q (ID,TITLE) VALUES (?,?)",[1,'Кабель']);
f154Commit($db);
// Capture immediately reads ibase_errcode and ibase_errmsg; it does not publish raw diagnostics.
$failed=f154Capture(static fn()=>ibase_query($db,"SELECT NOT_A_FIELD FROM $q"));
$savedCode=ibase_errcode();$savedMessage=ibase_errmsg();
$messagePresent=is_string($savedMessage)&&$savedMessage!=='';
$mentionsBadColumn=$messagePresent&&str_contains($savedMessage,'NOT_A_FIELD');
unset($savedMessage);
if ($failed['value']!==false || $savedCode!==-206 || !$messagePresent) {
throw new RuntimeException('Ожидался контролируемый отказ из-за неизвестного столбца');
}
$result=f154Query($db,"SELECT ID,TITLE FROM $q ORDER BY ID");
$afterCode=ibase_errcode();$afterMessage=ibase_errmsg();
try {$validRows=f154Rows($result);}
finally {
if (ibase_free_result($result)!==true) {f154Failure('Результат исправленного SELECT не освобождён');}
$result=null;
}
$rows=[['Ошибочный SELECT вернул false',$failed['value']===false],['Сохранённый код ошибки',$savedCode],
['Сообщение получено сразу',$messagePresent],['Сообщение указало NOT_A_FIELD',$mentionsBadColumn],
['После исправленного SELECT код ошибки отсутствует',$afterCode===false],
['После исправленного SELECT сообщение отсутствует',$afterMessage===false],
['Исправленный SELECT вернул название',$validRows[0]['TITLE']],['Сохранённый код после исправления',$savedCode]];
$checks=['invalid_sql_false'=>$failed['value']===false,'expected_error_code'=>$savedCode===-206,
'error_message_captured'=>$messagePresent&&$failed['message_present'],'column_identified'=>$mentionsBadColumn,
'php_warning_captured'=>$failed['warning'],'next_query_cleared_error'=>$afterCode===false&&$afterMessage===false,
'correct_query_rows'=>$validRows===[['ID'=>1,'TITLE'=>'Кабель']],'saved_snapshot_retained'=>$savedCode===-206];
$observations=['failed_return'=>$failed['value'],'captured_code'=>$savedCode,'warning_captured'=>$failed['warning'],
'message_present'=>$messagePresent,'mentions_bad_column'=>$mentionsBadColumn,'raw_error_text_published'=>false,
'code_after_success'=>$afterCode,'message_after_success'=>$afterMessage,'correct_rows'=>$validRows];
} finally {
if (is_resource($db)) {try {$removed=f154Cleanup($db,$owned);} finally {ibase_close($db);}}
}
f154Report('fb154-sql-error','Сохранить причину ошибки до следующего обращения к драйверу',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Ошибочный SELECT вернул false — да
Сохранённый код ошибки — -206
Сообщение получено сразу — да
Сообщение указало NOT_A_FIELD — да
После исправленного SELECT код ошибки отсутствует — да
После исправленного SELECT сообщение отсутствует — да
Исправленный SELECT вернул название — Кабель
Сохранённый код после исправления — -206
Запустите example.php по README.txt. lab.php содержит открытый код проверки отдельной базы, захвата ожидаемых предупреждений, чтения и вывода отчёта. Создаётся одна собственная таблица; после наблюдений она удаляется. Рядом остаются result.html, result.json и metrics.json.
Открыть наглядный результат · Посмотреть данные и наблюдения JSON.
Границы и ошибки
Не вызывайте другой запрос, affected_rows, освобождение ресурса, rollback или close перед снимком: драйвер может заменить или сбросить состояние. Один код SQLCODE покрывает класс причин, поэтому для разработчика нужен и текст, а не только число. Текст может содержать SQL, значения и путь к базе; пример публикует только безопасные признаки. В рабочем приложении подробности относятся к ограниченному журналу с идентификатором операции. Отсутствие кода Firebird не исключает PHP warning или TypeError из-за неверного аргумента. После успешного SELECT сброс — поведение проверенного профиля, а не способ удалить уже записанный журнал.
Запрос с TOP был написан в диалекте другой СУБД и завершался ошибкой неизвестного SQL-токена. Исходный вопрос на Stack Overflow. Сначала читаем конкретную причину. Для Firebird выбираем его поддерживаемый способ ограничения строк, задаём ORDER BY и передаём значения параметрами. Наша программа намеренно создаёт другую точно известную ошибку — неизвестный столбец -206; не выдаём её за воспроизведение -104.
Ошибка преобразования кодировки появлялась при fetch, хотя соединение и запрос уже были созданы. Исходный вопрос на Stack Overflow. Проверки не заканчиваются на query: фиксируем и сбой чтения. Кодировку согласуют с действительными данными и клиентом; исторический совет поставить NONE не переносим как универсальное лечение Unicode. Сам сценарий сбоя кодировки в этом пакете не заявляется выполненным.
Попробуйте изменить условие
Что будет, если сначала выполнить диагностический SELECT 1, а только затем прочитать ibase_errmsg?
Разбор решения
Вы рискуете получить false вместо исходной причины. Сначала сохраните оба значения в обычные PHP-переменные; уже после этого выполняйте дополнительные действия. Успешный следующий запрос не изменяет сохранённые переменные.
Отменить пакет изменения каталога при дублирующемся ключе
Редактор сохраняет пакет правок каталога: снижает цену существующего товара и добавляет новый. В загружаемых данных у нового товара по ошибке тот же ID. Первая команда уже успела изменить цену; отказ второй не должен оставить половину пакета. Сохраним причину нарушения ключа и откатим всю явную транзакцию.
- Исходный товар: ID 1, Кабель, 2500 коп.
- Начать $tx; UPDATE меняет цену на 2000 коп.
- INSERT другого товара с ID 1 → false, код -803
- Сразу сохранить диагностику до rollback
- rollback($tx) отменяет UPDATE вместе со всем пакетом
- Новая выборка: один товар, прежние 2500 коп.
Самый короткий шаг
$tx = ibase_trans(IBASE_WRITE, $db);
// Все команды одного пакета получают именно $tx.
$saved = ibase_query($tx, 'INSERT INTO PRODUCTS (ID, TITLE) VALUES (?, ?)', 1, 'Переходник');
if ($saved === false) {
$code = ibase_errcode(); $message = ibase_errmsg();
if (ibase_rollback($tx) !== true) { throw new RuntimeException('Откат не выполнен'); }
$tx = null;
}PRIMARY KEY делает ID уникальным: две строки с ID 1 недопустимы. Транзакция — одна группа изменений с общим решением подтвердить или отменить. В ibase_query передаётся $tx, а не $db, чтобы обе записи принадлежали именно этой группе. affected_rows($tx) проверяет её первую команду. Нельзя поставить commit между UPDATE и INSERT: после подтверждения первая цена уже не отменяется последующим rollback.
Результат самостоятельного запуска
| Наблюдение | Результат |
|---|---|
| Цена до пакета, коп. | 2500 |
| Цена внутри транзакции после UPDATE, коп. | 2000 |
| Первая команда изменила строк | 1 |
| Вторая команда вернула false | да |
| Код нарушения уникальности | -803 |
| Весь пакет явно отменён | да |
| Цена после rollback, коп. | 2500 |
| Товаров после rollback | 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);
$items=f154OwnTable($db,$owned,'constraint','(ID INTEGER NOT NULL PRIMARY KEY, TITLE VARCHAR(40) CHARACTER SET UTF8 NOT NULL, PRICE_KOPECKS INTEGER NOT NULL CHECK (PRICE_KOPECKS >= 0))');
$q=f154Qualified($items);
f154Command($db,"INSERT INTO $q (ID,TITLE,PRICE_KOPECKS) VALUES (?,?,?)",[1,'Кабель',2500]);
f154Commit($db);
$tx=ibase_trans(IBASE_WRITE,$db);
if ($tx===false) {f154Failure('Транзакция изменения каталога не начата');}
try {
f154Command($tx,"UPDATE $q SET PRICE_KOPECKS=? WHERE ID=?",[2000,1]);
$changed=ibase_affected_rows($tx);
if ($changed!==1) {throw new RuntimeException('Должна измениться цена одного товара');}
$tentative=(int)f154Read($tx,"SELECT PRICE_KOPECKS FROM $q WHERE ID=?",[1])[0]['PRICE_KOPECKS'];
$duplicate=f154Capture(static fn()=>ibase_query($tx,"INSERT INTO $q (ID,TITLE,PRICE_KOPECKS) VALUES (?,?,?)",1,'Переходник',900));
if ($duplicate['value']!==false || $duplicate['code']!==-803 || !$duplicate['message_present']) {
throw new RuntimeException('Ожидался контролируемый отказ из-за повторного ID');
}
// A failed second statement does not itself undo the successful first statement.
$rolledBack=ibase_rollback($tx);
if ($rolledBack!==true) {f154Failure('Не удалось отменить весь пакет каталога');}
$tx=null;
} finally {
if (is_resource($tx)) {
if (ibase_rollback($tx)!==true) {f154Failure('Не удалось завершить незаконченный пакет');}
$tx=null;
}
}
$stored=f154Read($db,"SELECT ID,TITLE,PRICE_KOPECKS FROM $q ORDER BY ID");
$rows=[['Цена до пакета, коп.',2500],['Цена внутри транзакции после UPDATE, коп.',$tentative],
['Первая команда изменила строк',$changed],['Вторая команда вернула false',$duplicate['value']===false],
['Код нарушения уникальности',$duplicate['code']],['Весь пакет явно отменён',$rolledBack],
['Цена после rollback, коп.',$stored[0]['PRICE_KOPECKS']],['Товаров после rollback',count($stored)]];
$checks=['first_write_happened'=>$changed===1&&$tentative===2000,'duplicate_rejected'=>$duplicate['value']===false,
'constraint_diagnostic'=>$duplicate['code']===-803&&$duplicate['message_present']&&$duplicate['warning'],
'explicit_rollback'=>$rolledBack===true,'original_price_restored'=>$stored[0]['PRICE_KOPECKS']===2500,
'catalog_unchanged'=>$stored===[['ID'=>1,'TITLE'=>'Кабель','PRICE_KOPECKS'=>2500]]];
$observations=['price_before'=>2500,'price_inside_transaction'=>2000,'first_affected'=>$changed,
'duplicate_return'=>$duplicate['value'],'duplicate_error_code'=>$duplicate['code'],
'duplicate_message_present'=>$duplicate['message_present'],'raw_error_text_published'=>false,
'explicit_rollback'=>$rolledBack,'commits_for_business_batch'=>0,'stored_rows_after'=>$stored];
} finally {
if (is_resource($db)) {try {$removed=f154Cleanup($db,$owned);} finally {ibase_close($db);}}
}
f154Report('fb154-constraint-rollback','Отменить пакет изменения каталога при дублирующемся ключе',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Цена до пакета, коп. — 2500
Цена внутри транзакции после UPDATE, коп. — 2000
Первая команда изменила строк — 1
Вторая команда вернула false — да
Код нарушения уникальности — -803
Весь пакет явно отменён — да
Цена после rollback, коп. — 2500
Товаров после rollback — 1
Запустите example.php по README.txt. lab.php содержит открытый код проверки отдельной базы, захвата ожидаемых предупреждений, чтения и вывода отчёта. Создаётся одна собственная таблица; после наблюдений она удаляется. Рядом остаются result.html, result.json и metrics.json.
Открыть наглядный результат · Посмотреть данные и наблюдения JSON.
Границы и ошибки
В этом примере -803 вызван точно известным повтором первичного ключа. В произвольной схеме тот же класс ошибки может относиться к другому UNIQUE-ограничению: изучайте сохранённое сообщение и схему, а не угадывайте по числу. Не повторяйте INSERT бесконечно и не меняйте параметризацию на склейку SQL. Нарушение отдельной команды не является автоматическим откатом всего пакета в Firebird. Показан один последовательный запуск без конкурентного конфликта и сетевого обрыва. Для успешной ветки отдельный commit тоже требует проверки; её полный разбор находится в главе транзакций.
Попробуйте изменить условие
Новый товар получил ID 2, но первая цена была подтверждена до INSERT. Достаточно ли rollback только вокруг INSERT, чтобы вернуть исходный пакет?
Разбор решения
Нет. Уже выполненный commit не отменяется. Обе команды должны быть внутри одной незавершённой транзакции; при успехе подтверждается весь пакет, при отказе откатывается весь пакет.
Подтвердить изменения, закрыть соединение и прочитать их заново
Фоновая задача обновила служебный счётчик и заканчивает работу с базой. Нужно отделить два события: данные подтверждены и соединение больше не нужно. Сначала явно подтвердим значение 166, проверим commit, закроем соединение и удалим ссылку. Затем подключимся заново и увидим сохранённое 166, а не прежнее 100.
- Первое соединение: COUNTER_VALUE = 100 → UPDATE 166
- Проверить affected_rows = 1
- Явный commit вернул true
- close($db) вернул true; $db = null
- Новое соединение → SELECT возвращает 166
- Освободить результат; обычный массив PHP остаётся доступен
Самый короткий шаг
if (ibase_commit($db) !== true) {
throw new RuntimeException('Запись не подтверждена');
}
if (ibase_close($db) !== true) {
throw new RuntimeException('Соединение не закрыто');
}
$db = null;commit отвечает за судьбу незавершённых изменений, close — за завершение использования соединения. Это разные операции с отдельными результатами. После close переменная обнуляется, чтобы случайно не использовать прежний ресурс. Повторное подключение получает новый ресурс; SELECT проверяет сохранённые данные. Массив $values уже относится к памяти PHP, поэтому его можно показать после освобождения результата.
Результат самостоятельного запуска
| Наблюдение | Результат |
|---|---|
| Первое соединение изменило строк | 1 |
| Commit проверен до close | да |
| Первое соединение закрыто | да |
| Новое соединение читает сохранённое значение | 166 |
| Копия результата остаётся в PHP | 166 |
| Обращений к прежнему закрытому ресурсу | 0 |
Полный файл, создание таблицы и архив для запуска
<?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);
$counters=f154OwnTable($db,$owned,'close','(ID INTEGER NOT NULL PRIMARY KEY, COUNTER_VALUE INTEGER NOT NULL)');
$q=f154Qualified($counters);
f154Command($db,"INSERT INTO $q (ID,COUNTER_VALUE) VALUES (?,?)",[1,100]);
f154Commit($db);
f154Command($db,"UPDATE $q SET COUNTER_VALUE=? WHERE ID=?",[166,1]);
$changed=ibase_affected_rows($db);
if ($changed!==1) {throw new RuntimeException('Счётчик не обновлён');}
$committed=ibase_commit($db);
if ($committed!==true) {f154Failure('Изменение счётчика не подтверждено');}
$closed=ibase_close($db);
if ($closed!==true) {f154Failure('Первое соединение не закрыто');}
$db=null;
$db=f154Connect($config);
$result=f154Query($db,"SELECT COUNTER_VALUE FROM $q WHERE ID=?",[1]);
try {$values=f154Rows($result);}
finally {
if (ibase_free_result($result)!==true) {f154Failure('Результат повторного чтения не освобождён');}
$result=null;
}
$persisted=(int)$values[0]['COUNTER_VALUE'];
// End the read transaction explicitly as well, before final table cleanup and close.
f154Commit($db);
$rows=[['Первое соединение изменило строк',$changed],['Commit проверен до close',$committed],
['Первое соединение закрыто',$closed],['Новое соединение читает сохранённое значение',$persisted],
['Копия результата остаётся в PHP',$values[0]['COUNTER_VALUE']],['Обращений к прежнему закрытому ресурсу',0]];
$checks=['one_row_updated'=>$changed===1,'commit_before_close'=>$committed===true,'close_succeeded'=>$closed===true,
'reopened_connection_works'=>is_resource($db),'committed_value_persisted'=>$persisted===166,
'php_array_copy_retained'=>$values===[['COUNTER_VALUE'=>166]]];
$observations=['initial_value'=>100,'committed_value'=>166,'reopened_value'=>$persisted,
'write_commit_explicit'=>true,'read_commit_explicit'=>true,'first_connection_closed'=>$closed,
'old_handle_called_after_close'=>false,'persistent_connection_used'=>false,
'lifecycle'=>['connect','write','explicit commit','close + null','connect again','read','free_result + null','explicit commit','cleanup own table','close']];
} finally {
if (is_resource($db)) {try {$removed=f154Cleanup($db,$owned);} finally {ibase_close($db);}}
}
f154Report('fb154-connection-close','Подтвердить изменения, закрыть соединение и прочитать их заново',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Первое соединение изменило строк — 1
Commit проверен до close — да
Первое соединение закрыто — да
Новое соединение читает сохранённое значение — 166
Копия результата остаётся в PHP — 166
Обращений к прежнему закрытому ресурсу — 0
Запустите example.php по README.txt. lab.php содержит открытый код проверки отдельной базы, захвата ожидаемых предупреждений, чтения и вывода отчёта. Создаётся одна собственная таблица; после наблюдений она удаляется. Рядом остаются result.html, result.json и metrics.json.
Открыть наглядный результат · Посмотреть данные и наблюдения JSON.
Границы и ошибки
По базовому контракту ibase_close подтверждает стандартную транзакцию соединения, а остальные незавершённые транзакции откатывает. Это правило нужно знать, но бизнес-логику строят на явном commit/rollback с проверкой до close: случайное закрытие не должно решать, принимать ли половину работы. Здесь используется обычный connect, а не pconnect. Persistent-соединение имеет другой срок жизни и не закрывается этой функцией как обычное. Не обращайтесь к старым результатам/подготовкам после закрытия; сначала освободите их. Повторное чтение доказывает видимость подтверждённых данных в этом запуске, но не является испытанием аварийного отключения питания.
Попробуйте изменить условие
Можно ли вызвать close, проигнорировать ошибку commit и всё равно написать посетителю «сохранено»?
Разбор решения
Нет. Сначала проверяется исход записи и явного подтверждения. Если подтверждение не установлено, нельзя сообщать успех. Для сетевого обрыва в момент commit требуется отдельная проверка состояния операции по её уникальному идентификатору; сам close её не заменяет.
Справочник результата записи и диагностики
| Функция | Назначение |
|---|---|
| ibase_affected_rows | Получить число затронутых строк именно нужной транзакции. |
| ibase_errcode | Немедленно сохранить числовой код последней ошибки расширения. |
| ibase_errmsg | Немедленно сохранить текст причины отказа для диагностики. |
| ibase_close | Закрыть обычное соединение после явного решения о транзакциях. |
Связать проверку с действительным результатом
Для каждой записи сначала определите, что должно произойти: какой товар, какое количество, какая версия и какая группа изменений. Выразите условия в SQL, сразу проверьте исход и число строк, подтвердите либо отмените транзакцию. Ошибку сохраняйте до завершающих вызовов. Закрытие соединения выполняет последнюю обязанность владельца ресурсов, но не принимает бизнес-решение за приложение.
Дальше соберите полный успешный пакет и его откат; повторяемые запросы находятся в главе параметров и подготовок. Ошибки подключения, чтения и записи относятся к разным шагам: диагностируйте тот вызов, который действительно отказал. В этой главе четыре воспроизводимых сценария и четыре карточки; это не заявление о завершении всего Firebird API.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
ibase_affected_rowsПолучить число затронутых строк именно нужной транзакции.
Встроено в PHP 5 и PHP 7 до 7.4; затем отдельное расширение. Проверенный профиль: interbase 3.0.1 / PHP 8.2. · interbase
ibase_affected_rows(resource $connectionOrTransaction = ...): int|falseСкладской UPDATE может завершиться успешно, но не изменить ни одной строки. ibase_affected_rows показывает количество строк предыдущей записи в нужном транзакционном контексте. По этому числу приложение решает, состоялось ли резервирование, обновление карточки или удаление выбранной записи.
- Команда вернула false → сначала сохранить диагностику
- Команда успешна; affected_rows = 0 → условие не нашло строк
- Команда успешна; affected_rows = 1 → затронута одна строка
- Отдельно проверить commit; только затем подтверждать результат пользователю
ibase_affected_rows(): int|false
ibase_affected_rows(resource $connectionOrTransaction): int|false| Аргумент/результат | Смысл |
|---|---|
| Без аргумента | Стандартная транзакция последнего открытого соединения. Для ясности лучше передавать ресурс явно. |
| $connectionOrTransaction | Соединение — его стандартная транзакция; ресурс явной транзакции — именно этот контекст, а не другая транзакция того же соединения. |
| int | Число затронутых строк предыдущего INSERT, UPDATE или DELETE; 0 допустим. Это не число колонок и не общий размер таблицы. |
| false | В реализации 3.0.1, например, когда у соединения ещё нет транзакционного контекста. Проверяйте строго, не превращая false в ноль. |
Краткая сигнатура PHP Manual перечисляет int; здесь дополнительно показан фактический false из реализации выбранной версии. Флагов нет. Не передавайте результат SELECT вместо соединения или транзакции. Неподходящий/освобождённый ресурс либо неверный PHP-тип может вызвать предупреждение или исключение; это не обещанный обычный нулевой результат. Читайте счётчик сразу после нужной команды, до следующих SQL-вызовов и завершения её транзакции. Счётчик не подтверждает commit, не определяет количество строк SELECT и не обещает число всех побочных действий триггеров.
Остатка должно хватить
$saved = ibase_query($db,
'UPDATE PRODUCTS SET STOCK = STOCK - ? WHERE ID = ? AND STOCK >= ?', 2, 1, 2);
if ($saved === false) { throw new RuntimeException('SQL не выполнен'); }
$changed = ibase_affected_rows($db);
if ($changed === false) { throw new RuntimeException('Не получен счётчик'); }
$reserved = ($changed === 1);Контекстный фрагмент использует готовую PRODUCTS. Полная программа с собственной таблицей показывает первую запись с числом 1 и вторую с числом 0. Проверка количества не заменяет проверку подтверждения транзакции.
Не перезаписать чужую правку
$saved = ibase_query($tx,
'UPDATE ARTICLES SET TITLE = ?, REVISION = REVISION + 1 WHERE ID = ? AND REVISION = ?',
$newTitle, $articleId, $knownRevision);
if ($saved === false) { throw new RuntimeException('Правка не выполнена'); }
$changed = ibase_affected_rows($tx);
if ($changed === false) { throw new RuntimeException('Нет счётчика правки'); }
// 1 — версия совпала; 0 — запись отсутствует или её версия уже другая.
// Завершите свою транзакцию явным commit/rollback по решению приложения.Этот вариант предполагает поле REVISION и существующую явную транзакцию. Условие версии находится в самой записи, поэтому заранее прочитанная старая карточка не перезапишет новую молча. Это контекстный вариант, не отдельный измеренный конкурентный тест. Если результат важен как бизнес-событие, предусмотрите уникальный ключ операции и ограничения в базе. Дальше: ibase_commit, ibase_rollback и ibase_errcode.
ibase_errcodeНемедленно сохранить числовой код последней ошибки расширения.
Встроено в PHP 5 и PHP 7 до 7.4; затем отдельное расширение. Проверенный профиль: interbase 3.0.1 / PHP 8.2. · interbase
ibase_errcode(): int|falseПосле отказа записи нужно отличить неизвестное поле от нарушения ограничения. ibase_errcode возвращает сохранённый SQL-код последней ошибки расширения. Число удобно включить в диагностическую запись, но конкретную причину уточняют по сообщению и контексту операции.
ibase_errcode(): int|false| Часть контракта | Объяснение |
|---|---|
| Параметры | Нет. Нельзя передать $db, $tx или результат; функция не выбирает соединение. |
| int | Сохранённый код последнего отказа. Например, -206 в нашем запросе неизвестного поля; -803 при дублирующемся ключе. |
| false | Драйвер не хранит ошибку. Это не доказательство, что любой PHP-вызов был корректен. |
| Область | Текущее состояние расширения в запросе PHP, а не история ошибок каждого соединения. Другой вызов может заменить его. |
Чтение ibase_errcode и ibase_errmsg само не сбрасывает этот снимок. Многие другие функции драйвера очищают его перед своей работой, включая успешные обращения. Поэтому сначала проверяют возвращённое значение сбойного действия, затем немедленно сохраняют оба поля. Не выполняйте ibase_affected_rows для диагностики команды, которая уже вернула false. Код SQLCODE не равен SQLSTATE, системному errno или полному вектору статусов Firebird.
Поймать опечатку отчёта до очистки состояния
$result = ibase_query($db, 'SELECT NOT_A_FIELD FROM PRODUCTS');
if ($result === false) {
$failure = ['code' => ibase_errcode(), 'message' => ibase_errmsg()];
// Сохранить безопасный снимок; затем выполнять восстановление/закрытие.
}В самостоятельном примере код равен -206. Следующий исправленный SELECT очищает текущее состояние, но $failure или $savedCode остаётся обычной переменной PHP. Не выводите сырые $failure["message"] и SQL на публичную страницу.
Откатить пакет, сохранив причину отказа
$saved = ibase_query($tx, 'INSERT INTO PRODUCTS (ID, TITLE) VALUES (?, ?)', 1, 'Новый');
if ($saved === false) {
$failureCode = ibase_errcode();
$failureMessage = ibase_errmsg();
if (ibase_rollback($tx) !== true) { throw new RuntimeException('Откат не подтверждён'); }
$tx = null;
// Снимок относится к INSERT, а не к более позднему rollback.
}Полная программа изменения каталога сохраняет -803 до отката. Сам код не говорит, какое из нескольких UNIQUE-ограничений нарушено, и не разрешает автоматически повторять операцию. В реализации 3.0.1 ошибки самого модуля могут использовать -999; неверные аргументы PHP не обязаны иметь SQL-код вообще. Флагов и специальных режимов у функции нет. Связанные объяснения: ibase_errmsg, ibase_rollback и ibase_affected_rows.
ibase_errmsgНемедленно сохранить текст причины отказа для диагностики.
Встроено в PHP 5 и PHP 7 до 7.4; затем отдельное расширение. Проверенный профиль: interbase 3.0.1 / PHP 8.2. · interbase
ibase_errmsg(): string|falseЧисла -206 недостаточно, чтобы найти опечатку среди полей отчёта. ibase_errmsg даёт текст последней ошибки: например, упоминает неизвестное имя столбца. Это инструмент разработчика для установления причины; посетителю нужен понятный итог действия без внутреннего SQL и пути к базе.
ibase_errmsg(): string|false| Часть контракта | Объяснение |
|---|---|
| Параметры | Нет: функция не принимает ни соединение, ни код ошибки. |
| string | Сохранённое текстовое пояснение последней ошибки расширения. Точный текст зависит от драйвера/сервера и причины. |
| false | Сохранённой ошибки нет. Проверяйте строго: это не пустая строка и не документированное сообщение «успешно». |
| Время чтения | Сразу после неудачного действия, вместе с ibase_errcode, до следующего обращения к драйверу. |
- Ошибка запроса → code + message немедленно в локальные переменные
- Посетитель → «Изменение не сохранено», идентификатор операции
- Ограниченный журнал → код и отредактированные технические подробности
- Cleanup/rollback выполняются после сохранения исходной причины
Проверить причину без публикации внутренних данных
$result = ibase_query($db, 'SELECT NOT_A_FIELD FROM PRODUCTS');
if ($result === false) {
$errorCode = ibase_errcode();
$message = ibase_errmsg();
$hasMessage = is_string($message) && $message !== '';
$mentionsColumn = $hasMessage && str_contains($message, 'NOT_A_FIELD');
// Для учебного отчёта достаточно errorCode, hasMessage и mentionsColumn.
}Полный запускаемый пример сохраняет именно эти безопасные признаки и удаляет сырую строку. В рабочем журнале подробности требуют контроля доступа и удаления персональных данных. Поиск подстроки здесь проверяет намеренно созданную учебную ошибку, а не служит универсальным классификатором всех ошибок базы.
Не принять ошибку чтения за конец списка
$row = ibase_fetch_assoc($result);
if ($row === false) {
$fetchCode = ibase_errcode();
$fetchMessage = ibase_errmsg();
if ($fetchMessage !== false || $fetchCode !== false) {
throw new RuntimeException('Не удалось прочитать результат');
}
// При отсутствии других предупреждений это обычный конец выдачи.
}Контекстный фрагмент требует действительного результата SELECT. Ошибка может возникнуть только при получении строки, например при преобразовании данных. В полном lab.php временный обработчик дополнительно фиксирует PHP warning, чтобы отсутствие SQL-кода не скрывало его. Не заменяйте проверку на @ или безусловный break при любом false.
Текст не является стабильным машинным протоколом и может содержать путь, имена таблиц или данные. Не стройте бизнес-решения на полном точном совпадении английской фразы и не вставляйте сообщение в HTML без экранирования. Экранирование предотвращает интерпретацию HTML, но не скрывает приватное содержимое. Функция ничего не исправляет, не откатывает и не очищает журнал. Вернитесь к ibase_errcode и откату всего пакета после отказа.
ibase_closeЗакрыть обычное соединение после явного решения о транзакциях.
Встроено в PHP 5 и PHP 7 до 7.4; затем отдельное расширение. Проверенный профиль: interbase 3.0.1 / PHP 8.2. · interbase
ibase_close(resource $connection = ...): boolЗадача закончила читать или писать базу. ibase_close завершает использование обычного соединения; заранее нужно решить судьбу своих транзакций и освободить результаты. Отдельная проверка commit перед close делает момент сохранения явным.
ibase_close(): bool
ibase_close(resource $connection): bool| Параметр/результат | Правило |
|---|---|
| $connection | Обычное соединение ibase_connect. Не ресурс транзакции, не подготовленный запрос, не результат SELECT. |
| Без аргумента | Последнее открытое соединение. При нескольких подключениях указывайте нужное явно. |
| Возврат | true при успехе, false при отказе по контракту PHP Manual. Неверный тип/непригодный ресурс может вызвать отдельную PHP-ошибку. |
| Транзакции | Базовый контракт закрытия: стандартная транзакция подтверждается; прочие незавершённые транзакции откатываются. |
| Persistent | Соединение ibase_pconnect не закрывается этой функцией как обычное; его срок жизни относится к persistent-механизму. |
В краткой официальной сигнатуре встречается значение по умолчанию null. Для необязательного последнего соединения используйте вызов без аргумента: реализация проверенного драйвера 3.0.1 разбирает переданный аргумент как resource, поэтому передача явного null не равна отсутствию аргумента. Специальных флагов закрытия нет. Закрытие соединения не удаляет файл базы и не является ibase_drop_db.
Сохранить счётчик и закончить работу
if (ibase_commit($db) !== true) { throw new RuntimeException('Commit не выполнен'); }
if (ibase_close($db) !== true) { throw new RuntimeException('Close не выполнен'); }
$db = null;
// Новая задача получает новое соединение; старый ресурс больше не используется.Самостоятельная программа снова подключается и получает сохранённое значение 166. Она не полагается на неявное подтверждение при закрытии и освобождает результат повторного чтения отдельно.
Отменить неудачный пакет, затем закрыть соединение
// Условие: приложение уже решило отменить собственную явную транзакцию $tx.
if (ibase_rollback($tx) !== true) { throw new RuntimeException('Rollback не выполнен'); }
$tx = null;
// Все подготовленные запросы и результаты были освобождены раньше.
if (ibase_close($db) !== true) { throw new RuntimeException('Close не выполнен'); }
$db = null;Это продолжение отказа второго шага каталога: откат и закрытие имеют отдельные результаты, а исходная ошибка сохранена до обоих вызовов. При обычной успешной работе результат SELECT освобождают через free_result, подготовку — через free_query, транзакцию завершают и затем закрывают соединение.
Не передавайте ресурс повторно после закрытия и не выводите из is_resource заключение, что уже освобождённый внутренний объект ещё пригоден. Результаты и подготовки могут удерживать ссылки; завершайте их явно и убирайте переменные через null/unset. Обычные массивы, уже скопированные в PHP, при этом остаются. Автоматическое закрытие в конце PHP-запроса не заменяет явного решения о сохранении. Связанные карточки: ibase_commit, ibase_rollback, ibase_errmsg; для подключения — ibase_connect.
Источники и обсуждения
- PHP Manual — ibase_affected_rows
- PHP Manual — ibase_errcode
- PHP Manual — ibase_errmsg
- PHP Manual — ibase_close
- PHP Manual — ibase_pconnect
- PHP Manual — ibase_commit
- PHP Manual — ibase_rollback
- FirebirdSQL — interbase.c версии 3.0.1
- FirebirdSQL — ibase_query.c версии 3.0.1
- Firebird — язык SQL 5.0
- PHP — установка Firebird/InterBase
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


