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

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

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

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

Глава 59 · После первой таблицы Firebird, SQL SELECT и базовых функций PHP

Firebird: подготовленные запросы, параметры и описание результата

Повторяемые фильтры каталога, имя с апострофом и UTF8, контролируемый недостаток аргумента, заголовки по metadata и отдельное владение подготовкой/результатом. Четыре самостоятельные программы и четыре полные карточки.

Подготовленная команда, значения и результат

Каталог меняет фильтр, форма присылает имя с апострофом, а выгрузка требует устойчивых заголовков. Эти задачи связывает один путь: SQL готовится отдельно, значения передаются по позициям, результат читается и освобождается. Четыре программы ниже создают собственные учебные данные и показывают конкретную выдачу, отказ и повторное использование.

Что происходит с нашим фильтром
  1. SQL-шаблон: CATEGORY = ? AND PRICE_KOPECKS <= ?
  2. prepare → подготовленный $query
  3. execute($query, cable, 2000) → текущий $result
  4. fetch → массивы с нужными полями; field_info → описание этих полей
  5. free_result закрывает текущую выдачу; новый execute сохраняет SQL
  6. free_query завершает подготовку; соединение закрывается после всей работы

Удобная модель — бланк запроса с пустыми полями. prepare подготавливает бланк, execute передаёт значения, а полученный список относится к конкретному выполнению. Это аналогия разделения обязанностей, не описание устройства памяти Firebird. Подготовка не является сохранённой копией товаров.

Сначала пройдите подключение Firebird, первую таблицу и чтение строк. Здесь подробно разбираются подготовка, выполнение, количество колонок и описание колонки. Значения параметров и возвратов объясняются ниже до полного файла.

Пометка PHP 7 < 7.4 в базовом справочнике относится к прежнему включению расширения в дистрибутив PHP. Для этих архивов используется отдельно установленный interbase 3.0.1 с PHP 8.2; конкретная версия Firebird и способ локального подключения записаны в результате. Это не обещание одинакового поведения всех старых InterBase/Firebird клиентов и современных веток драйвера. Установка описана в начале маршрута Firebird.

Повторить поиск товара по категории и пределу цены

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

Путь данных
  1. prepare: CATEGORY = ? и PRICE_KOPECKS <= ? — два места для значений
  2. execute(cable, 2000) → ID 1
  3. execute(cable, 5000) → ID 1, 3; товар 2 без остатка исключён
  4. execute(accessory, 10000) → ID 4
  5. execute(missing, 5000) → корректный пустой результат
  6. После каждого чтения free_result; после всего цикла free_query

Сначала короткий вызов

$query = ibase_prepare($db, 'SELECT ID, TITLE FROM PRODUCTS WHERE CATEGORY = ?');
if ($query === false) { throw new RuntimeException('Не удалось подготовить поиск'); }
$result = ibase_execute($query, 'cable');
// Полный файл проверяет результат, читает строки и освобождает оба ресурса.

Вызов prepare получает соединение и текст SQL. Знак ? обозначает одно значение, его не заключают в кавычки. Execute получает подготовленный ресурс и значения слева направо: сначала category, потом maximum. PRICE_KOPECKS хранит целые копейки, поэтому 2000 — это 20 рублей. Ключевые слова WHERE, ORDER BY и ROWS задают фильтр, порядок и предел результата ещё до его передачи в PHP.

Что получит читатель после запуска

НаблюдениеРезультат
Подготовок SQL1
Выполнений с разными значениями4
cable: цена до 2000 копеек1
cable: цена до 5000 копеек1, 3
accessory: цена до 10000 копеек4
Строк для отсутствующей категории0
Всего товаров после поиска4
Полная программа и архив для самостоятельного запуска
Повторить поиск товара по категории и пределу ценыСамостоятельный пример
<?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);

    $items=f153OwnTable($db,$owned,'reuse','(ID INTEGER NOT NULL PRIMARY KEY, CATEGORY VARCHAR(16) NOT NULL, TITLE VARCHAR(40) CHARACTER SET UTF8 NOT NULL, PRICE_KOPECKS INTEGER NOT NULL, STOCK INTEGER NOT NULL)');
    $q=f153Qualified($items);
    $fixtures=[[1,'cable','Кабель USB',1500,12],[2,'cable','Переходник',3200,0],[3,'cable','Кабель HDMI',4500,7],[4,'accessory','Подставка',8000,3]];
    foreach ($fixtures as $item) {f153Command($db,"INSERT INTO $q (ID,CATEGORY,TITLE,PRICE_KOPECKS,STOCK) VALUES (?,?,?,?,?)",$item);}
    f153Commit($db);
    $query=ibase_prepare($db,"SELECT ID,TITLE,PRICE_KOPECKS FROM $q WHERE CATEGORY=? AND PRICE_KOPECKS<=? AND STOCK>0 ORDER BY PRICE_KOPECKS,ID ROWS 10");
    if ($query===false) {f153Failure('Не удалось подготовить фильтр');}
    $selections=[];$result=null;
    try {
        foreach ([['cable',2000],['cable',5000],['accessory',10000],['missing',5000]] as [$category,$maximum]) {
            $result=ibase_execute($query,$category,$maximum);
            if ($result===false) {f153Failure('Фильтр не выполнен');}
            if (!is_resource($result)) {throw new RuntimeException('SELECT должен вернуть результат');}
            try {$selections[]=f153Rows($result);}
            finally {
                if (ibase_free_result($result)!==true) {f153Failure('Результат фильтра не освобождён');}
                $result=null;
            }
        }
    } finally {
        if (is_resource($result)) {ibase_free_result($result);$result=null;}
        if (ibase_free_query($query)!==true) {f153Failure('Подготовленный фильтр не освобождён');}
        $query=null;
    }
    $ids=array_map(static fn(array $page):array=>array_column($page,'ID'),$selections);
    $total=(int)f153Read($db,"SELECT COUNT(*) AS TOTAL FROM $q")[0]['TOTAL'];
    $rows=[['Подготовок SQL',1],['Выполнений с разными значениями',4],['cable: цена до 2000 копеек',implode(', ',$ids[0])],
        ['cable: цена до 5000 копеек',implode(', ',$ids[1])],['accessory: цена до 10000 копеек',implode(', ',$ids[2])],
        ['Строк для отсутствующей категории',count($selections[3])],['Всего товаров после поиска',$total]];
    $checks=['first_filter'=>$ids[0]===[1],'second_filter'=>$ids[1]===[1,3],'third_filter'=>$ids[2]===[4],
        'empty_result'=>$selections[3]===[],'out_of_stock_excluded'=>!in_array(2,array_merge(...$ids),true),'stored_rows_unchanged'=>$total===4];
    $observations=['filters'=>[['cable',2000],['cable',5000],['accessory',10000],['missing',5000]],'selected_rows'=>$selections,
        'parameter_order'=>['category','maximum_kopecks'],'selected_columns'=>['ID','TITLE','PRICE_KOPECKS'],'row_limit'=>10,
        'prepare_calls'=>1,'execute_calls'=>4,'performance_benchmark'=>false];

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

f153Report('fb153-prepare-reuse','Повторить поиск товара по категории и пределу цены',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Подготовок SQL — 1
Выполнений с разными значениями — 4
cable: цена до 2000 копеек — 1
cable: цена до 5000 копеек — 1, 3
accessory: цена до 10000 копеек — 4
Строк для отсутствующей категории — 0
Всего товаров после поиска — 4

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

Открыть результат программы · Данные, метаданные и наблюдения JSON.

Границы и типичные ошибки

Подготовка сохраняет форму команды и позволяет повторно её выполнить, но не кеширует строки таблицы и не обещает конкретное ускорение. Здесь время не измеряется. Параметры не заменяют имя таблицы, столбец или ASC/DESC: для таких вариантов используйте заранее разрешённые SQL-шаблоны. ROWS 10 — верхний предел, а не обещание десяти строк. При изменении SQL нужна новая подготовка. Не завершайте транзакцию посреди работы с привязанными к ней ресурсами; сначала закончите чтение и освободите их.

Поиск по штрихкоду собирался вставкой значения прямо в текст SQL; prepare не исправлял уже склеенный SQL. Исходный вопрос на Stack Overflow. Показываем настоящий ? и отдельный аргумент execute. Для единственного выполнения параметризованный ibase_query тоже уместен; подготовка полезна именно при повторении формы запроса.

Измените одно требование

Нужно показывать только товары дороже 1000 копеек. Можно ли просто передать третье значение в прежний execute?

Как рассуждать и проверить

Нет. В SQL нужно добавить третье условие с третьим ?, заново подготовить этот SQL и передать три значения в том же порядке. Лишний аргумент сам по себе не создаёт условие.

Сохранить имя с апострофом и поймать пропущенный параметр

Форма клиента присылает имя О’Коннор, а реальная строка в примере содержит обычный апостроф: О'Коннор. Сохраним его без ручной сборки кавычек в SQL и найдём тем же значением. Затем намеренно забудем email: программа покажет отказ и сохранность двух правильных записей, а не выдаст ложное «всё сохранено».

Путь данных
  1. INSERT (ID, CLIENT_NAME, EMAIL) VALUES (?, ?, ?)
  2. execute(1, О'Коннор, o.connor@example.test) → три значения на три позиции
  3. SQL и строка имени передаются отдельно; UTF8 сохраняет кириллицу
  4. execute(3, Без адреса) → не хватает третьего значения → PHP warning и false
  5. SELECT ... CLIENT_NAME = ? получает имя как значение
  6. Строка x' OR 1=1 -- остаётся строкой поиска и не расширяет WHERE

Сначала короткий вызов

$query = ibase_prepare($db, 'SELECT ID FROM CLIENTS WHERE CLIENT_NAME = ?');
$result = ibase_execute($query, "О'Коннор");
// Апостроф входит в значение, а не в текст SQL.

В execute значения перечисляются отдельными аргументами, а не вставляются через конкатенацию. Три ? требуют три значения в правильном порядке: правильное количество ещё не означает правильный столбец. NULL, пустая строка и отсутствие аргумента — разные вещи. Передача значений не выполняет бизнес-проверку email. Проверка адреса должна быть до INSERT, а HTML-экранирование — при выводе на страницу, а не перед записью в базу.

Что получит читатель после запуска

НаблюдениеРезультат
Сохранённое и найденное имяО'Коннор
ID найденного клиента1
Недостающий параметр вернул falseда
Предупреждение PHP перехваченода
Код ошибки сервера при этом0
Совпадений с буквальным x' OR 1=1 --0
Записей после ошибочного вызова2
Полная программа и архив для самостоятельного запуска
Сохранить имя с апострофом и поймать пропущенный параметрСамостоятельный пример
<?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);

    $clients=f153OwnTable($db,$owned,'bound','(ID INTEGER NOT NULL PRIMARY KEY, CLIENT_NAME VARCHAR(80) CHARACTER SET UTF8 NOT NULL, EMAIL VARCHAR(120) CHARACTER SET UTF8 NOT NULL)');
    $q=f153Qualified($clients);$name="О'Коннор";
    $insert=ibase_prepare($db,"INSERT INTO $q (ID,CLIENT_NAME,EMAIL) VALUES (?,?,?)");
    if ($insert===false) {f153Failure('INSERT не подготовлен');}
    try {
        $first=ibase_execute($insert,1,$name,'o.connor@example.test');
        if ($first===false) {f153Failure('Имя не сохранено');}
        $second=ibase_execute($insert,2,'Анна','anna@example.test');
        if ($second===false) {f153Failure('Вторая запись не сохранена');}
        // Deliberate failure: the third required parameter is missing. Capture before another driver call.
        $missing=f153Capture(static fn()=>ibase_execute($insert,3,'Без адреса'));
    } finally {
        if (ibase_free_query($insert)!==true) {f153Failure('INSERT не освобождён');}
        $insert=null;
    }
    f153Commit($db);
    $find=ibase_prepare($db,"SELECT ID,CLIENT_NAME FROM $q WHERE CLIENT_NAME=? ORDER BY ID");
    if ($find===false) {f153Failure('Поиск клиента не подготовлен');}
    $result=null;
    try {
        $result=ibase_execute($find,$name);
        if ($result===false) {f153Failure('Поиск имени не выполнен');}
        try {$found=f153Rows($result);} finally {ibase_free_result($result);$result=null;}
        $result=ibase_execute($find,"x' OR 1=1 --");
        if ($result===false) {f153Failure('Поиск буквального текста не выполнен');}
        try {$literal=f153Rows($result);} finally {ibase_free_result($result);$result=null;}
    } finally {
        if (is_resource($result)) {ibase_free_result($result);$result=null;}
        if (ibase_free_query($find)!==true) {f153Failure('Поиск не освобождён');}
        $find=null;
    }
    $all=f153Read($db,"SELECT ID,CLIENT_NAME,EMAIL FROM $q ORDER BY ID");
    $rows=[['Сохранённое и найденное имя',$found[0]['CLIENT_NAME']],['ID найденного клиента',$found[0]['ID']],
        ['Недостающий параметр вернул false',$missing['value']===false],['Предупреждение PHP перехвачено',$missing['warning']],
        ['Код ошибки сервера при этом',$missing['code']],['Совпадений с буквальным x\' OR 1=1 --',count($literal)],['Записей после ошибочного вызова',count($all)]];
    $checks=['apostrophe_utf8_intact'=>$found===[['ID'=>1,'CLIENT_NAME'=>$name]],'missing_argument_rejected'=>$missing['value']===false,
        'php_warning_captured'=>$missing['warning']===true,'no_server_error_code'=>$missing['code']===0,'literal_not_sql'=>$literal===[],
        'only_valid_rows_saved'=>array_column($all,'ID')===[1,2],'correct_parameter_position'=>$all[0]['EMAIL']==='o.connor@example.test'];
    $observations=['saved_clients'=>$all,'search_rows'=>$found,'literal_search_rows'=>$literal,
        'missing_argument'=>['returned_false'=>$missing['value']===false,'php_warning'=>$missing['warning'],'server_code'=>$missing['code']],
        'parameter_order'=>['ID','CLIENT_NAME','EMAIL'],'connection_charset'=>'UTF8','raw_warning_text_published'=>false];

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

f153Report('fb153-bound-input','Сохранить имя с апострофом и поймать пропущенный параметр',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Сохранённое и найденное имя — О'Коннор
ID найденного клиента — 1
Недостающий параметр вернул false — да
Предупреждение PHP перехвачено — да
Код ошибки сервера при этом — 0
Совпадений с буквальным x' OR 1=1 -- — 0
Записей после ошибочного вызова — 2

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

Открыть результат программы · Данные, метаданные и наблюдения JSON.

Границы и типичные ошибки

Ожидаемая ошибка в этом опыте возникает в PHP-драйвере из-за нехватки аргумента: warning есть, а ibase_errcode не даёт серверной ошибки. Helper сохраняет признак предупреждения сразу и не публикует сырой текст с возможными путями. В проверенной версии 3.0.1 лишние значения могут дать notice и не остановить выполнение: всегда сами соблюдайте число и порядок параметров. Не заменяйте параметризацию ручным экранированием или прямой вставкой SQL при ошибке. UTF8 задан и соединению, и текстовым столбцам; параметризация сама не перекодирует испорченные данные.

Обновление профиля попадало строкой email в другое поле с иным типом; дополнительно отличался регистр имени переменной результата. Исходный вопрос на Stack Overflow. Сопоставляем порядок параметров и столбцов, сразу проверяем именно возвращённое значение. Не выдаём сторонний совет «просто commit» за установленную причину всех отказов.

Измените одно требование

Имя и email оба строки. Что произойдёт, если поменять их местами при трёх правильных аргументах?

Как рассуждать и проверить

Драйвер может принять такой запрос: число аргументов и типы подходят. Получится неверная запись. Сопоставьте позиции с перечнем столбцов и проверяйте сохранённые значения; параметризация защищает границу SQL/данные, но не знает смысл имени и адреса.

Собрать заголовки отчёта и отличить описание поля от NULL

Для отчёта нужны понятные заголовки и реальные значения. SELECT переименует TITLE в PRODUCT_NAME, а metadata покажет и исходное имя, и имя выдачи. На двух товарах увидим, что количество столбцов не равно количеству строк, а NULL не равен пустому комментарию.

Путь данных
  1. Таблица: ID, TITLE, NOTE; первая NOTE = NULL, вторая NOTE = пустая строка
  2. SELECT выдаёт ITEM_ID, PRODUCT_NAME, COMMENT_TEXT
  3. num_fields(result) → 3 столбца, независимо от двух строк
  4. field_info(result, 1) → TITLE / PRODUCT_NAME / VARCHAR / 160 байт
  5. fetch → две строки; NULL сохраняется значением PHP null
  6. Заголовки берём из alias, значения — из результата, а не из metadata

Сначала короткий вызов

$count = ibase_num_fields($result);
$second = ibase_field_info($result, 1); // Нумерация начинается с 0.
echo $second['name'], ' → ', $second['alias'];
// В полном отчёте: TITLE → PRODUCT_NAME.

Метаданные — описание столбцов результата, а не сами товары. Номер 1 указывает на второй столбец; допустимый диапазон 0..count−1. name хранит исходное имя, alias — имя в текущем SELECT, relation — таблицу происхождения, length — размер представления поля в байтах, type — SQL-тип. Для UTF8 VARCHAR(40) в проверенном драйвере length равен строке "160"; это не длина текста «Кабель №7» и не 160 допустимых символов.

Что получит читатель после запуска

НаблюдениеРезультат
Столбцов в результате3
Строк данных2
Заголовки по aliasITEM_ID, PRODUCT_NAME, COMMENT_TEXT
Имя исходного второго столбцаTITLE
Его alias в отчётеPRODUCT_NAME
Тип второго столбцаVARCHAR
length второго столбца, байт160
Первый комментарий действительно NULLда
Второй комментарий — пустая строкада
Полная программа и архив для самостоятельного запуска
Собрать заголовки отчёта и отличить описание поля от NULLСамостоятельный пример
<?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);

    $items=f153OwnTable($db,$owned,'metadata','(ID INTEGER NOT NULL PRIMARY KEY, TITLE VARCHAR(40) CHARACTER SET UTF8 NOT NULL, NOTE VARCHAR(20) CHARACTER SET UTF8)');
    $q=f153Qualified($items);
    f153Command($db,"INSERT INTO $q (ID,TITLE,NOTE) VALUES (?,?,?)",[1,'Кабель №7',null]);
    f153Command($db,"INSERT INTO $q (ID,TITLE,NOTE) VALUES (?,?,?)",[2,'Адаптер','']);
    f153Commit($db);
    $query=ibase_prepare($db,"SELECT ID AS ITEM_ID,TITLE AS PRODUCT_NAME,NOTE AS COMMENT_TEXT FROM $q WHERE ID>=? ORDER BY ID");
    if ($query===false) {f153Failure('Отчёт не подготовлен');}
    $result=null;
    try {
        $result=ibase_execute($query,1);
        if ($result===false) {f153Failure('Отчёт не выполнен');}
        $count=ibase_num_fields($result);
        if (!is_int($count)) {throw new RuntimeException('Количество полей не получено');}
        $fields=[];
        for ($index=0;$index<$count;$index++) {
            $info=ibase_field_info($result,$index);
            if (!is_array($info)) {throw new RuntimeException('Описание поля не получено');}
            $fields[]=array_intersect_key($info,array_flip(['name','alias','relation','length','type']));
        }
        $data=f153Rows($result);
    } finally {
        if (is_resource($result)) {ibase_free_result($result);$result=null;}
        if (ibase_free_query($query)!==true) {f153Failure('Отчёт не освобождён');}
        $query=null;
    }
    $headers=array_column($fields,'alias');
    $nullValue=array_key_exists('COMMENT_TEXT',$data[0]) && $data[0]['COMMENT_TEXT']===null;
    $emptyValue=$data[1]['COMMENT_TEXT']==='';
    $rows=[['Столбцов в результате',$count],['Строк данных',count($data)],['Заголовки по alias',implode(', ',$headers)],
        ['Имя исходного второго столбца',$fields[1]['name']],['Его alias в отчёте',$fields[1]['alias']],['Тип второго столбца',$fields[1]['type']],
        ['length второго столбца, байт',(int)$fields[1]['length']],['Первый комментарий действительно NULL',$nullValue],['Второй комментарий — пустая строка',$emptyValue]];
    $checks=['three_columns'=>$count===3,'two_data_rows'=>count($data)===2,'header_aliases'=>$headers===['ITEM_ID','PRODUCT_NAME','COMMENT_TEXT'],
        'source_name_preserved'=>$fields[1]['name']==='TITLE','varchar_type'=>$fields[1]['type']==='VARCHAR',
        'utf8_capacity_bytes'=>(int)$fields[1]['length']===160,'null_not_empty'=>$nullValue && $emptyValue,
        'same_source_relation'=>array_unique(array_column($fields,'relation'))===[$items]];
    $observations=['field_metadata'=>$fields,'data_rows'=>$data,'headers'=>$headers,'source_character_limit'=>40,
        'reported_length_measures_bytes'=>true,'metadata_does_not_supply_nullable_flag'=>true];

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

f153Report('fb153-result-metadata','Собрать заголовки отчёта и отличить описание поля от NULL',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Столбцов в результате — 3
Строк данных — 2
Заголовки по alias — ITEM_ID, PRODUCT_NAME, COMMENT_TEXT
Имя исходного второго столбца — TITLE
Его alias в отчёте — PRODUCT_NAME
Тип второго столбца — VARCHAR
length второго столбца, байт — 160
Первый комментарий действительно NULL — да
Второй комментарий — пустая строка — да

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

Открыть результат программы · Данные, метаданные и наблюдения JSON.

Границы и типичные ошибки

Документированный набор field_info не содержит флага nullable. Нельзя придумывать его из длины или одной прочитанной строки. Проверка $value === null отвечает о конкретном значении; ограничения столбца проверяют отдельно по схеме/DDL. Для выражений и вычисляемых полей происхождение может отсутствовать; не стройте универсальный редактор таблиц только по relation/name. Используйте уникальные alias, особенно в JOIN, и экранируйте заголовки и значения при HTML-выводе. Описание не читает всю таблицу и не говорит число её строк.

SELECT * через старую связку PHP/InterBase затрагивал тип, который эта связка не обрабатывала; ошибка возникала при чтении. Исходный вопрос на Stack Overflow. Выбираем нужные поля явно, изучаем их метаданные и фиксируем версию драйвера. Историческая проблема BOOLEAN не означает запрета BOOLEAN в современных версиях; замена типа не предлагается как универсальный ремонт.

Измените одно требование

Если фильтр не найдёт ни одного товара, исчезнут ли три заголовка результата?

Как рассуждать и проверить

Нет: успешный SELECT сохраняет описание трёх столбцов и при нуле строк. Колонки описывает num_fields/field_info, строки читает fetch. Пустой набор не является ошибкой выполнения.

Закрыть ненужную выдачу и повторить тот же запрос

Пользователь изменил фильтр после первой строки списка: старый остаток больше не нужен. Освободим текущую выдачу, выполним тот же подготовленный SQL с новым порогом и получим ID 3. Затем освободим саму подготовку и убедимся, что соединение по-прежнему читает таблицу.

Путь данных
  1. prepare создаёт $query — форму SQL с порогом ?
  2. execute($query, 1) создаёт $result → читаем только ID 1
  3. free_result($result) закрывает старую выдачу; $result = null убирает ссылку
  4. execute($query, 3) → новый результат с ID 3
  5. После чтения free_result; после повторов free_query
  6. Массив $copy остаётся Кабель; соединение выполняет COUNT = 3

Сначала короткий вызов

if (ibase_free_result($result) !== true) {
    throw new RuntimeException('Результат не освобождён');
}
$result = null;
$result = ibase_execute($query, 3);
// $query ещё существует; он освобождается после последнего execute.

Результат одной выборки и ресурс её подготовки имеют разное назначение. free_result принимает результат execute; free_query принимает подготовку prepare. Переменная $copy уже содержит обычный массив PHP и не исчезает вместе с курсором. finally выполняет завершающие действия даже при исключении. Присваивание null не заменяет вызов освобождения, но удаляет оставшуюся ссылку, чтобы больше не обратиться к закрытому состоянию.

Что получит читатель после запуска

НаблюдениеРезультат
Прочитанный ID первого списка1
Остаток первого результата освобождёнда
ID после повторного execute с порогом 33
Скопированное имя после освобожденияКабель
Подготовленный запрос освобождён в концеда
Соединение после free_query видит товаров3
Повторных вызовов на освобождённом результате0
Полная программа и архив для самостоятельного запуска
Закрыть ненужную выдачу и повторить тот же запросСамостоятельный пример
<?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);

    $items=f153OwnTable($db,$owned,'lifecycle','(ID INTEGER NOT NULL PRIMARY KEY, TITLE VARCHAR(40) CHARACTER SET UTF8 NOT NULL)');
    $q=f153Qualified($items);
    foreach ([[1,'Кабель'],[2,'Адаптер'],[3,'Подставка']] as $item) {f153Command($db,"INSERT INTO $q (ID,TITLE) VALUES (?,?)",$item);}
    f153Commit($db);
    $query=ibase_prepare($db,"SELECT ID,TITLE FROM $q WHERE ID>=? ORDER BY ID");
    if ($query===false) {f153Failure('Список не подготовлен');}
    $result=null;
    try {
        $result=ibase_execute($query,1);
        if ($result===false) {f153Failure('Первый список не получен');}
        $firstCall=f153Capture(static fn()=>ibase_fetch_assoc($result));
        if ($firstCall['warning'] || !is_array($firstCall['value'])) {f153Failure('Первая строка не прочитана',$firstCall);}
        $copy=$firstCall['value'];unset($firstCall);
        $freed=ibase_free_result($result);
        if ($freed!==true) {f153Failure('Остаток первого списка не освобождён');}
        $result=null; // Do not use is_resource as proof that a freed old-driver handle is still usable.
        $result=ibase_execute($query,3);
        if ($result===false) {f153Failure('Повторный список не получен');}
        $again=f153Rows($result);
        if (ibase_free_result($result)!==true) {f153Failure('Второй результат не освобождён');}
        $result=null;
    } finally {
        if (is_resource($result)) {ibase_free_result($result);$result=null;}
        $queryFreed=ibase_free_query($query);
        $query=null;
        if ($queryFreed!==true) {f153Failure('Подготовка не освобождена');}
    }
    $total=(int)f153Read($db,"SELECT COUNT(*) AS TOTAL FROM $q")[0]['TOTAL'];
    $rows=[['Прочитанный ID первого списка',$copy['ID']],['Остаток первого результата освобождён',$freed],
        ['ID после повторного execute с порогом 3',implode(', ',array_column($again,'ID'))],['Скопированное имя после освобождения',$copy['TITLE']],
        ['Подготовленный запрос освобождён в конце',$queryFreed],['Соединение после free_query видит товаров',$total],['Повторных вызовов на освобождённом результате',0]];
    $checks=['first_row'=>$copy===['ID'=>1,'TITLE'=>'Кабель'],'first_result_freed'=>$freed===true,
        'prepared_query_reused'=>$again===[['ID'=>3,'TITLE'=>'Подставка']],'query_released'=>$queryFreed===true,
        'php_copy_survives'=>$copy['TITLE']==='Кабель','connection_still_works'=>$total===3];
    $observations=['first_row_copy'=>$copy,'second_execution_rows'=>$again,'first_execution_unread_rows'=>2,
        'resource_order'=>['prepare','execute(1)','fetch one','free_result + null','execute(3)','fetch all','free_result + null','free_query + null','query count'],
        'invalid_released_handle_called'=>false,'table_rows_after'=>3];

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

f153Report('fb153-prepared-cleanup','Закрыть ненужную выдачу и повторить тот же запрос',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Прочитанный ID первого списка — 1
Остаток первого результата освобождён — да
ID после повторного execute с порогом 3 — 3
Скопированное имя после освобождения — Кабель
Подготовленный запрос освобождён в конце — да
Соединение после free_query видит товаров — 3
Повторных вызовов на освобождённом результате — 0

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

Открыть результат программы · Данные, метаданные и наблюдения JSON.

Границы и типичные ошибки

В версии расширения 3.0.1 is_resource после free_result может всё ещё вернуть true; это не разрешение повторно читать закрытый результат. Не делайте двойное освобождение: после успешного free присвойте null или unset. Освободите результаты до подготовки, а подготовку до завершения связанной транзакции и удаления таблицы. free_query не означает commit и не закрывает соединение. Если заранее нужен только один товар, ограничьте исходный SQL вместо загрузки ненужного остатка; пример нарочно создаёт остаток, чтобы показать его владельца.

Измените одно требование

Можно ли после free_query вызвать execute с прежним $query, если is_resource ещё возвращает true?

Как рассуждать и проверить

Нет. После освобождения договор владения завершён. Нужен новый prepare. Не используйте проверку PHP-типа как проверку логической пригодности уже освобождённого драйверного ресурса.

Справочник подготовки, выполнения и метаданных

ФункцияНазначение
ibase_prepareПодготовить повторяемую SQL-команду с позициями для значений.
ibase_executeПередать значения по позициям и выполнить подготовленный SQL.
ibase_num_fieldsУзнать число столбцов конкретного результата.
ibase_field_infoПолучить имя, alias, происхождение, размер и SQL-тип столбца.

Собрать предсказуемую работу с результатом

Сначала решите, какие строки и поля нужны экрану или следующему действию. Зафиксируйте SQL, передайте значения отдельно, проверьте результат выполнения и прочитайте только эту выдачу. Если формат важен получателю, дополнительно проверьте количество и alias столбцов. Завершайте ресурсы в обратном порядке владения и отдельно подтверждайте свою транзакцию.

Для начала и ограниченной выборки вернитесь к первым запросам Firebird. Для общего устройства связей, выборки нужных полей и индексов полезен маршрут по реляционным базам и PDO; его PHP API отличается от ibase_*, поэтому не переносите имена методов механически. Эти четыре карточки дополняют библиотеку и не означают завершённого покрытия всех возможностей Firebird.

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

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

ibase_prepareПодготовить повторяемую SQL-команду с позициями для значений.

Встроено в PHP 5 и PHP 7 до 7.4; далее отдельное расширение. Проверенный профиль: interbase 3.0.1 / PHP 8.2. · interbase

ibase_prepare(resource $connectionOrTransaction, string $query): resource|false

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

Подготовка и выполнение
  1. SQL: WHERE CATEGORY = ? AND PRICE_KOPECKS <= ?
  2. prepare($db, SQL) → $query
  3. execute($query, cable, 2000) → $result
  4. Чтение и free_result; следующий execute меняет только значения
  5. После последнего выполнения free_query($query)
ibase_prepare(string $query): resource|false
ibase_prepare(resource $connectionOrTransaction, string $query): resource|false
ibase_prepare(resource $connection, resource $transaction, string $query): resource|false
АргументЧто передавать
$queryТекст одной SQL-команды; каждый позиционный ? означает отдельное значение.
$connectionOrTransactionСоединение ibase_connect; в поддерживаемом API также допустим ресурс явно начатой транзакции.
$connection + $transactionСвязанные соединение и транзакция при трёхаргументной форме. Если транзакция не указана, используется стандартная транзакция соединения.

Одноаргументная форма использует последнее открытое соединение. В приложении с несколькими базами лучше передавать нужный ресурс явно. В краткой сигнатуре PHP Manual третий вариант подписывает trans как string, но его же описание называет дескриптором транзакции; реализация проверенной версии 3.0.1 принимает здесь ресурс. Строка с именем транзакции ресурс не заменяет.

Результат — подготовленный ресурс или false при отказе подготовки. Неверный SQL, неизвестный столбец, недостаточные права или неподходящая транзакция требуют диагностики сразу после отказа. Неверный PHP-тип аргумента может вызвать предупреждение/исключение и не обязан стать обычным false. Успешный prepare не означает, что будущие значения пройдут ограничения таблицы или что запись уже сохранена.

Один фильтр для нескольких бюджетов

$query = ibase_prepare($db,
    'SELECT ID, TITLE FROM PRODUCTS WHERE CATEGORY = ? AND PRICE_KOPECKS <= ?');
if ($query === false) { throw new RuntimeException('Фильтр не подготовлен'); }
$firstResult = ibase_execute($query, 'cable', 2000);
// Прочитать и освободить firstResult, затем выполнить с новым бюджетом.
$secondResult = ibase_execute($query, 'cable', 5000);

Контекстный фрагмент использует уже существующую таблицу PRODUCTS. Полная программа с собственными данными получает ID 1 при первом бюджете и ID 1, 3 при втором. Она показывает проверки и освобождение между выполнениями.

Один INSERT для нескольких клиентов

$insert = ibase_prepare($db,
    'INSERT INTO CLIENTS (ID, CLIENT_NAME, EMAIL) VALUES (?, ?, ?)');
if ($insert === false) { throw new RuntimeException('INSERT не подготовлен'); }
$saved = ibase_execute($insert, 1, "О'Коннор", 'o.connor@example.test');
if ($saved === false) { throw new RuntimeException('Клиент не записан'); }
// После серии записей: освободить запрос, затем явно подтвердить транзакцию.

Здесь апостроф принадлежит значению имени. Самостоятельный запуск добавляет вторую запись и показывает контролируемую ошибку при отсутствии третьего аргумента.

Знак ? нельзя использовать вместо таблицы, имени столбца или ключевого слова сортировки. Составляйте SQL из фиксированных разрешённых вариантов, а пользовательские значения передавайте отдельно. Подготовка не является кешем результата и не устраняет необходимость индексов/ограниченной выдачи. Для разовой команды подходит параметризованный ibase_query. Продолжение: ibase_execute и порядок освобождения ресурсов.

ibase_executeПередать значения по позициям и выполнить подготовленный SQL.

Встроено в PHP 5 и PHP 7 до 7.4; далее отдельное расширение. Проверенный профиль: interbase 3.0.1 / PHP 8.2. · interbase

ibase_execute(resource $query, mixed ...$values): resource|int|bool

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

Значения по позициям
  1. SQL: CLIENT_NAME = ? AND ID > ?
  2. Аргумент 1 после $query: О'Коннор → CLIENT_NAME
  3. Аргумент 2 после $query: 0 → ID
  4. execute → результат строк либо статус/число изменённых строк
  5. false → сразу сохранить диагностику; не объявлять операцию успешной
АргументЧто передавать
$queryПодготовленный ресурс ibase_prepare, не соединение и не текст SQL.
...$valuesЗначения в порядке позиционных ?; без заполнителей дополнительные значения не нужны. Для массива параметров PHP позволяет ...$params с последовательными числовыми ключами.
ВозвратКак трактовать
resourceУспешная команда с возвращаемыми полями, например SELECT; результат может иметь ноль строк. Его нужно читать и освобождать.
Положительный intДля подходящей команды — количество затронутых строк. Это не ID вставленного товара.
trueУспешная команда без результата/положительного количества; в частности UPDATE, который не нашёл строк. true само по себе не доказывает «изменена одна строка».
falseОтказ выполнения; проверяйте строго === false и сохраняйте PHP/драйверную диагностику до других вызовов.

Сокращённая сигнатура PHP Manual указывает resource, но текст возврата шире: resource, bool и количество строк. Тип результата зависит от SQL. INSERT ... RETURNING может возвращать поля, поэтому обработчик, который для любого INSERT ожидает только true или число, неверен. DDL без набора полей не следует передавать в fetch. Выполнение команды также не равно успешному commit: результат подтверждения транзакции проверяют отдельно.

Получить нужную страницу каталога

$result = ibase_execute($query, 'cable', 5000);
if ($result === false) { throw new RuntimeException('Поиск не выполнен'); }
if (!is_resource($result)) { throw new RuntimeException('Ожидалась выдача SELECT'); }
// Далее читать result; пустой список — нормальный результат поиска.

В полной программе поиска SQL имеет два ?, поэтому передаются ровно два значения. Для отсутствующей категории execute успешен, но чтение возвращает пустой массив.

Повторить INSERT с другим именем

$saved = ibase_execute($insert, 2, 'Анна', 'anna@example.test');
if ($saved === false) { throw new RuntimeException('Запись не выполнена'); }
// Это результат выполнения, а не идентификатор новой записи.
// После освобождения подготовки явно проверить commit своей транзакции.

Опыт с именем и email сопоставляет все три позиции и проверяет фактически записанные поля. Подготовка не определяет бизнес-смысл одинаково типизированных параметров.

Недостаток значений в проверенной версии даёт false и PHP warning, при этом серверный код может отсутствовать. Излишек значений в версии 3.0.1 может дать notice и всё же запустить команду: не рассчитывайте на отказ как на валидацию количества. Не передавайте один массив вместо нескольких значений без распаковки; не считайте quoted "?" заполнителем. Сырые сообщения не выводят посетителям, поскольку они могут раскрывать SQL и путь к базе.

Перед повторным выполнением закончите использование предыдущей выдачи и освободите её. Полный жизненный цикл сохраняет подготовку для нового execute. Для новой формы SQL нужен ibase_prepare; для понимания столбцов — ibase_num_fields и ibase_field_info.

ibase_num_fieldsУзнать число столбцов конкретного результата.

Встроено в PHP 5 и PHP 7 до 7.4; далее отдельное расширение. Проверенный профиль: interbase 3.0.1 / PHP 8.2. · interbase

ibase_num_fields(resource $result_id): int

Экспорт или HTML-отчёт должен знать, сколько столбцов возвращает конкретный SELECT. ibase_num_fields сообщает ширину результата. Это не количество товаров, не число прочитанных строк и не количество полей всей таблицы.

Три столбца и две строки
  1. SELECT ID AS ITEM_ID, TITLE AS PRODUCT_NAME, NOTE AS COMMENT_TEXT
  2. execute → ресурс результата
  3. num_fields(result) → 3
  4. Цикл индексов 0, 1, 2 описывает колонки
  5. Чтение данных отдельно получает две строки
АргументЗначение
$result_idРесурс результата выполненного запроса Firebird/InterBase. Не передавайте булев успех DDL/UPDATE или уже освобождённую выдачу.

Документированный возврат — int, число полей. Успешный пустой SELECT всё ещё имеет описание столбцов. Официальная страница не задаёт отдельного гарантированного значения ошибки для произвольного неверного аргумента: используйте живой ресурс результата, проверенный после выполнения. Поведение низкоуровневого расширения на недействительном ресурсе не следует использовать как обычную ветку программы.

Построить заголовки по всей выдаче

$count = ibase_num_fields($result);
$headers = [];
for ($i = 0; $i < $count; $i++) {
    $info = ibase_field_info($result, $i);
    if (!is_array($info)) { throw new RuntimeException('Колонка не описана'); }
    $headers[] = $info['alias'];
}

Полный отчёт получает ITEM_ID, PRODUCT_NAME, COMMENT_TEXT. HTML-страница результата отдельно показывает число столбцов 3 и число строк 2.

Остановить экспорт при изменении его формата

$expectedColumns = 3;
if (ibase_num_fields($result) !== $expectedColumns) {
    throw new RuntimeException('Формат экспортируемой строки изменился');
}
// Затем сверить alias и порядок: равное количество ещё не гарантирует тот же смысл.

Это контекстный фрагмент для результата запроса, на котором построен экспорт. Он обнаружит лишнюю колонку после изменения SELECT; совместно с проверкой alias защитит фиксированный контракт файла.

Для количества записей используйте COUNT с нужным WHERE на сервере или считайте фактически прочитанные строки ограниченного результата. Не вызывайте SELECT * только ради этой функции: она описывает именно список полей SELECT. Базовый пример PHP Manual показывает обход field_info; здесь сохранена та же связь и добавлены причина, конкретный результат и проверка контракта. Следующий шаг — отличить metadata от значений NULL.

ibase_field_infoПолучить имя, alias, происхождение, размер и SQL-тип столбца.

Встроено в PHP 5 и PHP 7 до 7.4; далее отдельное расширение. Проверенный профиль: interbase 3.0.1 / PHP 8.2. · interbase

ibase_field_info(resource $result, int $field_number): array|false

В таблице поле называется TITLE, а в выдаче отчёта — PRODUCT_NAME. ibase_field_info позволяет получить обе подписи и SQL-тип, чтобы построить заголовок, объяснить формат выгрузки или заметить неожиданное изменение поля.

Описание второго столбца
  1. Вход: живой $result и номер 1
  2. SQL: TITLE AS PRODUCT_NAME
  3. field_info → name TITLE, alias PRODUCT_NAME, type VARCHAR
  4. length 160 описывает байтовую ёмкость UTF8 VARCHAR(40)
  5. Значение Кабель №7 приходит отдельно из fetch
АргументЗначение
$resultРесурс результата после выполнения SELECT.
$field_numberЦелочисленная позиция с нуля. Для трёх полей допустимы 0, 1, 2; верхнюю границу даёт ibase_num_fields.
Ключ массиваСмысл
nameИсходное имя поля, например TITLE. Для выражения исходное поле может отсутствовать.
aliasИмя столбца в текущем результате, например PRODUCT_NAME.
relationТаблица происхождения; не универсальное описание сложного выражения или JOIN.
lengthБайтовый размер представления поля; в наблюдении драйвер возвращает строку "160" для UTF8 VARCHAR(40). Это не strlen текущего текста.
typeСтрока с SQL-типом, например INTEGER или VARCHAR; не PHP gettype значения.

Возвращается массив метаданных. В проверенном драйвере дополнительно присутствуют дублирующие числовые ключи 0..4; используйте именованные ключи, чтобы назначение было видно. При выходе номера за диапазон реализация 3.0.1 возвращает false; номер проверяйте заранее. Неправильный PHP-тип или недействительный ресурс не обязаны следовать этой же ветке. Документированного nullable-ключа здесь нет.

Показать человеку имя поля и alias

$info = ibase_field_info($result, 1);
if (!is_array($info)) { throw new RuntimeException('Второй столбец не найден'); }
echo $info['name'], ' → ', $info['alias'];
// Для SELECT TITLE AS PRODUCT_NAME: TITLE → PRODUCT_NAME.

Самостоятельный пример с таблицей показывает название, тип, длину, две строки и отдельную проверку NULL. Заголовок для страницы выбирается по alias, а не по исходному имени.

Проверить порядок колонок перед отправкой выгрузки

$expected = ['ITEM_ID', 'PRODUCT_NAME', 'COMMENT_TEXT'];
if (ibase_num_fields($result) !== count($expected)) {
    throw new RuntimeException('Изменилось число колонок');
}
foreach ($expected as $index => $alias) {
    $info = ibase_field_info($result, $index);
    if (!is_array($info) || $info['alias'] !== $alias) {
        throw new RuntimeException('Изменился порядок или alias колонки');
    }
}

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

NULL в строке не доказывает свойства любого другого столбца; отсутствие NULL в прочитанных строках не означает NOT NULL в DDL. Проверяйте конкретное значение строго === null, а ограничения смотрите в схеме. Для UTF8 длина в байтах и число символов различаются. Не считайте metadata автоматическим разрешением менять указанную таблицу и не вставляйте её имена в SQL из пользовательского запроса. Связанные шаги: ibase_num_fields, ibase_execute и освобождение после чтения.

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

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

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

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

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