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

Дерево руководства
Глава 51 · После первого подключения SQLSRV и параметризованного SELECT
SQLSRV: параметры, Unicode, точные цены и даты в результате
Связанные параметры, Unicode без потерь, точная сумма, календарная дата и проверяемое изменение: четыре самостоятельных сценария SQLSRV.
От номера счёта к нужной строке на экране
Оператор открывает счёт, меняет количество и показывает дату доставки. Экрану нужны конкретные поля одной записи, а не вся таблица. SQLSRV должен передать номер как параметр, вернуть кириллицу без потерь, сохранить точную цену и дать понять, что именно произошло при записи.
- PHP: id=1 → параметр?
- SQL Server: WHERE id=? выбирает нужную запись
- SQLSRV: строка содержит title, DECIMAL и DateTime
- PHP: форматирует дату, сохраняет точную цену строкой, экранирует текст
- 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 выполняет её с текущим значением связанной переменной. Сервер возвращает одну нужную строку на каждый экран.
- $id=1 → параметр по ссылке &$id → WHERE id=?
- execute → INV-149-A, qty=2 → чтение до конца
- $id=2 → тот же слот параметра получает 2
- execute того же statement → INV-149-B, qty=7
- 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 не обязан хранить все её десятичные разряды.
- UTF-8 строка «Кабель “Север”» → PHP-параметр STRING(UTF-8)
- SQL NVARCHAR(80) хранит текст; DECIMAL(19,4) хранит 19 цифр, из них 4 после точки
- SELECT title,price WHERE id=1 → одна строка
- SQLSRV возвращает DECIMAL строкой → JSON сохраняет все цифры
- 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 / Scale | 19 / 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 хранит числовое смещение. Программа читает одну запись в двух режимах и показывает различия.
- SQL date=2026-11-08 → DateTime → format(d.m.Y) → 08.11.2026
- SQL datetime2(6) → время с 6 дробными цифрами, без сохранённого пояса
- SQL datetimeoffset(6) → время+03:00 → подпись со смещением
- ReturnDatesAsStrings=true на команде → строковые поля
- 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 — пустая строка | да |
<?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.
Отличить успешное изменение, ноль строк и неизвестный счётчик
Оператор сохраняет количество товара. Сообщение «Готово» должно опираться на реальный исход: запрос выполнился, нужная запись нашлась и количество сохранилось. Сервер может не отправить счётчик строк. Проверим три разных случая на одной собственной таблице и подтвердим последнее значение чтением.
- UPDATE id=1 → rows_affected=1 → строка затронута
- UPDATE id=999 → rows_affected=0 → совпадений нет
- SET NOCOUNT ON → UPDATE id=1 → счётчик−1
- SELECT qty WHERE id=1 → фактически сохранено 9
- Вернуть настройку 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 |
| Несуществующий id999 | 0 |
| Счётчик при NOCOUNT ON | -1 |
| Сохранённое количество после NOCOUNT | 9 |
| Исходная настройка восстановлена | да |
<?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Один раз выполнить параметризованный запрос и получить ресурс результата.
- id=1 → массив параметров[1]
- query: SELECT invoice_no,qty WHERE id=?
- Возвращён 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 подключения. |
| SendStreamParamsAtExec | true по умолчанию: отправить поток при выполнении; false: отправлять частями через sqlsrv_send_stream_data. |
| Scrollable | SQLSRV_CURSOR_FORWARD по умолчанию; STATIC, DYNAMIC, KEYSET или CLIENT_BUFFERED выбирают поведение курсора. |
| ClientBufferMaxKBSize | Положительный лимит буфера клиентского курсора в КБ; обычное значение 10240. Не следует буферизовать весь каталог ради страницы. |
| ReturnDatesAsStrings | false по умолчанию: DateTime; true: строка. Настройка команды перекрывает настройку соединения. |
| FormatDecimals | false по умолчанию. true добавляет ведущий ноль в десятичные строки меньше единицы; данные в таблице не изменяет. |
| DecimalPlaces | 0–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 с параметрами для повторных выполнений.
- $id=1 → параметр связан с &$id
- prepare сохраняет SQL WHERE id=? и связь параметра
- 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 подключения. |
| SendStreamParamsAtExec | true по умолчанию: отправить поток при выполнении; false: отправлять частями через sqlsrv_send_stream_data. |
| Scrollable | SQLSRV_CURSOR_FORWARD по умолчанию; STATIC, DYNAMIC, KEYSET или CLIENT_BUFFERED выбирают поведение курсора. |
| ClientBufferMaxKBSize | Положительный лимит буфера клиентского курсора в КБ; обычное значение 10240. Не следует буферизовать весь каталог ради страницы. |
| ReturnDatesAsStrings | false по умолчанию: DateTime; true: строка. Настройка команды перекрывает настройку соединения. |
| FormatDecimals | false по умолчанию. true добавляет ведущий ноль в десятичные строки меньше единицы; данные в таблице не изменяет. |
| DecimalPlaces | 0–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Выполнить подготовленную команду с текущими связанными значениями.
- $id изменён с 1 на 2
- execute($stmt) использует новое значение параметра
- 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-типы полей до преобразования результата.
- SELECT title,price → statement
- field_metadata читает описание столбцов, не строку товара
- 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Прочитать одну строку, отдельно отличая конец результата и ошибку.
- SELECT одной накладной → statement
- fetch_array(ASSOC) → массив с ключами invoice_no иqty
- Следующий вызов → null: чтение закончено; false: чтение сорвалось
| Параметр | Назначение |
|---|---|
| $stmt | Выполненный statement. |
| $fetchType | ASSOC — ключи-имена, 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 после чтения или отказа.
- Данные накладной уже прочитаны в PHP-массив
- free_stmt освобождает ресурс выполненной команды
- 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Различить изменённые строки, отсутствие совпадений и неизвестное количество.
- UPDATE qty=7 WHERE id=1 → выполненный statement
- rows_affected →1 при обычном сообщении сервера
- 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.
Источники и обсуждения
- PHP Manual — function.sqlsrv-query
- PHP Manual — function.sqlsrv-prepare
- PHP Manual — function.sqlsrv-execute
- PHP Manual — function.sqlsrv-field-metadata
- PHP Manual — function.sqlsrv-fetch-array
- PHP Manual — function.sqlsrv-free-stmt
- PHP Manual — function.sqlsrv-rows-affected
- Microsoft — sqlsrv-prepare
- Microsoft — default-php-data-types
- Microsoft — formatting-decimals-sqlsrv-driver
- Microsoft — how-to-send-and-retrieve-utf-8-data-using-built-in-utf-8-support
- Microsoft — how-to-retrieve-date-and-time-type-as-strings-using-the-sqlsrv-driver
- Microsoft — sqlsrv-field-metadata
- Microsoft — how-to-retrieve-output-parameters-using-the-sqlsrv-driver
- Microsoft — cursor-types-sqlsrv-driver
- Microsoft SQLSRV5.13.3 — освобождение statement
- Microsoft — SET NOCOUNT
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


