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

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

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

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

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

SQLSRV: параметры, Unicode, точные цены и даты в результате

Связанные параметры, Unicode без потерь, точная сумма, календарная дата и проверяемое изменение: четыре самостоятельных сценария SQLSRV.

От номера счёта к нужной строке на экране

Оператор открывает счёт, меняет количество и показывает дату доставки. Экрану нужны конкретные поля одной записи, а не вся таблица. SQLSRV должен передать номер как параметр, вернуть кириллицу без потерь, сохранить точную цену и дать понять, что именно произошло при записи.

Счёт INV-A: запрос и представление — разные этапы
  1. PHP: id=1 → параметр?
  2. SQL Server: WHERE id=? выбирает нужную запись
  3. SQLSRV: строка содержит title, DECIMAL и DateTime
  4. PHP: форматирует дату, сохраняет точную цену строкой, экранирует текст
  5. HTML: человек видит счёт, а не ресурс statement

Перед этой главой пройдите первое подключение и диагностику. Массив параметров объяснён в главе о массивах, связь переменной — в параметрах функций. Здесь каждый новый вызов разбирается отдельно. $db — открытое соединение SQLSRV; $q — проверенное имя собственной учебной таблицы из архива.

Первое чтение без таблицы

$stmt = sqlsrv_query($db, "SELECT 2 AS quantity");
if ($stmt === false) {
    $errors = sqlsrv_errors();
} else {
    $row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
    // row содержит ["quantity" => 2], затем ресурс освобождают.
    sqlsrv_free_stmt($stmt);
}

query получает соединение и SQL. AS даёт полю имя quantity. Возвращаемый statement — дескриптор операции, не массив. Только fetch_array читает строку; ASSOC выбирает ключ по имени. Строгое сравнение с false отделяет отказ от допустимого результата. Этот вводный фрагмент требует соединения; все самостоятельные архивы ниже включают подготовку и очистку.

Выбирайте нужные поля, WHERE и ORDER BY в SQL. Для ограниченной страницы SQL Server использует TOP либо OFFSET/FETCH; LIMIT из MySQL сюда не переносится. SELECT * плюс последующая фильтрация большого PHP-массива расходует сеть и память на данные, которые экран не показывает. При этом COUNT(*) — серверный подсчёт, а не загрузка всех столбцов.

Один подготовленный запрос — две накладные

Экран накладной сначала открывает INV-149-A, затем INV-149-B. Форма запроса остаётся прежней: меняется только номер. prepare хранит подготовленную команду, execute выполняет её с текущим значением связанной переменной. Сервер возвращает одну нужную строку на каждый экран.

Путь данных этой программы
  1. $id=1 → параметр по ссылке &$id → WHERE id=?
  2. execute → INV-149-A, qty=2 → чтение до конца
  3. $id=2 → тот же слот параметра получает 2
  4. execute того же statement → INV-149-B, qty=7
  5. free_stmt освобождает команду; PHP-массивы остаются

Сначала небольшой фрагмент

$id = 1;
$stmt = sqlsrv_prepare($db, "SELECT invoice_no, qty FROM $q WHERE id = ?", [&$id]);
// После проверки stmt:
sqlsrv_execute($stmt);
// Прочитать результат до конца.
$id = 2;
sqlsrv_execute($stmt);

Знак ? обозначает место значения в SQL. Массив параметров идёт в том же порядке. &$id связывает это место с переменной: присваивание 2 меняет значение для следующего execute. prepare сам запрос не выполняет. execute возвращает bool, а строки читают отдельным fetch_array. Полная программа ниже проверяет каждый результат и освобождает ресурс в finally.

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

НаблюдениеРезультат
Первое выполнениеINV-149-A: 2
Второе выполнениеINV-149-B: 7
Использован тот же statementда
Получено строк за два запроса2
Один подготовленный запрос — две накладныеСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s149Runtime();
try {

    $config=s149Config();$db=s149Connect($config);$runtime=s149Runtime($db,$config);

    $name=s149OwnTable($db,$owned,'params','(id int NOT NULL PRIMARY KEY, invoice_no nvarchar(30) NOT NULL, qty int NOT NULL)');
    $q=s149Qualified($name);
    s149Command($db,"INSERT INTO $q (id,invoice_no,qty) VALUES (1,N'INV-149-A',2),(2,N'INV-149-B',7)");
    $id=1;
    // & links the parameter slot to this variable, not to a one-time copy of 1.
    $params=[ [&$id,SQLSRV_PARAM_IN,SQLSRV_PHPTYPE_INT,SQLSRV_SQLTYPE_INT] ];
    $stmt=sqlsrv_prepare($db,"SELECT id,invoice_no,qty FROM $q WHERE id = ?",$params,['QueryTimeout'=>10]);
    if ($stmt===false) {s149Failure('Подготовка накладной');}
    $statementId=get_resource_id($stmt);
    try {
        if (!sqlsrv_execute($stmt)) {s149Failure('Первая накладная');}
        $first=s149Rows($stmt);
        $id=2;
        if (!sqlsrv_execute($stmt)) {s149Failure('Вторая накладная');}
        $second=s149Rows($stmt);
        $sameStatement=get_resource_id($stmt)===$statementId;
    } finally {sqlsrv_free_stmt($stmt);$stmt=null;}
    $rows=[['Первое выполнение',$first[0]['invoice_no'].': '.$first[0]['qty']],
        ['Второе выполнение',$second[0]['invoice_no'].': '.$second[0]['qty']],
        ['Использован тот же statement',$sameStatement],['Получено строк за два запроса',count($first)+count($second)]];
    $checks=['first_invoice'=>$first===[['id'=>1,'invoice_no'=>'INV-149-A','qty'=>2]],
        'second_invoice'=>$second===[['id'=>2,'invoice_no'=>'INV-149-B','qty'=>7]],
        'same_statement'=>$sameStatement,'statement_released'=>$stmt===null];
    $observations=['first'=>$first,'second'=>$second,'parameter_after_second'=>$id];

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

s149Report('sqlsrv149-parameter-lifetime','Один подготовленный запрос — две накладные',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Первое выполнение — INV-149-A: 2
Второе выполнение — INV-149-B: 7
Использован тот же statement — да
Получено строк за два запроса — 2

Запустите в отдельной папке с подготовленным LocalDB. Программа получает реальные данные SQL Server, сохраняет HTML/JSON и удаляет только свою учебную таблицу.

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

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

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

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

Добавьте накладную 3 с количеством 4 и получите её третьим execute без нового prepare.

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

Создайте строку 3 в подготовке таблицы, после второго чтения присвойте $id=3 и повторите execute/fetch. Ожидается одна новая строка с qty4, а ресурс statement остаётся тем же.

Сохранить кириллицу и точную цену для обмена

Поставщик передаёт название товара и цену строкой. Нужно сохранить их так, чтобы экспорт вернул те же символы и цифры. Большая сумма здесь намеренно проверяет пределы преобразования: обычный float не обязан хранить все её десятичные разряды.

Путь данных этой программы
  1. UTF-8 строка «Кабель “Север”» → PHP-параметр STRING(UTF-8)
  2. SQL NVARCHAR(80) хранит текст; DECIMAL(19,4) хранит 19 цифр, из них 4 после точки
  3. SELECT title,price WHERE id=1 → одна строка
  4. SQLSRV возвращает DECIMAL строкой → JSON сохраняет все цифры
  5. HTML экранирует строку, не меняя её значение

Сначала небольшой фрагмент

$price = '123456789012345.6789';
$params = [[$price, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING('UTF-8'), SQLSRV_SQLTYPE_DECIMAL(19, 4)]];
$stmt = sqlsrv_query($db, "UPDATE $q SET price = ? WHERE id = 1", $params);

Внешний массив содержит параметры запроса. Внутренний описывает один параметр: значение, направление IN, PHP-тип, SQL-тип. DECIMAL(19,4) означает 19 значащих десятичных цифр всего и 4 после точки. Кавычки вокруг числа в PHP сохраняют исходную десятичную запись. У SQLSRV_SQLTYPE_NVARCHAR размер передаётся строкой, например "80": при strict_types=1 целое 80 в проверенном драйвере вызывает TypeError. У SQLSRV_SQLTYPE_DECIMAL precision и scale — целые числа. Это ещё не арифметика: для точных вычислений выбирают SQL DECIMAL, BCMath либо целые минимальные единицы подходящего диапазона.

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

НаблюдениеРезультат
Название после чтенияКабель «Север» — 2 м
Точная цена строкой123456789012345.6789
PHP-тип ценыstring
Precision / Scale19 / 4
Преобразование в float потеряло точностьда
Сохранить кириллицу и точную цену для обменаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s149Runtime();
try {

    $config=s149Config();$db=s149Connect($config);$runtime=s149Runtime($db,$config);

    $name=s149OwnTable($db,$owned,'unicode','(id int NOT NULL PRIMARY KEY, title nvarchar(80) NOT NULL, price decimal(19,4) NOT NULL)');
    $q=s149Qualified($name);
    $title='Кабель «Север» — 2 м';$price='123456789012345.6789';
    $params=[[$title,SQLSRV_PARAM_IN,SQLSRV_PHPTYPE_STRING('UTF-8'),SQLSRV_SQLTYPE_NVARCHAR('80')],
        [$price,SQLSRV_PARAM_IN,SQLSRV_PHPTYPE_STRING('UTF-8'),SQLSRV_SQLTYPE_DECIMAL(19,4)]];
    $insert=s149Query($db,"INSERT INTO $q (id,title,price) VALUES (1,?,?)",$params);
    sqlsrv_free_stmt($insert);
    $stmt=s149Query($db,"SELECT title,price FROM $q WHERE id = ?",[1]);
    try {
        $metadata=sqlsrv_field_metadata($stmt);
        if ($metadata===false) {s149Failure('Метаданные цены');}
        $data=s149Rows($stmt);
    } finally {sqlsrv_free_stmt($stmt);}
    $row=$data[0];$byName=array_column($metadata,null,'Name');
    $floatChanged=sprintf('%.4f',(float)$price)!==$price;
    $rows=[['Название после чтения',$row['title']],['Точная цена строкой',$row['price']],
        ['PHP-тип цены',get_debug_type($row['price'])],['Precision / Scale',$byName['price']['Precision'].' / '.$byName['price']['Scale']],
        ['Преобразование в float потеряло точность',$floatChanged]];
    $checks=['unicode_roundtrip'=>$row['title']===$title,'decimal_roundtrip'=>$row['price']===$price,
        'decimal_string'=>is_string($row['price']),'metadata_precision'=>$byName['price']['Precision']===19,
        'metadata_scale'=>$byName['price']['Scale']===4,'float_loss_demonstrated'=>$floatChanged,'one_row'=>count($data)===1];
    $observations=['input'=>['title'=>$title,'price'=>$price],'stored_row'=>$row,'metadata'=>$metadata];

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

s149Report('sqlsrv149-unicode-exact-price','Сохранить кириллицу и точную цену для обмена',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Название после чтения — Кабель «Север» — 2 м
Точная цена строкой — 123456789012345.6789
PHP-тип цены — string
Precision / Scale — 19 / 4
Преобразование в float потеряло точность — да

Запустите в отдельной папке с подготовленным LocalDB. Программа получает реальные данные SQL Server, сохраняет HTML/JSON и удаляет только свою учебную таблицу.

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

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

CharacterSet=UTF-8 настраивает обмен драйвера; он не превращает неподходящий столбец в Unicode. Здесь выбран NVARCHAR. Не лечите кодировку случайными utf8_encode/utf8_decode. Не применяйте JSON_NUMERIC_CHECK к экспортируемой точной цене: автоматическое превращение строки в число может уничтожить контракт. Значения precision/scale относятся к метаданным результата, а не к длине подписи на экране.

Кодировка веб-страницы и кодировка обмена с SQL Server смешиваются в одну настройку. Ситуация на Stack Overflow. Разделить UTF-8 входа/выхода, CharacterSet драйвера и SQL-тип хранения; современный пример проверяет NVARCHAR и явный PHP-тип. Исторические ограничения старого ответа не повторяются как текущие.

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

Замените название на «Крепёж №7» и цену на строку 0.1250. Сравните запись в SQL, тип PHP и отображение.

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

Исходные цифры должны сохраниться. У SQLSRV отдельные настройки FormatDecimals управляют представлением ведущего нуля; не путайте такое оформление с изменением самой цены. Если включаете форматирование, задайте его явно и обновите ожидаемую подпись.

Показать дату доставки и не перепутать NULL с пустой строкой

В карточке доставки нужна календарная дата 08.11.2026, в журнале — точное время отправки со смещением. Это разные данные: date не обозначает момент UTC, datetime2 не хранит часовой пояс, datetimeoffset хранит числовое смещение. Программа читает одну запись в двух режимах и показывает различия.

Путь данных этой программы
  1. SQL date=2026-11-08 → DateTime → format(d.m.Y) → 08.11.2026
  2. SQL datetime2(6) → время с 6 дробными цифрами, без сохранённого пояса
  3. SQL datetimeoffset(6) → время+03:00 → подпись со смещением
  4. ReturnDatesAsStrings=true на команде → строковые поля
  5. NULL примечания и пустой external_ref остаются разными значениями

Сначала небольшой фрагмент

$stmt = sqlsrv_query($db, "SELECT delivery_date FROM $q WHERE id = ?", [1]);
$row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
if (is_array($row) && $row["delivery_date"] instanceof DateTimeInterface) {
    echo $row["delivery_date"]->format("d.m.Y");
}

fetch_array отдаёт массив, но значения внутри него не обязаны быть строками. instanceof проверяет объект даты. Стрелка -> вызывает его метод format, а d.m.Y задаёт день, месяц и год в подписи. ReturnDatesAsStrings=true переводит даты конкретной команды в строки и имеет приоритет над настройкой соединения. array_key_exists видит ключ даже при NULL; isset для такого ключа вернётfalse.

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

НаблюдениеРезультат
Дата доставки на экране08.11.2026
Время без часового пояса2026-11-08 10:20:30.123456
Время с сохранённым смещением2026-11-08 10:20:30.123456+03:00
ReturnDatesAsStrings дал строкида
delivery_note существует и равен NULLда
external_ref — пустая строкада
Показать дату доставки и не перепутать NULL с пустой строкойСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s149Runtime();
try {

    $config=s149Config();$db=s149Connect($config);$runtime=s149Runtime($db,$config);

    $name=s149OwnTable($db,$owned,'dates','(id int NOT NULL PRIMARY KEY, delivery_date date NOT NULL, picked_at datetime2(6) NOT NULL, sent_at datetimeoffset(6) NOT NULL, delivery_note nvarchar(30) NULL, external_ref nvarchar(30) NOT NULL)');
    $q=s149Qualified($name);
    s149Command($db,"INSERT INTO $q (id,delivery_date,picked_at,sent_at,delivery_note,external_ref) VALUES (1,CONVERT(date,?,23),CONVERT(datetime2(6),?,126),CONVERT(datetimeoffset(6),?,127),NULL,N'')",['2026-11-08','2026-11-08T10:20:30.123456','2026-11-08T10:20:30.123456+03:00']);
    $sql="SELECT delivery_date,picked_at,sent_at,delivery_note,external_ref FROM $q WHERE id = ?";
    $objects=s149Read($db,$sql,[1])[0];
    $strings=s149Read($db,$sql,[1],['ReturnDatesAsStrings'=>true])[0];
    $dateObjects=$objects['delivery_date'] instanceof DateTimeInterface && $objects['picked_at'] instanceof DateTimeInterface && $objects['sent_at'] instanceof DateTimeInterface;
    if (!$dateObjects) {throw new RuntimeException('Ожидались объекты даты SQLSRV.');}
    $date=$objects['delivery_date']->format('Y-m-d');
    $picked=$objects['picked_at']->format('Y-m-d H:i:s.u');
    $sent=$objects['sent_at']->format('Y-m-d H:i:s.uP');
    $rows=[['Дата доставки на экране',$objects['delivery_date']->format('d.m.Y')],['Время без часового пояса',$picked],
        ['Время с сохранённым смещением',$sent],['ReturnDatesAsStrings дал строки',is_string($strings['delivery_date'])&&is_string($strings['picked_at'])&&is_string($strings['sent_at'])],
        ['delivery_note существует и равен NULL',array_key_exists('delivery_note',$objects)&&$objects['delivery_note']===null],
        ['external_ref — пустая строка',$objects['external_ref']==='']];
    $checks=['date_objects'=>$dateObjects,'calendar_date'=>$date==='2026-11-08',
        'datetime_precision'=>$picked==='2026-11-08 10:20:30.123456','offset_preserved'=>$sent==='2026-11-08 10:20:30.123456+03:00',
        'string_mode'=>is_string($strings['delivery_date'])&&is_string($strings['picked_at'])&&is_string($strings['sent_at']),
        'null_key_present'=>array_key_exists('delivery_note',$objects)&&$objects['delivery_note']===null,'empty_distinct'=>$objects['external_ref']===''];
    $observations=['formatted_objects'=>['date'=>$date,'datetime2'=>$picked,'datetimeoffset'=>$sent],
        'string_mode'=>$strings,'null_vs_empty'=>['note'=>$objects['delivery_note'],'external_ref'=>$objects['external_ref']]];

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

s149Report('sqlsrv149-temporal-row-contract','Показать дату доставки и не перепутать NULL с пустой строкой',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Дата доставки на экране — 08.11.2026
Время без часового пояса — 2026-11-08 10:20:30.123456
Время с сохранённым смещением — 2026-11-08 10:20:30.123456+03:00
ReturnDatesAsStrings дал строки — да
delivery_note существует и равен NULL — да
external_ref — пустая строка — да

Запустите в отдельной папке с подготовленным LocalDB. Программа получает реальные данные SQL Server, сохраняет HTML/JSON и удаляет только свою учебную таблицу.

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

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

Не передавайте DateTime в strtotime и не выводите объект простым echo. Для date не сдвигайте часовой пояс: день доставки — календарное правило. datetimeoffset хранит смещение, а не имя зоны и историю переходов. Для datetime2 договоритесь о зоне отдельно; PHP-объект не добавляет отсутствующее серверное знание. Вывод в HTML экранируется после форматирования.

Разработчик видит дату в диагностическом выводе, но ошибается при её выводе в таблицу. Ситуация на Stack Overflow. Проверить фактический PHP-тип: DateTime форматировать, строковый режим задавать явно; вывод не должен зависеть от var_dump.

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

Поменяйте sent_at на то же местное время со смещением+05:00. Проверьте, что подпись сохраняет новое смещение, а дата доставки не изменилась.

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

format с P должен показать+05:00. Это другой момент относительно UTC при той же местной цифре часов. Отдельное поле delivery_date по-прежнему показывает 08.11.2026.

Отличить успешное изменение, ноль строк и неизвестный счётчик

Оператор сохраняет количество товара. Сообщение «Готово» должно опираться на реальный исход: запрос выполнился, нужная запись нашлась и количество сохранилось. Сервер может не отправить счётчик строк. Проверим три разных случая на одной собственной таблице и подтвердим последнее значение чтением.

Путь данных этой программы
  1. UPDATE id=1 → rows_affected=1 → строка затронута
  2. UPDATE id=999 → rows_affected=0 → совпадений нет
  3. SET NOCOUNT ON → UPDATE id=1 → счётчик−1
  4. SELECT qty WHERE id=1 → фактически сохранено 9
  5. Вернуть настройку NOCOUNT и удалить свою таблицу

Сначала небольшой фрагмент

$stmt = sqlsrv_query($db, "UPDATE $q SET qty = ? WHERE id = ?", [7, 1]);
if ($stmt === false) {
    $errors = sqlsrv_errors();
} else {
    $count = sqlsrv_rows_affected($stmt);
    // Разобрать false, -1, 0 и положительное число отдельно.
    sqlsrv_free_stmt($stmt);
}

query возвращает ресурс либоfalse. rows_affected принимает ресурс, а не результат непроверенного вызова. === сравнивает и значение, и тип:0 не равенfalse при строгом сравнении. NOCOUNT — настройка сообщения SQL Server о количестве строк; она не отменяет UPDATE. Бит 512 в @@OPTIONS позволяет восстановить исходную настройку текущего сеанса.

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

НаблюдениеРезультат
Изменена существующая строка1
Несуществующий id9990
Счётчик при NOCOUNT ON-1
Сохранённое количество после NOCOUNT9
Исходная настройка восстановленада
Отличить успешное изменение, ноль строк и неизвестный счётчикСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s149Runtime();
try {

    $config=s149Config();$db=s149Connect($config);$runtime=s149Runtime($db,$config);

    $name=s149OwnTable($db,$owned,'affected','(id int NOT NULL PRIMARY KEY, qty int NOT NULL)');
    $q=s149Qualified($name);
    s149Command($db,"INSERT INTO $q (id,qty) VALUES (1,2)");
    $originalNoCount=(s149Read($db,'SELECT CASE WHEN (@@OPTIONS & 512) = 512 THEN 1 ELSE 0 END AS enabled')[0]['enabled']===1);
    try {
        s149Command($db,'SET NOCOUNT OFF');
        $update=s149Query($db,"UPDATE $q SET qty = ? WHERE id = ?",[7,1]);
        try {$changed=sqlsrv_rows_affected($update);if ($changed===false) {s149Failure('Счётчик изменения');}}
        finally {sqlsrv_free_stmt($update);}
        $missing=s149Query($db,"UPDATE $q SET qty = ? WHERE id = ?",[9,999]);
        try {$zero=sqlsrv_rows_affected($missing);if ($zero===false) {s149Failure('Счётчик отсутствующей записи');}}
        finally {sqlsrv_free_stmt($missing);}
        s149Command($db,'SET NOCOUNT ON');
        $silent=s149Query($db,"UPDATE $q SET qty = ? WHERE id = ?",[9,1]);
        try {$unknown=sqlsrv_rows_affected($silent);if ($unknown===false) {s149Failure('Счётчик NOCOUNT');}}
        finally {sqlsrv_free_stmt($silent);}
        $actual=s149Read($db,"SELECT qty FROM $q WHERE id = ?",[1])[0]['qty'];
    } finally {s149Command($db,$originalNoCount?'SET NOCOUNT ON':'SET NOCOUNT OFF');}
    $restored=(s149Read($db,'SELECT CASE WHEN (@@OPTIONS & 512) = 512 THEN 1 ELSE 0 END AS enabled')[0]['enabled']===1)===$originalNoCount;
    $rows=[['Изменена существующая строка',$changed],['Несуществующий id999',$zero],['Счётчик при NOCOUNT ON',$unknown],['Сохранённое количество после NOCOUNT',$actual],['Исходная настройка восстановлена',$restored]];
    $checks=['one_affected'=>$changed===1,'zero_not_error'=>$zero===0,'unknown_not_failure'=>$unknown===-1,
        'actual_write'=>$actual===9,'setting_restored'=>$restored];
    $observations=['ordinary_count'=>$changed,'missing_count'=>$zero,'nocount_count'=>$unknown,'readback_qty'=>$actual,'query_returns_resources'=>true];

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

s149Report('sqlsrv149-affected-count-receipt','Отличить успешное изменение, ноль строк и неизвестный счётчик',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Изменена существующая строка — 1
Несуществующий id999 — 0
Счётчик при NOCOUNT ON — -1
Сохранённое количество после NOCOUNT — 9
Исходная настройка восстановлена — да

Запустите в отдельной папке с подготовленным LocalDB. Программа получает реальные данные SQL Server, сохраняет HTML/JSON и удаляет только свою учебную таблицу.

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

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

Этот учебный readback подтверждает результат в отдельной таблице без конкурентов. На действующем сайте между UPDATE и SELECT другой клиент может изменить строку: там выбирают транзакцию, OUTPUT или проверку версии по бизнес-правилу. rows_affected не сообщает число строк SELECT и не доказывает, что новое значение отличается от старого. Не переносите на SQL Server специальные настройки affected rows из MySQL.

После неудачного query в rows_affected передаётся не ресурс, а false;−1 принимается за неудачную запись. Ситуация на Stack Overflow. Проверять каждую границу отдельно и сохранять диагностику до следующего вызова; неизвестный счётчик подтверждать чтением реальных данных.

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

Повторите UPDATE существующей строки тем же количеством. Отдельно проверьте счётчик и фактическое значение; не считайте его автоматически числом отличавшихся значений.

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

В SQL Server строка, совпавшая с WHERE в UPDATE, учитывается даже при присваивании прежнего значения. При обычной отправке счётчика ожидается 1, а сохранённое количество остаётся прежним.

Выбрать операцию SQLSRV по действию

ФункцияКогда нужна
sqlsrv_queryОдин раз выполнить параметризованный запрос и получить ресурс результата.
sqlsrv_prepareСвязать SQL с параметрами для повторных выполнений.
sqlsrv_executeВыполнить подготовленную команду с текущими связанными значениями.
sqlsrv_field_metadataУзнать имена и SQL-типы полей до преобразования результата.
sqlsrv_fetch_arrayПрочитать одну строку, отдельно отличая конец результата и ошибку.
sqlsrv_free_stmtОсвободить ресурс statement после чтения или отказа.
sqlsrv_rows_affectedРазличить изменённые строки, отсутствие совпадений и неизвестное количество.

Сохранить правила обмена в своём приложении

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

Вернуться к соединению и диагностике или к общим принципам получения нужного экрана.

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

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

sqlsrv_queryОдин раз выполнить параметризованный запрос и получить ресурс результата.

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

sqlsrv_query(resource $conn, string $sql, array $params = [], array $options = []): resource|false

Один раз выполнить параметризованный запрос и получить ресурс результата.

Действие в задаче накладной
  1. id=1 → массив параметров[1]
  2. query: SELECT invoice_no,qty WHERE id=?
  3. Возвращён statement → fetch_array → INV-149-A, qty2
ПараметрНазначение
$connОткрытый ресурс sqlsrv_connect; это не PDO и не имя сервера.
$sqlТекст T-SQL с позиционными ? для значений.
$paramsПараметры в том же порядке: литерал, переменная или кортеж [значение, направление, PHP-тип, SQL-тип].
$optionsНастройки команды из таблицы ниже; можно передать пустой массив.

Ресурс означает, что команда подготовлена и выполнена; строки ещё читаются отдельно. false означает отказ, после которого сразу сохраните sqlsrv_errors().

Направление по умолчанию — SQLSRV_PARAM_IN. PHP-тип задаётся SQLSRV_PHPTYPE_*, серверный — SQLSRV_SQLTYPE_*. Не путайте строковое число с выбранным SQL DECIMAL. Параметры OUT/INOUT читаются после завершения всех возвращённых результатов; для строк задаётся достаточная длина.

Настройка командыСмысл и граница
QueryTimeoutПоложительное число секунд; без ограничения по умолчанию. Ограничивает команду, не заменяет LoginTimeout подключения.
SendStreamParamsAtExectrue по умолчанию: отправить поток при выполнении; false: отправлять частями через sqlsrv_send_stream_data.
ScrollableSQLSRV_CURSOR_FORWARD по умолчанию; STATIC, DYNAMIC, KEYSET или CLIENT_BUFFERED выбирают поведение курсора.
ClientBufferMaxKBSizeПоложительный лимит буфера клиентского курсора в КБ; обычное значение 10240. Не следует буферизовать весь каталог ради страницы.
ReturnDatesAsStringsfalse по умолчанию: DateTime; true: строка. Настройка команды перекрывает настройку соединения.
FormatDecimalsfalse по умолчанию. true добавляет ведущий ноль в десятичные строки меньше единицы; данные в таблице не изменяет.
DecimalPlaces0–4 для отображения money/smallmoney при FormatDecimals=true; не управляет scale столбца DECIMAL. Иные значения игнорируются.
DataClassificationПри true запрашивает метаданные классификации полей, если сервер их предоставляет. Драйвер 5.8+, SQL Server2019+, ODBC17.4.2+; по умолчанию false.

Показать один счёт

SELECT выбирает поля и нужный ID на сервере. Параметр не подставляется вручную внутрь SQL.

$stmt = sqlsrv_query($db, "SELECT invoice_no, qty FROM $q WHERE id = ?", [1]);
if ($stmt === false) { $errors = sqlsrv_errors(); }

Это фрагмент после подготовки соединения и таблицы; полная запускаемая программа: перейти к сценарию и его результату.

Сохранить подпись и точную сумму

В одной команде передаются Unicode-текст и строка DECIMAL. query подходит для однократной записи, а возвращаемый ресурс не является вставленным объектом.

$params = [
    [$title, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING("UTF-8"), SQLSRV_SQLTYPE_NVARCHAR("80")],
    [$price, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING("UTF-8"), SQLSRV_SQLTYPE_DECIMAL(19, 4)]
];
$stmt = sqlsrv_query($db, "INSERT INTO $q (id, title, price) VALUES (1, ?, ?)", $params);

Это фрагмент после подготовки соединения и таблицы; полная запускаемая программа: перейти к сценарию и его результату.

? заменяет значение, не имя таблицы, поле или ASC/DESC. Идентификатор $q в самостоятельном примере создан и проверен helper; значение формы сюда не попадает. Для повторного выполнения одной структуры используйте prepare/execute. Не передавайте false дальше в fetch или rows_affected.

Связанные действия: sqlsrv_query, sqlsrv_prepare, sqlsrv_execute, sqlsrv_fetch_array, диагностика SQLSRV.

sqlsrv_prepareСвязать SQL с параметрами для повторных выполнений.

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

sqlsrv_prepare(resource $conn, string $sql, array $params = [], array $options = []): resource|false

Связать SQL с параметрами для повторных выполнений.

Действие в задаче накладной
  1. $id=1 → параметр связан с &$id
  2. prepare сохраняет SQL WHERE id=? и связь параметра
  3. execute при id1 → INV-149-A; при id2 → INV-149-B
ПараметрНазначение
$connОткрытое соединение SQLSRV.
$sqlПостоянная структура команды с ?.
$paramsЛитералы либо ссылки на переменные; полный кортеж [&$value, SQLSRV_PARAM_IN|OUT|INOUT, $phpType, $sqlType].
$optionsТе же настройки команды, что у query.

Ресурс подготовленной команды или false. Подготовка ещё не выполняет предметную запись: для неё вызывается execute.

Направление по умолчанию — SQLSRV_PARAM_IN. PHP-тип задаётся SQLSRV_PHPTYPE_*, серверный — SQLSRV_SQLTYPE_*. Не путайте строковое число с выбранным SQL DECIMAL. Параметры OUT/INOUT читаются после завершения всех возвращённых результатов; для строк задаётся достаточная длина.

Настройка командыСмысл и граница
QueryTimeoutПоложительное число секунд; без ограничения по умолчанию. Ограничивает команду, не заменяет LoginTimeout подключения.
SendStreamParamsAtExectrue по умолчанию: отправить поток при выполнении; false: отправлять частями через sqlsrv_send_stream_data.
ScrollableSQLSRV_CURSOR_FORWARD по умолчанию; STATIC, DYNAMIC, KEYSET или CLIENT_BUFFERED выбирают поведение курсора.
ClientBufferMaxKBSizeПоложительный лимит буфера клиентского курсора в КБ; обычное значение 10240. Не следует буферизовать весь каталог ради страницы.
ReturnDatesAsStringsfalse по умолчанию: DateTime; true: строка. Настройка команды перекрывает настройку соединения.
FormatDecimalsfalse по умолчанию. true добавляет ведущий ноль в десятичные строки меньше единицы; данные в таблице не изменяет.
DecimalPlaces0–4 для отображения money/smallmoney при FormatDecimals=true; не управляет scale столбца DECIMAL. Иные значения игнорируются.
DataClassificationПри true запрашивает метаданные классификации полей, если сервер их предоставляет. Драйвер 5.8+, SQL Server2019+, ODBC17.4.2+; по умолчанию false.

Прочитать два разных счёта

&$id связывает statement с самой переменной; изменение её значения будет видно следующему execute.

$id = 1;
$stmt = sqlsrv_prepare($db, "SELECT invoice_no, qty FROM $q WHERE id = ?", [&$id]);
// Проверить false, выполнить, прочитать строки.
$id = 2; // Следующий execute использует 2.

Это фрагмент после подготовки соединения и таблицы; полная запускаемая программа: перейти к сценарию и его результату.

Повторно менять количество

Одна структура UPDATE и две связанные переменные удобны для последовательных позиций документа. Завершайте результат каждого выполнения.

$qty = 1; $id = 1;
$stmt = sqlsrv_prepare($db, "UPDATE $q SET qty = ? WHERE id = ?", [&$qty, &$id]);
// Изменять qty/id и вызывать execute для каждой операции.

Это фрагмент после подготовки соединения и таблицы; полная запускаемая программа: перейти к сценарию и его результату.

Для изменяемого параметра передавайте ссылку, а не копию значения. Не переиспользуйте эту же переменную как случайную переменную foreach по ссылке: изменится связанное хранилище. OUT/INOUT тоже требуют связанной переменной и завершения всех результатов до чтения выхода; потоки, null и DateTime нельзя использовать как выходные значения.

Связанные действия: sqlsrv_query, sqlsrv_prepare, sqlsrv_execute, sqlsrv_fetch_array, диагностика SQLSRV.

sqlsrv_executeВыполнить подготовленную команду с текущими связанными значениями.

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

sqlsrv_execute(resource $stmt): bool

Выполнить подготовленную команду с текущими связанными значениями.

Действие в задаче накладной
  1. $id изменён с 1 на 2
  2. execute($stmt) использует новое значение параметра
  3. bool сообщает выполнение; fetch_array читает INV-149-B
ПараметрНазначение
$stmtРесурс sqlsrv_prepare, который ещё не освобождён.

true — исполнение принято, false — ошибка. true не равно строкам SELECT и не равно количеству изменённых строк.

Получить текущий счёт

Сначала execute, затем fetch_array до null. Между повторениями меняется $id, а ресурс остаётся тем же.

$id = 1;
if (sqlsrv_execute($stmt) === false) { $errors = sqlsrv_errors(); }
// Прочитать результат полностью.
$id = 2;
// Выполнить и прочитать второй результат.

Это фрагмент после подготовки соединения и таблицы; полная запускаемая программа: перейти к сценарию и его результату.

Проверить отказ изменения

После execute(false) сохраните ошибки; нельзя выводить пользователю «сохранено» по одному наличию подготовленного ресурса.

$ok = sqlsrv_execute($update);
if ($ok === false) {
    $errors = sqlsrv_errors();
} else {
    $affected = sqlsrv_rows_affected($update);
}

Это фрагмент после подготовки соединения и таблицы; полная запускаемая программа: перейти к сценарию и его результату.

Не выполняйте ресурс после free_stmt. Подготовленные параметры живут вместе со statement и связанными переменными. Перечитывание или освобождение результата — отдельный этап; успешный execute не гарантирует прохождения последующего fetch.

Связанные действия: sqlsrv_query, sqlsrv_prepare, sqlsrv_execute, sqlsrv_fetch_array, диагностика SQLSRV.

sqlsrv_field_metadataУзнать имена и SQL-типы полей до преобразования результата.

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

sqlsrv_field_metadata(resource $stmt): array|false

Узнать имена и SQL-типы полей до преобразования результата.

Действие в задаче накладной
  1. SELECT title,price → statement
  2. field_metadata читает описание столбцов, не строку товара
  3. price: Precision19, Scale4 → проверить контракт экспорта
ПараметрНазначение
$stmtПодготовленная либо выполненная команда; метаданные допустимо запрашивать до execute и после него.

Массив описаний полей или false. Name — имя/псевдоним, Type — числовой SQL-тип, Size — длина текста либо байтов бинарного поля; Precision/Scale — точность и дробная часть, Nullable — YES/NO/UNKNOWN. Неприменимые сведения могут быть null.

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

Описание SQL DECIMAL и PHP-тип его прочитанного значения — разные сведения. Сохраняйте и точность, и строковое значение.

$fields = sqlsrv_field_metadata($stmt);
if ($fields === false) { $errors = sqlsrv_errors(); }
// Найти поле price по Name и проверить Precision/Scale.

Это фрагмент после подготовки соединения и таблицы; полная запускаемая программа: перейти к сценарию и его результату.

Различить дату и момент со смещением

Метаданные объясняют, почему дата без часового пояса и datetimeoffset требуют разных правил вывода.

$fields = sqlsrv_field_metadata($stmt);
foreach ($fields as $field) {
    echo $field["Name"], ": SQL type ", $field["Type"], "\n";
}

Это фрагмент после подготовки соединения и таблицы; полная запускаемая программа: перейти к сценарию и его результату.

Name относится к выражению результата, включая AS; одинаковые имена после JOIN не создают автоматически уникальные ключи. Nullable:0 — NULL запрещён,1 — разрешён,2 — неизвестно. Nullable UNKNOWN не означает «запрещён NULL». При DataClassification=true возможны дополнительные сведения о классификации и ранге; они не заменяют проверки прав приложения.

Связанные действия: sqlsrv_query, sqlsrv_prepare, sqlsrv_execute, sqlsrv_fetch_array, диагностика SQLSRV.

sqlsrv_fetch_arrayПрочитать одну строку, отдельно отличая конец результата и ошибку.

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

sqlsrv_fetch_array(resource $stmt, int $fetchType = SQLSRV_FETCH_BOTH, int $row = SQLSRV_SCROLL_NEXT, int $offset = 0): array|null|false

Прочитать одну строку, отдельно отличая конец результата и ошибку.

Действие в задаче накладной
  1. SELECT одной накладной → statement
  2. fetch_array(ASSOC) → массив с ключами invoice_no иqty
  3. Следующий вызов → null: чтение закончено; false: чтение сорвалось
ПараметрНазначение
$stmtВыполненный statement.
$fetchTypeASSOC — ключи-имена, NUMERIC — индексы от 0, BOTH — оба набора ключей (по умолчанию).
$rowДля прокручиваемого курсора: NEXT, PRIOR, FIRST, LAST, ABSOLUTE, RELATIVE. Если задаётся row, fetchType тоже указывается явно.
$offsetСмещение для ABSOLUTE/RELATIVE; первая строка имеет индекс 0. Возможность перехода определяется курсором.

array — строка, null — строк больше нет, false — ошибка чтения. Эти три состояния требуют строгого сравнения.

Показать строки без потери ошибки

Цикл с явными ветвями не принимает ошибку за пустой каталог. SQL уже ограничивает нужные строки и порядок.

while (true) {
    $row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
    if ($row === null) { break; }
    if ($row === false) { $errors = sqlsrv_errors(); break; }
    // Вывести выбранные поля row с HTML-экранированием.
}

Это фрагмент после подготовки соединения и таблицы; полная запускаемая программа: перейти к сценарию и его результату.

Вывести календарную дату

DateTime форматируется методом format; (string) и strtotime не превращают автоматически объект в нужную подпись.

$row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
if (is_array($row) && $row["delivery_date"] instanceof DateTimeInterface) {
    echo $row["delivery_date"]->format("d.m.Y");
}

Это фрагмент после подготовки соединения и таблицы; полная запускаемая программа: перейти к сценарию и его результату.

Задавайте уникальные AS для одноимённых полей JOIN; безымянное выражение получает пустой ключ. BOTH содержит одно значение под двумя ключами — count(row) не равно числу столбцов. Обычное чтение идёт вперёд; скроллинг требует соответствующей Scrollable-настройки. PHP.net в описании row ошибочно называет RELATIVE значением по умолчанию: рабочий последовательный вызов читает NEXT.

Связанные действия: sqlsrv_query, sqlsrv_prepare, sqlsrv_execute, sqlsrv_fetch_array, диагностика SQLSRV.

sqlsrv_free_stmtОсвободить ресурс statement после чтения или отказа.

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

sqlsrv_free_stmt(resource $stmt): bool

Освободить ресурс statement после чтения или отказа.

Действие в задаче накладной
  1. Данные накладной уже прочитаны в PHP-массив
  2. free_stmt освобождает ресурс выполненной команды
  3. PHP-массив остаётся; прежний statement больше не выполняют
ПараметрНазначение
$stmtОткрытый ресурс команды. Перед условной уборкой проверьте is_resource($stmt). Старое примечание Microsoft допускает null, но на проверенных PHP8.3.35/SQLSRV5.13.3 такой вызов вызывает TypeError.

true при освобождении, false при ошибке операции; неверный PHP-тип или уже закрытый ресурс может вызвать TypeError. После освобождения caller-переменная в проверенном профиле имеет resource(closed), а не null; присвойте null явно. Освобождённый statement повторно выполнять нельзя.

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

Ресурс освобождается в finally, а сохранённый PHP-массив остаётся доступен.

try {
    $row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
    // Обработать row/null/false.
} finally {
    if (is_resource($stmt)) { sqlsrv_free_stmt($stmt); }
    $stmt = null;
}

Это фрагмент после подготовки соединения и таблицы; полная запускаемая программа: перейти к сценарию и его результату.

Закончить серию повторных выполнений

Освобождайте подготовленный statement после последнего execute, а не перед следующим использованием.

// Все prepare/execute/fetch этой серии завершены.
$released = sqlsrv_free_stmt($prepared);
$prepared = null;
// Соединение db можно использовать для новой команды.

Это фрагмент после подготовки соединения и таблицы; полная запускаемая программа: перейти к сценарию и его результату.

cancel сохраняет возможность повторного execute подготовленной команды; free_stmt освобождает её ресурс. Не используйте освобождение как способ отката записи. Старое примечание PHP.net о прекращении незавершённой команды не является переносимым бизнес-контрактом: в исходниках драйвера 5.13.3 деструктор дочитывает ожидающие результаты перед освобождением. Управление транзакцией и явный rollback разбираются отдельно.

Связанные действия: sqlsrv_query, sqlsrv_prepare, sqlsrv_execute, sqlsrv_fetch_array, диагностика SQLSRV.

sqlsrv_rows_affectedРазличить изменённые строки, отсутствие совпадений и неизвестное количество.

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

sqlsrv_rows_affected(resource $stmt): int|false

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

Действие в задаче накладной
  1. UPDATE qty=7 WHERE id=1 → выполненный statement
  2. rows_affected →1 при обычном сообщении сервера
  3. id999 →0; NOCOUNT ON →−1; отказ самого вызова →false
ПараметрНазначение
$stmtВыполненный ресурс INSERT, UPDATE или DELETE.

Неотрицательное число — количество затронутых строк;0 — ни одной;−1 — количество недоступно;false — ошибка. Для количества строк SELECT служат другие операции.

Подтвердить изменение заказа

Сначала проверяется успешность query/execute, затем счётчик. Строгое ===false не путает 0 с ошибкой.

$count = sqlsrv_rows_affected($stmt);
if ($count === false) { $errors = sqlsrv_errors(); }
elseif ($count === -1) { /* Число не сообщено. */ }
elseif ($count === 0) { /* Нет совпавшей строки. */ }
else { /* Получено конкретное количество. */ }

Это фрагмент после подготовки соединения и таблицы; полная запускаемая программа: перейти к сценарию и его результату.

Проверить команду с NOCOUNT

Если сервер не отправляет счётчик, результат операции подтверждается данными или явной квитанцией SQL, а не предположением «−1 значит провал».

$stmt = sqlsrv_query($db, "UPDATE $q SET qty = ? WHERE id = ?", [9, 1]);
if ($stmt !== false) {
    $count = sqlsrv_rows_affected($stmt);
    // При NOCOUNT ON ожидается отсутствие сообщения о числе.
}

Это фрагмент после подготовки соединения и таблицы; полная запускаемая программа: перейти к сценарию и его результату.

Не передавайте false вместо statement и не подавляйте ошибки символом@. Значение−1 не является доказательством отсутствия записи. SELECT COUNT(*) считает строки по фильтру на сервере; rows_affected не заменяет этот запрос. Для конкурентного изменения нужна отдельная бизнес-квитанция или транзакционный сценарий.

Связанные действия: sqlsrv_query, sqlsrv_prepare, sqlsrv_execute, sqlsrv_fetch_array, диагностика SQLSRV.

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

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

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

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

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