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

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

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

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

Глава 303 · После подключения FreeTDS и чтения значений

PDO_DBLIB: складская квитанция, целая накладная и завершение команды

Несколько результатов процедуры, связи таблиц и откат импорта, освобождение чтения и поздняя ошибка на реальном FreeTDS.

Разделить ответ, подтверждение и освобождение

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

От складских данных к подтверждённой операции
  1. Процедура склада 7: BELT 3 + TAPE 4 → квитанция quantity = 7
  2. Накладная IN-179: заголовок связан со строками через invoice_id
  3. Ошибка количества 0: отменить всю накладную; исправление → commit
  4. Предпросмотр A-1 закрыт: освободить результат перед следующим SELECT

PDO позволяет PHP отправить запрос и получить строки. В PDO_DBLIB запрос проходит через клиентскую библиотеку FreeTDS и протокол TDS к SQL Server. Поэтому переносимые вызовы PDO соседствуют с собственными правилами типов, ожидания и завершения результатов.

Проекты запускаются через отдельный Linux PHP с PDO_DBLIB и FreeTDS в собственной учебной базе SQL Server. Начало SQL и PDO объясняет таблицу, запрос и получение строки, а глава подключения PDO_DBLIB — устройство маршрута и подготовку собственного окружения.

Потребуются соединение PDO_DBLIB и параметры и получение нужных значений. В примерах $db — объект соединения с SQL Server, а $statement — объект выполняемой команды и её результата. Термин «курсор» здесь обозначает состояние чтения через PDOStatement; это не обязательно объявленный SQL-курсор DECLARE CURSOR.

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

Понять несколько наборов и ATTR_SKIP_EMPTY_ROWSETS

Один SELECT обычно даёт один набор: известные колонки и ноль или больше строк. Хранимая процедура может выполнить несколько SELECT и команд изменения. Поэтому после первого fetch приложение может находиться только в начале общего ответа. Наборы имеют свои схемы; их нельзя объединять в один массив без понимания, что каждый означает.

$statement = $db->query("SELECT 'BELT' AS sku; SELECT 7 AS quantity;");
$item = $statement->fetch(PDO::FETCH_ASSOC);
$statement->nextRowset();
$total = $statement->fetch(PDO::FETCH_ASSOC);
$statement->closeCursor();

После первого fetch переменная $item содержит ['sku' => 'BELT']; после перехода $total содержит ['quantity' => 7]. AS задаёт имена колонок, по которым PHP обращается к значениям. Это минимальная форма на открытом соединении. nextRowset() не получает параметров и возвращает bool: true означает переход к следующему набору, false — отсутствие следующего набора. При отказе выбранный режим обработки ошибок может дать исключение. В полном проекте переход проверяется условием цикла.

Что вернул серверКак это распознатьЧто делает приложение
Позиции складаcolumnCount() > 0 и строки с kind=itemСобирает только договорённые поля sku и stock
Команда UPDATEНет колонок для fetch; возможен счётчик измененийУчитывает отдельно от списка позиций
Дефицита нетУ SELECT есть колонки, но fetch не выдаёт строкСохраняет смысл пустого результата
Итоговая квитанцияСтрока с kind=receipt и известными полямиПроверяет нужный склад, количество и номер выгрузки

Pdo\Dblib::ATTR_SKIP_EMPTY_ROWSETS по умолчанию выключен. Включённый атрибут убирает промежуточные результаты без колонок. Он не означает «пропускать любой пустой массив»: SELECT с нулём строк всё ещё имеет схему. Первый проект вызывает одну процедуру с false и true и получает одну и ту же бизнес-структуру. На проверенном стеке обе цепочки содержат три набора с колонками; отдельного результата без колонок для UPDATE не возникло. Код проверяет схему каждого набора, а пустой SELECT сохраняет в обоих режимах.

В вопросе о разных результатах SQLSRV и DBLIB одинаковый прикладной вызов ведёт себя по-разному после смены драйвера. Здесь мы явно разделяем колонки, строки и счётчик DML. SET NOCOUNT ON часто уменьшает количество служебных счётчиков, но не отменяет необходимость понимать договор процедуры. PRINT, выходной параметр и SELECT также не являются одним видом ответа.

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

Складская процедура возвращает позиции BELT и TAPE, отмечает факт выгрузки и сообщает итог: 7 единиц. Между этими ответами находятся команда UPDATE и пустой поиск дефицита. Экран должен показать позиции и квитанцию нужного склада, даже если ответы имеют разную форму.

Путь данных и полученный результат
  1. Вход: warehouse = 7
  2. Первый SELECT: BELT 3 и TAPE 4
  3. UPDATE: отметить выгрузку; пустой SELECT: дефицита нет
  4. Последний SELECT: quantity = 7, export_number = 2

Минимальный вызов

$statement = $db->prepare('EXEC dbo.stock_receipt @warehouse = ?');
$statement->bindValue(1, 7, PDO::PARAM_INT);
$statement->execute();
// В полном проекте процедура и её таблица создаются перед вызовом.

EXEC вызывает процедуру SQL Server. @warehouse — имя её входного параметра; ? — место значения, которое PDO подставит с учётом типа. bindValue(1, 7, PDO::PARAM_INT) передаёт целое 7 первому маркеру. execute() начинает команду; он ещё не означает, что приложение прочитало последнюю квитанцию. Демонстрационная процедура имеет случайное собственное имя, поэтому запускать следует полный проект ниже.

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

НаблюдениеРезультат
Позиции склада 7BELT: 3; TAPE: 4
Всего единиц7
Номер второй выгрузки2
Три набора: позиции, пустой поиск, итогда
Схемы сохранены при обоих значениях атрибутада
Пустой SELECT сохранён в обоих режимахда
Файлreceipt.json
Самостоятельный проект, исходник и архив
Получить позиции и итог одной складской процедурыСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p179Config();$db=p179Connect($config);$runtime=p179Runtime($db);

    $table=p179OwnTable($db,$owned,'stock','(id int PRIMARY KEY, warehouse int NOT NULL, sku varchar(12) NOT NULL, stock int NOT NULL, exported int NOT NULL)');
    $q=p179Qualified($table);
    p179Ddl($db,"INSERT INTO $q VALUES (1,7,'BELT',3,0),(2,7,'TAPE',4,0),(3,8,'BOX',99,0)");
    $proc=p179OwnName($db,$owned,'PROCEDURE','stock_receipt');$qp=p179Qualified($proc);
    p179Ddl($db,"CREATE PROCEDURE $qp @warehouse int AS
      BEGIN
        SET NOCOUNT OFF;
        SELECT 'item' AS kind,sku,stock FROM $q WHERE warehouse=@warehouse ORDER BY id;
        UPDATE $q SET exported=exported+1 WHERE warehouse=@warehouse;
        SELECT 'missing' AS kind,sku FROM $q WHERE warehouse=@warehouse AND stock<0;
        SELECT 'receipt' AS kind,@warehouse AS warehouse,SUM(stock) AS quantity,MAX(exported) AS export_number
        FROM $q WHERE warehouse=@warehouse;
      END");
    $runs=[];
    foreach ([false,true] as $skip) {
        $db->setAttribute(Pdo\Dblib::ATTR_SKIP_EMPTY_ROWSETS,$skip);
        $statement=$db->prepare("EXEC $qp @warehouse=?");
        $sets=[];$items=[];$summary=null;$empty=0;$commands=0;
        try {
            $statement->bindValue(1,7,PDO::PARAM_INT);$statement->execute();
            do {
                $columns=$statement->columnCount();
                if ($columns===0) {$sets[]=['type'=>'command','affected'=>$statement->rowCount()];++$commands;continue;}
                $data=p179Rows($statement);
                if ($data===[]) {$sets[]=['type'=>'empty_rows','columns'=>$columns];++$empty;continue;}
                $kind=$data[0]['kind']??null;
                if ($kind==='item') {
                    foreach($data as $item) {$items[]=['sku'=>$item['sku'],'quantity'=>(int)$item['stock']];}
                } elseif ($kind==='receipt' && count($data)===1) {
                    $summary=['warehouse'=>(int)$data[0]['warehouse'],'quantity'=>(int)$data[0]['quantity'],
                        'export_number'=>(int)$data[0]['export_number']];
                } else {throw new RuntimeException('Незнакомая схема результата процедуры.');}
                $sets[]=['type'=>$kind,'rows'=>$data];
            } while ($statement->nextRowset());
        } finally {p179Release($statement);}
        if ($summary===null) {throw new RuntimeException('Итоговая квитанция не получена.');}
        $runs[]=['skip'=>$skip,'sets'=>$sets,'items'=>$items,'summary'=>$summary,'empty'=>$empty,'commands'=>$commands];
    }
    $db->setAttribute(Pdo\Dblib::ATTR_SKIP_EMPTY_ROWSETS,false);
    $visible=$runs[0];$filtered=$runs[1];
    $receipt=['warehouse'=>$filtered['summary']['warehouse'],'export_number'=>$filtered['summary']['export_number'],
        'quantity'=>$filtered['summary']['quantity'],'items'=>$filtered['items']];
    file_put_contents(__DIR__.'/receipt.json',json_encode($receipt,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR)."\n");
    $checks=[
        'selected_items'=>$receipt['items']===[['sku'=>'BELT','quantity'=>3],['sku'=>'TAPE','quantity'=>4]],
        'selected_total'=>$receipt['warehouse']===7 && $receipt['quantity']===7,
        'first_receipt'=>$visible['summary']['export_number']===1,
        'second_receipt'=>$receipt['export_number']===2,
        'three_typed_sets'=>count($visible['sets'])===3 && array_column($visible['sets'],'type')===['item','empty_rows','receipt'],
        'same_typed_sets'=>array_column($filtered['sets'],'type')===array_column($visible['sets'],'type'),
        'empty_select_preserved'=>$visible['empty']===1 && $filtered['empty']===1,
        'items_preserved'=>$visible['items']===$filtered['items'],
        'other_warehouse_unchanged'=>(int)p179Read($db,"SELECT exported FROM $q WHERE id=3")[0]['exported']===0];
    $rows=[['Позиции склада 7','BELT: 3; TAPE: 4'],['Всего единиц',7],['Номер второй выгрузки',2],
        ['Три набора: позиции, пустой поиск, итог',$checks['three_typed_sets']],
        ['Схемы сохранены при обоих значениях атрибута',$checks['same_typed_sets']],
        ['Пустой SELECT сохранён в обоих режимах',$checks['empty_select_preserved']],['Файл','receipt.json']];
    $observations=['runs'=>$runs,'receipt'=>$receipt];

} finally {

    if ($db instanceof PDO) {try {$removed=p179Cleanup($db,$owned);} finally {p179Close($db);}}
}

p179Report('pdo179-receipt-rowsets','Получить позиции и итог одной складской процедуры',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Позиции склада 7 — BELT: 3; TAPE: 4
Всего единиц — 7
Номер второй выгрузки — 2
Три набора: позиции, пустой поиск, итог — да
Схемы сохранены при обоих значениях атрибута — да
Пустой SELECT сохранён в обоих режимах — да
Файл — receipt.json

Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.

Открыть результат выполнения · Посмотреть данные JSON · Скачать складскую квитанцию

Границы решения

ATTR_SKIP_EMPTY_ROWSETS=true пропускает результаты без колонок. На проверенном сочетании PHP 8.5.11, FreeTDS 1.3.17 и SQL Server 2025 эта процедура даёт три набора с колонками при обоих значениях атрибута; отдельный набор UPDATE не появился. Это наблюдение конкретного стека, а не обещание одинакового поведения всех версий. SELECT с известными колонками и нулём строк остаётся отдельным набором. Не вызывайте fetch только потому, что execute вернул true: сначала определите схему текущего результата. Здесь процедура дважды увеличивает счётчик выгрузок для демонстрации двух режимов; это не безопасный автоматический повтор произвольной бизнес-команды. closeCursor отменяет оставшееся чтение, поэтому не заменяет получение обязательной квитанции.

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

Добавьте на склад 7 позицию ROPE с количеством 2. Как изменится квитанция и что останется у склада 8?

Разбор

В списке появится третья позиция, quantity станет 9. Склад 8 по-прежнему исключён условием warehouse=@warehouse; его exported останется 0. Схема последней строки остаётся прежней.

Связать заголовок и строки одной транзакцией

В реляционной схеме заголовок накладной имеет собственный id, а строки хранят invoice_id. FOREIGN KEY связывает строку с существующим заголовком. CHECK quantity > 0 запрещает нулевое и отрицательное количество. Эти ограничения проверяют допустимость отдельных изменений; транзакция связывает несколько изменений в одну прикладную операцию.

Ошибка второй строки не должна оставить первую
  1. beginTransaction: начинаем импорт IN-179
  2. INSERT заголовка + CABLE: 2
  3. CLIP: 0 → CHECK отвергает команду → catch вызывает rollBack
  4. Повторное чтение обеих таблиц: 0 заголовков и 0 строк
$db->beginTransaction();
// Здесь выполняются изменения одной задачи.
$db->rollBack();

Без изменений между этими двумя вызовами таблицы останутся прежними. beginTransaction открывает транзакцию на конкретном соединении; commit подтверждает её, rollBack отменяет незавершённые изменения. Скобки пусты, потому что аргументы этим методам не нужны. beginTransaction возвращает bool и выбрасывает PDOException при уже начатой транзакции или неподдерживаемом драйвере. Второй вызов не создаёт автоматически независимую вложенную транзакцию.

inTransaction() сообщает отслеживаемое PDO состояние. Не смешивайте его без отдельного договора с произвольными BEGIN/COMMIT, отправленными вручную из другой части приложения. В проекте обе стороны проверяются после завершения: состояние PDO и @@TRANCOUNT сервера, а затем реальные строки. Ни закрытие курсора, ни освобождение переменной PHP не должны служить вашей явной командой подтверждения записи.

ГраницаСмыслПрактическое следствие
beginTransaction → commitПодтвердить все успешные изменения этой операцииКвитанцию строить по сохранённым данным и собственному ключу
Ошибка → rollBackОтменить незавершённые измененияПроверить обе связанные таблицы, а не только текст исключения
XACT_ABORT OFF / ONПолитика SQL Server при серверных ошибках различаетсяНе считать любой неуспешный INSERT доказательством полного отката
Потеря соединения при commitОтвет мог не дойти до клиентаСверить уникальный бизнес-ключ перед повторением операции

В учебном примере XACT_ABORT OFF выбран явно, чтобы показать необходимость полного отката в catch. При ON многие ошибки исполнения завершают серверную транзакцию; THROW учитывает этот режим, RAISERROR — иначе. Это не повод пропускать обработку отказа в PHP. Подробности настроек приведены в описании SET XACT_ABORT. Схема таблиц создаётся до импорта; поведение DDL и implicit commit других СУБД нельзя автоматически переносить на SQL Server.

В следующем JOIN условие h.id=? выбирает одну накладную, ORDER BY l.line_no задаёт порядок её строк, а SELECT перечисляет только поля CSV. Приложение не загружает весь склад для последующей фильтрации большим PHP-массивом. Общая глава транзакций продолжает тему конкуренции, изоляции и повторных попыток.

Импортировать накладную и её строки целиком

Накладная IN-179 состоит из заголовка и двух строк. Если количество второй позиции равно нулю, сохранять заголовок и первую строку отдельно нельзя. После исправления нужен CSV с двумя позициями этой накладной, а не все данные обеих таблиц.

Путь данных и полученный результат
  1. beginTransaction → заголовок IN-179
  2. Строка CABLE: 2 проходит CHECK
  3. Строка CLIP: 0 нарушает CHECK → rollback: обе таблицы пусты
  4. Исправление CLIP: 5 → commit → JOIN только IN-179

Минимальный вызов

$db->beginTransaction();
try {
    $db->exec("INSERT INTO invoice (id, number) VALUES (17901, 'IN-179')");
    $db->commit();
} catch (Throwable $error) {
    if ($db->inTransaction()) { $db->rollBack(); }
    throw $error;
}

beginTransaction() открывает одну единицу работы на этом соединении. commit() подтверждает её; rollBack() отменяет незавершённые изменения. try содержит действия, catch получает ошибку, а throw передаёт её вызывающему коду после уборки. Здесь показана минимальная граница на условной таблице invoice; полный проект создаёт две свои таблицы, связывает их внешним ключом и добавляет обе строки внутри той же границы.

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

НаблюдениеРезультат
После ошибки: накладных0
После ошибки: строк0
Сохранённая накладнаяIN-179
Сохранённые позицииCABLE: 2; CLIP: 5
Файлreceipt.csv
Самостоятельный проект, исходник и архив
Импортировать накладную и её строки целикомСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p179Config();$db=p179Connect($config);$runtime=p179Runtime($db);

    p179Ddl($db,'SET XACT_ABORT OFF');
    $head=p179OwnTable($db,$owned,'invoice','(id int PRIMARY KEY, number varchar(16) NOT NULL UNIQUE)');$qh=p179Qualified($head);
    $line=p179OwnTable($db,$owned,'invoice_line',"(invoice_id int NOT NULL REFERENCES $qh(id), line_no int NOT NULL, sku varchar(12) NOT NULL, quantity int NOT NULL CHECK(quantity>0), PRIMARY KEY(invoice_id,line_no))");$ql=p179Qualified($line);
    $import=static function(PDO $connection,array $items) use($qh,$ql): void {
        p179Begin($connection);
        try {
            p179Command($connection,"INSERT INTO $qh(id,number) VALUES (?,?)",[17901,'IN-179']);
            foreach($items as $i=>$item) {
                p179Command($connection,"INSERT INTO $ql(invoice_id,line_no,sku,quantity) VALUES (?,?,?,?)",[17901,$i+1,$item['sku'],$item['quantity']]);
            }
            p179Commit($connection);
        } catch(Throwable $error) {
            p179Rollback($connection);
            throw $error;
        }
    };
    $failure=p179Capture(static fn()=>$import($db,[['sku'=>'CABLE','quantity'=>2],['sku'=>'CLIP','quantity'=>0]]));
    $afterFailure=['invoices'=>(int)p179Read($db,"SELECT COUNT(*) AS n FROM $qh")[0]['n'],
        'lines'=>(int)p179Read($db,"SELECT COUNT(*) AS n FROM $ql")[0]['n']];
    $afterFailure['pdo_transaction']=$db->inTransaction();
    $afterFailure['server_transactions']=(int)p179Read($db,'SELECT @@TRANCOUNT AS n')[0]['n'];
    $import($db,[['sku'=>'CABLE','quantity'=>2],['sku'=>'CLIP','quantity'=>5]]);
    $saved=p179Read($db,"SELECT h.number AS invoice,l.line_no,l.sku,l.quantity FROM $qh AS h JOIN $ql AS l ON l.invoice_id=h.id WHERE h.id=? ORDER BY l.line_no",[17901]);
    $out=fopen(__DIR__.'/receipt.csv','wb');
    try {fputcsv($out,['invoice','line','sku','quantity'],';', '"','',"\n");
        foreach($saved as $r) {fputcsv($out,[$r['invoice'],$r['line_no'],$r['sku'],$r['quantity']],';','"','',"\n");}}
    finally {fclose($out);}
    $checks=['check_error_caught'=>$failure['failed'],
        'invoice_rolled_back'=>$afterFailure['invoices']===0,'lines_rolled_back'=>$afterFailure['lines']===0,
        'pdo_transaction_finished'=>$afterFailure['pdo_transaction']===false,
        'server_transaction_finished'=>$afterFailure['server_transactions']===0,
        'receipt_has_two_lines'=>count($saved)===2,
        'first_line'=>$saved[0]['sku']==='CABLE' && (int)$saved[0]['quantity']===2,
        'second_line'=>$saved[1]['sku']==='CLIP' && (int)$saved[1]['quantity']===5,
        'same_invoice'=>$saved[0]['invoice']==='IN-179' && $saved[1]['invoice']==='IN-179',
        'committed'=>$db->inTransaction()===false && (int)p179Read($db,'SELECT @@TRANCOUNT AS n')[0]['n']===0];
    $rows=[['После ошибки: накладных',$afterFailure['invoices']],['После ошибки: строк',$afterFailure['lines']],
        ['Сохранённая накладная','IN-179'],['Сохранённые позиции','CABLE: 2; CLIP: 5'],['Файл','receipt.csv']];
    $observations=['failure'=>$failure,'after_failure'=>$afterFailure,'saved'=>$saved];

} finally {

    if ($db instanceof PDO) {try {$removed=p179Cleanup($db,$owned);} finally {p179Close($db);}}
}

p179Report('pdo179-import-transaction','Импортировать накладную и её строки целиком',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
После ошибки: накладных — 0
После ошибки: строк — 0
Сохранённая накладная — IN-179
Сохранённые позиции — CABLE: 2; CLIP: 5
Файл — receipt.csv

Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.

Открыть результат выполнения · Посмотреть данные JSON · Скачать накладную CSV

Границы решения

В учебном процессе явно установлен XACT_ABORT OFF: ошибка отдельной команды не должна приниматься приложением за обещание полного отката. catch вызывает rollback, а повторный SELECT подтверждает состояние обеих таблиц. XACT_ABORT ON меняет поведение серверных ошибок; это отдельная настройка, а не замена обработке исключений. Транзакция не отправляет письма и не откатывает HTTP-запросы. При потере соединения во время commit нельзя бездумно повторять запись: нужен уникальный бизнес-ключ и проверка результата после восстановления.

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

Замените количество первой позиции на −1. Можно ли сохранить корректную вторую строку без заголовка?

Разбор

CHECK отвергнет первую строку, catch отменит заголовок. Обе таблицы останутся пустыми; внешний ключ также запрещает отдельную строку без родительской накладной. После исправления выполняется вся единица работы.

Закончить чтение и не потерять позднюю ошибку

Ситуация «я получил первую строку» отличается от «я получил весь нужный ответ». Если экрану больше ничего не нужно, он освобождает результат. Если процедуре положено вернуть итог, нужно пройти до этого итога и проверить его. Выбор зависит от задачи, а не от наличия хотя бы одного успешного fetch.

try {
    $first = $statement->fetch(PDO::FETCH_ASSOC);
    // Используем нужную первую строку.
} finally {
    $statement->closeCursor();
}

finally выполняет освобождение при обычном завершении и при исключении внутри try. Он не гарантирует работу после аварийного прекращения процесса. closeCursor возвращает bool; у PDO_DBLIB он вызывает dbcancel для оставшихся результатов FreeTDS. Общий алгоритм PDO, который дочитывает наборы, не следует принимать за точное описание каждого драйвера. Закрытие результата не равно rollback и не удаляет строки из таблицы.

Исторический случай с последующими вызовами процедур показывает практическую потребность: первый вызов вернул данные, а следующая команда столкнулась с непрочитанным ответом. Решение в наших примерах — закончить необходимое чтение либо отменить ненужный остаток, а не собирать случайные советы для другой версии PHP.

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

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

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

Пользователь открыл предпросмотр очереди, получил A-1 и закрыл его. Остальные строки этому экрану больше не нужны, но соединение понадобится следующему запросу. Отдельная часть примера показывает команду, которая сначала возвращает строку, а затем сообщает ошибку.

Путь данных и полученный результат
  1. SELECT TOP (3): A-1, A-2, A-3
  2. Экран прочитал A-1 → closeCursor отменяет ненужный остаток
  3. Новый SELECT видит 3 заявки
  4. Другой пакет: первая строка → ошибка → закрытие → следующий запрос A-3

Минимальный вызов

$statement = $db->query('SELECT TOP (3) id, code FROM queue ORDER BY id');
$first = $statement->fetch(PDO::FETCH_ASSOC);
$statement->closeCursor();
// После закрытия результата можно выполнить следующий запрос.

query() начинает чтение, fetch() получает одну строку, closeCursor() завершает использование оставшегося результата. Сама таблица queue не закрывается и её строки не удаляются. В PDO_DBLIB закрытие вызывает отмену pending results через FreeTDS; это не commit. Полный проект создаёт собственную очередь, проверяет новое чтение и затем разбирает ошибку на поздней стадии пакета.

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

НаблюдениеРезультат
Прочитана первая заявкаA-1
После closeCursor доступно заявок3
Поздняя ошибка замеченада
Следующий запрос вернулA-3
Файлrecovery.json
Самостоятельный проект, исходник и архив
Закончить ненужное чтение и распознать позднюю ошибкуСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p179Config();$db=p179Connect($config);$runtime=p179Runtime($db);

    $table=p179OwnTable($db,$owned,'queue','(id int PRIMARY KEY, code varchar(12) NOT NULL)');$q=p179Qualified($table);
    p179Ddl($db,"INSERT INTO $q VALUES (1,'A-1'),(2,'A-2'),(3,'A-3')");
    $statement=p179Request($db,"SELECT TOP (3) id,code FROM $q ORDER BY id");
    try {$first=$statement->fetch(PDO::FETCH_ASSOC);}
    finally {p179Release($statement);}
    $afterCancel=(int)p179Read($db,"SELECT COUNT(*) AS n FROM $q")[0]['n'];
    $late=null;$firstSet=[];
    try {
        $late=$db->query("SELECT 'before-error' AS phase; THROW 51079, N'Учебная ошибка DBLIB179', 1;");
        $firstSet=p179Rows($late);
        $error=p179Capture(static function() use($late):void {while($late->nextRowset()) {if($late->columnCount()>0) {p179Rows($late);}}});
    } finally {p179Release($late);}
    $last=p179Read($db,"SELECT code FROM $q WHERE id=?",[3])[0]['code'];
    $recovery=['first'=>$first['code'],'after_cancel'=>$afterCancel,'late_error'=>$error['failed'],'after_error'=>$last];
    file_put_contents(__DIR__.'/recovery.json',json_encode($recovery,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR)."\n");
    $checks=['first_row'=>$first['code']==='A-1','cancel_releases_statement'=>$statement===null,
        'read_after_cancel'=>$afterCancel===3,'early_result_received'=>$firstSet===[['phase'=>'before-error']],
        'late_error_caught'=>$error['failed'],'late_statement_released'=>$late===null,
        'read_after_error'=>$last==='A-3','no_transaction_left'=>$db->inTransaction()===false];
    $rows=[['Прочитана первая заявка','A-1'],['После closeCursor доступно заявок',3],
        ['Поздняя ошибка замечена',$error['failed']],['Следующий запрос вернул','A-3'],['Файл','recovery.json']];
    $observations=['first_row'=>$first,'early_result'=>$firstSet,'late_error'=>$error,'recovery'=>$recovery];

} finally {

    if ($db instanceof PDO) {try {$removed=p179Cleanup($db,$owned);} finally {p179Close($db);}}
}

p179Report('pdo179-pending-recovery','Закончить ненужное чтение и распознать позднюю ошибку',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Прочитана первая заявка — A-1
После closeCursor доступно заявок — 3
Поздняя ошибка замечена — да
Следующий запрос вернул — A-3
Файл — recovery.json

Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.

Открыть результат выполнения · Посмотреть данные JSON · Скачать отчёт восстановления

Границы решения

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

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

Экрану требуется итоговая квитанция процедуры из первого проекта. Можно ли закрыть результат сразу после первой позиции?

Разбор

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

Применить договор операции в своём сервисе

Теперь складская квитанция собирается из нужных наборов, накладная сохраняется целиком, а ненужный результат освобождает соединение. Соедините эти действия со стабильными типами экспортируемых значений и явной настройкой FreeTDS. Если следующая задача использует другой сервер или PHP-драйвер, перепроверьте его договоры типов и завершения, сохранив сам прикладной принцип: выбирать нужные данные, получать подтверждённый результат и учитывать ошибки.

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

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

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

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

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