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

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

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

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

Глава 53 · После подключения SQLSRV, параметризованного запроса и простого цикла

SQLSRV: строки, поля, курсоры и несколько результатов команды

Последовательное чтение без потери первой строки, объекты с псевдонимами, небольшой STATIC-курсор и полная квитанция batch с диагностикой отложенной ошибки.

Что именно движется: строка или результат команды

Каталог должен показать каждый выбранный товар ровно один раз; накладная — понятные поля; составная SQL-команда — итоговую квитанцию и возможную ошибку. Эти задачи требуют различать три уровня: ресурс команды, текущий результат и строку внутри него. Указатель может двигаться по строкам, а next_result — между целыми результатами.

Два разных движения
  1. query → statement с уже активным первым результатом
  2. fetch → строка; get_field → поле этой строки
  3. fetch снова → следующая строка; null → конец строк
  4. next_result → следующий набор или счётчик; null → конец всей команды

Сначала нужны подключение и диагностика SQLSRV и параметризованный SELECT и типы результата. $db — открытое соединение, $stmt — ресурс выполненной команды, $q — проверенное имя собственной учебной таблицы. Цикл, массив и аргументы функции разбираются отдельно; дополнительные функции helper в архивах отвечают за подключение, отчёт и уборку.

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

$stmt = sqlsrv_query($db, "SELECT 3 AS quantity");
if ($stmt === false) {
    $errors = sqlsrv_errors();
} else {
    $state = sqlsrv_fetch($stmt);
    if ($state === true) {
        $quantity = sqlsrv_get_field($stmt, 0); // 3.
    }
    sqlsrv_free_stmt($stmt);
}

query выполняет SQL, но не выдаёт само число. fetch выбирает строку, get_field читает её первый столбец с индексом 0. Во второй раз fetch вернул бы null: единственная строка закончилась. Ни statement, ни true сами по себе не являются quantity. Самостоятельные программы ниже дополняют короткую схему полной диагностикой и видимым HTML/JSON-результатом.

Запрашивайте только нужные поля и строки, задавайте порядок в SQL. SELECT * с выгрузкой всей таблицы и последующей сортировкой большого PHP-массива передаёт лишнее по сети и занимает память. Для страницы выбирайте SQL-пагинацию; для общего числа — отдельный COUNT(*) по фильтру. Короткий STATIC-пример ниже нужен для изучения курсора, а не как способ показать огромный каталог.

Прочитать каталог по полям и сохранить первую строку

Каталогу нужны названия и остатки активных товаров. В базе есть и архивный товар; WHERE убирает его на сервере, ORDER BY определяет порядок. На экране ожидается «Крепёж:0 | Кабель:7». Ноль — допустимый остаток, NULL примечания означает отсутствие значения. Программа показывает их без потери первой строки.

Путь данных программы
  1. SQL выбирает title,qty,note только у active=1, по id
  2. has_rows → true; первая строка ещё не прочитана
  3. fetch → строка1; get_field(0,1,2) → Крепёж,0,NULL
  4. fetch → строка2; поля → Кабель,7,Полка 2
  5. fetch → null: закончить; поиск id999 → 0 строк и всё ещё 3 столбца

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

$state = sqlsrv_fetch($stmt);
if ($state === true) {
    $title = sqlsrv_get_field($stmt, 0);
    $qty = sqlsrv_get_field($stmt, 1);
    // Для первой строки: title="Крепёж", qty=0.
}

Указатель результата похож на закладку между строками: fetch передвигает её, get_field читает выбранное поле. Это модель чтения, не обещание, что вся таблица уже находится в памяти PHP. Индексы 0 и 1 означают первый и второй столбцы SELECT. Полный цикл отдельно обрабатывает null и false; после успешного fetch читает поля по возрастанию индекса. Массив items собирается только из двух нужных строк для учебного HTML/JSON.

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

НаблюдениеРезультат
Каталог на экранеКрепёж:0 | Кабель:7
Наличие строк до чтенияда
Столбцов результата3
Первый товар не пропущенда
NULL примечания сохранёнда
Пустой поиск без ошибкида
Столбцов пустого поиска3
Прочитать каталог по полям и сохранить первую строкуСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s150Runtime();
try {

    $config=s150Config();$db=s150Connect($config);$runtime=s150Runtime($db,$config);

    $name=s150OwnTable($db,$owned,'fields','(id int NOT NULL PRIMARY KEY, title nvarchar(40) NOT NULL, qty int NOT NULL, note nvarchar(40) NULL, active bit NOT NULL)');
    $q=s150Qualified($name);
    s150Command($db,"INSERT INTO $q (id,title,qty,note,active) VALUES (1,N'Крепёж',0,NULL,1),(2,N'Архивный товар',9,N'Архив',0),(3,N'Кабель',7,N'Полка 2',1)");
    $stmt=s150Query($db,"SELECT title,qty,note FROM $q WHERE active = ? ORDER BY id",[1]);
    try {
        $hasRows=sqlsrv_has_rows($stmt);
        if ($hasRows===false && sqlsrv_errors(SQLSRV_ERR_ERRORS)!==null) {s150Failure('Наличие строк каталога');}
        $fields=sqlsrv_num_fields($stmt);
        if ($fields===false) {s150Failure('Количество полей каталога');}
        $items=[];
        while (true) {
            $state=sqlsrv_fetch($stmt);
            if ($state===null) {break;}
            if ($state===false) {s150Failure('Чтение строки каталога');}
            // Read columns in increasing index order; SQL NULL is a valid field.
            $title=sqlsrv_get_field($stmt,0);
            if ($title===false) {s150Failure('Название');}
            $qty=sqlsrv_get_field($stmt,1);
            if ($qty===false) {s150Failure('Количество');}
            $note=sqlsrv_get_field($stmt,2);
            if ($note===false) {s150Failure('Примечание');}
            $items[]=['title'=>$title,'qty'=>$qty,'note'=>$note];
        }
    } finally {sqlsrv_free_stmt($stmt);}
    $empty=s150Query($db,"SELECT title,qty,note FROM $q WHERE id = ?",[999]);
    try {
        $emptyHasRows=sqlsrv_has_rows($empty);
        $emptyErrors=$emptyHasRows===false ? sqlsrv_errors(SQLSRV_ERR_ERRORS) : null;
        $emptyFields=sqlsrv_num_fields($empty);
        $emptyFetch=sqlsrv_fetch($empty);
    } finally {sqlsrv_free_stmt($empty);}
    $summary=implode(' | ',array_map(fn($item)=>$item['title'].':'.$item['qty'],$items));
    $rows=[['Каталог на экране',$summary],['Наличие строк до чтения',$hasRows],['Столбцов результата',$fields],
        ['Первый товар не пропущен',$items[0]['title']==='Крепёж'],['NULL примечания сохранён',$items[0]['note']===null],
        ['Пустой поиск без ошибки',$emptyHasRows===false&&$emptyErrors===null&&$emptyFetch===null],['Столбцов пустого поиска',$emptyFields]];
    $checks=['ordered_filtered_rows'=>$summary==='Крепёж:0 | Кабель:7','has_rows_before_fetch'=>$hasRows===true,
        'field_count'=>$fields===3,'first_row_preserved'=>$items[0]['title']==='Крепёж','zero_is_value'=>$items[0]['qty']===0,
        'sql_null_is_value'=>$items[0]['note']===null,'clean_end'=>$state===null,
        'empty_not_error'=>$emptyHasRows===false&&$emptyErrors===null&&$emptyFetch===null,'empty_keeps_columns'=>$emptyFields===3];
    $observations=['selected_items'=>$items,'empty_errors'=>$emptyErrors,'fetch_end'=>$state];

} finally {
    if (is_resource($db)) {try {$removed=s150Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}

s150Report('sqlsrv150-forward-fields','Прочитать каталог по полям и сохранить первую строку',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Каталог на экране — Крепёж:0 | Кабель:7
Наличие строк до чтения — да
Столбцов результата — 3
Первый товар не пропущен — да
NULL примечания сохранён — да
Пустой поиск без ошибки — да
Столбцов пустого поиска — 3

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

Таблица фактического запуска · Данные результата JSON.

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

has_rows=false может означать ошибку; сразу сохраните диагностику. Значение поля null не завершает цикл — завершение относится к состоянию fetch. Не ставьте предварительный fetch в if перед while: так первая строка будет прочитана дважды по управлению потоком и один раз потеряна для вывода. Этот небольшой результат можно сохранить в массив; большие выгрузки обрабатывают последовательно, не накапливая весь каталог.

В списке пропала первая запись: разработчик вызвал fetch для проверки, затем начал цикл со следующего fetch. Разбор исходной ситуации на Stack Overflow. Первый вызов уже потребил строку. Здесь наличие проверяется has_rows, а единственный цикл обрабатывает каждую полученную строку.

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

Сделайте active=1 у архивного товара и задайте ему qty=0. Предскажите строку вывода и количество полей до запуска.

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

Появится третья позиция между Крепежом и Кабелем по id. Количество полей останется 3: новая строка не добавляет столбцов. Нулевой остаток должен выводиться, а не исчезнуть как ложное значение.

Получить объект накладной с понятными именами свойств

В представлении накладной удобно обращаться к invoiceId и quantity по именам. Сначала получим простой объект stdClass, затем ту же строку в собственном классе, который собирает подпись «Накладная #7: Кабель × 3». Объект здесь хранит прочитанные данные; само создание объекта не сохраняет изменения обратно в SQL Server.

Путь данных программы
  1. SQL id=7,title=Кабель,qty=3
  2. AS invoiceId и AS quantity задают имена свойств
  3. fetch_object без класса → stdClass
  4. fetch_object с InvoiceView150 → заполнение объявленных свойств → конструктор с аргументом Накладная
  5. caption → Накладная #7: Кабель × 3; следующий fetch простого результата → null

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

$item = sqlsrv_fetch_object($stmt);
if ($item !== null && $item !== false) {
    echo $item->invoiceId, ": ", $item->quantity;
}
// Для строки примера: 7: 3.

Объект — значение со свойствами; -> читает свойство, а AS в SQL задаёт его имя. class объявляет форму собственного объекта, public делает свойство доступным представлению, __construct вызывается при создании. ::class получает имя PHP-класса. Массив ["Накладная"] передаёт один аргумент конструктору. В проверенном SQLSRV поля заполняются до конструктора: ctorSawId фиксирует уже полученный 7. В полном примере все свойства объявлены явно и не имеют PHP-типов, чтобы изучать отображение данных отдельно от правил типизированной модели.

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

НаблюдениеРезультат
Класс простого результатаstdClass
Псевдоним invoiceId7
Количество3
Подпись моделиНакладная #7: Кабель × 3
ID, видимый конструктору7
Конец после единственной строкида
Получить объект накладной с понятными именами свойствСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s150Runtime();
try {

    $config=s150Config();$db=s150Connect($config);$runtime=s150Runtime($db,$config);

    class InvoiceView150 {
        public $invoiceId;
        public $title;
        public $quantity;
        public $ctorSawId;
        public $caption;
        public function __construct(string $prefix) {
            $this->ctorSawId=$this->invoiceId;
            $this->caption=$prefix.' #'.$this->invoiceId.': '.$this->title.' × '.$this->quantity;
        }
    }
    $name=s150OwnTable($db,$owned,'objects','(id int NOT NULL PRIMARY KEY, title nvarchar(40) NOT NULL, qty int NOT NULL)');
    $q=s150Qualified($name);
    s150Command($db,"INSERT INTO $q (id,title,qty) VALUES (7,N'Кабель',3)");
    $sql="SELECT id AS invoiceId,title,qty AS quantity FROM $q WHERE id = ?";
    $plain=s150Query($db,$sql,[7]);
    try {
        $item=sqlsrv_fetch_object($plain);
        if ($item===false) {s150Failure('Объект накладной');}
        if ($item===null) {throw new RuntimeException('Учебная накладная не найдена.');}
        $plainEnd=sqlsrv_fetch_object($plain);
        if ($plainEnd===false) {s150Failure('Завершение накладной');}
    } finally {sqlsrv_free_stmt($plain);}
    $custom=s150Query($db,$sql,[7]);
    try {
        $view=sqlsrv_fetch_object($custom,InvoiceView150::class,['Накладная']);
        if ($view===false) {s150Failure('Модель представления');}
        if (!$view instanceof InvoiceView150) {throw new RuntimeException('Ожидался объект представления.');}
    } finally {sqlsrv_free_stmt($custom);}
    $rows=[['Класс простого результата',get_class($item)],['Псевдоним invoiceId',$item->invoiceId],['Количество',$item->quantity],
        ['Подпись модели',$view->caption],['ID, видимый конструктору',$view->ctorSawId],['Конец после единственной строки',$plainEnd===null]];
    $checks=['stdclass_result'=>$item instanceof stdClass,'aliased_id'=>$item->invoiceId===7,
        'quantity_value'=>$item->quantity===3,'declared_view'=>$view instanceof InvoiceView150,
        'hydrated_before_constructor'=>$view->ctorSawId===7,'caption_value'=>$view->caption==='Накладная #7: Кабель × 3',
        'clean_object_end'=>$plainEnd===null,'declared_fields'=>get_object_vars($item)===['invoiceId'=>7,'title'=>'Кабель','quantity'=>3]];
    $observations=['plain'=>get_object_vars($item),'custom'=>get_object_vars($view),'constructor_args'=>['Накладная']];

} finally {
    if (is_resource($db)) {try {$removed=s150Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}

s150Report('sqlsrv150-object-row','Получить объект накладной с понятными именами свойств',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Класс простого результата — stdClass
Псевдоним invoiceId — 7
Количество — 3
Подпись модели — Накладная #7: Кабель × 3
ID, видимый конструктору — 7
Конец после единственной строки — да

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

Таблица фактического запуска · Данные результата JSON.

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

Это не ORM: изменение $item->quantity не выполняет UPDATE. Конструктор может перезаписать прочитанное свойство; не используйте его для безусловного сброса полей. Драйверное заполнение свойств не гарантирует бизнес-проверки. Имена полей чувствительны к регистру, для JOIN нужны уникальные AS. Не полагайтесь на старое описание «типы свойств игнорируются» как на разрешение несовместимых типов в современном PHP. Для защищённой предметной модели сначала прочитайте массив, проверьте значения и вызовите её обычный конструктор явно.

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

Поменяйте SQL-псевдоним quantity на itemCount. Перед запуском согласуйте объявление свойства и обращение в конструкторе.

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

После согласованной замены подпись останется той же. Одного переименования в SELECT недостаточно: прежнее quantity перестанет получать данные. Тест полей результата тоже должен отражать новый контракт.

Посчитать короткий список и перейти к выбранной позиции

Оператор сверяет три выбранных артикула и переходит между первым, последним и предыдущим. Курсор — способ перемещения по результату запроса. Обычный FORWARD читает только вперёд и не предоставляет num_rows; STATIC позволяет прокрутку и счётчик. Программа сравнивает оба режима на одном коротком, ограниченном SQL-результате.

Путь данных программы
  1. SQL TOP(3) ORDER BY id → SKU-A,SKU-B,SKU-C
  2. FORWARD: num_rows=false, но строки читаются
  3. STATIC: num_rows=3
  4. FIRST→A, LAST→C, PRIOR→B, RELATIVE(-1)→A, ABSOLUTE(1)→B
  5. ABSOLUTE(9)→null: такой позиции нет

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

$stmt = sqlsrv_query($db, "SELECT TOP (3) sku FROM $q ORDER BY id", [],
    ["Scrollable" => SQLSRV_CURSOR_STATIC]);
// Проверить stmt !== false.
$count = sqlsrv_num_rows($stmt); // 3.
$state = sqlsrv_fetch($stmt, SQLSRV_SCROLL_ABSOLUTE, 1);
// При true get_field(0) вернёт SKU-B.

Scrollable — настройка команды, а не ещё один SELECT. TOP (3) ограничивает строки на сервере; ORDER BY делает позиции определёнными. ABSOLUTE(1) означает вторую строку, независимо от её id=20. В полном коде moves — небольшой список указаний для foreach: название, направление, смещение. Он позволяет увидеть несколько переходов без повторения одинаковых проверок. Каждый переход проверяется до чтения поля.

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

НаблюдениеРезультат
FORWARD: счётчик недоступенда
FORWARD: прочитанные товарыSKU-A, SKU-B, SKU-C
STATIC: число строк3
Переходы по результатуFIRST=SKU-A | LAST=SKU-C | PRIOR=SKU-B | RELATIVE(-1)=SKU-A | ABSOLUTE(1)=SKU-B
Позиция 9 отсутствуетда
Посчитать короткий список и перейти к выбранной позицииСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s150Runtime();
try {

    $config=s150Config();$db=s150Connect($config);$runtime=s150Runtime($db,$config);

    $name=s150OwnTable($db,$owned,'cursor','(id int NOT NULL PRIMARY KEY, sku nvarchar(20) NOT NULL)');
    $q=s150Qualified($name);
    s150Command($db,"INSERT INTO $q (id,sku) VALUES (10,N'SKU-A'),(20,N'SKU-B'),(30,N'SKU-C')");
    $sql="SELECT TOP (3) sku FROM $q ORDER BY id";
    $forward=s150Query($db,$sql);
    try {
        $forwardCount=sqlsrv_num_rows($forward);
        $forwardErrors=$forwardCount===false ? sqlsrv_errors(SQLSRV_ERR_ERRORS) : null;
        $forwardRows=s150Rows($forward);
    } finally {sqlsrv_free_stmt($forward);}
    $stmt=s150Query($db,$sql,[],['Scrollable'=>SQLSRV_CURSOR_STATIC]);
    try {
        $count=sqlsrv_num_rows($stmt);
        if ($count===false) {s150Failure('Счётчик STATIC');}
        $moves=[['FIRST',SQLSRV_SCROLL_FIRST,0],['LAST',SQLSRV_SCROLL_LAST,0],
            ['PRIOR',SQLSRV_SCROLL_PRIOR,0],['RELATIVE(-1)',SQLSRV_SCROLL_RELATIVE,-1],['ABSOLUTE(1)',SQLSRV_SCROLL_ABSOLUTE,1]];
        $positions=[];
        foreach ($moves as [$label,$direction,$offset]) {
            $state=sqlsrv_fetch($stmt,$direction,$offset);
            if ($state===false) {s150Failure('Переход '.$label);}
            if ($state===null) {throw new RuntimeException('В учебном списке ожидается строка '.$label);}
            $sku=sqlsrv_get_field($stmt,0,SQLSRV_PHPTYPE_STRING('UTF-8'));
            if ($sku===false) {s150Failure('Чтение SKU');}
            $positions[$label]=$sku;
        }
        $outside=sqlsrv_fetch($stmt,SQLSRV_SCROLL_ABSOLUTE,9);
        if ($outside===false) {s150Failure('Позиция за границей результата');}
    } finally {sqlsrv_free_stmt($stmt);}
    $parts=[];foreach ($positions as $label=>$sku) {$parts[]=$label.'='.$sku;}
    $summary=implode(' | ',$parts);
    $rows=[['FORWARD: счётчик недоступен',$forwardCount===false],['FORWARD: прочитанные товары',implode(', ',array_column($forwardRows,'sku'))],
        ['STATIC: число строк',$count],['Переходы по результату',$summary],['Позиция 9 отсутствует',$outside===null]];
    $checks=['forward_count_unsupported'=>$forwardCount===false,'forward_diagnostic'=>is_array($forwardErrors)&&count($forwardErrors)>0,
        'forward_still_readable'=>array_column($forwardRows,'sku')===['SKU-A','SKU-B','SKU-C'],'static_count'=>$count===3,
        'position_contract'=>$positions===['FIRST'=>'SKU-A','LAST'=>'SKU-C','PRIOR'=>'SKU-B','RELATIVE(-1)'=>'SKU-A','ABSOLUTE(1)'=>'SKU-B'],
        'outside_is_null'=>$outside===null];
    $observations=['forward_count'=>$forwardCount,'forward_errors'=>s150Diagnostics($forwardErrors),'positions'=>$positions,'outside'=>$outside];

} finally {
    if (is_resource($db)) {try {$removed=s150Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}

s150Report('sqlsrv150-static-cursor','Посчитать короткий список и перейти к выбранной позиции',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
FORWARD: счётчик недоступен — да
FORWARD: прочитанные товары — SKU-A, SKU-B, SKU-C
STATIC: число строк — 3
Переходы по результату — FIRST=SKU-A | LAST=SKU-C | PRIOR=SKU-B | RELATIVE(-1)=SKU-A | ABSOLUTE(1)=SKU-B
Позиция 9 отсутствует — да

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

Таблица фактического запуска · Данные результата JSON.

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

STATIC предоставляет снимок результата и имеет стоимость подготовки; CLIENT_BUFFERED дополнительно хранит выбранные данные у клиента. Для веб-страниц обычно нужен параметризованный SQL с WHERE, ORDER BY, OFFSET/FETCH или keyset-пагинацией, а не прокручиваемая копия всей таблицы в PHP. COUNT(*) по фильтру считает на сервере без передачи каждой записи. Позиции курсора не заменяют устойчивый идентификатор строки. После ошибочного num_rows сохранена диагностика, а затем на том же FORWARD явно проверено успешное чтение.

SELECT нашёл запись, но проверка через num_rows создала впечатление, что результата нет. Разбор исходной ситуации на Stack Overflow. Отличить false из-за FORWARD-курсора от нуля; выбрать подходящий курсор либо читать последовательный результат. Учебная задача использует каталог и параметризованный запрос, а не переносит небезопасный пример авторизации из вопроса.

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

Измените ORDER BY id на ORDER BY id DESC. Предскажите FIRST, LAST и ABSOLUTE(1).

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

FIRST станет SKU-C, LAST — SKU-A, ABSOLUTE(1) останется SKU-B для этих трёх строк. Число строк останется 3. При изменении самих фильтров средняя строка тоже может стать другой.

Прочитать квитанцию batch и заметить ошибку после первого SELECT

После изменения остатка приложение получает квитанцию. Одна команда SQL может вернуть несколько результатов: число изменённых строк, пустой табличный набор и заполненный набор. Нужно добраться до qty=5, сохранив смысл каждого шага. Затем отдельно воспроизведём ошибку после успешного SELECT: первые данные уже пришли, но вся операция не завершилась успешно.

Путь данных программы
  1. Начало: id1,qty2 → UPDATE qty+3
  2. Активный результат0: 0 полей, affected=1
  3. next_result=true → результат1: 2 поля, пусто
  4. next_result=true → результат2: id1,qty5; затем null
  5. Отдельная команда: SELECT qty5 → RAISERROR → next_result=false и код50000

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

// Уже активный результат сначала обработан.
$next = sqlsrv_next_result($stmt);
if ($next === true) {
    // Есть следующий результат: проверить поля и прочитать.
} elseif ($next === false) {
    $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
} else {
    // null — всё прочитано.
}

Batch — несколько SQL-операторов в одной команде; это не обязательно транзакция. Набор строк — таблица результата SELECT, а счётчик UPDATE — отдельное сообщение без столбцов. num_fields помогает отличить их. Пустой SELECT имеет два столбца, но ни одной строки. next_result не читает строку, а меняет активный результат. В полном цикле сначала обрабатывается первый уже активный результат; только потом происходит переход. === не позволяет спутать false с null.

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

НаблюдениеРезультат
Последовательность столбцов batch0, 2, 2
UPDATE затронул строк1
Строк в промежуточном SELECT0
Количество в квитанции5
NOCOUNT: сразу набор с двумя полямида
До отложенной ошибки прочитано5
Ошибка следующего результата обнаруженада
Настройка NOCOUNT восстановленада
Прочитать квитанцию batch и заметить ошибку после первого SELECTСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s150Runtime();
try {

    $config=s150Config();$db=s150Connect($config);$runtime=s150Runtime($db,$config);

    $name=s150OwnTable($db,$owned,'batch','(id int NOT NULL PRIMARY KEY, qty int NOT NULL)');
    $q=s150Qualified($name);
    s150Command($db,"INSERT INTO $q (id,qty) VALUES (1,2)");
    $originalNoCount=(s150Read($db,'SELECT CASE WHEN (@@OPTIONS & 512) = 512 THEN 1 ELSE 0 END AS enabled')[0]['enabled']===1);
    try {
        s150Command($db,'SET NOCOUNT OFF');
        $stmt=s150Query($db,"UPDATE $q SET qty = qty + 3 WHERE id = 1; SELECT id,qty FROM $q WHERE id = 999; SELECT id,qty FROM $q WHERE id = 1;");
        try {
            $results=[];$transitions=[];
            while (true) {
                // The first result is already active: do not advance before processing.
                $fields=sqlsrv_num_fields($stmt);
                if ($fields===false) {s150Failure('Описание результата batch');}
                if ($fields===0) {
                    $affected=sqlsrv_rows_affected($stmt);
                    if ($affected===false) {s150Failure('Счётчик batch');}
                    $results[]=['fields'=>0,'affected'=>$affected,'rows'=>[]];
                } else {
                    $hasRows=sqlsrv_has_rows($stmt);
                    $errors=$hasRows===false ? sqlsrv_errors(SQLSRV_ERR_ERRORS) : null;
                    if ($errors!==null) {throw new RuntimeException('Ошибка проверки набора batch.');}
                    $results[]=['fields'=>$fields,'has_rows'=>$hasRows,'rows'=>s150Rows($stmt)];
                }
                $next=sqlsrv_next_result($stmt);
                if ($next===false) {s150Failure('Следующий результат batch');}
                $transitions[]=$next;
                if ($next===null) {break;}
            }
        } finally {sqlsrv_free_stmt($stmt);}
        // NOCOUNT removes the UPDATE count result, not its work or SELECT rows.
        s150Command($db,'SET NOCOUNT ON');
        $quiet=s150Query($db,"UPDATE $q SET qty = 5 WHERE id = 1; SELECT id,qty FROM $q WHERE id = 1;");
        try {
            $quietFields=sqlsrv_num_fields($quiet);
            if ($quietFields===false) {s150Failure('NOCOUNT: поля');}
            $quietRows=s150Rows($quiet);
            $quietEnd=sqlsrv_next_result($quiet);
            if ($quietEnd===false) {s150Failure('NOCOUNT: завершение');}
        } finally {sqlsrv_free_stmt($quiet);}
        $failing=s150Query($db,"SELECT qty FROM $q WHERE id = 1; RAISERROR(N'Controlled lesson error after SELECT',16,1);");
        try {
            $beforeError=s150Rows($failing);
            $failedNext=sqlsrv_next_result($failing);
            $lateErrors=$failedNext===false ? sqlsrv_errors(SQLSRV_ERR_ERRORS) : null;
        } finally {sqlsrv_free_stmt($failing);}
    } finally {s150Command($db,$originalNoCount?'SET NOCOUNT ON':'SET NOCOUNT OFF');}
    $restored=(s150Read($db,'SELECT CASE WHEN (@@OPTIONS & 512) = 512 THEN 1 ELSE 0 END AS enabled')[0]['enabled']===1)===$originalNoCount;
    $errorCodes=is_array($lateErrors)?array_column($lateErrors,'code'):[];
    $rows=[['Последовательность столбцов batch',implode(', ',array_column($results,'fields'))],['UPDATE затронул строк',$results[0]['affected']],
        ['Строк в промежуточном SELECT',count($results[1]['rows'])],['Количество в квитанции',$results[2]['rows'][0]['qty']],
        ['NOCOUNT: сразу набор с двумя полями',$quietFields===2&&$quietRows===[['id'=>1,'qty'=>5]]&&$quietEnd===null],
        ['До отложенной ошибки прочитано',$beforeError[0]['qty']],['Ошибка следующего результата обнаружена',$failedNext===false&&in_array(50000,$errorCodes,true)],['Настройка NOCOUNT восстановлена',$restored]];
    $checks=['three_result_kinds'=>array_column($results,'fields')===[0,2,2],'update_count'=>$results[0]['affected']===1,
        'empty_set_preserved'=>$results[1]['has_rows']===false&&$results[1]['rows']===[],
        'receipt_value'=>$results[2]['rows']===[['id'=>1,'qty'=>5]],'strict_transitions'=>$transitions===[true,true,null],
        'nocount_keeps_receipt'=>$quietFields===2&&$quietRows===[['id'=>1,'qty'=>5]]&&$quietEnd===null,
        'first_select_not_whole_success'=>$beforeError===[['qty'=>5]]&&$failedNext===false,
        'late_error_saved'=>in_array(50000,$errorCodes,true),'setting_restored'=>$restored];
    $observations=['results'=>$results,'transitions'=>$transitions,'nocount_rows'=>$quietRows,'before_error'=>$beforeError,'late_errors'=>s150Diagnostics($lateErrors)];

} finally {
    if (is_resource($db)) {try {$removed=s150Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}

s150Report('sqlsrv150-batch-results','Прочитать квитанцию batch и заметить ошибку после первого SELECT',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Последовательность столбцов batch — 0, 2, 2
UPDATE затронул строк — 1
Строк в промежуточном SELECT — 0
Количество в квитанции — 5
NOCOUNT: сразу набор с двумя полями — да
До отложенной ошибки прочитано — 5
Ошибка следующего результата обнаружена — да
Настройка NOCOUNT восстановлена — да

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

Таблица фактического запуска · Данные результата JSON.

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

NOCOUNT ON убирает сообщения о количестве затронутых строк; сами изменения и SELECT остаются. Если процедура изменилась, жёсткое «перейти два раза» сломается. Всегда разбирайте полученные результаты и контракт процедуры. Успех первой части не гарантирует успех остальных; RAISERROR здесь намеренно создаёт контролируемый отказ, который сохраняется как наблюдение. Автоматического отката всех предыдущих изменений нет: для атомарности нужна транзакция. Пример возвращает настройку NOCOUNT и удаляет только свою таблицу. SQL-диагностику показывают в учебном локальном отчёте; посетителю рабочего сайта не раскрывают внутренние сообщения базы.

Процедура вернула строки, а последующая ошибка RAISERROR не была замечена при переходе между результатами. Разбор исходной ситуации на Stack Overflow. Разделить true, null и false; сразу после false сохранить диагностику. Полнота всей операции не определяется успехом первого SELECT.

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

Добавьте ещё один пустой SELECT перед квитанцией. Предскажите список полей и переходов; не добавляйте специальный «лишний next_result» вне общего цикла.

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

Появится ещё один элемент с двумя полями и пустым rows. Цикл сам его прочитает; будет ещё одно true перед конечным null. Квитанция по-прежнему содержит qty=5. Измените ожидаемый контракт учебного результата, чтобы проверка отражала новый batch.

Выбрать действие над результатом

ФункцияКогда нужна
sqlsrv_fetchПередвинуть указатель на строку, чтобы затем читать её поля отдельно.
sqlsrv_get_fieldПрочитать одно поле уже выбранной строки и при необходимости задать PHP-тип.
sqlsrv_fetch_objectПрочитать строку как объект с именованными свойствами.
sqlsrv_has_rowsУзнать, есть ли строки, не съедая первую строку чтением.
sqlsrv_num_rowsПолучить число строк результата, когда выбранный тип курсора это поддерживает.
sqlsrv_num_fieldsУзнать количество столбцов текущего результата, отдельно от количества строк.
sqlsrv_next_resultПерейти от одного результата команды к следующему и заметить отложенную ошибку.

От чтения результата к завершённой операции

Для обычной выдачи достаточно одного последовательного чтения с различением конца и ошибки. Для отдельных полей нужен fetch вместе с get_field; для модели представления — понятные имена и проверяемое отображение свойств. Курсор выбирают под размер и задачу, а составной результат дочитывают до подтверждённого окончания.

Если несколько изменений должны сохраниться вместе или полностью откатиться, продолжите транзакциями SQLSRV. Для построения запросов к нужному экрану вернитесь к серверной фильтрации, сортировке и выбору полей; конкретный синтаксис пагинации SQL Server отличается от MySQL.

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

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

sqlsrv_fetchПередвинуть указатель на строку, чтобы затем читать её поля отдельно.

SQLSRV: точная первая версия не указана в PHP Manual; профиль запуска указан в главе · sqlsrv

sqlsrv_fetch(resource $stmt, int $row = SQLSRV_SCROLL_NEXT, int $offset = 0): bool|null

Передвинуть указатель на строку, чтобы затем читать её поля отдельно.

Что происходит с результатом
  1. Указатель перед первой строкой
  2. fetch → true: текущая строка «Крепёж»
  3. get_field(0) → «Крепёж»; следующий fetch передвигает указатель
ПараметрНазначение
$stmtВыполненный ресурс SQLSRV с текущим набором строк.
$rowНаправление NEXT, PRIOR, FIRST, LAST, ABSOLUTE или RELATIVE. Переходы назад и по позиции требуют подходящего Scrollable-курсора.
$offsetЦелочисленное смещение для ABSOLUTE или RELATIVE. Для ABSOLUTE первая строка имеет индекс 0; для остальных режимов смещение не определяет выбранную строку.

true — текущая строка выбрана, false — ошибка, null — строки в этой позиции нет или чтение завершено. Функция не возвращает массив данных: значения читает get_field.

ПереходПозиция
SQLSRV_SCROLL_NEXTСледующая строка; обычный последовательный вызов.
SQLSRV_SCROLL_PRIORПредыдущая строка прокручиваемого результата.
SQLSRV_SCROLL_FIRSTПервая строка.
SQLSRV_SCROLL_LASTПоследняя строка.
SQLSRV_SCROLL_ABSOLUTEСтрока по абсолютному смещению: offset=0 означает первую.
SQLSRV_SCROLL_RELATIVEСмещение относительно текущей позиции, например −1 назад.

Прочитать название первой позиции

После true поле 0 относится к выбранной строке. В нашем каталоге это «Крепёж»; нулевое количество в следующем поле остаётся допустимым значением.

$status = sqlsrv_fetch($stmt);
if ($status === true) {
    $title = sqlsrv_get_field($stmt, 0);
} elseif ($status === false) {
    $errors = sqlsrv_errors();
} // null: строки нет.

Фрагмент предполагает открытое соединение и выполненный SELECT; полный код, входные данные и результат: самостоятельный пример.

Вернуться к выбранному товару

В STATIC-результате ABSOLUTE с индексом 1 выбирает вторую строку в порядке SQL. Это позиция результата, а не первичный ключ товара.

$status = sqlsrv_fetch($stmt, SQLSRV_SCROLL_ABSOLUTE, 1);
if ($status === true) {
    $sku = sqlsrv_get_field($stmt, 0);
}

Фрагмент предполагает открытое соединение и выполненный SELECT; полный код, входные данные и результат: самостоятельный пример.

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

Не делайте пробный fetch перед основным циклом, если эту строку затем не обработаете: указатель уже сдвинут. Для проверки наличия используйте has_rows, для обычного списка — цикл с отдельными ветвями null и false. fetch_array и fetch_object тоже продвигают указатель; смешивать их с fetch без понимания позиции нельзя.

Связанные действия: sqlsrv_fetch, sqlsrv_get_field, sqlsrv_next_result, query, fetch_array и освобождение statement, диагностика SQLSRV.

sqlsrv_get_fieldПрочитать одно поле уже выбранной строки и при необходимости задать PHP-тип.

SQLSRV: точная первая версия не указана в PHP Manual; профиль запуска указан в главе · sqlsrv

sqlsrv_get_field(resource $stmt, int $fieldIndex, int $getAsType = ?): mixed

Прочитать одно поле уже выбранной строки и при необходимости задать PHP-тип.

Что происходит с результатом
  1. SELECT title,qty,note: индексы 0,1,2
  2. fetch выбрал строку «Крепёж»,0,NULL
  3. get_field последовательно → текст, число 0, null
ПараметрНазначение
$stmtВыполненный statement после успешного sqlsrv_fetch.
$fieldIndexИндекс столбца от 0 в порядке SELECT, а не имя поля. Читайте индексы по возрастанию: после поля 1 нельзя вернуться к полю 0.
$getAsTypeНеобязательный SQLSRV_PHPTYPE_*: INT, FLOAT, STRING(encoding), STREAM(encoding), DATETIME или NULL, если преобразование допустимо. Если аргумент опущен, драйвер выбирает стандартный PHP-тип по SQL-типу. Символ ? в сигнатуре обозначает неуказанное документацией значение, не строку для передачи.

Значение поля, включая 0, пустую строку и SQL NULL, либо false при ошибке. При стандартном чтении SQL NULL возвращается как null и не означает конец набора: конец определяет fetch. При явном SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY) на проверенном драйвере 5.13.3 значение SQL NULL читается как пустой поток, так же, как пустое бинарное значение. Чтобы сохранить различие, запросите отдельный признак payload IS NULL или DATALENGTH(payload) и прочитайте его до поля-потока. Это показано в примере пустого файла и отсутствующего вложения. Невозможное преобразование и нарушение порядка полей требуют диагностики.

Не потерять нулевой остаток

Поле qty равно 0 — товар найден, остаток нулевой. Строгое сравнение не принимает ноль за ошибку.

$title = sqlsrv_get_field($stmt, 0);
$qty = sqlsrv_get_field($stmt, 1);
if ($qty === false) { $errors = sqlsrv_errors(); }
else { /* qty может быть 0. */ }

Фрагмент предполагает открытое соединение и выполненный SELECT; полный код, входные данные и результат: самостоятельный пример.

Получить код как UTF-8 строку

Явный тип пригодится текстовому экспорту. Сначала fetch выбирает строку, затем поле 0 читается в нужном представлении; неподдерживаемое преобразование нужно обработать.

$status = sqlsrv_fetch($stmt);
if ($status === true) {
    $sku = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STRING("UTF-8"));
    if ($sku === false) { $errors = sqlsrv_errors(); }
}

Фрагмент предполагает открытое соединение и выполненный SELECT; полный код, входные данные и результат: самостоятельный пример.

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

Не вызывайте get_field до успешного fetch и после null. Для повторного произвольного доступа к полям небольшой строки сначала получите массив fetch_array. Для большого бинарного поля можно запросить поток и читать частями; тип STREAM не превращает произвольный SQL-тип в поток. Явный PHP-тип не меняет SQL-тип столбца и правила его хранения.

Связанные действия: sqlsrv_fetch, sqlsrv_get_field, sqlsrv_next_result, query, fetch_array и освобождение statement, диагностика SQLSRV.

sqlsrv_fetch_objectПрочитать строку как объект с именованными свойствами.

SQLSRV: точная первая версия не указана в PHP Manual; профиль запуска указан в главе · sqlsrv

sqlsrv_fetch_object(resource $stmt, string $className = "stdClass", array $ctorParams = [], int $row = SQLSRV_SCROLL_NEXT, int $offset = 0): object|null|false

Прочитать строку как объект с именованными свойствами.

Что происходит с результатом
  1. SELECT id AS invoiceId,title,qty AS quantity
  2. Поля строки → одноимённые свойства объекта
  3. Конструктор модели представления → подпись «Накладная #7: Кабель × 3»
ПараметрНазначение
$stmtВыполненный statement с набором строк.
$classNameИмя доступного PHP-класса; по умолчанию stdClass.
$ctorParamsАргументы конструктора по порядку; пустой массив, если параметров нет. Обязательные параметры конструктора нужно передать.
$rowРежим перехода курсора. Для задания этого аргумента явно передайте также className и ctorParams.
$offsetСмещение ABSOLUTE/RELATIVE; индекс первой строки 0.

Объект текущей строки, null при отсутствии следующей строки, false при ошибке драйвера, в том числе неизвестном имени класса. Ошибки PHP-конструктора, включая исключения, являются отдельным путём отказа и не превращаются автоматически в false.

ПереходПозиция
SQLSRV_SCROLL_NEXTСледующая строка; обычный последовательный вызов.
SQLSRV_SCROLL_PRIORПредыдущая строка прокручиваемого результата.
SQLSRV_SCROLL_FIRSTПервая строка.
SQLSRV_SCROLL_LASTПоследняя строка.
SQLSRV_SCROLL_ABSOLUTEСтрока по абсолютному смещению: offset=0 означает первую.
SQLSRV_SCROLL_RELATIVEСмещение относительно текущей позиции, например −1 назад.

Показать строку накладной

AS даёт полям уникальные имена. У объекта читают свойства через ->; значение quantity в примере равно 3.

$item = sqlsrv_fetch_object($stmt);
if ($item === false) { $errors = sqlsrv_errors(); }
elseif ($item !== null) {
    echo $item->invoiceId, ": ", $item->quantity;
}

Фрагмент предполагает открытое соединение и выполненный SELECT; полный код, входные данные и результат: самостоятельный пример.

Добавить подпись своей модели представления

В самостоятельном примере объявлены все свойства результата и конструктор с префиксом. Он получает уже заполненный invoiceId; это подтверждается наблюдением ctorSawId=7.

$item = sqlsrv_fetch_object($stmt, InvoiceView150::class, ["Накладная"]);
if ($item instanceof InvoiceView150) {
    echo $item->caption;
}

Фрагмент предполагает открытое соединение и выполненный SELECT; полный код, входные данные и результат: самостоятельный пример.

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

Имена свойств чувствительны к регистру; title и Title — разные имена. Каждому вычислению и одноимённым столбцам JOIN задавайте AS. Безымянное поле игнорируется с предупреждением, дубликаты имён нельзя надёжно представить одним свойством. Драйвер способен заполнять и непубличные свойства: это не проверка бизнес-правил. Для обычного класса заранее объявляйте свойства, чтобы не создавать динамические свойства с устаревшим поведением в PHP 8.2+. Старую формулировку документации об игнорировании типов свойств нельзя использовать как обещание обхода ограничений современного PHP: здесь проверен класс с нетипизированными полями. Для строгой модели прочитайте массив и вызовите собственную фабрику с проверками.

Связанные действия: sqlsrv_fetch, sqlsrv_get_field, sqlsrv_next_result, query, fetch_array и освобождение statement, диагностика SQLSRV.

sqlsrv_has_rowsУзнать, есть ли строки, не съедая первую строку чтением.

SQLSRV: точная первая версия не указана в PHP Manual; профиль запуска указан в главе · sqlsrv

sqlsrv_has_rows(resource $stmt): bool

Узнать, есть ли строки, не съедая первую строку чтением.

Что происходит с результатом
  1. SELECT двух активных товаров
  2. has_rows → true, указатель не потребляет товар
  3. Первый fetch → «Крепёж», а не второй товар
ПараметрНазначение
$stmtРесурс выполненной команды с текущим результатом.

true — набор содержит строки; false — строк нет либо возникла ошибка. Само false не является полным диагнозом: сразу сохраните sqlsrv_errors и различите пустой результат и отказ.

Показать пустое состояние каталога

Проверка перед циклом не делает лишний fetch. Первый обработанный товар остаётся «Крепёж».

$hasRows = sqlsrv_has_rows($stmt);
$errors = $hasRows === false ? sqlsrv_errors(SQLSRV_ERR_ERRORS) : null;
if ($errors !== null) { /* Сообщить об отказе. */ }
elseif (!$hasRows) { /* Ничего не найдено. */ }
else { /* Начать чтение с первой строки. */ }

Фрагмент предполагает открытое соединение и выполненный SELECT; полный код, входные данные и результат: самостоятельный пример.

Различить пустой набор и следующий результат

Пустой SELECT внутри batch всё равно является набором с описанными столбцами. После него надо вызвать next_result; пустота не означает конец всей команды.

$hasRows = sqlsrv_has_rows($stmt);
$errors = $hasRows === false ? sqlsrv_errors(SQLSRV_ERR_ERRORS) : null;
// При отсутствии ошибки показать пустой набор.
// Затем отдельно проверить sqlsrv_next_result($stmt).

Фрагмент предполагает открытое соединение и выполненный SELECT; полный код, входные данные и результат: самостоятельный пример.

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

has_rows не считает строки и не сообщает, сколько осталось после уже сделанных fetch. Он описывает наличие строк в наборе, а не текущий остаток чтения. Не передавайте false от неудачного query. Ошибки нужно сохранять до других вызовов драйвера. Исторический дефект старых драйверов с передвижением курсора не является текущим контрактом; самостоятельный пример проверяет сохранение первой строки.

Связанные действия: sqlsrv_fetch, sqlsrv_get_field, sqlsrv_next_result, query, fetch_array и освобождение statement, диагностика SQLSRV.

sqlsrv_num_rowsПолучить число строк результата, когда выбранный тип курсора это поддерживает.

SQLSRV: точная первая версия не указана в PHP Manual; профиль запуска указан в главе · sqlsrv

sqlsrv_num_rows(resource $stmt): int|false

Получить число строк результата, когда выбранный тип курсора это поддерживает.

Что происходит с результатом
  1. SELECT TOP (3) sku ORDER BY id
  2. FORWARD → false: счётчик недоступен
  3. Тот же ограниченный SELECT с STATIC → 3
ПараметрНазначение
$stmtВыполненный ресурс с STATIC, KEYSET либо CLIENT_BUFFERED-курсором. У обычного FORWARD и DYNAMIC количество недоступно.

Целое количество строк, включая 0, либо false. false у FORWARD не означает, что SELECT пустой: это неподдерживаемая операция для этого курсора.

КурсорПоведениеГраница
SQLSRV_CURSOR_FORWARDПо умолчанию; читать вперёдnum_rows возвращает false; счётчик не является проверкой наличия строк.
SQLSRV_CURSOR_STATICПрокрутка по снимку результатаnum_rows доступен; последующие изменения исходных данных в снимке не отражаются.
SQLSRV_CURSOR_KEYSETПрокрутка с фиксированным набором ключейnum_rows доступен; состав ключей фиксирован, изменения значений видны. Удалённая запись требует отдельной обработки.
SQLSRV_CURSOR_DYNAMICПрокрутка с отражением измененийnum_rows не поддерживается; результат может меняться.
SQLSRV_CURSOR_CLIENT_BUFFEREDВесь выбранный результат в памяти клиентаnum_rows доступен; память ограничивают ClientBufferMaxKBSize. Большой каталог ради страницы так не загружают.

Посчитать небольшой прокручиваемый список

Запрос ограничен TOP (3) и отсортирован SQL. STATIC позволяет вывести «3 позиции» и перейти к нужной позиции.

$stmt = sqlsrv_query($db, "SELECT TOP (3) sku FROM $q ORDER BY id", [],
    ["Scrollable" => SQLSRV_CURSOR_STATIC]);
if ($stmt !== false) {
    $count = sqlsrv_num_rows($stmt);
    if ($count === false) { $errors = sqlsrv_errors(); }
}

Фрагмент предполагает открытое соединение и выполненный SELECT; полный код, входные данные и результат: самостоятельный пример.

Не принять отказ счётчика за отсутствие товаров

FORWARD подходит последовательной выдаче. В примере num_rows возвращает false, но чтение всё равно получает SKU-A, SKU-B и SKU-C.

$count = sqlsrv_num_rows($forward);
if ($count === false) {
    $errors = sqlsrv_errors();
    // Причина: курсор не предоставляет счётчик.
}
// Прочитать forward обычным fetch; не выводить «0 товаров».

Фрагмент предполагает открытое соединение и выполненный SELECT; полный код, входные данные и результат: самостоятельный пример.

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

Для общего количества совпадений на большой таблице выполните отдельный SELECT COUNT(*) с тем же WHERE; для страницы получите только нужные поля и диапазон. Не выбирайте CLIENT_BUFFERED лишь ради подсчёта сотен тысяч строк. num_rows считает текущий результат, не всю исходную таблицу и не число изменённых записей. Microsoft отдельно ограничивает подсчёт нескольких результатов batch клиентским буферизованным курсором; в примере STATIC используется только для одного SELECT.

Связанные действия: sqlsrv_fetch, sqlsrv_get_field, sqlsrv_next_result, query, fetch_array и освобождение statement, диагностика SQLSRV.

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

SQLSRV: точная первая версия не указана в PHP Manual; профиль запуска указан в главе · sqlsrv

sqlsrv_num_fields(resource $stmt): int|false

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

Что происходит с результатом
  1. UPDATE → результат без столбцов → 0
  2. SELECT id,qty WHERE id=999 → 2 столбца, 0 строк
  3. SELECT id,qty WHERE id=1 → 2 столбца, 1 строка
ПараметрНазначение
$stmtРесурс подготовленной либо выполненной команды; функция допустима до и после execute, если драйвер располагает описанием результата.

Количество полей либо false. Для SELECT без найденных строк столбцы всё равно существуют. Результат-счётчик UPDATE не имеет полей: в проверенном batch num_fields возвращает 0.

Проверить форму строки каталога

SELECT title,qty,note даёт три поля. Это не три товара: количество записей определяется чтением.

$fieldCount = sqlsrv_num_fields($stmt);
if ($fieldCount === false) { $errors = sqlsrv_errors(); }
elseif ($fieldCount !== 3) { /* Контракт SELECT изменился. */ }

Фрагмент предполагает открытое соединение и выполненный SELECT; полный код, входные данные и результат: самостоятельный пример.

Выбрать способ обработки результата batch

Нулевой fieldCount означает отсутствие табличных полей, а положительный — описанный набор, даже если тот пустой.

$fieldCount = sqlsrv_num_fields($stmt);
if ($fieldCount === false) { $errors = sqlsrv_errors(); }
elseif ($fieldCount === 0) { $affected = sqlsrv_rows_affected($stmt); }
else { /* Читать строки, различая null и false. */ }

Фрагмент предполагает открытое соединение и выполненный SELECT; полный код, входные данные и результат: самостоятельный пример.

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

Не путайте количество столбцов и строк, а также count массива SQLSRV_FETCH_BOTH: в нём один столбец представлен числовым и строковым ключом. Для названий, SQL-типов, размера и допуска NULL нужна field_metadata. После next_result описание относится уже к новому результату.

Связанные действия: sqlsrv_fetch, sqlsrv_get_field, sqlsrv_next_result, query, fetch_array и освобождение statement, диагностика SQLSRV.

sqlsrv_next_resultПерейти от одного результата команды к следующему и заметить отложенную ошибку.

SQLSRV: точная первая версия не указана в PHP Manual; профиль запуска указан в главе · sqlsrv

sqlsrv_next_result(resource $stmt): bool|null

Перейти от одного результата команды к следующему и заметить отложенную ошибку.

Что происходит с результатом
  1. Первый результат уже активен: UPDATE затронул 1 строку
  2. true → пустой SELECT; true → квитанция qty=5
  3. null → конец; false → ошибка, сохранить диагностику
ПараметрНазначение
$stmtРесурс команды с возможными следующими результатами: наборы строк, счётчики изменений, выходные параметры.

true — следующий результат активирован; null — результатов больше нет; false — ошибка перехода или выполнения следующей части команды. Первый результат активен сразу после query/execute: предварительный next_result его пропустит.

Получить квитанцию после изменения

В batch сначала приходит счётчик UPDATE, затем пустой SELECT, затем строка квитанции. Все три обрабатываются последовательно; пустой набор не обрывает чтение.

// Сначала обработать уже активный результат.
$next = sqlsrv_next_result($stmt);
if ($next === true) { /* Обработать следующий. */ }
elseif ($next === false) { $errors = sqlsrv_errors(); }
else { /* null: команда полностью прочитана. */ }

Фрагмент предполагает открытое соединение и выполненный SELECT; полный код, входные данные и результат: самостоятельный пример.

Не объявить весь отчёт успешным после первого SELECT

Последующий оператор может вернуть ошибку SQL Server. Пример сначала читает quantity=5, затем получает false и сохраняет код 50000.

$next = sqlsrv_next_result($stmt);
if ($next === false) {
    $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
    // Уже полученные строки не доказывают успех всей команды.
}

Фрагмент предполагает открытое соединение и выполненный SELECT; полный код, входные данные и результат: самостоятельный пример.

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

while(sqlsrv_next_result($stmt)) смешивает false и null, поэтому теряет информацию об ошибке. Настройка SET NOCOUNT ON подавляет сообщения о числе изменённых строк, но не SELECT, не SQL-ошибки и не создаёт транзакцию. Не рассчитывайте на фиксированное число переходов для изменяемой процедуры. OUT/INOUT-параметры читают после завершения результатов. Переход дальше может отбросить непрочитанные строки текущего набора; сначала обработайте нужные данные.

Связанные действия: sqlsrv_fetch, sqlsrv_get_field, sqlsrv_next_result, query, fetch_array и освобождение statement, диагностика SQLSRV.

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

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

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

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

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