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

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

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

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

Глава 64 · После подготовленных запросов Firebird

Firebird: число и типы параметров, порядок фильтров и срок жизни запроса

Пропущенное поле импорта, метаданные входных мест, перепутанные границы цены и три вкладки одним SELECT: четыре рабочих сценария.

От полей импорта до проверяемой команды

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

Команда, входные места и результат
  1. prepare: SQL с местами ? → query
  2. num_params: сколько входных мест
  3. param_info: описание места с индексом 0, 1, 2…
  4. execute: значения строго в порядке мест
  5. result: строки SELECT → free_result
  6. Команда больше не нужна → free_query

Подготовленную команду можно представить бланком с пронумерованными полями. num_params сообщает число полей, param_info — сведения об их представлении, а execute заполняет их переданными значениями. Номер поля не объясняет его деловой смысл: два целых могут означать минимум и максимум. Это модель API, не обещание физического устройства памяти сервера.

Начните с соединения и простого SELECT и prepare/execute и колонок результата. Полные примеры используют PHP 8.2.35, отдельное расширение FirebirdSQL interbase 3.0.1 и Firebird 5.0.4. Исторические страницы PHP Manual помечены PHP 5 / PHP 7 ниже 7.4: старый встроенный модуль был удалён из поставки PHP 7.4, но эти имена реализует выбранный поддерживаемый драйвер. README объясняет его отдельную установку и локальную учебную базу.

ЗадачаПерейти к сценарию
Пропущено одно поле импортаЧисло параметров и исправленный INSERT
Непонятно, какой тип ждёт очередное местоОписание параметров и его ограничения
Параметров два, а каталог пустСмысл и порядок границ цены
Повторяется один SELECT с разными статусамиОдин prepare, три результата и освобождение

Как открыть и понять самостоятельные примеры

Внутри каждого ZIP находятся example.php, lab.php и README.txt. lab.php задаёт соединение только с отдельной учебной базой, создаёт одну таблицу со случайным собственным именем и удаляет только её после работы. f156Capture сразу сохраняет предупреждение и код без раскрытия частного пути. В исходнике видны CREATE TABLE, INSERT и SELECT; служебная обёртка не подменяет реальные запросы.

prepare156 проверяет создание команды. execute156 принимает числовой список, сравнивает его длину с num_params и вызывает ibase_execute($query, ...$values). readPrepared156 читает ограниченную выборку и освобождает её результат. releaseQuery156 проверяет free_query и обнуляет переменную. Для начала читайте маленький фрагмент каждого раздела, затем один полный сценарий. Все ожидаемые значения ниже относятся именно к показанным собственным данным.

Импорт товара: заметить пропущенное количество до INSERT

В строке импорта пришли код и название товара, но пропало количество. Проверка должна объяснить проблему до INSERT. Затем та же подготовленная команда принимает исправленный список и сохраняет «Кабель», 3 штуки.

Путь данных
  1. SQL: три места ? и текстовый литерал '?'
  2. Драйвер сообщает: нужны 3 значения
  3. Пришло 2 → сообщение приложения, INSERT ещё не вызван
  4. Пришло 3 → INSERT → commit → строка товара

Сначала один шаг

$query = ibase_prepare($db, "INSERT INTO catalog(id, title, qty) VALUES(?, ?, ?)");
if ($query === false) { throw new RuntimeException("Не удалось подготовить INSERT"); }
$needed = ibase_num_params($query);
echo $needed; // 3

Здесь catalog — уже созданная таблица, а $db — соединение. prepare передаёт SQL серверу и возвращает ресурс подготовленной команды. num_params читает число входных мест. Каждый знак ? вне кавычек ждёт отдельное значение; '?' внутри SQL-кавычек является обычным текстом. В полном файле execute156 проверяет длину списка и распаковывает его через ...$values: первое значение становится первым аргументом после $query, затем второе и третье.

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

НаблюдениеРезультат
Параметров у команды3
Знаков ? в тексте SQL4
Отказ проверки спискаОжидалось 3 значения, получено 2
Строк после отклонения0
Сохранённый товарКабель
Количество3
Литерал marker?
Импорт товара: заметить пропущенное количество до INSERTСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f156Config();$db=f156Connect($config);$runtime=f156Runtime($db,$config);

    function prepare156($db,string $sql) {
        $call=f156Capture(static fn()=>ibase_prepare($db,$sql));
        if (!is_resource($call['value']) || $call['warning']) {f156Failure('Команда не подготовлена',$call);}
        return $call['value'];
    }
    function parameterCount156($query): int {
        $call=f156Capture(static fn()=>ibase_num_params($query));
        if (!is_int($call['value']) || $call['warning']) {f156Failure('Число параметров не получено',$call);}
        return $call['value'];
    }
    function execute156($query,array $values) {
        // Native ibase parameters are positional: a numeric list, not named PHP arguments.
        if (!array_is_list($values)) {throw new InvalidArgumentException('Нужен упорядоченный список значений.');}
        $needed=parameterCount156($query);
        if (count($values)!==$needed) {
            throw new DomainException('Ожидалось '.$needed.' значения, получено '.count($values));
        }
        $call=f156Capture(static fn()=>ibase_execute($query,...$values));
        if ($call['value']===false || $call['warning']) {f156Failure('Подготовленная команда не выполнена',$call);}
        return $call['value'];
    }
    function readPrepared156($query,array $values): array {
        $result=execute156($query,$values);
        if (!is_resource($result)) {throw new RuntimeException('Ожидался результат SELECT.');}
        try {return f156Rows($result);}
        finally {
            $call=f156Capture(static fn()=>ibase_free_result($result));$result=null;
            if ($call['value']!==true || $call['warning']) {f156Failure('Результат не освобождён',$call);}
        }
    }
    function releaseQuery156(&$query): bool {
        $call=f156Capture(static fn()=>ibase_free_query($query));
        if ($call['value']!==true || $call['warning']) {f156Failure('Подготовленная команда не освобождена',$call);}
        $query=null;
        return true;
    }

    $name=f156OwnTable($db,$owned,'count','(item_id INTEGER NOT NULL PRIMARY KEY,title VARCHAR(40) CHARACTER SET UTF8 NOT NULL,qty INTEGER NOT NULL CHECK(qty>=0),marker VARCHAR(1))');
    $q=f156Qualified($name);

    $sql="INSERT INTO $q(item_id,title,qty,marker) VALUES(?,?,?,'?')";
    $query=prepare156($db,$sql);
    try {
        $needed=parameterCount156($query);
        $rejected='';
        try {execute156($query,[101,'Кабель']);}
        catch (DomainException $error) {$rejected=$error->getMessage();}
        $before=f156Read($db,"SELECT COUNT(*) AS \"n\" FROM $q")[0]['n'];
        execute156($query,[101,'Кабель',3]);
    } finally {if (is_resource($query)) {releaseQuery156($query);}}
    f156Commit($db);
    $saved=f156Read($db,"SELECT item_id AS \"id\",title AS \"title\",qty AS \"qty\",marker AS \"marker\" FROM $q WHERE item_id=?",[101])[0];
    $rows=[['Параметров у команды',$needed],['Знаков ? в тексте SQL',substr_count($sql,'?')],
        ['Отказ проверки списка',$rejected],['Строк после отклонения',(int)$before],
        ['Сохранённый товар',$saved['title']],['Количество',$saved['qty']],['Литерал marker',$saved['marker']]];
    $checks=['three_parameters'=>$needed===3,'literal_not_parameter'=>substr_count($sql,'?')===4,
        'missing_value_rejected'=>$rejected==='Ожидалось 3 значения, получено 2','no_partial_insert'=>(int)$before===0,
        'correct_row_saved'=>$saved===['id'=>101,'title'=>'Кабель','qty'=>3,'marker'=>'?'],'query_released'=>$query===null];
    $observations=['sql_shape'=>"INSERT INTO own_table(item_id,title,qty,marker) VALUES(?,?,?,'?')",
        'valid_parameter_order'=>['item_id','title','qty'],'application_refusal_before_execute'=>true,'selected'=>$saved];

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

f156Report('fb156-parameter-count','Импорт товара: заметить пропущенное количество до INSERT',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Параметров у команды — 3
Знаков ? в тексте SQL — 4
Отказ проверки списка — Ожидалось 3 значения, получено 2
Строк после отклонения — 0
Сохранённый товар — Кабель
Количество — 3
Литерал marker — ?

Самостоятельный CLI-пример создаёт одну свою таблицу в отдельной Firebird, выполняет настоящий prepare/execute/SELECT, создаёт result.html и result.json и удаляет таблицу. lab.php и README.txt приложены.

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

Ошибки, ограничения и применение

Ошибка с двумя значениями намеренно обнаруживается нашим кодом, а не выдаётся за ошибку Firebird. num_params не считает колонки результата, найденные строки или символы вопроса в исходной строке. Проверка количества ещё не проверяет порядок, типы и бизнес-правила. Для marker выбран VARCHAR(1): фиксированный CHAR может вернуться с дополнительными пробелами, а здесь мы изучаем литерал и число параметров, не представление фиксированной строки. COUNT здесь считает только строки маленькой собственной таблицы; рабочий экран не должен загружать всю таблицу ради такого подсчёта.

В обсуждении отладки подготовленных запросов встречается и вариант native ibase: автору хочется увидеть SQL уже с подставленными значениями. Исходная ситуация на Stack Overflow. Мы показываем шаблон и упорядоченный список отдельно. Замена знаков ? строковыми значениями не восстанавливает реальную работу драйвера; знак в кавычках вообще не параметр. Для исполнения остаётся prepare/execute, а реальные персональные данные не выводятся в отладку.

Измените условие задачи

Добавьте warehouse_id как ещё один параметр INSERT, сохранив marker со значением '?'. Что изменится?

Ожидаемое решение

Драйвер вернёт 4 параметра, хотя в тексте будет 5 знаков ?. Передайте [id, title, qty, warehouse_id] в порядке колонок INSERT и явно проверьте существование склада по правилам приложения.

Диагностика импорта: увидеть входные типы, байты и предел метаданных

У интеграции есть INSERT из трёх полей. Перед разбором ошибки полезно увидеть, какой тип ждёт каждое входное место: целое, текст или точное число. Снимем метаданные, сопоставим их с известной схемой импорта и сохраним товар с ценой 19.90 без перевода денег в float.

Путь данных
  1. INSERT(CODE, TITLE, PRICE) VALUES(?, ?, ?)
  2. Индексы 0, 1, 2 → описания входных мест
  3. Метки формы и ограничения задаёт приложение
  4. [7, «Кабель USB», «19.90»] → INSERT → видимая строка

Сначала один шаг

$info = ibase_param_info($query, 1);
if ($info === false) { throw new RuntimeException("Такого параметра нет"); }
echo $info["type"], " / ", $info["length"];
// Для нашего TITLE: VARCHAR / 80

Индекс 1 означает второе место ?, потому что нумерация начинается с 0. type описывает входное представление, length — его длину в байтах. Полный пример перебирает индексы от 0 до num_params($query)-1, а затем намеренно обращается к индексу 3: для трёх параметров он уже вне диапазона.

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

НаблюдениеРезультат
Параметр 0: CODEINTEGER; length=4
Параметр 1: TITLEVARCHAR; length=80
Параметр 2: PRICENUMERIC(18,2); length=8
name / alias / relation пустыда
Индекс 3 вернул falseда
Сохранённое названиеКабель USB
Цена после чтения19.90
Диагностика импорта: увидеть входные типы, байты и предел метаданныхСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f156Config();$db=f156Connect($config);$runtime=f156Runtime($db,$config);

    function prepare156($db,string $sql) {
        $call=f156Capture(static fn()=>ibase_prepare($db,$sql));
        if (!is_resource($call['value']) || $call['warning']) {f156Failure('Команда не подготовлена',$call);}
        return $call['value'];
    }
    function parameterCount156($query): int {
        $call=f156Capture(static fn()=>ibase_num_params($query));
        if (!is_int($call['value']) || $call['warning']) {f156Failure('Число параметров не получено',$call);}
        return $call['value'];
    }
    function execute156($query,array $values) {
        // Native ibase parameters are positional: a numeric list, not named PHP arguments.
        if (!array_is_list($values)) {throw new InvalidArgumentException('Нужен упорядоченный список значений.');}
        $needed=parameterCount156($query);
        if (count($values)!==$needed) {
            throw new DomainException('Ожидалось '.$needed.' значения, получено '.count($values));
        }
        $call=f156Capture(static fn()=>ibase_execute($query,...$values));
        if ($call['value']===false || $call['warning']) {f156Failure('Подготовленная команда не выполнена',$call);}
        return $call['value'];
    }
    function readPrepared156($query,array $values): array {
        $result=execute156($query,$values);
        if (!is_resource($result)) {throw new RuntimeException('Ожидался результат SELECT.');}
        try {return f156Rows($result);}
        finally {
            $call=f156Capture(static fn()=>ibase_free_result($result));$result=null;
            if ($call['value']!==true || $call['warning']) {f156Failure('Результат не освобождён',$call);}
        }
    }
    function releaseQuery156(&$query): bool {
        $call=f156Capture(static fn()=>ibase_free_query($query));
        if ($call['value']!==true || $call['warning']) {f156Failure('Подготовленная команда не освобождена',$call);}
        $query=null;
        return true;
    }

    $name=f156OwnTable($db,$owned,'metadata','(CODE INTEGER NOT NULL PRIMARY KEY,TITLE VARCHAR(20) CHARACTER SET UTF8 NOT NULL,PRICE DECIMAL(10,2) NOT NULL CHECK(PRICE>=0))');
    $q=f156Qualified($name);

    $query=prepare156($db,"INSERT INTO $q(CODE,TITLE,PRICE) VALUES(?,?,?)");
    $metadata=[];
    try {
        $count=parameterCount156($query);
        for ($index=0;$index<$count;$index++) {
            $call=f156Capture(static fn()=>ibase_param_info($query,$index));
            if (!is_array($call['value']) || $call['warning']) {f156Failure('Описание параметра не получено',$call);}
            $info=$call['value'];
            $metadata[]=['position'=>$index,'name'=>$info['name'],'alias'=>$info['alias'],'relation'=>$info['relation'],
                'length'=>$info['length'],'type'=>$info['type']];
        }
        $outside=f156Capture(static fn()=>ibase_param_info($query,3));
        $code=7;$title='Кабель USB';$price='19.90';
        // An INTEGER descriptor does not express our business rule: product code must be positive.
        if ($code<=0) {throw new DomainException('Код товара должен быть положительным.');}
        execute156($query,[$code,$title,$price]);
    } finally {if (is_resource($query)) {releaseQuery156($query);}}
    f156Commit($db);
    $saved=f156Read($db,"SELECT CODE AS \"code\",TITLE AS \"title\",PRICE AS \"price\" FROM $q WHERE CODE=?",[7])[0];
    $labels=[];
    foreach ($metadata as $item) {$labels[]=$item['type'].'; length='.$item['length'];}
    $namesEmpty=true;
    foreach ($metadata as $item) {
        $namesEmpty=$namesEmpty && $item['name']==='' && $item['alias']==='' && $item['relation']==='';
    }
    $rows=[['Параметр 0: CODE',$labels[0]],['Параметр 1: TITLE',$labels[1]],['Параметр 2: PRICE',$labels[2]],
        ['name / alias / relation пусты',$namesEmpty],['Индекс 3 вернул false',$outside['value']===false],
        ['Сохранённое название',$saved['title']],['Цена после чтения',$saved['price']]];
    $checks=['three_input_descriptors'=>$count===3 && count($metadata)===3,
        'expected_types'=>array_column($metadata,'type')===['INTEGER','VARCHAR','NUMERIC(18,2)'],
        'lengths_are_byte_strings'=>array_column($metadata,'length')===['4','80','8'],
        'names_not_guessed'=>$namesEmpty,'outside_index_refused'=>$outside['value']===false && !$outside['warning'],
        'decimal_preserved'=>$saved===['code'=>7,'title'=>'Кабель USB','price'=>'19.90'],'query_released'=>$query===null];
    $observations=['input_metadata'=>$metadata,'declared_columns'=>['CODE INTEGER','TITLE VARCHAR(20) CHARACTER SET UTF8','PRICE DECIMAL(10,2) CHECK(PRICE>=0)'],
        'explicit_form_labels'=>['Код товара','Название товара','Цена'],'saved'=>$saved,'out_of_range_warning'=>$outside['warning']];

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

f156Report('fb156-parameter-metadata','Диагностика импорта: увидеть входные типы, байты и предел метаданных',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Параметр 0: CODE — INTEGER; length=4
Параметр 1: TITLE — VARCHAR; length=80
Параметр 2: PRICE — NUMERIC(18,2); length=8
name / alias / relation пусты — да
Индекс 3 вернул false — да
Сохранённое название — Кабель USB
Цена после чтения — 19.90

Самостоятельный CLI-пример создаёт одну свою таблицу в отдельной Firebird, выполняет настоящий prepare/execute/SELECT, создаёт result.html и result.json и удаляет таблицу. lab.php и README.txt приложены.

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

Ошибки, ограничения и применение

Для VARCHAR(20) CHARACTER SET UTF8 длина 80 — возможное представление 20 символов по 4 байта, а не разрешение ввести 80 символов. Драйвер описывает DECIMAL(10,2) через NUMERIC(18,2): это представление масштабируемого 64-битного числа, не точная копия объявления DDL. Само DECIMAL(10,2) также не заменяет явный бизнес-предел цены: Firebird может использовать более широкую точность хранения. Проверяйте допустимый диапазон отдельно и при необходимости добавляйте CHECK. name/alias/relation у наших параметров пусты, поэтому метки формы задаются явно. NUMERIC не сообщает правило «цена неотрицательная», а INTEGER не сообщает правило «код больше нуля».

Измените условие задачи

Для товара нужна цена от 0 до 100000.00 включительно. Достаточно ли увидеть NUMERIC(18,2)?

Ожидаемое решение

Нет. Проверяйте сумму в приложении, например в целых копейках от 0 до 10000000, а в таблице задайте CHECK(PRICE BETWEEN 0 AND 100000.00). Затем передавайте точную десятичную строку. Метаданные полезны для диагностики типа, но не создают это бизнес-правило.

Фильтр каталога: правильное количество ещё не означает правильный порядок

Покупатель просит товары от 10 до 30 рублей, которые есть на складе. Имена полей формы понятны человеку, но native ibase связывает значения по позиции. Если передать сначала максимум, SQL честно ищет цену одновременно не меньше 3000 и не больше 1000 копеек — и возвращает пустой результат.

Путь данных
  1. Форма: min_kopecks=1000, max_kopecks=3000
  2. Первое ? = минимум; второе ? = максимум
  3. WHERE отсеивает цену и нулевой остаток
  4. ORDER BY и ROWS 10 ограничивают экран
  5. PHP получает только «Кабель» и «Держатель»

Сначала один шаг

$values = [$form["min_kopecks"], $form["max_kopecks"]];
$result = ibase_execute($query, ...$values);
// Первый аргумент после $query попадает в первое ?, второй — во второе.

Массив $form в полном примере уже содержит целые копейки; это не сырые строки $_GET. Разбор и проверку входной формы выполняют до построения списка параметров. Числа 1000 и 3000 имеют одинаковый тип, поэтому ни num_params, ни param_info не обнаружат перестановку. Смысл параметров задаёт ваш SQL и явное сопоставление полей.

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

НаблюдениеРезультат
Параметров у SELECT2
Перепутанные границы3000 → 1000
Строк при неверном порядке0
Границы после сопоставления1000 → 3000
Коды показанных товаров2, 4
ТоварыКабель / Держатель
Цены в копейках1200, 3000
Фильтр каталога: правильное количество ещё не означает правильный порядокСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f156Config();$db=f156Connect($config);$runtime=f156Runtime($db,$config);

    function prepare156($db,string $sql) {
        $call=f156Capture(static fn()=>ibase_prepare($db,$sql));
        if (!is_resource($call['value']) || $call['warning']) {f156Failure('Команда не подготовлена',$call);}
        return $call['value'];
    }
    function parameterCount156($query): int {
        $call=f156Capture(static fn()=>ibase_num_params($query));
        if (!is_int($call['value']) || $call['warning']) {f156Failure('Число параметров не получено',$call);}
        return $call['value'];
    }
    function execute156($query,array $values) {
        // Native ibase parameters are positional: a numeric list, not named PHP arguments.
        if (!array_is_list($values)) {throw new InvalidArgumentException('Нужен упорядоченный список значений.');}
        $needed=parameterCount156($query);
        if (count($values)!==$needed) {
            throw new DomainException('Ожидалось '.$needed.' значения, получено '.count($values));
        }
        $call=f156Capture(static fn()=>ibase_execute($query,...$values));
        if ($call['value']===false || $call['warning']) {f156Failure('Подготовленная команда не выполнена',$call);}
        return $call['value'];
    }
    function readPrepared156($query,array $values): array {
        $result=execute156($query,$values);
        if (!is_resource($result)) {throw new RuntimeException('Ожидался результат SELECT.');}
        try {return f156Rows($result);}
        finally {
            $call=f156Capture(static fn()=>ibase_free_result($result));$result=null;
            if ($call['value']!==true || $call['warning']) {f156Failure('Результат не освобождён',$call);}
        }
    }
    function releaseQuery156(&$query): bool {
        $call=f156Capture(static fn()=>ibase_free_query($query));
        if ($call['value']!==true || $call['warning']) {f156Failure('Подготовленная команда не освобождена',$call);}
        $query=null;
        return true;
    }

    $name=f156OwnTable($db,$owned,'filters','(item_id INTEGER NOT NULL PRIMARY KEY,title VARCHAR(40) CHARACTER SET UTF8 NOT NULL,price_kopecks INTEGER NOT NULL,stock INTEGER NOT NULL)');
    $q=f156Qualified($name);
    foreach ([[1,'Зажим',500,10],[2,'Кабель',1200,8],[3,'Переходник',2500,0],[4,'Держатель',3000,5],[5,'Зарядка',6000,3]] as $item) {
        f156Command($db,"INSERT INTO $q(item_id,title,price_kopecks,stock) VALUES(?,?,?,?)",$item);
    }
    f156Commit($db);

    $query=prepare156($db,"SELECT item_id AS \"id\",title AS \"title\",price_kopecks AS \"price\" FROM $q WHERE price_kopecks>=? AND price_kopecks<=? AND stock>0 ORDER BY price_kopecks,item_id ROWS 10");
    $form=['max_kopecks'=>3000,'min_kopecks'=>1000];
    try {
        // Deliberate mistake for comparison: array insertion order is not SQL meaning.
        $wrong=array_values($form);
        $empty=readPrepared156($query,$wrong);
        $minimum=$form['min_kopecks'];$maximum=$form['max_kopecks'];
        if ($minimum>$maximum) {throw new DomainException('Минимальная цена больше максимальной.');}
        $correct=[$minimum,$maximum];
        $items=readPrepared156($query,$correct);
        $needed=parameterCount156($query);
    } finally {if (is_resource($query)) {releaseQuery156($query);}}
    $rows=[['Параметров у SELECT',$needed],['Перепутанные границы',implode(' → ',$wrong)],
        ['Строк при неверном порядке',count($empty)],['Границы после сопоставления',implode(' → ',$correct)],
        ['Коды показанных товаров',implode(', ',array_column($items,'id'))],['Товары',implode(' / ',array_column($items,'title'))],
        ['Цены в копейках',implode(', ',array_column($items,'price'))]];
    $checks=['two_parameters'=>$needed===2,'same_count_different_order'=>count($wrong)===$needed && $wrong!==$correct,
        'wrong_order_returns_empty'=>$empty===[],'explicit_mapping'=>array_column($items,'id')===[2,4],
        'price_order_in_sql'=>array_column($items,'price')===[1200,3000],'out_of_stock_excluded'=>!in_array(3,array_column($items,'id'),true),
        'query_released'=>$query===null];
    $observations=['form'=>$form,'wrong_parameters'=>$wrong,'correct_parameters'=>$correct,'selected'=>$items,
        'sql_filters'=>['price_kopecks >= minimum','price_kopecks <= maximum','stock > 0'],'sql_order'=>'price_kopecks, item_id','sql_limit'=>10];

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

f156Report('fb156-filter-order','Фильтр каталога: правильное количество ещё не означает правильный порядок',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Параметров у SELECT — 2
Перепутанные границы — 3000 → 1000
Строк при неверном порядке — 0
Границы после сопоставления — 1000 → 3000
Коды показанных товаров — 2, 4
Товары — Кабель / Держатель
Цены в копейках — 1200, 3000

Самостоятельный CLI-пример создаёт одну свою таблицу в отдельной Firebird, выполняет настоящий prepare/execute/SELECT, создаёт result.html и result.json и удаляет таблицу. lab.php и README.txt приложены.

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

Ошибки, ограничения и применение

array_values($form) сохраняет порядок добавления элементов, но не знает значения имён min/max; здесь этот приём намеренно показан как ошибка. Именованные аргументы PHP не превращают ibase_execute в PDO с SQL-плейсхолдерами :min. Не сортируйте полученные тысячи строк в PHP: WHERE, ORDER BY и ограничение ROWS выполняются в базе. Для большой рабочей таблицы отдельно подбирают индексы и смотрят план. Пустая выборка в неверной ветке — нормальный результат логически неверного условия, а не ошибка сервера.

Разработчик хочет передать в ibase_execute именованные параметры так же, как в примере PDO. Исходная ситуация на Stack Overflow. В native ibase явно связываем имена полей формы с позициями ?: сначала минимум, затем максимум. Не выдаём API другого драйвера или именованные аргументы PHP за поддержку SQL-параметров :name.

Измените условие задачи

Добавьте минимальный остаток как третье место ?: stock >= ?. Сохраните фильтрацию и порядок в SQL.

Ожидаемое решение

Соберите [$minimum, $maximum, $minimumStock] именно в порядке появления ?. num_params станет 3. Проверьте неотрицательный минимум остатка, затем запросите ограниченный набор строк; не загружайте каталог целиком для последующего отбора.

Три вкладки заказов: переиспользовать команду и освободить её после работы

В одном служебном отчёте нужны три небольшие выборки: новые, оплаченные и отменённые заказы. Текст SELECT одинаковый, меняется статус. Подготовим его один раз, освободим результат каждой выборки и только после цикла — саму команду. Свежий запрос покажет, что сохранённый заказ остался в таблице.

Путь данных
  1. prepare один раз → ресурс query
  2. execute(new) → строки → free_result
  3. execute(paid) → строки → free_result
  4. execute(cancelled) → пусто → free_result
  5. free_query → query=null
  6. Новый prepare → заказ №3 по-прежнему существует

Сначала один шаг

ibase_free_result($result);
$result = null;
// Если повторных execute больше не будет:
$ok = ibase_free_query($query);
if ($ok !== true) { throw new RuntimeException("Не удалось освободить команду"); }
$query = null;

Результат SELECT и подготовленная команда — два разных ресурса. free_result завершает конкретное чтение; free_query освобождает подготовленную команду после завершения всех повторных execute. В полном файле readPrepared156 читает ограниченные строки и закрывает результат в finally, даже если чтение завершилось исключением. Освобождённая команда больше не используется; для нового SQL создан другой ресурс.

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

НаблюдениеРезультат
Подготовок команды вкладок1
Выполнений команды вкладок3
Освобождено результатов вкладок3
Новые заказы1, 2
Оплаченные заказы3
Отменённых заказов0
Команда освобожденада
Параметров у нового SELECT0
Заказ после освобождения команды3
Три вкладки заказов: переиспользовать команду и освободить её после работыСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f156Config();$db=f156Connect($config);$runtime=f156Runtime($db,$config);

    function prepare156($db,string $sql) {
        $call=f156Capture(static fn()=>ibase_prepare($db,$sql));
        if (!is_resource($call['value']) || $call['warning']) {f156Failure('Команда не подготовлена',$call);}
        return $call['value'];
    }
    function parameterCount156($query): int {
        $call=f156Capture(static fn()=>ibase_num_params($query));
        if (!is_int($call['value']) || $call['warning']) {f156Failure('Число параметров не получено',$call);}
        return $call['value'];
    }
    function execute156($query,array $values) {
        // Native ibase parameters are positional: a numeric list, not named PHP arguments.
        if (!array_is_list($values)) {throw new InvalidArgumentException('Нужен упорядоченный список значений.');}
        $needed=parameterCount156($query);
        if (count($values)!==$needed) {
            throw new DomainException('Ожидалось '.$needed.' значения, получено '.count($values));
        }
        $call=f156Capture(static fn()=>ibase_execute($query,...$values));
        if ($call['value']===false || $call['warning']) {f156Failure('Подготовленная команда не выполнена',$call);}
        return $call['value'];
    }
    function readPrepared156($query,array $values): array {
        $result=execute156($query,$values);
        if (!is_resource($result)) {throw new RuntimeException('Ожидался результат SELECT.');}
        try {return f156Rows($result);}
        finally {
            $call=f156Capture(static fn()=>ibase_free_result($result));$result=null;
            if ($call['value']!==true || $call['warning']) {f156Failure('Результат не освобождён',$call);}
        }
    }
    function releaseQuery156(&$query): bool {
        $call=f156Capture(static fn()=>ibase_free_query($query));
        if ($call['value']!==true || $call['warning']) {f156Failure('Подготовленная команда не освобождена',$call);}
        $query=null;
        return true;
    }

    $name=f156OwnTable($db,$owned,'release','(order_id INTEGER NOT NULL PRIMARY KEY,status VARCHAR(16) NOT NULL,total_kopecks INTEGER NOT NULL)');
    $q=f156Qualified($name);
    foreach ([[1,'new',1500],[2,'new',2400],[3,'paid',3900]] as $order) {
        f156Command($db,"INSERT INTO $q(order_id,status,total_kopecks) VALUES(?,?,?)",$order);
    }
    f156Commit($db);

    $query=prepare156($db,"SELECT order_id AS \"id\",total_kopecks AS \"total\" FROM $q WHERE status=? ORDER BY order_id ROWS 2");
    $tabs=[];$executions=0;$resultsReleased=0;
    try {
        foreach (['new','paid','cancelled'] as $status) {
            $tabs[$status]=readPrepared156($query,[$status]);
            $executions++;$resultsReleased++; // readPrepared156 returns only after successful free_result.
        }
    } finally {if (is_resource($query)) {$released=releaseQuery156($query);}}
    // A separate prepared query has no input parameters. Zero is a valid count.
    $next=prepare156($db,"SELECT order_id AS \"id\",total_kopecks AS \"total\" FROM $q WHERE order_id=3");
    try {$zero=parameterCount156($next);$saved=readPrepared156($next,[]);}
    finally {if (is_resource($next)) {releaseQuery156($next);}}
    $rows=[['Подготовок команды вкладок',1],['Выполнений команды вкладок',$executions],['Освобождено результатов вкладок',$resultsReleased],
        ['Новые заказы',implode(', ',array_column($tabs['new'],'id'))],['Оплаченные заказы',implode(', ',array_column($tabs['paid'],'id'))],
        ['Отменённых заказов',count($tabs['cancelled'])],['Команда освобождена',$released && $query===null],
        ['Параметров у нового SELECT',$zero],['Заказ после освобождения команды',$saved[0]['id']]];
    $checks=['one_template_three_executions'=>$executions===3,'all_results_released'=>$resultsReleased===3,
        'correct_tab_rows'=>array_column($tabs['new'],'id')===[1,2] && array_column($tabs['paid'],'id')===[3] && $tabs['cancelled']===[],
        'prepared_query_released'=>$released===true && $query===null,'zero_parameters_are_valid'=>$zero===0,
        'stored_data_still_present'=>$saved===[['id'=>3,'total'=>3900]],'second_query_released'=>$next===null];
    $observations=['tabs'=>$tabs,'template_prepares'=>1,'template_executions'=>$executions,'template_result_releases'=>$resultsReleased,
        'separate_zero_parameter_query'=>['parameters'=>$zero,'selected'=>$saved],'resource_cleanup_is_not_transaction_commit'=>true];

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

f156Report('fb156-statement-release','Три вкладки заказов: переиспользовать команду и освободить её после работы',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Подготовок команды вкладок — 1
Выполнений команды вкладок — 3
Освобождено результатов вкладок — 3
Новые заказы — 1, 2
Оплаченные заказы — 3
Отменённых заказов — 0
Команда освобождена — да
Параметров у нового SELECT — 0
Заказ после освобождения команды — 3

Самостоятельный CLI-пример создаёт одну свою таблицу в отдельной Firebird, выполняет настоящий prepare/execute/SELECT, создаёт result.html и result.json и удаляет таблицу. lab.php и README.txt приложены.

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

Ошибки, ограничения и применение

Этот пример не измеряет память и не обещает устранить любую утечку драйвера. Он показывает ограниченное владение ресурсами без prepare внутри цикла. free_query не делает commit/rollback, не удаляет строки и не заменяет свободный результат. После закрытия PHP-переменная может ещё описывать закрытый ресурс, поэтому обнуляем её и не вызываем API повторно. При ошибке execute или fetch сначала сохраняйте диагностику, затем освобождайте ресурсы. Долгое чтение и транзакции требуют собственной политики завершения.

В исторической задаче массовый цикл создавал новую prepared-команду на каждом проходе и завершался сообщением о слишком большом числе открытых handles. Исходная ситуация на Stack Overflow. Наш ограниченный сценарий готовит один шаблон, закрывает каждый результат и затем команду. В обсуждении нет доказанной универсальной причины сбоя, а комментарий о порядке закрытия был отозван: здесь не обещается исправление конкретной старой утечки и не запускается искусственный многотысячный стресс-тест.

Измените условие задачи

Отчёт стал читать пять статусов. Нужно ли пять раз выполнять prepare? Когда закрывать query?

Ожидаемое решение

Если SQL одинаковый, подготовьте его один раз до цикла и выполните пять раз с разными статусами. После каждого чтения закройте его результат; query закройте в finally после всего цикла. Если меняется сам SQL, создайте отдельную подготовленную команду.

Три карточки входного контракта и ресурса

Что требуетсяКарточка
Узнать число входных значений подготовленной команды.ibase_num_params
Прочитать описание одного входного параметра по индексу.ibase_param_info
Освободить подготовленную команду после последних execute.ibase_free_query

От проверенного входа к удобному результату

Теперь ошибку импорта можно объяснить до INSERT, описание входного параметра не путается с правилами формы, диапазон цены собирается в порядке SQL, а повторяемый SELECT имеет явный срок жизни. Следующий шаг — числовой массив и объект результата, генератор идентификаторов и именованный курсор.

Для полей выходного SELECT используйте метаданные результата, для сохранения операции целиком — транзакции, для отказов — немедленную диагностику. Эта глава раскрывает три операции native interbase в выбранной версии; она не объявляет всю ветку Firebird завершённой.

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

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

ibase_num_paramsУзнать число входных значений подготовленной команды.

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

ibase_num_params(resource $query): int

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

ЭлементКонтракт
queryДействующий ресурс подготовленной команды из ibase_prepare; не результат SELECT из execute/query.
Результат intЧисло входных параметров, которые потребуются ibase_execute. 0 — допустимая команда без входных параметров.
Ошибки и версииИсторическая страница перечисляет int. В поддерживаемой реализации сначала проверяется ресурс query; неверный тип/закрытый ресурс нельзя трактовать как нулевое число параметров. Ошибка аргумента может возникнуть на уровне PHP. Проверяйте подготовку до этого вызова и не используйте освобождённую команду.

Проверка строки импорта

$needed = ibase_num_params($query);
$values = [101, "Кабель", 3];
if (count($values) !== $needed) {
    throw new DomainException("Число значений не совпало с SQL");
}
$result = ibase_execute($query, ...$values);

Контекст — INSERT товара с тремя входными значениями. Короткий фрагмент предполагает успешный prepare; полный пример дополнительно проверяет execute, сохраняет строку, подтверждает транзакцию и освобождает команду. Не считайте параметры через substr_count: литерал '?' не является местом связывания.

Отчёт без входных условий

$query = ibase_prepare($db, "SELECT order_id FROM orders WHERE order_id=3");
if ($query === false) { throw new RuntimeException("Отчёт не подготовлен"); }
echo ibase_num_params($query); // 0
$result = ibase_execute($query);
// Прочитать/освободить $result, затем освободить $query.

Ноль не означает «нет найденных заказов»: запрос ещё не выполнен. Этот случай реально показан в заключительной выборке заказа. Число колонок результата сообщает ibase_num_fields, а число прочитанных строк — отдельный вопрос, зависящий от SELECT. При динамической форме одного количества недостаточно: два правильно подсчитанных значения можно переставить.

ibase_param_infoПрочитать описание одного входного параметра по индексу.

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

ibase_param_info(resource $query, int $param_number): array; выбранный драйвер также возвращает false вне диапазона

Когда значение не подходит подготовленному запросу, нужно понять, какое входное представление ожидает сервер в этой позиции. param_info даёт описание отдельного параметра. Оно помогает диагностировать импорт, но не заменяет схему данных, смысл поля и правила формы.

Параметр / результатЗначение
queryРесурс подготовленной команды из ibase_prepare. Метаданные читаются до execute; выполняться SQL ради этого не должен.
param_numberЦелый индекс от 0 до ibase_num_params($query)-1. Для третьего места ? передайте 2.
array / falseИсторическая сигнатура PHP Manual показывает array. Поддерживаемый драйвер 3.0.1 возвращает false при отсутствии входного дескриптора и при индексе вне диапазона; это не обязательно SQL-ошибка или предупреждение. Неподходящий PHP-аргумент/недействительный ресурс обрабатывается отдельно.
name, alias, relationИмя, псевдоним и отношение из дескриптора. Они могут быть пустыми: у всех трёх параметров нашего INSERT это пустые строки. Не выводите по ним автоматические названия полей формы.
lengthДлина входного представления в байтах. В выбранном драйвере значение — строка, например "80". Это не число символов интерфейса и не обязательно точная бизнес-граница значения.
typeТекст типа представления, например INTEGER, VARCHAR, NUMERIC(18,2). Это не гарантированная копия исходного объявления DDL.
Числовые ключиДрайвер 3.0.1 также добавляет ключи 0…4 с теми же значениями. Для понятного кода используйте именованные ключи; не считайте десять элементов массива десятью независимыми свойствами.

Диагностика второго поля импорта

$info = ibase_param_info($query, 1);
if ($info === false) { throw new RuntimeException("Параметра с индексом 1 нет"); }
echo "Название: ", $info["type"], "; байт: ", $info["length"];
// Наш INSERT: Название: VARCHAR; байт: 80

Это полный пример товара с UTF-8-названием. Название поля здесь задано приложением. VARCHAR(20) допускает 20 символов, а UTF-8-представление может потребовать 80 байт. Проверка strlen($title) <= 80 не доказывает соблюдение лимита 20 символов. Источник и кодировку строки нужно знать до проверки длины.

Согласовать типы двух границ фильтра

$minimumInfo = ibase_param_info($query, 0);
$maximumInfo = ibase_param_info($query, 1);
if ($minimumInfo === false || $maximumInfo === false) {
    throw new RuntimeException("Фильтр должен иметь две границы");
}
echo $minimumInfo["type"], " / ", $maximumInfo["type"];
// Для price_kopecks INTEGER: INTEGER / INTEGER.
// Кто минимум, а кто максимум, всё равно определяет порядок SQL.

В фильтре каталога оба значения имеют одинаковый тип. Проверка метаданных может выявить неожиданную смену входного представления после изменения запроса, но не ошибку «максимум передан вместо минимума». Для готового набора выходных колонок применяется другая функция — ibase_field_info.

Для DECIMAL(10,2) наш драйвер показывает NUMERIC(18,2): он формирует текст из 64-битного хранения и масштаба. Нельзя по этому тексту восстановить написанное в CREATE TABLE ограничение или считать цену автоматически приемлемой. Если приложение ограничивает её 100000.00, задайте это явно в проверке и CHECK. Метаданные не включают все NOT NULL, CHECK, внешние ключи, правила доступа и зависимости полей. Пустые names и неполная бизнес-схема — нормальный предел этого API, а не повод угадывать правила.

Подробности хранения и точности описаны в справочнике NUMERIC/DECIMAL Firebird, отличие символов от байтов — в описании строковых типов и UTF8.

ibase_free_queryОсвободить подготовленную команду после последних execute.

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

ibase_free_query(resource $query): bool

Подготовленная команда нужна столько времени, сколько приложение собирается вызывать её execute. Когда работа закончена, free_query освобождает этот ресурс. Отдельный результат SELECT освобождается через free_result; таблица и уже сохранённые данные продолжают существовать.

ЭлементКонтракт
queryРесурс, ранее полученный из ibase_prepare. Не соединение, не транзакция и не очередной результат SELECT.
booltrue при успешном освобождении, false при отказе по документированному контракту. Ошибки типа/недействительного ресурса могут также возникать на уровне PHP. Не проверяйте отказ повторным вызовом на уже закрытом handle.
Срок жизниЗакончите чтение и освободите принадлежащие вам результаты, затем освободите команду. После успеха уберите собственные PHP-ссылки, например $query=null.
Граница данныхfree_query не делает commit/rollback, не удаляет таблицу, не отменяет ранее подтверждённые изменения. Транзакцией управляет вызывающая бизнес-операция.

Один шаблон для нескольких вкладок

$query = ibase_prepare($db, $sql);
if ($query === false) { throw new RuntimeException("Команда не подготовлена"); }
try {
    foreach (["new", "paid", "cancelled"] as $status) {
        $rows = readPrepared156($query, [$status]);
        // readPrepared156 читает и освобождает отдельный результат.
    }
} finally {
    ibase_free_query($query);
    $query = null;
}

В полной самостоятельной программе проверяется и результат освобождения. Вы увидите заказы 1 и 2 на первой вкладке, 3 на второй и пустую третью. Подготовленная команда живёт до конца цикла; строки каждого результата закрываются раньше. Это ограниченное переиспользование SQL внутри одной программы, а не сохранение PHP-ресурса между HTTP-запросами.

Однократный импорт, включая отказ проверки

$query = prepare156($db, $insertSql);
try {
    execute156($query, $values);
} finally {
    releaseQuery156($query); // Проверяет true и ставит null.
}
// Подтвердить транзакцию, только если вся бизнес-операция успешна.

Это завершение импорта товара. finally выполняется и после исключения проверки количества. После успешного INSERT освобождение команды ещё не означает сохранение операции: отдельно нужен commit или rollback. Если SELECT/execute дал ошибку, снимите код и сообщение немедленно до последующих вызовов очистки.

Не делайте prepare на каждой итерации без необходимости. Но и само число вызовов free_query не является доказательством отсутствия утечек во всех версиях драйвера. Здесь проверяется конкретная правильная цепочка: результат прочитан и закрыт, команда закрыта и обнулена, следующая команда получает сохранённый заказ. Освобождённый ресурс не используется повторно.

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

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

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

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

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