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

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

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

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

Глава 65 · После первых запросов, параметров и транзакций Firebird

Firebird: строки для CSV, объекты для карточки, ID и текущая строка курсора

CSV с реальными строками и кавычками, карточка с NULL/пустым текстом/нулём, ID после rollback и обновление текущей строки курсора.

Сначала выбрать результат, затем способ чтения

SQL уже выбрал нужные данные. Теперь надо решить, в какой форме их получить и что сделать с результатом: записать каталог в CSV, показать карточку, выдать новому товару ID или обновить текущую строку старого курсорного приложения. Эти задачи похожи только тем, что используют базу; каждой соответствует свой контракт и видимый результат.

От нужного результата к подходящему вызову
  1. Файл с фиксированным порядком столбцов → fetch_row и fputcsv
  2. Карточка с именованными свойствами → fetch_object
  3. Новый технический ключ → gen_id существующей последовательности
  4. Обновление текущей позиции SELECT → name_result и WHERE CURRENT OF
  5. Для всех веток: конкретные поля и условия 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. Таблица: 1 / Кабель; силовой / 0 / активен; 2 / Клемма "мини" / 4 / активен; 3 / архив / 9 / выключен
  2. WHERE ACTIVE = 1, ORDER BY ID; SELECT только ID, TITLE, STOCK
  3. Первый fetch_row → [1, "Кабель; силовой", 0]
  4. fputcsv → 1;"Кабель; силовой";0
  5. Второй fetch_row → второй товар; следующий → false без ошибки
  6. 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.phpcatalog.csv
Заголовок CSVid;title;stock
Первая строка CSV1;"Кабель; силовой";0
Вторая строка CSV2;"Клемма ""мини""";4
Выгружено товаров2
Нулевой остаток сохранёнда
Архивный товар исключён SQLда
Обратное чтение CSV восстановило поляда
Полная программа, подготовка данных и архив для запуска
Выгрузить выбранные товары в 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().

Путь конкретных данных
  1. SELECT ID, TITLE AS "title", NOTE AS "note", STOCK AS "stock"
  2. Строка 1 → stdClass: ID = 1, title = Кабель, note = null, stock = 0
  3. ->title читает название; ->stock сохраняет число 0
  4. property_exists(note) = true, но isset(note) = false
  5. Строка 2 → title = "", note = "", stock = 4
  6. Данные объектов остаются после освобождения результата 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
После двух карточек получен конец выдачида
Полная программа, подготовка данных и архив для запуска
Получить карточку товара с понятными свойствами и различить NULL, пустую строку и нольСамостоятельный пример
<?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 не равен количеству записей и пропуск не доказывает потерю данных.

Путь конкретных данных
  1. CREATE SEQUENCE → собственный счётчик = 0
  2. ibase_gen_id(name) → 1; INSERT товара с ID = 1
  3. ROLLBACK → товар 1 исчез; счётчик остался 1
  4. ibase_gen_id(name, 1, db) → 2
  5. INSERT товара 2 → COMMIT
  6. В таблице одна строка: 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 после rollback0
Счётчик после rollback1
Следующий выданный ID2
Сохранённый товарНовый товар
ID сохранённого товара2
Число сохранённых товаров1
Rollback вернул номер в последовательностьнет
Полная программа, подготовка данных и архив для запуска
Выдать новый ID и увидеть, почему 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 по первичному ключу обычно проще.

Путь конкретных данных
  1. SELECT двух товаров FOR UPDATE OF STOCK → открытый результат
  2. ibase_name_result → имя FB156_STOCK_CURSOR
  3. Когда fetch вернул ID 1, STOCK 0: пересчёт = 10
  4. UPDATE ... WHERE CURRENT OF → именно прочитанный товар получает новое значение
  5. Когда fetch вернул ID 2, STOCK 4: пересчёт = 14; порядок обхода не важен
  6. Освободить 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, а не обязательно первому столбцу таблицы.

Один вызов — одна строка
  1. SELECT ID, TITLE, STOCK
  2. fetch_row → [1, "Кабель; силовой", 0]
  3. [0] = ID; [1] = TITLE; [2] = STOCK
  4. Следующий 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.
Разные значения в одном контейнере
  1. ID = 1 → $product->ID
  2. TITLE AS "title" → $product->title = "Кабель"
  3. NOTE AS "note" → $product->note = null
  4. STOCK AS "stock" → $product->stock = 0
  5. Никакого 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 не создаёт её.

Строка и счётчик живут по разным правилам
  1. Счётчик 0 → gen_id(..., 1) возвращает 1
  2. INSERT ID = 1 → rollback удаляет эту незавершённую запись
  3. Счётчик остаётся 1
  4. Следующая выдача → 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 → назначить имя → подготовить связанную команду → получить строку → изменить текущую строку → продолжить чтение.
Срок жизниИменованный курсор относится к открытому результату и его транзакции. После освобождения результата или завершения транзакции его позицию использовать нельзя.
Привязка команды к текущей строке
  1. SELECT ... FOR UPDATE → $result
  2. name_result($result, STOCK_CURSOR) → имя для SQL
  3. fetch → конкретная строка стала текущей
  4. UPDATE ... WHERE CURRENT OF STOCK_CURSOR → изменена она
  5. Следующий 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/версию и короткую транзакцию. См. жизненный цикл транзакции, число затронутых строк и владение подготовленным запросом.

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

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

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

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

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