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

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

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

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

Глава 58 · После массивов, условий и первого знакомства с таблицами

Firebird: подключение, нужные строки экрана и понятный результат

Карточка товара 189.00, две позиции экрана, NULL отдельно от нуля и освобождение выборки: четыре запуска с HTML-результатами и полный разбор connect/query/fetch_assoc/free_result.

Зачем обращаться к Firebird и что вернётся в PHP

Сайт должен показать конкретный товар и несколько доступных позиций, а не заставлять человека разбираться в ресурсе базы. В этой главе получатся карточка «Кабель 3x2.5 — 12 шт. — 189.00 руб.», две позиции каталога, три состояния остатка и повторное чтение после освобождения выборки. Каждая программа создаёт свои данные и оставляет открываемый HTML-отчёт с фактическим результатом.

База — хранилище данных с правилами их чтения и изменения. Таблица похожа на ведомость: строка — один товар, столбцы — id, название, количество. Сравнение с листом помогает представить данные, но таблица не хранит обещанный «порядок как на листе»: порядок выдачи задаётся ORDER BY. SQL — язык запроса к этому хранилищу. PHP передаёт SQL через расширение и оформляет ответ для человека.

От товара в хранилище до карточки
  1. Таблица: id 101, CAB-25, 12 штук, 18900 копеек
  2. Соединение db даёт PHP доступ к базе
  3. SELECT нужных полей WHERE id=101 → ресурс result
  4. fetch_assoc(result) → массив product с конкретными значениями
  5. PHP оформляет цену 189.00; free_result завершает чтение

Firebird, InterBase и имя расширения interbase

Firebird — реляционная СУБД с открытым исходным кодом, выросшая из открытой версии InterBase; современная InterBase — отдельный продукт. Их версии и SQL-возможности нельзя считать взаимозаменяемыми. Старый раздел PHP Manual описывает расширение interbase, удалённое из поставки PHP начиная с 7.4, и содержит историческое предупреждение о сопровождении. Для этих примеров выбран продолжаемый проект FirebirdSQL/php-firebird, выпуск 3.0.1 с функциями ibase_* и PHP 8.2. Это не означает, что историческое встроенное расширение осталось в любом PHP 8.2. Стенд и версии записаны в результатах запуска. Альтернативный API PDO_FIREBIRD разбирается отдельно; код PDO и ibase нельзя смешивать в одном вызове.

На Windows должны совпасть версия PHP, архитектура, TS/NTS сборка расширения и совместимая клиентская библиотека. Серверный режим принимает подключения к серверу Firebird; embedded использует локальный движок для отдельного файла. Наш лабораторный запуск изолирован и не подключается к рабочему складу. README в каждом архиве объясняет настройку. Сам файл базы создаётся до программы; connect открывает его, а CREATE TABLE создаёт только таблицу внутри базы.

Что хранит переменнаяЧто с ней делают
$db — соединениеПередают в запросы и в явное завершение транзакции. Это не массив строк.
$result — выборкаЧитают очередную строку, затем освобождают. Это не сама таблица.
$row — массив одной строкиЧитают ключи, сохраняют нужные данные и формируют вывод.
Таблица базыХранит подтверждённые данные независимо от PHP-массива и уже закрытой выборки.

Предварительные понятия: ключи и значения массива, условие и цикл, таблица и первое обращение к базе. Внутри этой главы всё начинается с одной карточки; многократные параметризованные команды вынесены в следующую связанную главу.

Четыре маршрута от SQL к видимому результату

Ниже четыре самостоятельные программы: у каждой своя таблица и собственные исходные записи. Файл lab.php из того же архива задаёт соединение, сохраняет диагностику без раскрытия путей и удаляет только созданную таблицу. Это общий учебный помощник, а не встроенная функция PHP. Внутри сценариев видны SQL, параметры, чтение и результат. Короткие фрагменты объясняют отдельный шаг и явно предполагают подготовленное окружение; для запуска целиком используйте полный архив.

ЗадачаРабочий сценарий
Подключиться и получить карточку складского товараКод, результат и упражнение
Получить две доступные позиции для экрана, а не весь складКод, результат и упражнение
Различить неизвестный остаток, пустую подпись и нольКод, результат и упражнение
Освободить результат и продолжить работу с сохранёнными даннымиКод, результат и упражнение

После INSERT программы вызывают f153Commit, внутри которого проверяется ibase_commit. В Firebird чтение и изменение выполняются в транзакции; успешная команда и подтверждённая транзакция — разные события. Создание таблицы тоже фиксируется перед дальнейшими запросами. Освобождение результата не подтверждает запись. Учебная очистка выполняется после наблюдений и не является образцом удаления данных рабочего приложения.

Подключиться и получить карточку складского товара

Складской сайт должен показать товар CAB-25: название, остаток 12 и цену 189.00 рубля. Сначала нужна связь с существующей базой, затем запрос конкретной карточки. Полная программа создаёт две учебные записи, но получает только id 101. В отчёте видны сами данные, а не только сообщение «подключение успешно».

Путь данных в этом примере
  1. Настройки учебной базы → ibase_connect → ресурс db
  2. Собственная таблица: товары 101 и 102 → INSERT → commit
  3. WHERE id = 101 → один результат SELECT
  4. Первый fetch_assoc → массив с sku, title, qty и ценой
  5. Следующий fetch_assoc → false без ошибки → free_result

Сначала небольшой вызов

$result = ibase_query($db, 'SELECT title AS "title" FROM PRODUCTS WHERE id = ?', 101);
if ($result === false) { throw new RuntimeException("Товар не запрошен"); }
$row = ibase_fetch_assoc($result);
// Для подготовленного товара: $row["title"] === "Кабель 3x2.5".
ibase_free_result($result);
unset($result);

Этот короткий фрагмент предполагает соединение $db и таблицу PRODUCTS; полный пример ниже создаёт собственную таблицу автоматически. SELECT перечисляет нужные столбцы, FROM указывает таблицу, WHERE выбирает одну запись. Знак ? — место значения id: число передаётся отдельным аргументом после SQL. AS "title" задаёт точное имя ключа массива. Ресурс результата — открытый доступ к последовательности строк, а не сам массив товаров. Каждый fetch_assoc получает следующую строку; первое чтение уже выдаёт первый товар и его нельзя бездумно выбрасывать как «проверку».

Что получится

НаблюдениеРезультат
Соединение открытода
АртикулCAB-25
ТоварКабель 3x2.5
Остаток12
Цена, рублей189.00
Следующая строкаконец результата
Подключиться и получить карточку складского товараСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f153Config();$db=f153Connect($config);$runtime=f153Runtime($db,$config);

    $name=f153OwnTable($db,$owned,'receipt','(id INTEGER NOT NULL PRIMARY KEY,sku VARCHAR(24) NOT NULL,title VARCHAR(80) CHARACTER SET UTF8 NOT NULL,qty INTEGER NOT NULL,unit_kopecks INTEGER NOT NULL)');
    $q=f153Qualified($name);
    f153Command($db,"INSERT INTO $q(id,sku,title,qty,unit_kopecks) VALUES(?,?,?,?,?)",[101,'CAB-25','Кабель 3x2.5',12,18900]);
    f153Command($db,"INSERT INTO $q(id,sku,title,qty,unit_kopecks) VALUES(?,?,?,?,?)",[102,'BOX-10','Коробка монтажная',4,7500]);
    f153Commit($db);
    $result=f153Query($db,"SELECT id AS \"id\",sku AS \"sku\",title AS \"title\",qty AS \"qty\",unit_kopecks AS \"unit_kopecks\" FROM $q WHERE id = ?",[101]);
    try {
        $first=f153Capture(static fn()=>ibase_fetch_assoc($result));
        if ($first['warning'] || !is_array($first['value'])) {f153Failure('Карточка товара не прочитана',$first);}
        $product=$first['value'];
        $end=f153Capture(static fn()=>ibase_fetch_assoc($result));
    } finally {ibase_free_result($result);unset($result);}
    $price=intdiv($product['unit_kopecks'],100).'.'.str_pad((string)($product['unit_kopecks']%100),2,'0',STR_PAD_LEFT);
    $rows=[['Соединение открыто',is_resource($db)],['Артикул',$product['sku']],['Товар',$product['title']],['Остаток',$product['qty']],['Цена, рублей',$price],['Следующая строка',$end['value']===false?'конец результата':'неожиданная строка']];
    $checks=['connected'=>is_resource($db),'one_product'=>$product['id']===101,'cyrillic_intact'=>$product['title']==='Кабель 3x2.5',
        'quantity_int'=>$product['qty']===12,'money_exact'=>$price==='189.00','normal_end'=>$end['value']===false&&!$end['warning']&&$end['code']===0];
    $observations=['product'=>$product,'end_is_false'=>$end['value']===false,'end_error_code'=>$end['code'],'client_charset'=>$config['charset'],'sql_dialect'=>$config['dialect']];

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

f153Report('fb153-connection-receipt','Подключиться и получить карточку складского товара',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Соединение открыто — да
Артикул — CAB-25
Товар — Кабель 3x2.5
Остаток — 12
Цена, рублей — 189.00
Следующая строка — конец результата

Самостоятельная программа с lab.php и README.txt создаёт одну собственную таблицу Firebird, читает реальные данные, сохраняет result.html/result.json/metrics.json и удаляет только эту учебную таблицу.

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

Границы, ошибки и изменение требования

Файл .fdb должен существовать до подключения; connect не создаёт базу автоматически. Для удалённого Firebird путь относится к серверу базы, а не к компьютеру PHP. Ошибочный путь, недоступный сервер, права и неверная кодировка дают разные причины отказа; сохраните диагностику сразу и не выводите посетителю пароль или внутренний путь. UTF8 — кодировка клиентского соединения, не команда изменить кодировку всех столбцов. Рубли здесь хранятся целым числом копеек; деление служит только отображению. Запись становится подтверждённой после commit, а не после free_result.

Соединение отклоняло имя кодировки iso-8859-1, хотя программа не передавала charset явно. Исходная ситуация на Stack Overflow. Разбираем источник настройки и используем явное имя UTF8 для нашей UTF-8 программы. Имя кодировки Firebird не обязано совпадать с HTTP-обозначением. Переустановка расширения не объясняет ошибку уже работающего ibase_connect; NONE не предлагается как универсальное исправление.

Попробуйте изменить

Поменяйте выбранный id на 102. Что изменится в карточке, а что останется в SQL?

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

Получатся BOX-10, «Коробка монтажная», остаток 4, цена 75.00. Меняется значение параметра; состав полей и проверка конца результата остаются. Для id 999 нужна отдельная ветка «товар не найден» до обращения к ключам массива.

Получить две доступные позиции для экрана, а не весь склад

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

Путь данных в этом примере
  1. 6 учебных товаров на сервере
  2. WHERE active=1 AND qty>0 → 4 подходящие позиции
  3. ORDER BY unit_kopecks,id → однозначный порядок даже при равной цене
  4. ROWS 1 TO 2 → Розетка 2 и Выключатель 5
  5. PHP оформляет две строки; ROWS 3 TO 4 → следующая страница

Сначала небольшой вызов

$sql = 'SELECT id, title, unit_kopecks FROM PRODUCTS
    WHERE active = ? AND qty > ?
    ORDER BY unit_kopecks, id ROWS 1 TO 2';
$result = ibase_query($db, $sql, 1, 0);
// На наших данных: id 2, 5; каждая цена 9000 копеек.

Список после SELECT определяет поля, а WHERE — записи: это независимые ограничения. ORDER BY unit_kopecks сортирует по цене; id разрешает равенство цен. ROWS 1 TO 2 включает обе границы, нумерация начинается с 1. Для ROWS 3 TO 4 диапазон следующий. Ещё один вариант Firebird: SELECT FIRST 2 SKIP 2 ... ORDER BY ...; здесь FIRST — сколько взять, SKIP — сколько пропустить. Не смешивайте FIRST/SKIP и ROWS в одном SELECT. Современный SQL-вариант OFFSET 2 ROWS FETCH NEXT 2 ROWS ONLY требует проверки версии сервера; это не синтаксис LIMIT из MySQL.

Что получится

НаблюдениеРезультат
Первая страница: id2, 5
Первая страница: товарыРозетка / Выключатель
Цена каждой позиции, копеек9000
Следующая страница: id1, 6
Ещё одна страница: строк0
Передано полей на позицию3
Получить две доступные позиции для экрана, а не весь складСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f153Config();$db=f153Connect($config);$runtime=f153Runtime($db,$config);

    $name=f153OwnTable($db,$owned,'screen','(id INTEGER NOT NULL PRIMARY KEY,title VARCHAR(80) CHARACTER SET UTF8 NOT NULL,qty INTEGER NOT NULL,active SMALLINT NOT NULL,unit_kopecks INTEGER NOT NULL)');
    $q=f153Qualified($name);
    $fixtures=[[1,'Кабель',8,1,20000],[2,'Розетка',5,1,9000],[3,'Коробка',0,1,1000],[4,'Архивный товар',9,0,500],[5,'Выключатель',7,1,9000],[6,'Автомат',2,1,30000]];
    foreach ($fixtures as $item) {f153Command($db,"INSERT INTO $q(id,title,qty,active,unit_kopecks) VALUES(?,?,?,?,?)",$item);}
    f153Commit($db);
    $select="SELECT id AS \"id\",title AS \"title\",unit_kopecks AS \"unit_kopecks\" FROM $q WHERE active = ? AND qty > ? ORDER BY unit_kopecks,id";
    $page=f153Read($db,$select.' ROWS 1 TO 2',[1,0]);
    $next=f153Read($db,$select.' ROWS 3 TO 4',[1,0]);
    $empty=f153Read($db,$select.' ROWS 5 TO 6',[1,0]);
    $rows=[['Первая страница: id',implode(', ',array_column($page,'id'))],['Первая страница: товары',implode(' / ',array_column($page,'title'))],
        ['Цена каждой позиции, копеек',$page[0]['unit_kopecks']],['Следующая страница: id',implode(', ',array_column($next,'id'))],['Ещё одна страница: строк',count($empty)],['Передано полей на позицию',count($page[0])]];
    $checks=['only_two_rows'=>count($page)===2,'stable_tie_order'=>array_column($page,'id')===[2,5],
        'only_screen_fields'=>array_keys($page[0])===['id','title','unit_kopecks'],'filtered_before_transfer'=>array_column($next,'id')===[1,6],
        'same_price'=>$page[0]['unit_kopecks']===9000&&$page[1]['unit_kopecks']===9000,'end_page_empty'=>$empty===[]];
    $observations=['page'=>$page,'next_page'=>$next,'past_end'=>$empty,'sql_order'=>'unit_kopecks,id','sql_page'=>'ROWS 1 TO 2'];

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

f153Report('fb153-screen-select','Получить две доступные позиции для экрана, а не весь склад',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Первая страница: id — 2, 5
Первая страница: товары — Розетка / Выключатель
Цена каждой позиции, копеек — 9000
Следующая страница: id — 1, 6
Ещё одна страница: строк — 0
Передано полей на позицию — 3

Самостоятельная программа с lab.php и README.txt создаёт одну собственную таблицу Firebird, читает реальные данные, сохраняет result.html/result.json/metrics.json и удаляет только эту учебную таблицу.

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

Границы, ошибки и изменение требования

SELECT * выбирает все столбцы, но сам по себе не означает все строки: число строк определяется WHERE и ограничением результата. Плохой сценарий для большого склада — SELECT * без границ, fetch всех записей в массив и usort/array_slice на PHP. Он переносит ненужные данные и работу на приложение. Ограничение двух строк не обещает, что движок прочитает только две строки: индекс и план запроса остаются важны. При изменениях склада между запросами страницы со смещением могут сдвигаться; для долгой ленты нужен курсор по паре цена/id либо согласованный снимок. Здесь данные неизменны между чтениями.

Результат запроса не помещался в память; требовалось получать следующие порции строк. Исходная ситуация на Stack Overflow. Наш экран получает две строки через WHERE, ORDER BY и ROWS на сервере. Уникальный id завершает сортировку. Диапазоны ROWS и FIRST/SKIP объяснены отдельно, без загрузки всей таблицы и сортировки большого PHP-массива.

Попробуйте изменить

Нужно показать только товары дешевле 15000 копеек и одну карточку за раз. Где изменить программу?

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

Добавьте AND unit_kopecks < ? и значение 15000 в параметры. Первая страница ROWS 1 TO 1 вернёт id 2, вторая ROWS 2 TO 2 — id 5. Фильтрация и ограничение остаются в SQL; PHP не загружает лишние строки.

Различить неизвестный остаток, пустую подпись и ноль

У датчика остаток ещё не пришёл от поставщика. У крепления точно ноль. У кабеля пять штук. Если заменить всё «пустое» на ноль, сайт сообщит неверную информацию. Получим ассоциативные строки, сохраним NULL, пустую строку и число 0 раздельно и покажем человеку три понятные карточки.

Путь данных в этом примере
  1. SQL note/qty могут содержать NULL
  2. AS "qty" закрепляет точный ключ массива PHP
  3. fetch_assoc: NULL → null; пустой VARCHAR → ""; INTEGER 0 → 0
  4. === null проверяет отсутствие значения; === 0 — известный нулевой остаток
  5. Интерфейс показывает «неизвестен», «нет в наличии» или количество

Сначала небольшой вызов

$row = ibase_fetch_assoc($result);
if ($row !== false) {
    $label = $row["qty"] === null ? "остаток неизвестен" : (string)$row["qty"];
    // Для датчика: "остаток неизвестен"; для крепления: "0".
}

Ассоциативный массив связывает имя поля с его значением: $row["qty"] читает количество. В нашем драйвере ключ SQL NULL присутствует со значением null. array_key_exists проверяет наличие ключа; isset возвращает false и для отсутствующего ключа, и для null. Поэтому isset недостаточно, чтобы отличить изменение схемы запроса от допустимого пустого значения. Проверка === сравнивает также тип. empty() и нестрогое if (!$row["qty"]) смешали бы состояния. Для SQL-условия отсутствия пишут qty IS NULL, а не qty = NULL.

Что получится

НаблюдениеРезультат
Ключи первой строкиid, title, note, qty
Карточка 1Датчик: остаток неизвестен; подпись: не заполнена
Карточка 2Крепление: нет в наличии; подпись: пустая строка
Карточка 3Кабель: в наличии: 5; подпись: Проверено
Ключ qty существует у первой строкида
isset(qty) у первой строкинет
Различить неизвестный остаток, пустую подпись и нольСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f153Config();$db=f153Connect($config);$runtime=f153Runtime($db,$config);

    $name=f153OwnTable($db,$owned,'nulls','(id INTEGER NOT NULL PRIMARY KEY,title VARCHAR(60) CHARACTER SET UTF8 NOT NULL,note VARCHAR(60) CHARACTER SET UTF8,qty INTEGER)');
    $q=f153Qualified($name);
    foreach ([[1,'Датчик',null,null],[2,'Крепление','',0],[3,'Кабель','Проверено',5]] as $item) {
        f153Command($db,"INSERT INTO $q(id,title,note,qty) VALUES(?,?,?,?)",$item);
    }
    f153Commit($db);
    $items=f153Read($db,"SELECT id AS \"id\",title AS \"title\",note AS \"note\",qty AS \"qty\" FROM $q ORDER BY id ROWS 3");
    $labels=[];
    foreach ($items as $item) {
        if (!array_key_exists('qty',$item) || !array_key_exists('note',$item)) {throw new RuntimeException('Контракт полей результата изменился.');}
        $quantity=$item['qty']===null?'остаток неизвестен':($item['qty']===0?'нет в наличии':'в наличии: '.$item['qty']);
        $note=$item['note']===null?'не заполнена':($item['note']===''?'пустая строка':$item['note']);
        $labels[]=$item['title'].': '.$quantity.'; подпись: '.$note;
    }
    $rows=[['Ключи первой строки',implode(', ',array_keys($items[0]))],['Карточка 1',$labels[0]],['Карточка 2',$labels[1]],['Карточка 3',$labels[2]],
        ['Ключ qty существует у первой строки',array_key_exists('qty',$items[0])],['isset(qty) у первой строки',isset($items[0]['qty'])]];
    $checks=['explicit_aliases'=>array_keys($items[0])===['id','title','note','qty'],'null_key_retained'=>array_key_exists('qty',$items[0])&&$items[0]['qty']===null,
        'empty_string_retained'=>$items[1]['note']==='','zero_retained'=>$items[1]['qty']===0,'not_unknown'=>$items[2]['qty']===5,
        'null_not_isset'=>!isset($items[0]['qty']),'three_cards'=>count($labels)===3];
    $observations=['source_rows'=>$items,'visible_cards'=>$labels];

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

f153Report('fb153-assoc-null','Различить неизвестный остаток, пустую подпись и ноль',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Ключи первой строки — id, title, note, qty
Карточка 1 — Датчик: остаток неизвестен; подпись: не заполнена
Карточка 2 — Крепление: нет в наличии; подпись: пустая строка
Карточка 3 — Кабель: в наличии: 5; подпись: Проверено
Ключ qty существует у первой строки — да
isset(qty) у первой строки — нет

Самостоятельная программа с lab.php и README.txt создаёт одну собственную таблицу Firebird, читает реальные данные, сохраняет result.html/result.json/metrics.json и удаляет только эту учебную таблицу.

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

Границы, ошибки и изменение требования

Без двойных кавычек идентификаторы Firebird обычно становятся верхним регистром: SELECT qty даёт ключ QTY. В кавычках AS "qty" сохраняет выбранное написание. Разным полям JOIN назначайте разные aliases, например product_id и order_id. PHP Manual описывает историческую потерю одноимённых столбцов; выбранный драйвер 3.0.1 пытается дать дубликатам новые имена, но полагаться на автоматически появившиеся суффиксы нельзя. IBASE_TEXT загружает содержимое BLOB и может увеличить память; без него BLOB — идентификатор, а не текст документа. Эта программа использует обычный VARCHAR, не BLOB.

После перехода с MySQL на Firebird строка читалась по числовому индексу, но обращение к title выдавало Undefined index. Исходная ситуация на Stack Overflow. Вопрос относится к PDO, но SQL-причина применима и здесь: обычный идентификатор Firebird приводится к верхнему регистру. В наших ibase-запросах AS "title" задаёт явный контракт ключа. Настройки PDO не переносятся в расширение interbase.

Попробуйте изменить

Добавьте четвёртый товар с qty=0 и note=NULL. Какие две независимые надписи должны появиться?

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

Количество известно: «нет в наличии». Подпись отсутствует: «не заполнена». Не заменяйте null на пустую строку заранее, если интерфейс должен сохранить это различие. При выводе настоящего HTML экранируйте текст через htmlspecialchars.

Освободить результат и продолжить работу с сохранёнными данными

Страница уже получила первую карточку и больше не использует текущую выборку. Освободим результат раньше конца, затем запросим данные снова и обновим остаток. Это показывает четыре разные вещи: массив в PHP, ресурс выборки, строки таблицы и транзакцию. Освобождение одного результата не удаляет товар и не подтверждает изменение.

Путь данных в этом примере
  1. SELECT открыл последовательность двух товаров
  2. fetch_assoc скопировал первую строку в card
  3. free_result + unset закрывают эту выборку; card остаётся
  4. Новый SELECT снова видит две сохранённые записи
  5. UPDATE 4+2 → commit → следующий SELECT показывает 6

Сначала небольшой вызов

$card = ibase_fetch_assoc($result);
ibase_free_result($result);
unset($result);
// $card остаётся массивом. Таблица тоже остаётся.
// Для сохранения UPDATE нужен ibase_commit($db), а не free_result.

ibase_free_result принимает результат SELECT, а не массив $card и не соединение $db. Объём уже прочитанного массива от этого не исчезает. unset убирает переменную с ресурсом; это не SQL DELETE и не DROP TABLE. В выбранной версии после free_result переменная может ещё удовлетворять is_resource, поэтому этот признак не является разрешением читать освобождённый результат повторно. Рабочая схема: освободить ровно один раз, убрать ссылку и при необходимости создать новый запрос.

Что получится

НаблюдениеРезультат
Освобождение результатада
Прочитанная карточка осталась в PHPКоробка
Строки сохранились в таблице2
UPDATE существующей строки1
UPDATE отсутствующей строки вернул trueда
Изменено отсутствующим UPDATE0
Подтверждённый остаток6
Освободить результат и продолжить работу с сохранёнными даннымиСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f153Config();$db=f153Connect($config);$runtime=f153Runtime($db,$config);

    $name=f153OwnTable($db,$owned,'release','(id INTEGER NOT NULL PRIMARY KEY,title VARCHAR(60) CHARACTER SET UTF8 NOT NULL,qty INTEGER NOT NULL)');
    $q=f153Qualified($name);
    f153Command($db,"INSERT INTO $q(id,title,qty) VALUES(?,?,?)",[1,'Коробка',4]);
    f153Command($db,"INSERT INTO $q(id,title,qty) VALUES(?,?,?)",[2,'Кабель',8]);
    f153Commit($db);
    $result=f153Query($db,"SELECT id AS \"id\",title AS \"title\",qty AS \"qty\" FROM $q ORDER BY id");
    try {
        $first=f153Capture(static fn()=>ibase_fetch_assoc($result));
        if ($first['warning'] || !is_array($first['value'])) {f153Failure('Первая карточка не прочитана',$first);}
        $card=$first['value'];
    } finally {$freed=ibase_free_result($result);unset($result);}
    // We intentionally do not read the second row of this first result.
    $saved=f153Read($db,"SELECT id AS \"id\",title AS \"title\",qty AS \"qty\" FROM $q ORDER BY id ROWS 2");
    $changed=f153Query($db,"UPDATE $q SET qty=qty+? WHERE id=?",[2,1]);
    $missing=f153Query($db,"UPDATE $q SET qty=qty+? WHERE id=?",[2,999]);
    $missingAffected=ibase_affected_rows($db);
    f153Commit($db);
    $after=f153Read($db,"SELECT qty AS \"qty\" FROM $q WHERE id=?",[1])[0];
    $rows=[['Освобождение результата',$freed],['Прочитанная карточка осталась в PHP',$card['title']],['Строки сохранились в таблице',count($saved)],
        ['UPDATE существующей строки',$changed],['UPDATE отсутствующей строки вернул true',$missing===true],['Изменено отсутствующим UPDATE',$missingAffected],['Подтверждённый остаток',$after['qty']]];
    $checks=['free_success'=>$freed===true,'card_array_retained'=>$card['title']==='Коробка'&&$card['qty']===4,
        'table_not_deleted'=>array_column($saved,'id')===[1,2],'new_query_succeeded'=>count($saved)===2,
        'update_count'=>$changed===1,'zero_update_is_true'=>$missing===true&&$missingAffected===0,'committed_quantity'=>$after['qty']===6];
    $observations=['card_before_change'=>$card,'saved_rows'=>$saved,'update_return'=>$changed,'zero_update_return'=>$missing,'zero_update_affected'=>$missingAffected,'after_commit'=>$after];

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

f153Report('fb153-result-cleanup','Освободить результат и продолжить работу с сохранёнными данными',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Освобождение результата — да
Прочитанная карточка осталась в PHP — Коробка
Строки сохранились в таблице — 2
UPDATE существующей строки — 1
UPDATE отсутствующей строки вернул true — да
Изменено отсутствующим UPDATE — 0
Подтверждённый остаток — 6

Самостоятельная программа с lab.php и README.txt создаёт одну собственную таблицу Firebird, читает реальные данные, сохраняет result.html/result.json/metrics.json и удаляет только эту учебную таблицу.

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

Границы, ошибки и изменение требования

Для заранее подготовленного запроса есть отдельный ibase_free_query; для соединения — ibase_close; для транзакции — commit/rollback. Их нельзя взаимозаменять. Важная особенность ibase_query: UPDATE без совпавших строк возвращает true, а не число 0; обычный UPDATE одной строки — int 1. Поэтому (int)$return даст неверную единицу для нулевого изменения. Читайте ibase_affected_rows сразу после DML, пока другой запрос не изменил контекст. Для SELECT даже пустой результат сначала является ресурсом; число строк из bool query не получить. Все учебные объекты удаляются только в завершающей процедуре лаборатории.

Попробуйте изменить

Освободите результат после первого товара, затем запросите только id=2. Изменится ли содержимое $card первой карточки?

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

Нет. Новый запрос вернёт «Кабель», а ранее скопированный $card по-прежнему содержит «Коробка». Массив не является живым представлением таблицы: чтобы отразить дальнейшие изменения, нужно прочитать данные снова.

Справочник четырёх шагов

Нужно сделатьФункция
Открыть соединение с существующей базой и явно выбрать кодировку клиента.ibase_connect
Выполнить SQL и различить выборку, число изменений, true и ошибку.ibase_query
Прочитать следующую строку с именами полей и сохранить NULL отдельно от нуля.ibase_fetch_assoc
Закончить работу с выборкой, сохранив данные таблицы и уже прочитанный массив.ibase_free_result

От первой карточки к повторяемым запросам

Теперь известен весь короткий путь: соединение → SQL → строка массива → оформление → освобождение выборки. Для настоящего каталога выбирайте только данные текущего экрана, сохраняйте смысл NULL и задавайте предсказуемый порядок на сервере. Успешное чтение не означает, что раскрыты все API, типы и средства администрирования Firebird: следующая глава развивает подготовленные запросы и описание столбцов результата.

Далее: значения отдельно от SQL, повторное выполнение и metadata. Для веб-формы сначала проверьте входные данные в главе обработки формы; при HTML-выводе используйте htmlspecialchars. Параметризованный SQL защищает границу запроса, а экранирование HTML — другую границу, вывод в браузер.

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

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

ibase_connectОткрыть соединение с существующей базой и явно выбрать кодировку клиента.

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

ibase_connect(string $database = ..., string $username = ..., string $password = ..., string $charset = ..., int $buffers = 0, int $dialect = ..., string $role = ..., int $sync = 0): resource|false

Создаёт обычное соединение с уже существующей базой, чтобы приложение смогло получить товар или сохранить документ. Возвращает ресурс соединения, а не первую строку таблицы. Для нового проекта учитывайте поддерживаемые версии PHP, клиентской библиотеки и конкретного движка; PDO_FIREBIRD — отдельный интерфейс, не другое имя этой функции.

ПараметрСмысл и значение по умолчанию
$databaseСтрока подключения: локальный путь либо hostname:path/alias, при нужном порте hostname/port:path/alias. Путь удалённой базы относится к серверу. При пропуске/пустом значении выбранный драйвер обращается к ibase.default_db.
$username, $passwordИмя и пароль пользователя базы; при пропуске берутся ibase.default_user и ibase.default_password. Не храните реальные учётные данные в опубликованном коде. Пустой пароль не означает анонимный доступ.
$charsetКодировка клиента, например UTF8; при пропуске драйвер может брать ibase.default_charset. Это настройка обмена, не миграция хранилища. Нельзя считать неизвестное окружение гарантированно UTF-8.
$buffersНеобязательное число буферов серверного кэша; 0 сохраняет стандартное поведение. Не размер PHP-массива и не число строк SELECT.
$dialectSQL-диалект, при пропуске клиентский по умолчанию. В учебной базе используется 3. Подключение с dialect=3 не переводит автоматически старую базу из dialect=1.
$roleSQL-роль для соединения. Исторический контракт указывает InterBase 5 и новее. Роль должна существовать, а пользователь — иметь право её использовать.
$syncНеобязательная настройка записи. PHP Manual не объясняет её; в исходнике 3.0.1 ненулевое значение передаёт isc_dpb_force_write, сравнивая аргумент с режимом синхронной записи клиентского API. Это политика записи базы, не автоматический commit запроса. В примерах параметр не задаётся; не меняйте политику существующей базы попутно с чтением.

Успех — ресурс; отказ подключения — false с предупреждением/диагностикой. Повторный вызов с одинаковыми параметрами может вернуть уже открытое соединение: не рассчитывайте получить независимую транзакцию простым повторением connect. Обычное соединение закрывается при завершении скрипта либо явно через ibase_close. ibase_pconnect относится к постоянным соединениям и рассматривается отдельно. Исторические NetBEUI-примеры //hostname/ из старого Manual не являются рекомендацией для современного Windows-стенда.

Карточка товара: явная клиентская кодировка

$db = ibase_connect($database, $user, $password, "UTF8", 0, 3);
if ($db === false) {
    $reason = ibase_errmsg(); // Сохранить сразу для закрытого журнала.
    throw new RuntimeException("База каталога недоступна");
}
// Дальше SELECT по id; подключение само по себе не выводит товар.

Переменные настроек получены из закрытой конфигурации. Самостоятельный пример создаёт товар и показывает остаток 12 с ценой 189.00. Он также проверяет конец результата.

Сервис на другом компьютере: адрес и ошибка конфигурации

$database = "db.internal/3050:inventory"; // Условный сервер и его alias.
$db = ibase_connect($database, $user, $password, "UTF8", 0, 3, "CATALOG_READER");
if ($db === false) {
    $reason = ibase_errmsg();
    // Журнал: причина; посетителю: "Каталог временно недоступен".
    throw new RuntimeException("Соединение не создано");
}

Это фрагмент для отдельно настроенного сервера, не выполненный сетевой запрос лаборатории. Нужны созданный alias и назначенная роль. Ошибка имени charset исправляется настройкой клиента; HTML meta charset не меняет параметры подключения. Подробное первое чтение — в рабочем сценарии; многократные операции — в подготовленных запросах.

ibase_queryВыполнить SQL и различить выборку, число изменений, true и ошибку.

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

ibase_query(resource $connectionOrTransaction, string $query, mixed ...$bindArgs): resource|int|bool; дополнительные формы разобраны ниже

Выполняет SQL с отдельными значениями параметров. Для экрана возвращает доступ к строкам SELECT; для изменения — подтверждение выполнения или число затронутых строк. Сначала сформулируйте, какие именно поля и записи нужны пользователю.

Форма вызоваКуда выполняется команда
ibase_query($db, $sql, ...$values)Явное соединение и его текущая/автоматически начатая транзакция. Именно этот вариант используется в примерах.
ibase_query($transaction, $sql, ...$values)Явная транзакция, связанная с соединением. Полезно для управления несколькими изменениями.
ibase_query($db, $transaction, $sql, ...$values)Форма из исходника 3.0.1 с отдельными соединением и транзакцией; параметры должны принадлежать совместимому контексту.
ibase_query($sql, ...$values)Исторический вариант с последним открытым соединением; в приложении с несколькими базами лучше передать соединение явно.
ibase_query(IBASE_CREATE, $createDatabaseSql)Специальная ветка драйвера для CREATE DATABASE, возвращающая новое соединение. Это создание отдельной базы, не обычный запрос к существующей; учебные MAIN не используют её.

$sql — строка одной SQL-команды. Последующие аргументы — значения для знаков ? слева направо, не один массив параметров как у PDO. Скалярные значения и null связываются отдельно; число аргументов должно соответствовать местам в запросе. Таблицы, имена столбцов и ASC/DESC не заменяются через ?: выбирайте их из собственного разрешённого списка. Числовое описание bind_args в старой краткой сигнатуре PHP Manual не означает, что допускается только один int; исходник поддерживает переменное число mixed аргументов.

Результат вызоваКак его понимать
resourceЗапрос возвращает строки, в том числе пустой SELECT. Читайте fetch-функцией; не приводите к bool/числу для подсчёта строк.
intДля обычного INSERT/UPDATE/DELETE — число изменённых строк, если оно ненулевое.
trueУспешная команда без набора результата; также историческое поведение DML при нуле изменённых строк.
falseОшибка SQL/исполнения. Сразу сохраните ibase_errcode/ibase_errmsg; следующий вызов может заменить диагностику. Неверные PHP-аргументы могут приводить к отдельным ошибкам уровня PHP.

Две карточки для страницы каталога

$result = ibase_query($db,
    "SELECT id, title FROM PRODUCTS WHERE qty > ? ORDER BY id ROWS 2", 0);
// Читать максимум две строки; проверить result !== false перед fetch.

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

Изменение, которое могло не найти товар

$done = ibase_query($db, "UPDATE PRODUCTS SET qty = qty + ? WHERE id = ?", 2, 999);
if ($done === false) { throw new RuntimeException("Остаток не изменён из-за ошибки"); }
$count = ibase_affected_rows($db); // Читать до следующего запроса.
// Для отсутствующего id: done === true, count === 0.
if (ibase_commit($db) !== true) { throw new RuntimeException("Изменение не подтверждено"); }

Разница между успехом команды и наличием совпадения проверена в сценарии освобождения и повторного запроса. Для многократной команды используйте ibase_prepare/ibase_execute. free_result не заменяет commit. RETURNING и процедуры могут выдавать набор результата вместо простого счётчика; определяйте контракт своей SQL-команды, а не предполагайте int для любого текста, начинающегося с UPDATE.

ibase_fetch_assocПрочитать следующую строку с именами полей и сохранить NULL отдельно от нуля.

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

ibase_fetch_assoc(resource $result, int $fetch_flag = 0): array|false

Получает следующую строку выборки как массив «имя поля → значение». Так из строки склада получаются подпись карточки и остаток. Вызов двигает позицию; второй вызов не повторяет первую строку.

Аргумент / возвратЧто важно
$resultДействующий ресурс результата от ibase_query/ibase_execute, не соединение, не подготовленный запрос и не уже освобождённый результат.
$fetch_flag = 0Обычные преобразования. IBASE_TEXT загружает содержимое BLOB вместо его идентификатора; IBASE_UNIXTIME возвращает дату/время как Unix timestamp вместо форматированной строки. Флаги можно объединить через |.
arrayОдна строка; в выбранной версии SQL NULL сохранён как PHP null с существующим ключом. Остальные типы зависят от SQL-типа; INTEGER здесь int, а точные DECIMAL могут приходить строкой.
falseСтрок больше нет либо чтение не удалось. При false сразу проверьте предупреждение и ibase_errcode/ibase_errmsg; нормальный конец не является ошибкой базы.

Поля без кавычек обычно имеют верхний регистр. Задавайте явные уникальные AS "qty" и AS "title". Старый PHP Manual предупреждает, что одноимённые поля перезаписываются; исходник проверенного драйвера 3.0.1 пытается переименовать дубликаты и безымянные выражения. Этот автоматический выбор не должен становиться интерфейсом приложения: aliases надёжнее. Если требуются позиции без имён, используйте ibase_fetch_row, но порядок столбцов всё равно должен быть явным.

Остаток для карточки: ноль отдельно от отсутствия

$row = ibase_fetch_assoc($result);
if ($row === false) {
    $error = ibase_errcode();
    // false + нет диагностики: товар не найден или достигнут конец.
} else {
    $caption = $row["qty"] === null ? "Остаток неизвестен" : (string)$row["qty"];
}

Самостоятельный пример показывает три карточки и сохраняет null, пустую строку и 0. Проверка empty не подходит для такого различения; array_key_exists отличает отсутствующий ключ от поля с null.

Краткая заметка из BLOB для одного документа

$result = ibase_query($db, 'SELECT memo AS "memo" FROM DOCUMENTS WHERE id = ?', 71);
if ($result === false) { throw new RuntimeException("Документ не найден из-за ошибки запроса"); }
$row = ibase_fetch_assoc($result, IBASE_TEXT);
// Если memo содержит "Выдать со склада", row["memo"] — этот текст, не id BLOB.
ibase_free_result($result);
unset($result);

Это контекстный вариант для существующей таблицы DOCUMENTS с небольшим BLOB-полем, не отдельный выполненный MAIN этой главы. IBASE_TEXT получает содержимое целиком: для больших документов нужен отдельный потоковый BLOB-сценарий. Формат даты зависит от настроек драйвера; IBASE_UNIXTIME не добавляет сам по себе часовой пояс в SQL TIMESTAMP. Для обычных строк полный цикл и нормальный конец показаны на товаре CAB-25.

ibase_free_resultЗакончить работу с выборкой, сохранив данные таблицы и уже прочитанный массив.

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

ibase_free_result(resource $result_identifier): bool

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

Параметр / результатКонтракт
$result_identifierРесурс результата от query/execute. Ресурс подготовленной команды освобождается через ibase_free_query, соединение — через ibase_close.
trueОсвобождение выполнено успешно. Можно прекратить чтение до конца выборки.
false / ошибка PHPНеудачное освобождение; неверный тип или уже недействительный ресурс может обрабатываться как ошибка PHP. Повторный free не является способом проверить ресурс.

Функция не очищает уже скопированный массив PHP, не удаляет строки/таблицу, не завершает транзакцию и не закрывает соединение. В выбранной версии переменная после free может всё ещё выглядеть ресурсом; уберите её через unset и не используйте повторно. Если на ресурс есть другие ссылки, завершите их владение тоже.

Одна карточка уже прочитана

$card = ibase_fetch_assoc($result);
$ok = ibase_free_result($result);
unset($result);
// card остаётся самостоятельным массивом; новый запрос сможет прочитать таблицу снова.

Полный пример намеренно не дочитывает второй товар, освобождает выборку и подтверждает новым SELECT, что обе строки сохранены.

Освобождение при отказе оформления

$result = ibase_query($db, "SELECT id, title FROM PRODUCTS ORDER BY id ROWS 2");
if ($result === false) { throw new RuntimeException("Список не запрошен"); }
try {
    // Чтение и оформление: исключение в этой части не должно оставить результат.
    $row = ibase_fetch_assoc($result);
} finally {
    ibase_free_result($result);
    unset($result);
}

finally выполняется и при исключении. Это каркас управления ресурсом, а не готовая обработка всех ошибок fetch. В самостоятельных программах проверяется диагностика чтения. Для подтверждения изменения используйте commit, а для отмены — rollback; подготовленный запрос и его результат имеют отдельные сроки жизни.

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

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

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

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

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