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

Дерево руководства
Глава 65 · После первых запросов, параметров и транзакций Firebird
Firebird: строки для CSV, объекты для карточки, ID и текущая строка курсора
CSV с реальными строками и кавычками, карточка с NULL/пустым текстом/нулём, ID после rollback и обновление текущей строки курсора.
Сначала выбрать результат, затем способ чтения
SQL уже выбрал нужные данные. Теперь надо решить, в какой форме их получить и что сделать с результатом: записать каталог в CSV, показать карточку, выдать новому товару ID или обновить текущую строку старого курсорного приложения. Эти задачи похожи только тем, что используют базу; каждой соответствует свой контракт и видимый результат.
- Файл с фиксированным порядком столбцов → fetch_row и fputcsv
- Карточка с именованными свойствами → fetch_object
- Новый технический ключ → gen_id существующей последовательности
- Обновление текущей позиции SELECT → name_result и WHERE CURRENT OF
- Для всех веток: конкретные поля и условия SQL, проверка отказа, освобождение ресурсов
Предварительные главы: подключение, таблица и первый SELECT, параметры и повторяемые запросы, подтверждение и откат записей. Соседняя глава разбирает контракт параметров и освобождение подготовки. Здесь не требуется выгружать всё содержимое таблицы, чтобы затем фильтровать его в PHP: каждый сценарий задаёт нужные строки и поля в SQL.
Новая строка появляется при INSERT, а fetch только читает результат. Курсор — текущая позиция чтения, не массив всех строк. Последовательность — отдельный счётчик базы, не таблица и не PHP-генератор с yield. В примерах используется отдельная локальная база, PHP 8.2 x64 и внешнее расширение interbase 3.0.1. Пометка PHP Manual «PHP 5, PHP 7 < 7.4» описывает прежнее встроенное расширение. README каждого архива содержит подготовку используемого профиля и запуск; примеры не обращаются к рабочим таблицам сайта.
Выгрузить выбранные товары в CSV, сохранив ноль и кавычки
Для передачи каталога другому сервису нужны ID, название и остаток активных товаров. Таблица хранит ещё и признак активности, но в CSV он не нужен. SQL сразу выберет нужные столбцы, отфильтрует архив и задаст порядок. PHP получает одну строку за раз и записывает её через fputcsv: разделитель внутри названия и кавычки не сломают файл.
- Таблица: 1 / Кабель; силовой / 0 / активен; 2 / Клемма "мини" / 4 / активен; 3 / архив / 9 / выключен
- WHERE ACTIVE = 1, ORDER BY ID; SELECT только ID, TITLE, STOCK
- Первый fetch_row → [1, "Кабель; силовой", 0]
- fputcsv → 1;"Кабель; силовой";0
- Второй fetch_row → второй товар; следующий → false без ошибки
- catalog.csv содержит заголовок и ровно две строки
Самый короткий шаг
$row = ibase_fetch_row($result);
// При SELECT ID, TITLE, STOCK: $row[0] — ID, $row[1] — название, $row[2] — остаток.
if ($row !== false) {
fputcsv($file, $row, ';', '"', '', "\n");
}$result — уже полученный результат SELECT, $file — открытый файловый поток. Квадратные скобки обращаются к полям по их позиции в SELECT, начиная с нуля. Один fetch читает одну следующую строку, а не считает строки. fputcsv сам экранирует CSV-кавычки; пустой параметр escape отключает отдельное backslash-экранирование. В полной программе while повторяет чтение до конца и отдельно отличает предупреждение от обычного false.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Файл рядом с example.php | catalog.csv |
| Заголовок CSV | id;title;stock |
| Первая строка CSV | 1;"Кабель; силовой";0 |
| Вторая строка CSV | 2;"Клемма ""мини""";4 |
| Выгружено товаров | 2 |
| Нулевой остаток сохранён | да |
| Архивный товар исключён SQL | да |
| Обратное чтение CSV восстановило поля | да |
Полная программа, подготовка данных и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=f156Config();$db=f156Connect($config);$runtime=f156Runtime($db,$config);
$items=f156OwnTable($db,$owned,'csv','(ID INTEGER NOT NULL PRIMARY KEY, TITLE VARCHAR(60) CHARACTER SET UTF8 NOT NULL, STOCK INTEGER NOT NULL, ACTIVE SMALLINT NOT NULL)');
$q=f156Qualified($items);
foreach ([[1,'Кабель; силовой',0,1],[2,'Клемма "мини"',4,1],[3,'Архивный товар',9,0]] as $item) {
f156Command($db,"INSERT INTO $q (ID,TITLE,STOCK,ACTIVE) VALUES (?,?,?,?)",$item);
}
f156Commit($db);
$result=$stream=null;$fetched=[];$eof=false;
$csvFile=__DIR__.'/catalog.csv';
try {
$result=f156Query($db,"SELECT ID,TITLE,STOCK FROM $q WHERE ACTIVE=? ORDER BY ID",[1]);
$stream=fopen($csvFile,'wb');
if ($stream===false) {throw new RuntimeException('CSV не открыт для записи');}
if (fputcsv($stream,['id','title','stock'],';','"','',"\n")===false) {throw new RuntimeException('Заголовок CSV не записан');}
while (true) {
$call=f156Capture(static fn()=>ibase_fetch_row($result));
if ($call['warning'] || $call['code']!==0) {f156Failure('Ошибка чтения строки',$call);}
if ($call['value']===false) {$eof=true;break;}
$row=$call['value'];
if (fputcsv($stream,$row,';','"','',"\n")===false) {throw new RuntimeException('Строка CSV не записана');}
$fetched[]=$row; // Только две строки учебного отчёта; большой экспорт так не накапливают.
}
} finally {
if (is_resource($stream)) {fclose($stream);$stream=null;}
if (is_resource($result)) {ibase_free_result($result);$result=null;}
}
f156Commit($db);
$expectedCsv="id;title;stock\n1;\"Кабель; силовой\";0\n2;\"Клемма \"\"мини\"\"\";4\n";
$actualCsv=file_get_contents($csvFile);
if ($actualCsv===false) {throw new RuntimeException('CSV не прочитан после записи');}
$parsed=[];$stream=fopen($csvFile,'rb');
if ($stream===false) {throw new RuntimeException('CSV не открыт повторно');}
try {while (($line=fgetcsv($stream,0,';','"',''))!==false) {$parsed[]=$line;}}
finally {fclose($stream);$stream=null;}
$rows=[['Файл рядом с example.php','catalog.csv'],['Заголовок CSV','id;title;stock'],
['Первая строка CSV','1;"Кабель; силовой";0'],['Вторая строка CSV','2;"Клемма ""мини""";4'],
['Выгружено товаров',count($fetched)],['Нулевой остаток сохранён',$fetched[0][2]===0],
['Архивный товар исключён SQL',count($fetched)===2],['Обратное чтение CSV восстановило поля',$parsed===[['id','title','stock'],['1','Кабель; силовой','0'],['2','Клемма "мини"','4']]]];
$checks=['selected_rows'=>$fetched===[[1,'Кабель; силовой',0],[2,'Клемма "мини"',4]],
'clean_eof'=>$eof,'exact_csv'=>$actualCsv===$expectedCsv,'csv_roundtrip'=>$parsed===[['id','title','stock'],['1','Кабель; силовой','0'],['2','Клемма "мини"','4']],
'zero_preserved'=>$fetched[0][2]===0,'sql_filtered_archive'=>count($fetched)===2];
$observations=['query_columns'=>['ID','TITLE','STOCK'],'where_active'=>1,'order_by'=>'ID','fetched_rows'=>$fetched,
'csv_filename'=>'catalog.csv','csv_utf8'=>$actualCsv,'parsed_csv'=>$parsed,'sha256'=>hash('sha256',$actualCsv),'eof'=>$eof,'production_export_needs_row_accumulation'=>false];
} finally {
if (is_resource($db)) {try {$removed=f156Cleanup($db,$owned);} finally {ibase_close($db);}}
}
f156Report('fb156-csv-export','Выгрузить выбранные товары в CSV, сохранив ноль и кавычки',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Файл рядом с example.php — catalog.csv
Заголовок CSV — id;title;stock
Первая строка CSV — 1;"Кабель; силовой";0
Вторая строка CSV — 2;"Клемма ""мини""";4
Выгружено товаров — 2
Нулевой остаток сохранён — да
Архивный товар исключён SQL — да
Обратное чтение CSV восстановило поля — да
Запустите example.php по README.txt. Создаются одна собственная таблица в отдельной локальной базе. Все собственные SQL-объекты удаляются после наблюдений; result.html, result.json и metrics.json остаются рядом. lab.php содержит открытый код соединения, проверки отдельной базы, чтения, диагностики и отчёта. Дополнительно остаётся catalog.csv с двумя показанными товарами; отчёт проверяет его точные байты и обратное чтение.
Открыть наглядный результат · Посмотреть строки и наблюдения JSON.
Границы, ошибки и выбор другого способа
Результат CSV содержит текстовые ячейки: при обратном чтении число 0 станет строкой "0". Для SQL NULL нужно заранее выбрать правило обмена: CSV сам не различает NULL и пустую ячейку. Здесь все три экспортируемых столбца NOT NULL. Кодировка файла — UTF-8 без BOM, разделитель — точка с запятой; принимающий сервис должен ожидать этот формат. Названия в примере фиксированы. Если открывать произвольный пользовательский текст в электронных таблицах, отдельно учитывайте интерпретацию формул: CSV-кавычки её не отключают. Большой экспорт пишет строки в файл, не собирает весь результат в массив и не сортирует его в PHP. Две строки $fetched оставлены только для наглядного учебного отчёта.
В исходном вопросе первый fetch использовался как проверка количества, а затем другая функция чтения начинала уже со следующей строки. Исходная задача на Stack Overflow. В нашем обходе строка читается ровно один раз на итерацию. В опубликованном ответе есть дополнительная ошибка: while не получает следующую строку и повторяет старую. Этот ответ не копируем; направление курсора проверено по PHP Manual, драйверу и фактическому наблюдению.
Попробуйте изменить условие
Добавьте поле ACTIVE в начало SELECT. Почему без изменения заголовка CSV получится неверный файл?
Разбор решения
Позиции сдвинутся: первый элемент станет ACTIVE, а не ID. Исправьте одновременно явный список SELECT и заголовок либо сохраните прежние три столбца. Для экспорта контрактом служит порядок выбранных полей, а не порядок столбцов таблицы.
Получить карточку товара с понятными свойствами и различить NULL, пустую строку и ноль
Шаблону карточки удобнее получить $product->title, чем помнить, что название было вторым столбцом. ibase_fetch_object превращает следующую строку в обычный stdClass со свойствами. На двух товарах видно, почему отсутствие примечания, намеренно пустой текст и нулевой остаток нельзя объединять проверкой empty().
- SELECT ID, TITLE AS "title", NOTE AS "note", STOCK AS "stock"
- Строка 1 → stdClass: ID = 1, title = Кабель, note = null, stock = 0
- ->title читает название; ->stock сохраняет число 0
- property_exists(note) = true, но isset(note) = false
- Строка 2 → title = "", note = "", stock = 4
- Данные объектов остаются после освобождения результата SELECT
Самый короткий шаг
$product = ibase_fetch_object($result);
if ($product !== false) {
echo htmlspecialchars($product->title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}Стрелка -> обращается к свойству объекта. Имя title задано в SELECT как AS "title": двойные кавычки SQL сохраняют точный регистр псевдонима. Не заключённое в кавычки ID приходит как ID. Это контейнер данных stdClass; он не получает методы сохранения, проверку прав или автоматическое обновление базы. htmlspecialchars нужен при выводе текстового свойства в HTML.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Тип первой строки | stdClass |
| ID первой карточки | 1 |
| Название первой карточки | Кабель |
| Остаток первой карточки | 0 |
| Примечание первой карточки | не указано |
| Свойство note существует | да |
| isset для note с NULL | нет |
| Название второй карточки пустое | да |
| Примечание второй карточки | пустая строка |
| Остаток второй карточки | 4 |
| После двух карточек получен конец выдачи | да |
Полная программа, подготовка данных и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=f156Config();$db=f156Connect($config);$runtime=f156Runtime($db,$config);
$items=f156OwnTable($db,$owned,'object','(ID INTEGER NOT NULL PRIMARY KEY, TITLE VARCHAR(40) CHARACTER SET UTF8, NOTE VARCHAR(40) CHARACTER SET UTF8, STOCK INTEGER NOT NULL)');
$q=f156Qualified($items);
f156Command($db,"INSERT INTO $q (ID,TITLE,NOTE,STOCK) VALUES (?,?,?,?)",[1,'Кабель',null,0]);
f156Command($db,"INSERT INTO $q (ID,TITLE,NOTE,STOCK) VALUES (?,?,?,?)",[2,'','',4]);
f156Commit($db);
$result=f156Query($db,'SELECT ID,TITLE AS "title",NOTE AS "note",STOCK AS "stock" FROM '.$q.' ORDER BY ID');
try {
$call=f156Capture(static fn()=>ibase_fetch_object($result));
if ($call['value']===false || $call['warning']) {f156Failure('Первая карточка не прочитана',$call);}
$first=$call['value'];unset($call);
$call=f156Capture(static fn()=>ibase_fetch_object($result));
if ($call['value']===false || $call['warning']) {f156Failure('Вторая карточка не прочитана',$call);}
$second=$call['value'];unset($call);
$end=f156Capture(static fn()=>ibase_fetch_object($result));
if ($end['warning'] || $end['code']!==0) {f156Failure('Ошибка в конце выдачи',$end);}
} finally {ibase_free_result($result);$result=null;}
f156Commit($db);
$noteLabel=$first->note===null?'не указано':($first->note===''?'пустая строка':$first->note);
$secondNote=$second->note===null?'не указано':($second->note===''?'пустая строка':$second->note);
$rows=[['Тип первой строки',get_class($first)],['ID первой карточки',$first->ID],['Название первой карточки',$first->title],
['Остаток первой карточки',$first->stock],['Примечание первой карточки',$noteLabel],
['Свойство note существует',property_exists($first,'note')],['isset для note с NULL',isset($first->note)],
['Название второй карточки пустое',$second->title===''],['Примечание второй карточки',$secondNote],
['Остаток второй карточки',$second->stock],['После двух карточек получен конец выдачи',$end['value']===false]];
$checks=['stdclass'=>get_class($first)==='stdClass','quoted_alias'=>$first->title==='Кабель'&&property_exists($first,'stock'),
'unquoted_uppercase'=>$first->ID===1,'null_kept'=>$first->note===null,'null_property_exists'=>property_exists($first,'note')&&!isset($first->note),
'empty_kept'=>$second->title===''&&$second->note==='','zero_kept'=>$first->stock===0,
'second_stock'=>$second->stock===4,'clean_eof'=>$end['value']===false&&!$end['warning']&&$end['code']===0];
$observations=['first'=>get_object_vars($first),'second'=>get_object_vars($second),'class'=>get_class($first),
'null_label'=>$noteLabel,'empty_label'=>$secondNote,'eof'=>$end['value'],'result_freed_before_render'=>true];
} finally {
if (is_resource($db)) {try {$removed=f156Cleanup($db,$owned);} finally {ibase_close($db);}}
}
f156Report('fb156-object-card','Получить карточку товара с понятными свойствами и различить NULL, пустую строку и ноль',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Тип первой строки — stdClass
ID первой карточки — 1
Название первой карточки — Кабель
Остаток первой карточки — 0
Примечание первой карточки — не указано
Свойство note существует — да
isset для note с NULL — нет
Название второй карточки пустое — да
Примечание второй карточки — пустая строка
Остаток второй карточки — 4
После двух карточек получен конец выдачи — да
Запустите example.php по README.txt. Создаются одна собственная таблица в отдельной локальной базе. Все собственные SQL-объекты удаляются после наблюдений; result.html, result.json и metrics.json остаются рядом. lab.php содержит открытый код соединения, проверки отдельной базы, чтения, диагностики и отчёта.
Открыть наглядный результат · Посмотреть строки и наблюдения JSON.
Границы, ошибки и выбор другого способа
Одна строка-объект не является результатом целиком. Каждый следующий fetch сдвигает тот же курсор; после EOF читать заново можно только новой выборкой. Одинаковые имена выбранных полей конфликтуют: при JOIN задавайте уникальные псевдонимы. isset возвращает false и для отсутствующего свойства, и для свойства со значением NULL; property_exists отвечает только о наличии. Проверяйте === null, === "" и === 0 отдельно, когда это важно для интерфейса. В примере TITLE допускает NULL по схеме, но конкретные названия — строка и пустая строка; при выводе произвольных данных nullable title сначала задайте текстовую замену.
Автор получал массив через ibase_fetch_row, но пытался читать его как объект через ->; одновременно расходились имена переменных и регистр псевдонимов SQL. Исходная задача на Stack Overflow. Выбираем одно представление строки, задаём явные уникальные псевдонимы и обращаемся к ним точным способом: [0] для числового массива или ->title для объекта. Исправление только регистра в ответе не заменяет проверку типа результата.
Попробуйте изменить условие
Как показать «Нет на складе» для stock = 0, сохранив разницу между пустым примечанием и NULL?
Разбор решения
Для остатка используйте $product->stock === 0. Для примечания сначала сравните === null, затем === "". Не используйте одну empty($product->note) для всех смыслов: пустая строка и отсутствующее значение отвечают на разные вопросы.
Выдать новый ID и увидеть, почему rollback оставляет пропуск
Новому товару нужен устойчивый технический ключ, чтобы другие таблицы могли ссылаться именно на него. Последовательность хранит счётчик в базе и выдаёт следующее значение одним обращением. Отменим первый INSERT и сохраним второй: товар будет один, его ID будет 2. Так становится понятно, почему ID не равен количеству записей и пропуск не доказывает потерю данных.
- CREATE SEQUENCE → собственный счётчик = 0
- ibase_gen_id(name) → 1; INSERT товара с ID = 1
- ROLLBACK → товар 1 исчез; счётчик остался 1
- ibase_gen_id(name, 1, db) → 2
- INSERT товара 2 → COMMIT
- В таблице одна строка: ID = 2, Новый товар
Самый короткий шаг
$id = ibase_gen_id('PRODUCT_ID_SEQ', 1, $db);
if ($id === false) { throw new RuntimeException('Новый ID не получен'); }
// $id — выданное значение. INSERT и commit выполняются отдельно.PRODUCT_ID_SEQ — заранее созданная последовательность, а не таблица и не PHP-переменная. Второй аргумент 1 означает увеличение на единицу и возврат нового значения; 0 только читает текущее. Третий аргумент указывает соединение. Последовательность не создаёт строку и не выполняет INSERT сама: полученный $id передают параметром в отдельный запрос. PRIMARY KEY таблицы продолжает проверять уникальность.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Начальное значение своей последовательности | 0 |
| ID отменённой записи | 1 |
| Строк с отменённым ID после rollback | 0 |
| Счётчик после rollback | 1 |
| Следующий выданный ID | 2 |
| Сохранённый товар | Новый товар |
| ID сохранённого товара | 2 |
| Число сохранённых товаров | 1 |
| Rollback вернул номер в последовательность | нет |
Полная программа, подготовка данных и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=f156Config();$db=f156Connect($config);$runtime=f156Runtime($db,$config);
$items=f156OwnTable($db,$owned,'ids','(ID BIGINT NOT NULL PRIMARY KEY, TITLE VARCHAR(40) CHARACTER SET UTF8 NOT NULL)');
$q=f156Qualified($items);$sequence=f156OwnSequence($db,$owned,'id_seq');
$call=f156Capture(static fn()=>ibase_gen_id($sequence,0,$db));
if ($call['value']===false || $call['warning']) {f156Failure('Значение последовательности не прочитано',$call);}
$initial=$call['value'];unset($call);
$call=f156Capture(static fn()=>ibase_gen_id($sequence));
if ($call['value']===false || $call['warning']) {f156Failure('Первый ID не выдан',$call);}
$firstId=$call['value'];unset($call);$tx=null;
try {
$call=f156Capture(static fn()=>ibase_trans(IBASE_WRITE|IBASE_CONCURRENCY|IBASE_NOWAIT,$db));
if ($call['value']===false || $call['warning']) {f156Failure('Транзакция не создана',$call);}
$tx=$call['value'];unset($call);
f156Command($tx,"INSERT INTO $q (ID,TITLE) VALUES (?,?)",[$firstId,'Отменённый товар']);
if (ibase_rollback($tx)!==true) {f156Failure('Первая запись не отменена');}
$tx=null;
} finally {if (is_resource($tx)) {ibase_rollback($tx);$tx=null;}}
$call=f156Capture(static fn()=>ibase_gen_id($sequence,0,$db));
if ($call['value']===false || $call['warning']) {f156Failure('Счётчик после rollback не прочитан',$call);}
$afterRollback=$call['value'];unset($call);
$cancelledCount=(int)f156Read($db,"SELECT COUNT(*) AS N FROM $q WHERE ID=?",[$firstId])[0]['N'];
$call=f156Capture(static fn()=>ibase_gen_id($sequence,1,$db));
if ($call['value']===false || $call['warning']) {f156Failure('Следующий ID не выдан',$call);}
$nextId=$call['value'];unset($call);
f156Command($db,"INSERT INTO $q (ID,TITLE) VALUES (?,?)",[$nextId,'Новый товар']);f156Commit($db);
$stored=f156Read($db,"SELECT ID,TITLE FROM $q ORDER BY ID");f156Commit($db);
$rows=[['Начальное значение своей последовательности',$initial],['ID отменённой записи',$firstId],
['Строк с отменённым ID после rollback',$cancelledCount],['Счётчик после rollback',$afterRollback],
['Следующий выданный ID',$nextId],['Сохранённый товар',$stored[0]['TITLE']],['ID сохранённого товара',$stored[0]['ID']],
['Число сохранённых товаров',count($stored)],['Rollback вернул номер в последовательность',$afterRollback===$initial]];
$checks=['initial_zero'=>$initial===0,'default_increment_one'=>$firstId===1,'cancelled_row_absent'=>$cancelledCount===0,
'sequence_not_rolled_back'=>$afterRollback===1,'next_id_two'=>$nextId===2,'one_stored_row'=>$stored===[['ID'=>2,'TITLE'=>'Новый товар']],
'integer_on_current_profile'=>is_int($firstId)&&is_int($nextId)];
$observations=['initial'=>$initial,'allocated_before_rollback'=>$firstId,'current_after_rollback'=>$afterRollback,
'cancelled_rows'=>$cancelledCount,'next'=>$nextId,'stored'=>$stored,'generator_name_from_request'=>false,
'sequence_reset_attempted'=>false,'guaranteed_gapless'=>false];
} finally {
if (is_resource($db)) {try {$removed=f156Cleanup($db,$owned);} finally {ibase_close($db);}}
}
f156Report('fb156-generated-id','Выдать новый ID и увидеть, почему rollback оставляет пропуск',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Начальное значение своей последовательности — 0
ID отменённой записи — 1
Строк с отменённым ID после rollback — 0
Счётчик после rollback — 1
Следующий выданный ID — 2
Сохранённый товар — Новый товар
ID сохранённого товара — 2
Число сохранённых товаров — 1
Rollback вернул номер в последовательность — нет
Запустите example.php по README.txt. Создаются одна собственная таблица и одна последовательность в отдельной локальной базе. Все собственные SQL-объекты удаляются после наблюдений; result.html, result.json и metrics.json остаются рядом. lab.php содержит открытый код соединения, проверки отдельной базы, чтения, диагностики и отчёта.
Открыть наглядный результат · Посмотреть строки и наблюдения JSON.
Границы, ошибки и выбор другого способа
Генератор Firebird — синоним последовательности базы, а не PHP-генератор с yield. Изменение счётчика находится вне отката пользовательской транзакции. Нельзя после отказа уменьшать его на единицу: другое подключение уже могло получить следующий номер. Не заменяйте его SELECT MAX(ID) + 1. Технический ID с пропусками и непрерывная бизнес-нумерация документов — разные задачи; правила последней нужно проектировать отдельно. 64-битный результат на показанном PHP x64 приходит как int; на платформах с меньшим PHP int большое значение может быть строкой. Не превращайте его в float. Имя SQL-объекта задаёт автор приложения: ibase_gen_id в драйвере 3.0.1 вставляет его в SQL и ограничивает длину 31 байтом, поэтому произвольное имя из запроса недопустимо.
Разработчик хотел вернуть значение генератора после неудавшегося INSERT и сделать нумерацию без пропусков. Исходная задача на Stack Overflow. Отделяем технический ID от бизнес-нумерации. Наши собственные данные показывают отменённую строку 1 и сохранённую строку 2; счётчик не уменьшаем. Правило об откате проверено по руководству Firebird, а обсуждение использовано как источник реальной задачи.
Попробуйте изменить условие
Два вызова ibase_gen_id(name, 0, db) вернули 2. Можно ли выдать следующему товару число 3 обычным сложением в PHP?
Разбор решения
Нет: между чтением и INSERT другой процесс может получить тот же следующий номер. Нужно вызвать ibase_gen_id(name, 1, db) и использовать именно возвращённое значение. Вызов с шагом 0 полезен для наблюдения, а не для резервирования будущего ключа.
Обновить именно текущий товар открытого курсора и сохранить сверку остатков
В существующем приложении сверка склада уже устроена как обход изменяемого курсора. Для каждого полученного товара есть отдельное подтверждённое число пересчёта. ibase_name_result даёт результату SQL-имя; WHERE CURRENT OF связывает UPDATE с последней успешно прочитанной строкой именно этого курсора. Разберём такой способ и его границы: для обычной веб-формы UPDATE по первичному ключу обычно проще.
- SELECT двух товаров FOR UPDATE OF STOCK → открытый результат
- ibase_name_result → имя FB156_STOCK_CURSOR
- Когда fetch вернул ID 1, STOCK 0: пересчёт = 10
- UPDATE ... WHERE CURRENT OF → именно прочитанный товар получает новое значение
- Когда fetch вернул ID 2, STOCK 4: пересчёт = 14; порядок обхода не важен
- Освободить UPDATE и SELECT → COMMIT; readback ORDER BY ID показывает 10, 14, 9
Самый короткий шаг
if (ibase_name_result($result, 'STOCK_CURSOR') !== true) {
throw new RuntimeException('Курсор не назван');
}
// Теперь имя STOCK_CURSOR можно использовать в WHERE CURRENT OF.
// Сначала получите текущую строку через fetch; UPDATE выполняется до следующего fetch.Курсор здесь — позиция внутри открытого результата SELECT. Функция не переименовывает таблицу и не выбирает строку по текстовому имени: имя позволяет другой SQL-команде сослаться на текущую позицию. Пока ни одной строки не прочитано или уже получен EOF, подходящей текущей строки нет. Полная программа создаёт SELECT и подготовленный UPDATE в одной явной транзакции, проверяет каждое изменение и только затем подтверждает весь пересчёт.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Имя назначено открытому результату | да |
| Кабель: остаток до сверки | 0 |
| Кабель: остаток после сверки | 10 |
| Клемма: остаток до сверки | 4 |
| Клемма: остаток после сверки | 14 |
| Товар вне выборки сохранил остаток | 9 |
| Обновлено текущих строк | 2 |
| Оба обновления подтверждены одной транзакцией | да |
| Курсор дошёл до конца без ошибки | да |
Полная программа, подготовка данных и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=f156Config();$db=f156Connect($config);$runtime=f156Runtime($db,$config);
$items=f156OwnTable($db,$owned,'cursor','(ID INTEGER NOT NULL PRIMARY KEY, TITLE VARCHAR(40) CHARACTER SET UTF8 NOT NULL, STOCK INTEGER NOT NULL)');
$q=f156Qualified($items);
foreach ([[1,'Кабель',0],[2,'Клемма',4],[3,'Архив',9]] as $item) {f156Command($db,"INSERT INTO $q (ID,TITLE,STOCK) VALUES (?,?,?)",$item);}
f156Commit($db);
$counted=[1=>10,2=>14];$tx=$cursor=$update=null;$applied=[];$cleanEof=false;
try {
$call=f156Capture(static fn()=>ibase_trans(IBASE_WRITE|IBASE_CONCURRENCY|IBASE_NOWAIT,$db));
if ($call['value']===false || $call['warning']) {f156Failure('Транзакция сверки не создана',$call);}
$tx=$call['value'];unset($call);
$cursor=f156Query($tx,"SELECT ID,STOCK FROM $q WHERE ID<=2 FOR UPDATE OF STOCK");
$call=f156Capture(static fn()=>ibase_name_result($cursor,'FB156_STOCK_CURSOR'));
if ($call['value']!==true || $call['warning']) {f156Failure('Курсор не получил имя',$call);}
$named=$call['value'];unset($call);
$call=f156Capture(static fn()=>ibase_prepare($tx,"UPDATE $q SET STOCK=? WHERE CURRENT OF FB156_STOCK_CURSOR"));
if ($call['value']===false || $call['warning']) {f156Failure('Обновление текущей строки не подготовлено',$call);}
$update=$call['value'];unset($call);
while (true) {
$call=f156Capture(static fn()=>ibase_fetch_row($cursor));
if ($call['warning'] || $call['code']!==0) {f156Failure('Строка сверки не прочитана',$call);}
if ($call['value']===false) {$cleanEof=true;break;}
$current=$call['value'];unset($call);$id=$current[0];
if (!array_key_exists($id,$counted) || $counted[$id]<0) {throw new RuntimeException('Нет допустимого результата пересчёта');}
$newStock=$counted[$id];
$call=f156Capture(static fn()=>ibase_execute($update,$newStock));
if ($call['value']===false || $call['warning']) {f156Failure('Текущая строка не изменена',$call);}
$changed=ibase_affected_rows($tx);
if ($changed!==1) {throw new RuntimeException('Сверка должна изменить ровно текущую строку');}
$applied[$id]=['before'=>$current[1],'after'=>$newStock,'changed'=>$changed];unset($call);
}
if (ibase_free_query($update)!==true) {f156Failure('Подготовка UPDATE не освобождена');}$update=null;
if (ibase_free_result($cursor)!==true) {f156Failure('Курсор SELECT не освобождён');}$cursor=null;
if (ibase_commit($tx)!==true) {f156Failure('Сверка остатков не подтверждена');}$tx=null;
} finally {
if (is_resource($update)) {ibase_free_query($update);$update=null;}
if (is_resource($cursor)) {ibase_free_result($cursor);$cursor=null;}
if (is_resource($tx)) {ibase_rollback($tx);$tx=null;}
}
$stored=f156Read($db,"SELECT ID,STOCK FROM $q ORDER BY ID");f156Commit($db);
$rows=[['Имя назначено открытому результату',$named],['Кабель: остаток до сверки',$applied[1]['before']],
['Кабель: остаток после сверки',$stored[0]['STOCK']],['Клемма: остаток до сверки',$applied[2]['before']],
['Клемма: остаток после сверки',$stored[1]['STOCK']],['Товар вне выборки сохранил остаток',$stored[2]['STOCK']],
['Обновлено текущих строк',count($applied)],['Оба обновления подтверждены одной транзакцией',true],['Курсор дошёл до конца без ошибки',$cleanEof]];
$checks=['cursor_named'=>$named===true,'two_rows_updated'=>count($applied)===2,
'first_current_row'=>$applied[1]===['before'=>0,'after'=>10,'changed'=>1],
'second_current_row'=>$applied[2]===['before'=>4,'after'=>14,'changed'=>1],
'committed_values'=>$stored===[['ID'=>1,'STOCK'=>10],['ID'=>2,'STOCK'=>14],['ID'=>3,'STOCK'=>9]],
'outside_selection_unchanged'=>$stored[2]['STOCK']===9,'clean_eof'=>$cleanEof];
$observations=['cursor_name'=>'FB156_STOCK_CURSOR','counted'=>$counted,'applied'=>$applied,'stored'=>$stored,
'selection_condition'=>'ID <= 2','same_explicit_transaction'=>true,'update_before_next_fetch'=>true,
'query_and_result_released_before_commit'=>true,'concurrency_load_test'=>false];
} finally {
if (is_resource($db)) {try {$removed=f156Cleanup($db,$owned);} finally {ibase_close($db);}}
}
f156Report('fb156-positioned-update','Обновить именно текущий товар открытого курсора и сохранить сверку остатков',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Имя назначено открытому результату — да
Кабель: остаток до сверки — 0
Кабель: остаток после сверки — 10
Клемма: остаток до сверки — 4
Клемма: остаток после сверки — 14
Товар вне выборки сохранил остаток — 9
Обновлено текущих строк — 2
Оба обновления подтверждены одной транзакцией — да
Курсор дошёл до конца без ошибки — да
Запустите example.php по README.txt. Создаются одна собственная таблица в отдельной локальной базе. Все собственные SQL-объекты удаляются после наблюдений; result.html, result.json и metrics.json остаются рядом. lab.php содержит открытый код соединения, проверки отдельной базы, чтения, диагностики и отчёта.
Открыть наглядный результат · Посмотреть строки и наблюдения JSON.
Границы, ошибки и выбор другого способа
Этот способ работает с изменяемой выборкой, а не с любым отчётом JOIN, GROUP BY или агрегатом. Имя курсора задаётся постоянной строкой приложения; его нельзя подставлять из HTTP-параметра. Сохранённый PHP-массив строки не сохраняет серверную позицию после следующего fetch, free_result или завершения транзакции и не обновляется автоматически после UPDATE. Итог подтверждён отдельным SELECT с ORDER BY ID; порядок обхода исходного курсора без ORDER BY не обещается. Не держите транзакцию открытой, пока человек вводит цифры: здесь подтверждённые значения уже есть до начала операции. В веб-интерфейсе отправляют ID и проверенную версию записи, затем выполняют короткий UPDATE с условием и контролем affected_rows. Если правило одинаковое для всех строк, один подходящий UPDATE обычно проще обхода в PHP. Пример показывает API текущей строки, а не рекомендует курсоры вместо любых массовых SQL-операций и не является нагрузочным испытанием блокировок.
Попробуйте изменить условие
Можно ли сначала получить все строки курсора в массив, закрыть результат, а потом выполнить UPDATE ... WHERE CURRENT OF для каждого элемента?
Разбор решения
Нет. WHERE CURRENT OF использует живую серверную позицию, а не индекс PHP-массива. Для отложенного обновления сохраните первичный ключ и необходимые условия версии, затем выполните отдельный параметризованный UPDATE по ним. Для текущего курсора обновление должно произойти до следующего fetch и до освобождения результата.
Справочник форм результата, ID и позиции
| Функция | Когда нужна |
|---|---|
| ibase_fetch_row | Получить следующую строку как массив полей по порядку SELECT. |
| ibase_fetch_object | Получить следующую строку как объект с именованными свойствами. |
| ibase_gen_id | Изменить последовательность и получить выданное значение. |
| ibase_name_result | Назначить результату имя для UPDATE/DELETE WHERE CURRENT OF. |
Сочетать выбранную форму данных с задачей
Для выгрузки задайте состав полей и порядок в SQL, затем пишите каждую строку в файл. Для шаблона дайте полям ясные уникальные имена и сохраните разницу NULL, пустой строки и нуля. Для ключа используйте назначение последовательности, не пытаясь вернуть использованный номер. Для курсорного обновления соблюдайте время жизни результата и транзакции; обычную веб-форму удобнее связывать с первичным ключом и версией записи.
Следующий шаг — проверить параметры подготовленного запроса и различить успешную команду, ноль строк и отказ. Большие значения читаются отдельно: BLOB, файл и HTTP-выдача. Здесь четыре конкретных сценария и четыре карточки; выбор формы чтения не означает завершения всего API Firebird.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
ibase_fetch_rowПолучить следующую строку как массив полей по порядку SELECT.
Встроено в PHP 5 и PHP 7 до 7.4; затем внешнее расширение. Профиль примеров: interbase 3.0.1 / PHP 8.2 x64. · interbase
ibase_fetch_row(resource $result_identifier, int $fetch_flag = 0): array|falseЕсли полученные поля нужны в заданном порядке — например для CSV, табличного обмена или позиционного списка — ibase_fetch_row возвращает следующую строку как числовой массив. Поле с номером 0 соответствует первому выражению SELECT, а не обязательно первому столбцу таблицы.
- SELECT ID, TITLE, STOCK
- fetch_row → [1, "Кабель; силовой", 0]
- [0] = ID; [1] = TITLE; [2] = STOCK
- Следующий fetch_row → следующая строка; после последней → false
ibase_fetch_row(resource $result_identifier, int $fetch_flag = 0): array|false| Часть контракта | Объяснение |
|---|---|
| $result_identifier | Живой результат SELECT, полученный от ibase_query или ibase_execute. Соединение, подготовленный запрос и число изменённых строк не подходят. |
| $fetch_flag | Необязательная комбинация IBASE_TEXT и IBASE_UNIXTIME; по умолчанию 0. |
| array | Только одна строка. Ключи 0, 1, 2… соответствуют порядку полей SELECT. SQL NULL сохраняется как PHP null. |
| false | Больше строк нет; ошибка чтения также требует немедленной проверки диагностики и предупреждений. В заголовке PHP Manual написано array, но раздел возврата явно включает false. |
| fetch_flag | Что изменяется |
|---|---|
| 0 — значение по умолчанию | Обычное представление полей: BLOB передаётся как идентификатор; дата/время — как форматированная строка. |
| IBASE_TEXT | Вместо идентификаторов BLOB возвращается их содержимое. Имя флага не означает, что произвольные двоичные байты стали текстом нужной кодировки. |
| IBASE_UNIXTIME | Дата/время возвращаются в виде Unix timestamp вместо форматированной строки. Учитывайте настройки времени и смысл SQL DATE/TIME, особенно для значений без часового пояса. |
| IBASE_TEXT | IBASE_UNIXTIME | Побитовое OR объединяет обе настройки. Это не флаги направления, количества строк или преобразования NULL. |
Применение 1: строка каталога в CSV
// Условия: $result — SELECT ID, TITLE, STOCK; $file — открытый CSV-файл.
$row = ibase_fetch_row($result);
if ($row !== false) {
$written = fputcsv($file, $row, ';', '"', '', "\n");
if ($written === false) { throw new RuntimeException('CSV не записан'); }
}Для строки [1, "Кабель; силовой", 0] получится 1;"Кабель; силовой";0. Название заключено в кавычки из-за разделителя внутри, остаток 0 остаётся отдельной ячейкой. В самостоятельной программе с созданием таблицы есть полный цикл, закрытие файла, освобождение результата и обратное чтение готового CSV.
Применение 2: получить одно числовое значение отчёта
$result = ibase_query($db, 'SELECT COUNT(*) FROM PRODUCTS WHERE ACTIVE = ?', 1);
if ($result === false) { throw new RuntimeException('Счётчик не получен'); }
try {
$row = ibase_fetch_row($result);
if ($row === false) { throw new RuntimeException('Нет строки COUNT'); }
$activeCount = $row[0];
} finally {
ibase_free_result($result);
$result = null;
}Этот фрагмент предназначен для уже существующей PRODUCTS. Если активных товаров нет, COUNT вернёт строку [0], а не конец выдачи. Поэтому проверяют $row === false, а не значение $row[0] на истинность. COUNT(*) считает строки на стороне базы и не передаёт все поля всех товаров в PHP; это другое назначение звёздочки, чем SELECT * при выгрузке таблицы.
Не вызывайте fetch ради предварительной проверки «есть ли строки», а затем ещё раз для вывода: первый вызов уже забрал строку. Сохраните его результат или читайте в одном цикле. Не используйте $row->TITLE: это числовой массив. Для имён есть ibase_fetch_assoc, для свойств — ibase_fetch_object. Одинаковые названия столбцов не мешают числовым позициям, но изменение порядка SELECT меняет их смысл. Явно выбирайте поля и ORDER BY, если важен порядок строк.
Флаг IBASE_TEXT загружает содержимое BLOB в значение строки; для большого файла удобнее чтение BLOB частями. Ошибку fetch сохраняют до следующего обращения к драйверу: ibase_errcode и ibase_errmsg. После конца результата освободите его; уже прочитанные PHP-массивы остаются. В текущем пакете реально выполняется обычный fetch_flag = 0; специальные преобразования здесь описаны по контракту, а не заявлены отдельным запуском.
ibase_fetch_objectПолучить следующую строку как объект с именованными свойствами.
Встроено в PHP 5 и PHP 7 до 7.4; затем внешнее расширение. Профиль примеров: interbase 3.0.1 / PHP 8.2 x64. · interbase
ibase_fetch_object(resource $result_id, int $fetch_flag = 0): object|falseКарточке товара нужны понятные имена: title, note, stock. ibase_fetch_object получает следующую SQL-строку как stdClass, у которого поля становятся свойствами. Это помогает читать шаблон, когда у каждого значения есть имя; функция не создаёт объект вашей модели и не сохраняет изменения обратно в таблицу.
ibase_fetch_object(resource $result_id, int $fetch_flag = 0): object|false| Часть контракта | Объяснение |
|---|---|
| $result_id | Ресурс результата ibase_query либо ibase_execute. Чтение продолжает текущую позицию этого результата. |
| $fetch_flag | По умолчанию 0; IBASE_TEXT и IBASE_UNIXTIME можно объединить через |. |
| object | В проверенном драйвере stdClass со свойствами из имён/псевдонимов выбранных полей; значения не становятся строками только из-за объектной формы. |
| false | Обычный конец выдачи или отказ чтения; для отказа сохраните диагностику и предупреждение сразу. |
| Имя и регистр | AS "title" даёт свойство title; некавыченный ID приходит как ID. Обращения ->title и ->TITLE различаются. |
| fetch_flag | Что изменяется |
|---|---|
| 0 — значение по умолчанию | Обычное представление полей: BLOB передаётся как идентификатор; дата/время — как форматированная строка. |
| IBASE_TEXT | Вместо идентификаторов BLOB возвращается их содержимое. Имя флага не означает, что произвольные двоичные байты стали текстом нужной кодировки. |
| IBASE_UNIXTIME | Дата/время возвращаются в виде Unix timestamp вместо форматированной строки. Учитывайте настройки времени и смысл SQL DATE/TIME, особенно для значений без часового пояса. |
| IBASE_TEXT | IBASE_UNIXTIME | Побитовое OR объединяет обе настройки. Это не флаги направления, количества строк или преобразования NULL. |
- ID = 1 → $product->ID
- TITLE AS "title" → $product->title = "Кабель"
- NOTE AS "note" → $product->note = null
- STOCK AS "stock" → $product->stock = 0
- Никакого UPDATE от присваивания свойства не происходит
Применение 1: карточка и сообщение о примечании
// $result получен от SELECT TITLE AS "title", NOTE AS "note", STOCK AS "stock" ...
$product = ibase_fetch_object($result);
if ($product !== false) {
$note = $product->note === null ? 'не указано' : $product->note;
$title = $product->title ?? 'Без названия';
echo htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo ' / остаток: ', $product->stock;
echo ' / примечание: ', htmlspecialchars($note, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}Для первого товара получится «Кабель / остаток: 0 / примечание: не указано». NULL заменён текстом осознанно, а число 0 показано как число. Полная программа также показывает второй товар с пустыми строками и объясняет разницу property_exists и isset. Для реального HTML значение экранируется в месте вывода; объектная форма не выполняет экранирование сама.
Применение 2: передать выбранные свойства шаблону после закрытия результата
$product = ibase_fetch_object($result);
if ($product === false) { throw new RuntimeException('Карточка не получена'); }
ibase_free_result($result);
$result = null;
$view = [
'id' => $product->ID,
'title' => $product->title,
'has_note_field' => property_exists($product, 'note'),
'note_is_null' => $product->note === null,
'out_of_stock' => $product->stock === 0,
];В этом продолжении выборка имеет те же явные псевдонимы, что и полная программа. Для первого товара view содержит id = 1, title = Кабель, has_note_field = true, note_is_null = true, out_of_stock = true. Данные уже скопированы в PHP и доступны после free_result; серверный результат больше не нужен. Нельзя использовать isset($product->note), чтобы отличить отсутствие выбранного поля от выбранного SQL NULL.
Параметров имени класса и конструктора у функции нет. Если нужна собственная модель, создайте её отдельно из проверенных данных. Для JOIN назначайте уникальные псевдонимы, иначе одинаковые имена не представят два независимых свойства. Не смешивайте чтение массива [0] и объекта ->title; сравните ibase_fetch_row. После EOF функция не возвращается к первой строке. Большие BLOB при IBASE_TEXT занимают память целиком; см. чтение и экспорт BLOB. Неверный/освобождённый ресурс не является пустым результатом: это ошибка использования API, которую нужно исправить.
ibase_gen_idИзменить последовательность и получить выданное значение.
Встроено в PHP 5 и PHP 7 до 7.4; затем внешнее расширение. Профиль примеров: interbase 3.0.1 / PHP 8.2 x64. · interbase
ibase_gen_id(string $generator, int $increment = 1, resource $link_identifier = ...): int|string|falseЧтобы сохранить новый товар и затем связать с ним остатки или позиции заказа, нужен технический идентификатор. ibase_gen_id обращается к уже существующему счётчику Firebird, изменяет его на заданный шаг и возвращает новое значение. Выдача номера и вставка строки — отдельные действия.
ibase_gen_id(string $generator, int $increment = 1): int|string|false
ibase_gen_id(string $generator, int $increment, resource $link_identifier): int|string|false| Параметр/результат | Контракт |
|---|---|
| $generator | Имя существующей последовательности/генератора. В драйвере 3.0.1 допускается не более 31 байта; имя попадает в SQL как идентификатор. Это фиксированное имя приложения, не произвольный пользовательский ввод. |
| $increment = 1 | Шаг увеличения: 1 выдаёт следующее значение, 0 только читает текущее. Отрицательный шаг уменьшает счётчик и опасен для повторного использования ID. |
| $link_identifier | Соединение нужной базы. Если аргумент опущен, применяется соединение по умолчанию. Для нескольких баз передавайте ресурс явно. |
| int или string | Новое значение. Если оно не помещается в целое PHP на данной платформе, возвращается строка; её нельзя бездумно переводить во float. |
| false | Отказ: например неверное имя, отсутствующая последовательность или ошибка обращения к базе. Сохраните диагностику немедленно; PHP-ошибка типов/ресурсов рассматривается отдельно. |
Официальная краткая сигнатура показывает третий аргумент со значением null, но реализация 3.0.1 разбирает явно переданный аргумент как resource. Для выбора последнего соединения безопасно опустить третий аргумент, как в программе, а не передавать null. Историческая страница PHP Manual содержит только список аргументов и краткий возврат; детали здесь сопоставлены с исходным кодом драйвера и руководством Firebird. Новая последовательность создаётся SQL-командой CREATE SEQUENCE и подтверждается до использования; ibase_gen_id не создаёт её.
- Счётчик 0 → gen_id(..., 1) возвращает 1
- INSERT ID = 1 → rollback удаляет эту незавершённую запись
- Счётчик остаётся 1
- Следующая выдача → 2; INSERT и commit сохраняют товар 2
Применение 1: получить ID до вставки
$id = ibase_gen_id('PRODUCT_ID_SEQ', 1, $db);
if ($id === false) { throw new RuntimeException('ID не выдан'); }
$result = ibase_query($db,
'INSERT INTO PRODUCTS (ID, TITLE) VALUES (?, ?)', $id, 'Новый товар');
if ($result === false) { throw new RuntimeException('Товар не сохранён'); }
if (ibase_commit($db) !== true) { throw new RuntimeException('Commit не выполнен'); }Фрагмент предполагает существующие PRODUCTS и PRODUCT_ID_SEQ. Значение $id передаётся параметром и остаётся у приложения для ответа клиенту или следующей связанной записи. Если INSERT входит в несколько связанных изменений, используйте одну явную транзакцию и подтвердите весь пакет. Самостоятельная программа создаёт собственные объекты и показывает отменённый ID 1 и сохранённый ID 2.
Применение 2: наблюдать текущий счётчик без выдачи номера
$current = ibase_gen_id('PRODUCT_ID_SEQ', 0, $db);
if ($current === false) { throw new RuntimeException('Счётчик недоступен'); }
echo 'Текущее значение: ', $current;
// Это наблюдение. Другой процесс может увеличить счётчик сразу после чтения.После двух выдач из собственного примера будет 2, хотя в таблице только один товар. Такой вызов помогает понять состояние счётчика; это не COUNT строк, не максимальный существующий ID и не резерв следующего значения. Само чтение с шагом 0 тоже требует доступа к последовательности, а не обходит разрешения базы.
Последовательность находится вне отката транзакции. Пропуски допустимы; не уменьшайте счётчик после ошибки и не рассчитывайте новый ID как MAX(ID)+1. Последовательность атомарно выдаёт значение, но не гарантирует, что INSERT выполнится, пользователь имеет право создавать товар или повторный HTTP-запрос не создаст дубликат действия. PRIMARY KEY, права и ключ повторной операции решают отдельные задачи. Для новой схемы существуют identity-столбцы и серверная генерация с INSERT RETURNING; выбор зависит от того, нужен ли ID приложению до INSERT. Данная функция использует явный шаг GEN_ID, поэтому не следует молча приравнивать её к NEXT VALUE FOR с настроенным шагом последовательности. Пример шага 0 и 1 выполнен в локальном профиле; поведение экстремальных шагов и переполнения здесь не испытывается.
ibase_name_resultНазначить результату имя для UPDATE/DELETE WHERE CURRENT OF.
Встроено в PHP 5 и PHP 7 до 7.4; затем внешнее расширение. Профиль примеров: interbase 3.0.1 / PHP 8.2 x64. · interbase
ibase_name_result(resource $result, string $name): boolКогда приложение обходит изменяемую выборку и должно обновить именно уже прочитанную строку, ей можно дать имя курсора. ibase_name_result назначает это имя открытому результату; UPDATE или DELETE с WHERE CURRENT OF используют текущую позицию результата. Функция сама не меняет данные и не перемещает курсор.
ibase_name_result(resource $result, string $name): bool| Часть контракта | Объяснение |
|---|---|
| $result | Живой ресурс результата SELECT, не соединение и не подготовка UPDATE. Для изменения нужна подходящая изменяемая выборка. |
| $name | Имя курсора для SQL WHERE CURRENT OF. В примерах постоянный идентификатор автора, без внешних данных и конфликтов с другим открытым курсором. |
| Возврат | true при назначении имени; false при отказе. Диагностику сохраняют до следующего обращения. |
| Порядок | Открыть SELECT → назначить имя → подготовить связанную команду → получить строку → изменить текущую строку → продолжить чтение. |
| Срок жизни | Именованный курсор относится к открытому результату и его транзакции. После освобождения результата или завершения транзакции его позицию использовать нельзя. |
- SELECT ... FOR UPDATE → $result
- name_result($result, STOCK_CURSOR) → имя для SQL
- fetch → конкретная строка стала текущей
- UPDATE ... WHERE CURRENT OF STOCK_CURSOR → изменена она
- Следующий fetch перемещает позицию; старый PHP-массив её не удерживает
Применение 1: подтвердить найденный остаток
// $result уже открыт в той же $tx, что и обновление.
if (ibase_name_result($result, 'STOCK_CURSOR') !== true) {
throw new RuntimeException('Курсор не назван');
}
$update = ibase_prepare($tx,
'UPDATE PRODUCTS SET STOCK = ? WHERE CURRENT OF STOCK_CURSOR');
if ($update === false) { throw new RuntimeException('UPDATE не подготовлен'); }
$row = ibase_fetch_row($result);
if ($row !== false) {
$changed = ibase_execute($update, 10);
if ($changed === false) { throw new RuntimeException('Остаток не изменён'); }
}Число 10 попадёт в STOCK текущего товара, а не всех товаров и не обязательно товара с ID 10. Здесь показана середина работы с курсором; полная программа сверки создаёт таблицу и транзакцию, проверяет affected_rows, освобождает подготовку и результат перед commit. Итоговое чтение показывает Кабель 0 → 10, Клемма 4 → 14; товар вне выборки сохраняет 9.
Применение 2: удалить текущую устаревшую запись при обработке очереди
// $staleRows — открытый изменяемый SELECT в $tx; отбор устаревших записей сделан в SQL.
if (ibase_name_result($staleRows, 'STALE_CURSOR') !== true) {
throw new RuntimeException('Курсор очереди не назван');
}
$delete = ibase_prepare($tx,
'DELETE FROM JOB_QUEUE WHERE CURRENT OF STALE_CURSOR');
if ($delete === false) { throw new RuntimeException('DELETE не подготовлен'); }
$row = ibase_fetch_row($staleRows);
if ($row !== false) {
if (ibase_execute($delete) === false) { throw new RuntimeException('Задание не удалено'); }
}Фрагмент иллюстрирует тот же контракт для DELETE и предполагает готовую JOB_QUEUE. Он не является ещё одним выполненным MAIN. Проверка устаревания должна быть конкретной и выполнена до удаления; после действий освобождают подготовку, результат и подтверждают либо откатывают свою транзакцию. Для обычного удаления по известному ID проще параметризованный DELETE WHERE ID = ? с проверкой числа строк.
Не выполняйте WHERE CURRENT OF до первого успешного fetch или после EOF. SELECT и связанное изменение должны использовать тот же транзакционный контекст. FOR UPDATE в показанном SELECT обозначает намерение изменять выбранное поле; не выдавайте это за универсальную гарантию блокировки всего набора или защиты от всех конфликтов. Долгие транзакции и ожидание пользователя при открытом курсоре вредят совместной работе. Обычная агрегированная или объединённая отчётная выборка не обязательно изменяема. Имя не подставляется через ? как значение: оно является частью SQL, поэтому держите его фиксированным. Если нужна одна операция над всем набором по общему правилу, начните с одного UPDATE в SQL; если нужен отложенный выбор пользователя, используйте ID/версию и короткую транзакцию. См. жизненный цикл транзакции, число затронутых строк и владение подготовленным запросом.
Источники и обсуждения
- PHP Manual — ibase_fetch_row
- PHP Manual — ibase_fetch_object
- PHP Manual — ibase_gen_id
- PHP Manual — ibase_name_result
- PHP Manual — fputcsv
- FirebirdSQL — выборка и именованный курсор, драйвер 3.0.1
- FirebirdSQL — реализация ibase_gen_id, драйвер 3.0.1
- Firebird — генераторы и транзакции
- Firebird 5 — UPDATE и WHERE CURRENT OF
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


