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

Дерево руководства
Глава 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 через расширение и оформляет ответ для человека.
- Таблица: id 101, CAB-25, 12 штук, 18900 копеек
- Соединение db даёт PHP доступ к базе
- SELECT нужных полей WHERE id=101 → ресурс result
- fetch_assoc(result) → массив product с конкретными значениями
- 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. В отчёте видны сами данные, а не только сообщение «подключение успешно».
- Настройки учебной базы → ibase_connect → ресурс db
- Собственная таблица: товары 101 и 102 → INSERT → commit
- WHERE id = 101 → один результат SELECT
- Первый fetch_assoc → массив с sku, title, qty и ценой
- Следующий 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 приходят только две готовые карточки; следующая страница запрашивается отдельно.
- 6 учебных товаров на сервере
- WHERE active=1 AND qty>0 → 4 подходящие позиции
- ORDER BY unit_kopecks,id → однозначный порядок даже при равной цене
- ROWS 1 TO 2 → Розетка 2 и Выключатель 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.
Что получится
| Наблюдение | Результат |
|---|---|
| Первая страница: id | 2, 5 |
| Первая страница: товары | Розетка / Выключатель |
| Цена каждой позиции, копеек | 9000 |
| Следующая страница: id | 1, 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 раздельно и покажем человеку три понятные карточки.
- SQL note/qty могут содержать NULL
- AS "qty" закрепляет точный ключ массива PHP
- fetch_assoc: NULL → null; пустой VARCHAR → ""; INTEGER 0 → 0
- === null проверяет отсутствие значения; === 0 — известный нулевой остаток
- Интерфейс показывает «неизвестен», «нет в наличии» или количество
Сначала небольшой вызов
$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, ресурс выборки, строки таблицы и транзакцию. Освобождение одного результата не удаляет товар и не подтверждает изменение.
- SELECT открыл последовательность двух товаров
- fetch_assoc скопировал первую строку в card
- free_result + unset закрывают эту выборку; card остаётся
- Новый SELECT снова видит две сохранённые записи
- 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 | да |
| Изменено отсутствующим UPDATE | 0 |
| Подтверждённый остаток | 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. |
| $dialect | SQL-диалект, при пропуске клиентский по умолчанию. В учебной базе используется 3. Подключение с dialect=3 не переводит автоматически старую базу из dialect=1. |
| $role | SQL-роль для соединения. Исторический контракт указывает 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; подготовленный запрос и его результат имеют отдельные сроки жизни.
Источники и обсуждения
- PHP Manual — Firebird/InterBase
- PHP Manual — ibase_connect
- PHP Manual — ibase_query
- PHP Manual — ibase_fetch_assoc
- PHP Manual — ibase_free_result
- FirebirdSQL — выпуск драйвера 3.0.1
- FirebirdSQL — подключение и аргументы
- FirebirdSQL — query/fetch/free
- Firebird — SQL, идентификаторы, клиентская кодировка и ограничение строк
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


