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

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

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

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

Глава 57 · После параметризованных SQLSRV-запросов и чтения результатов

SQLSRV: выходные параметры, точные значения и справочник констант

Счёт 179.00, резерв 3 → 7, Unicode-квитанция и точные числовые строки: рабочие OUT/INOUT-процедуры плюс все группы констант SQLSRV.

Сумма, остаток и квитанция: какой ответ нужен приложению

После вызова базы сайт часто должен показать две вещи: нужные строки и отдельный итог, остаток или квитанцию. Например, для счёта 71 — три позиции и 179.00 рубля; для резерва 3 единиц — остаток 7. В SQL Server такую операцию можно оформить хранимой процедурой. Это именованный блок SQL на сервере, который принимает параметры и выполняет заданные действия; здесь каждая программа сама создаёт свою учебную процедуру и данные.

Параметр — место для значения в объявлении процедуры. Аргумент — конкретное значение при вызове. Направление задаёт, откуда оно приходит: IN из PHP, OUT из базы, INOUT в обе стороны. Это похоже на бланк: в одно поле вы вписываете номер заказа, другое заполняет склад. Аналогия описывает роли полей, но не сетевой протокол: драйвер выдаёт выходные значения только после завершения результатов.

От запроса пользователя к ответу
  1. PHP хранит invoiceId 71 и total 0
  2. CALL связывает ? с параметрами процедуры
  3. Процедура выполняет SQL только для invoiceId 71
  4. PHP читает строки SELECT и проверяет конец всех результатов
  5. OUT обновляет total, приложение показывает итог
Что нужно страницеКакой контракт подходит
Отдельный итог для выбранного счётаOUT:17900 копеек
Запрос резерва должен стать остаткомINOUT:3 → 7
Короткий текст с кириллицей без потери символовРазмер OUT и UTF-8
Точные цифры суммы/внешнего номераСтроковые DECIMAL и BIGINT
Найти имя режима и понять его ограниченияПолный сгруппированный каталог констант

Эта глава продолжает подключение и типизированные параметры SQLSRV. Процедуры нужны не для каждого SELECT: простой параметризованный запрос зачастую понятнее. OUT удобно использовать при работе с уже существующим интерфейсом SQL Server или небольшим отдельным результатом операции. Для большого списка строк, документа, NULL или DateTime обычно выбирают набор результата SELECT и обычное чтение.

Разобрать вызов по элементам: ссылки, направления, PHP-тип и SQL-тип

Общая запись одного расширенного параметра: [значение или ссылка, направление, тип PHP, тип SQL]. Массив таких описаний передаётся третьим аргументом sqlsrv_query или sqlsrv_prepare. Порядок строго соответствует знакам ? слева направо. Последние элементы необязательны, но явный контракт помогает сохранить деньги, кириллицу и внешние номера.

$invoiceId = 71;
$total = 0;
$params = [
    [$invoiceId, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_INT, SQLSRV_SQLTYPE_INT],
    [&$total, SQLSRV_PARAM_OUT, SQLSRV_PHPTYPE_INT, SQLSRV_SQLTYPE_INT],
];
// $proc — известное имя процедуры, созданной полным примером ниже.
$stmt = sqlsrv_query($db, "{CALL $proc(?,?)}", $params);
ФрагментЧто он означает
$total=0Инициализация PHP-переменной до привязки. Не инициализируйте OUT значением null.
&$totalДрайвер должен изменить ту же переменную. Сама переменная должна жить до завершения команды.
SQLSRV_PARAM_OUTНаправление параметра; отдельно от OUTPUT в SQL-объявлении.
SQLSRV_PHPTYPE_INTPHP получит int, а не строку, поток или float.
SQLSRV_SQLTYPE_INTТип соответствующего SQL-параметра; тип PHP и тип SQL — два разных решения.
{CALL $proc(?,?)}Каноническая запись вызова процедуры. ? привязывает значение; имя $proc не может быть получено из формы.
statement / falseВозврат query/prepare — дескриптор команды либо ошибка; OUT не является возвращаемым значением функции PHP.

Процедура может одновременно выдавать строки SELECT, сообщения о числе изменений и выходные значения. Текущий первый результат уже активен после выполнения. Сначала прочитайте его, затем переходите к следующему. true от next_result означает следующий результат, null — завершение, false — ошибку. Не используйте while(next_result(...)) без проверки false: так отказ будет похож на нормальный конец. При повторном sqlsrv_execute явно задавайте новые входные значения; не теряйте ссылки, заменяя связанный массив без подготовки новой команды.

OUT-параметр также отличается от SQL RETURN: RETURN в хранимой процедуре — отдельный целочисленный код завершения. Результат SELECT — набор строк. OUTPUT у UPDATE inserted.qty — способ получить изменённые значения строк. Эти механизмы не взаимозаменяемы; ниже они подписаны по своей роли.

Получить строки счёта и его итог через OUT

Страница счёта показывает три позиции и сумму 179.00 рубля. Процедура принимает номер 71, возвращает нужные строки и отдельно записывает итог в PHP-переменную. Соседний счёт 72 не загружается. Это полезно, когда существующий SQL-интерфейс уже отдаёт итог через OUTPUT и его нужно правильно подключить к сайту.

Как движутся значения
  1. PHP: invoiceId=71 и переменная total=0
  2. Первый ? → @invoiceId; второй ? → @total OUTPUT
  3. SQL выбирает только строки счёта 71 и считает SUM на сервере
  4. PHP читает текущие строки, затем next_result до null
  5. В total теперь 17900; интерфейс показывает 179.00

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

$total = 0;
$params = [71, [&$total, SQLSRV_PARAM_OUT,
    SQLSRV_PHPTYPE_INT, SQLSRV_SQLTYPE_INT]];
$stmt = sqlsrv_query($db, "{CALL $proc(?,?)}", $params);
// Прочитать все результаты и проверить ошибки; затем использовать $total.
// В полном примере: $total === 17900.

OUT — канал из процедуры в вызывающий PHP-код. & перед $total сохраняет ссылку на ту переменную, которую драйвер обновит. Знак ? — место значения, а не имя столбца. Две позиции массива соответствуют двум ? в том же порядке. Вложенный массив задаёт значение/ссылку, направление, тип PHP и тип SQL. SQL OUTPUT в объявлении процедуры и SQLSRV_PARAM_OUT в PHP нужны на разных сторонах. Функция sqlsrv_query возвращает statement либо false; сам итог приходит через $total. Переменная заранее инициализирована числом. finishContract152 показана в полном коде: она читает текущий результат, отличает false от null и не теряет первый набор строк.

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

НаблюдениеРезультат
Счёт71
Получено строк3
Номера строк1, 2, 3
Итог, копеек17900
К оплате, рублей179.00
PHP-тип итогаint
Получить строки счёта и его итог через OUTСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s152Runtime();
try {

    $config=s152Config();$db=s152Connect($config);$runtime=s152Runtime($db,$config);

    function finishContract152($stmt): array {
        $sets=[];
        for ($result=0;$result<16;$result++) {
            $fields=sqlsrv_num_fields($stmt);
            if ($fields===false) {
                $errors=sqlsrv_errors(SQLSRV_ERR_ALL);
                return ['done'=>false,'sets'=>$sets,'diagnostics'=>s152Diagnostics($errors)];
            }
            if ($fields>0) {
                $rows=[];
                while (true) {
                    $row=sqlsrv_fetch_array($stmt,SQLSRV_FETCH_ASSOC);
                    if ($row===null) {break;}
                    if ($row===false) {
                        $errors=sqlsrv_errors(SQLSRV_ERR_ALL);
                        return ['done'=>false,'sets'=>$sets,'diagnostics'=>s152Diagnostics($errors)];
                    }
                    $rows[]=$row;
                }
                $sets[]=$rows;
            }
            $next=sqlsrv_next_result($stmt);
            if ($next===false) {
                $errors=sqlsrv_errors(SQLSRV_ERR_ALL);
                return ['done'=>false,'sets'=>$sets,'diagnostics'=>s152Diagnostics($errors)];
            }
            if ($next===null) {return ['done'=>true,'sets'=>$sets,'diagnostics'=>[]];}
        }
        throw new RuntimeException('Учебная процедура неожиданно вернула больше 16 результатов.');
    }

    $name=s152OwnTable($db,$owned,'invoice','(invoice_id int NOT NULL,line_no int NOT NULL,qty int NOT NULL,unit_kopecks int NOT NULL,PRIMARY KEY(invoice_id,line_no))');
    $q=s152Qualified($name);
    s152Command($db,"INSERT INTO $q(invoice_id,line_no,qty,unit_kopecks) VALUES(71,1,1,10000),(71,2,2,2700),(71,3,1,2500),(72,1,9,900)");
    $procName=s152OwnProcedure($db,$owned,'invoice_total',"@invoiceId int,@total int OUTPUT AS BEGIN SET NOCOUNT ON;
        SELECT line_no,qty,unit_kopecks,qty*unit_kopecks AS line_total FROM $q WHERE invoice_id=@invoiceId ORDER BY line_no;
        SELECT @total=COALESCE(SUM(qty*unit_kopecks),0) FROM $q WHERE invoice_id=@invoiceId; END");
    $proc=s152Qualified($procName);$invoiceId=71;$total=0;
    $params=[[$invoiceId,SQLSRV_PARAM_IN,SQLSRV_PHPTYPE_INT,SQLSRV_SQLTYPE_INT],
        [&$total,SQLSRV_PARAM_OUT,SQLSRV_PHPTYPE_INT,SQLSRV_SQLTYPE_INT]];
    $stmt=s152Query($db,"{CALL $proc(?,?)}",$params);
    try {$result=finishContract152($stmt);} finally {sqlsrv_free_stmt($stmt);}
    if (!$result['done']) {throw new RuntimeException('Не все результаты счёта прочитаны.');}
    $lines=$result['sets'][0]??[];
    $amount=intdiv($total,100).'.'.str_pad((string)($total%100),2,'0',STR_PAD_LEFT);
    $rows=[['Счёт',$invoiceId],['Получено строк',count($lines)],['Номера строк',implode(', ',array_column($lines,'line_no'))],['Итог, копеек',$total],['К оплате, рублей',$amount],['PHP-тип итога',get_debug_type($total)]];
    $checks=['all_results_finished'=>$result['done'],'three_invoice_lines'=>count($lines)===3,
        'correct_line_order'=>array_column($lines,'line_no')===[1,2,3],
        'line_totals'=>array_column($lines,'line_total')===[10000,5400,2500],
        'one_result_set'=>count($result['sets'])===1,'output_exact'=>$total===17900,'visible_amount'=>$amount==='179.00'];
    $observations=['invoice_lines'=>$lines,'output_total'=>$total,'completion'=>$result];

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

s152Report('sqlsrv152-param-output','Получить строки счёта и его итог через OUT',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Счёт — 71
Получено строк — 3
Номера строк — 1, 2, 3
Итог, копеек — 17900
К оплате, рублей — 179.00
PHP-тип итога — int

Самостоятельная программа включает lab.php и README.txt. Она создаёт свою небольшую таблицу и процедуру SQL Server, вызывает процедуру с параметрами, сохраняет result.html/result.json/metrics.json и удаляет только собственные учебные объекты. Процедура создаётся одним SQL-вызовом без GO: GO — команда внешнего клиента, а не часть отправляемого T-SQL.

Фактический результат программы · Данные результата JSON.

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

OUT нельзя считать готовым до получения всех результатов процедуры: отдельный SELECT может прийти раньше. SET NOCOUNT ON убирает лишние сообщения о количестве изменённых строк, но не убирает наборы SELECT. Здесь массив PHP ограничен тремя позициями одного счёта; не используйте этот учебный сборщик для выгрузки всей таблицы. int подходит только в пределах его диапазона; для крупных сумм используйте точный DECIMAL из последнего сценария. Пустой счёт в этом контракте даёт 0; если это ошибка предметной области, добавьте отдельную проверку наличия счёта.

Процедура возвращала строки и выходной флаг, но приложение получало непонятное значение и пыталось читать строки после перехода к последнему результату. Исходная ситуация на Stack Overflow. В нашем сценарии сначала читаются три нужные строки счёта, затем подтверждается завершение всех результатов, и только после этого используется переменная OUT. Направление, PHP-тип и ссылка заданы явно.

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

Запросите счёт 72. Какие строки и итог должны появиться без фильтрации массива PHP?

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

Процедура вернёт одну строку с qty=9 и unit_kopecks=900, итог 8100 копеек. Меняется только входной параметр; WHERE и SUM остаются на сервере.

Передать количество резерва и получить остаток через INOUT

На складе 10 единиц кабеля, заказу нужны 3. После операции сайт должен показать «зарезервировано 3, осталось 7». В INOUT одна привязанная переменная сначала несёт запрос 3, затем принимает ответ 7. Сохраним исходное количество отдельно, чтобы квитанция не перепутала запрос и остаток.

Как движутся значения
  1. amount=3: желаемый резерв
  2. INOUT передаёт 3 в SQL-параметр @amount
  3. Один UPDATE уменьшает qty 10 → 7, только если хватает остатка
  4. OUTPUT inserted.qty фиксирует результат именно этого UPDATE
  5. После всех результатов amount=7; requested остаётся 3

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

$amount = 3;
$requested = $amount;
$params = [1, [&$amount, SQLSRV_PARAM_INOUT,
    SQLSRV_PHPTYPE_INT, SQLSRV_SQLTYPE_INT]];
// После успешного вызова и чтения всех результатов:
// $requested === 3; $amount === 7.

IN передаёт значение только в SQL; OUT получает его из SQL; INOUT делает оба действия. OUTPUT в SQL-объявлении используется и для OUT, и для INOUT; клиент задаёт направление отдельно. @requested — локальная SQL-переменная, а @changed — небольшая табличная переменная внутри процедуры, не новая постоянная таблица. SQL OUTPUT inserted.qty берёт значение непосредственно изменённой строки. Это другой смысл слова OUTPUT, чем у выходного параметра: сначала UPDATE кладёт остаток в @changed, затем SELECT присваивает его @amount. Условие достаточного остатка находится в самом UPDATE.

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

НаблюдениеРезультат
ТоварКабель-3x2.5
Запрошено к резерву3
INOUT после завершения7
Остаток в таблице7
Результат процедурызарезервировано 3, осталось 7
Передать количество резерва и получить остаток через INOUTСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s152Runtime();
try {

    $config=s152Config();$db=s152Connect($config);$runtime=s152Runtime($db,$config);

    function finishContract152($stmt): array {
        $sets=[];
        for ($result=0;$result<16;$result++) {
            $fields=sqlsrv_num_fields($stmt);
            if ($fields===false) {
                $errors=sqlsrv_errors(SQLSRV_ERR_ALL);
                return ['done'=>false,'sets'=>$sets,'diagnostics'=>s152Diagnostics($errors)];
            }
            if ($fields>0) {
                $rows=[];
                while (true) {
                    $row=sqlsrv_fetch_array($stmt,SQLSRV_FETCH_ASSOC);
                    if ($row===null) {break;}
                    if ($row===false) {
                        $errors=sqlsrv_errors(SQLSRV_ERR_ALL);
                        return ['done'=>false,'sets'=>$sets,'diagnostics'=>s152Diagnostics($errors)];
                    }
                    $rows[]=$row;
                }
                $sets[]=$rows;
            }
            $next=sqlsrv_next_result($stmt);
            if ($next===false) {
                $errors=sqlsrv_errors(SQLSRV_ERR_ALL);
                return ['done'=>false,'sets'=>$sets,'diagnostics'=>s152Diagnostics($errors)];
            }
            if ($next===null) {return ['done'=>true,'sets'=>$sets,'diagnostics'=>[]];}
        }
        throw new RuntimeException('Учебная процедура неожиданно вернула больше 16 результатов.');
    }

    $name=s152OwnTable($db,$owned,'stock','(id int NOT NULL PRIMARY KEY,sku nvarchar(40) NOT NULL,qty int NOT NULL CHECK(qty>=0))');
    $q=s152Qualified($name);
    s152Command($db,"INSERT INTO $q(id,sku,qty) VALUES(1,N'Кабель-3x2.5',10)");
    $procName=s152OwnProcedure($db,$owned,'reserve',"@id int,@amount int OUTPUT AS BEGIN SET NOCOUNT ON;
        DECLARE @requested int=@amount; DECLARE @changed TABLE(qty int NOT NULL);
        UPDATE $q SET qty=qty-@requested OUTPUT inserted.qty INTO @changed(qty)
            WHERE id=@id AND @requested>0 AND qty>=@requested;
        IF @@ROWCOUNT<>1 THROW 50001,N'Недостаточно остатка или неверный запрос',1;
        SELECT @amount=qty FROM @changed;
        SELECT @requested AS reserved,@amount AS remaining; END");
    $proc=s152Qualified($procName);$productId=1;$amount=3;$requested=$amount;
    $params=[[$productId,SQLSRV_PARAM_IN,SQLSRV_PHPTYPE_INT,SQLSRV_SQLTYPE_INT],
        [&$amount,SQLSRV_PARAM_INOUT,SQLSRV_PHPTYPE_INT,SQLSRV_SQLTYPE_INT]];
    $stmt=s152Query($db,"{CALL $proc(?,?)}",$params);
    try {$result=finishContract152($stmt);} finally {sqlsrv_free_stmt($stmt);}
    if (!$result['done']) {throw new RuntimeException('Резерв не завершён.');}
    $saved=s152Read($db,"SELECT sku,qty FROM $q WHERE id=?",[$productId])[0];
    $rows=[['Товар',$saved['sku']],['Запрошено к резерву',$requested],['INOUT после завершения',$amount],['Остаток в таблице',$saved['qty']],['Результат процедуры','зарезервировано 3, осталось 7']];
    $checks=['all_results_finished'=>$result['done'],'input_preserved_for_receipt'=>$requested===3,
        'inout_now_remaining'=>$amount===7,'database_updated_once'=>$saved['qty']===7,
        'receipt_matches'=>$result['sets']===[[['reserved'=>3,'remaining'=>7]]]];
    $observations=['requested'=>$requested,'remaining'=>$amount,'saved'=>$saved,'results'=>$result['sets']];

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

s152Report('sqlsrv152-param-inout','Передать количество резерва и получить остаток через INOUT',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Товар — Кабель-3x2.5
Запрошено к резерву — 3
INOUT после завершения — 7
Остаток в таблице — 7
Результат процедуры — зарезервировано 3, осталось 7

Самостоятельная программа включает lab.php и README.txt. Она создаёт свою небольшую таблицу и процедуру SQL Server, вызывает процедуру с параметрами, сохраняет result.html/result.json/metrics.json и удаляет только собственные учебные объекты. Процедура создаётся одним SQL-вызовом без GO: GO — команда внешнего клиента, а не часть отправляемого T-SQL.

Фактический результат программы · Данные результата JSON.

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

Нельзя сначала прочитать остаток в PHP, посчитать и затем без условия записать его обратно: параллельный заказ может изменить строку между этими действиями. Здесь один UPDATE проверяет и уменьшает запас атомарно; несколько связанных операций потребуют транзакции. При недостатке товара процедура делает THROW, а PHP проверяет как начальный вызов, так и последующие результаты. После ошибки OUT/INOUT не является подтверждением успеха. Повторный вызов с уже изменённой amount=7 запросит 7, поэтому перед повтором задавайте новое входное значение намеренно.

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

Перед повторным выполнением явно присвойте amount=2. Каким должен стать остаток после первого успешного резерва 3?

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

В таблице после первого вызова 7. Второй запрос 2 должен вернуть 5. Для безопасного повторения сетевого запроса дополнительно нужен идентификатор операции и защита от повторного списания; INOUT сам по себе этого не даёт.

Вернуть кириллицу полностью и распознать короткий буфер

После приёмки товара нужно показать «Получено: Кабель №7». Сначала получим эту строку целиком в UTF-8. Затем намеренно зададим клиентскую вместимость 4 вместо 80 и увидим управляемую ошибку обрезания. Сайт должен сообщить об отказе, а не выдавать обрезанный или ещё не завершённый OUT за готовую квитанцию.

Как движутся значения
  1. SQL: label=N'Кабель №7', выходной параметр nvarchar(80)
  2. Правильная привязка: PHP string UTF-8 + SQL NVARCHAR(80)
  3. Дочитать текущий результат и завершение → полная квитанция
  4. Повторная привязка NVARCHAR(4) → SQLSTATE 01004
  5. Диагностика сохраняется сразу; ошибочный OUT не используется

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

$receipt = '';
$binding = [&$receipt, SQLSRV_PARAM_OUT,
    SQLSRV_PHPTYPE_STRING('UTF-8'), SQLSRV_SQLTYPE_NVARCHAR('80')];
// После успешного завершения: 'Получено: Кабель №7'.

Кодировка PHP-строки и вместимость SQL-параметра — разные настройки. PHPTYPE_STRING('UTF-8') определяет PHP-представление. SQLTYPE_NVARCHAR('80') передаёт размер при привязке; в проверенном драйвере аргумент size имеет тип string, поэтому при strict_types используются кавычки. NVARCHAR(n) измеряется парами байтов UTF-16, а strlen PHP измеряет байты UTF-8; это не взаимозаменяемые числа. Кириллица этого примера занимает по одной UTF-16 единице на знак; символы вне BMP могут требовать две. finishContract152 проверяет не только sqlsrv_query и next_result, но и fetch_array: обрезание в проверенном драйвере проявилось при чтении строки.

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

НаблюдениеРезультат
Полная квитанцияПолучено: Кабель №7
PHP-тип квитанцииstring
Вместимость правильного параметраNVARCHAR(80)
Слишком короткий параметрNVARCHAR(4)
Обрезание распознано по SQLSTATE01004
Значение ошибочного OUTне используется
Исходная этикетка в базеКабель №7
Вернуть кириллицу полностью и распознать короткий буферСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s152Runtime();
try {

    $config=s152Config();$db=s152Connect($config);$runtime=s152Runtime($db,$config);

    function finishContract152($stmt): array {
        $sets=[];
        for ($result=0;$result<16;$result++) {
            $fields=sqlsrv_num_fields($stmt);
            if ($fields===false) {
                $errors=sqlsrv_errors(SQLSRV_ERR_ALL);
                return ['done'=>false,'sets'=>$sets,'diagnostics'=>s152Diagnostics($errors)];
            }
            if ($fields>0) {
                $rows=[];
                while (true) {
                    $row=sqlsrv_fetch_array($stmt,SQLSRV_FETCH_ASSOC);
                    if ($row===null) {break;}
                    if ($row===false) {
                        $errors=sqlsrv_errors(SQLSRV_ERR_ALL);
                        return ['done'=>false,'sets'=>$sets,'diagnostics'=>s152Diagnostics($errors)];
                    }
                    $rows[]=$row;
                }
                $sets[]=$rows;
            }
            $next=sqlsrv_next_result($stmt);
            if ($next===false) {
                $errors=sqlsrv_errors(SQLSRV_ERR_ALL);
                return ['done'=>false,'sets'=>$sets,'diagnostics'=>s152Diagnostics($errors)];
            }
            if ($next===null) {return ['done'=>true,'sets'=>$sets,'diagnostics'=>[]];}
        }
        throw new RuntimeException('Учебная процедура неожиданно вернула больше 16 результатов.');
    }

    $name=s152OwnTable($db,$owned,'labels','(id int NOT NULL PRIMARY KEY,label nvarchar(40) NOT NULL)');
    $q=s152Qualified($name);
    s152Command($db,"INSERT INTO $q(id,label) VALUES(1,?)",[
        ['Кабель №7',SQLSRV_PARAM_IN,SQLSRV_PHPTYPE_STRING('UTF-8'),SQLSRV_SQLTYPE_NVARCHAR('40')]]);
    $procName=s152OwnProcedure($db,$owned,'label_receipt',"@id int,@receipt nvarchar(80) OUTPUT AS BEGIN SET NOCOUNT ON;
        SELECT @receipt=N'Получено: '+label FROM $q WHERE id=@id;
        IF @@ROWCOUNT<>1 THROW 50003,N'Этикетка не найдена',1;
        SELECT @id AS receipt_id; END");
    $proc=s152Qualified($procName);$receipt='';
    $params=[1,[&$receipt,SQLSRV_PARAM_OUT,SQLSRV_PHPTYPE_STRING('UTF-8'),SQLSRV_SQLTYPE_NVARCHAR('80')]];
    $stmt=s152Query($db,"{CALL $proc(?,?)}",$params);
    try {$valid=finishContract152($stmt);} finally {sqlsrv_free_stmt($stmt);}
    if (!$valid['done']) {throw new RuntimeException('Полная квитанция не получена.');}
    // Deliberate local failure: the SQL parameter remains80; only the client binding is too short.
    $oldWarnings=sqlsrv_get_config('WarningsReturnAsErrors');
    if (!sqlsrv_configure('WarningsReturnAsErrors',true)) {s152Failure('Настройка учебной диагностики');}
    $short='';$bad=null;
    try {
        $shortParams=[1,[&$short,SQLSRV_PARAM_OUT,SQLSRV_PHPTYPE_STRING('UTF-8'),SQLSRV_SQLTYPE_NVARCHAR('4')]];
        $badStmt=sqlsrv_query($db,"{CALL $proc(?,?)}",$shortParams,['QueryTimeout'=>10]);
        if ($badStmt===false) {
            $errors=sqlsrv_errors(SQLSRV_ERR_ALL);
            $bad=['done'=>false,'sets'=>[],'diagnostics'=>s152Diagnostics($errors)];
        } else {
            try {$bad=finishContract152($badStmt);} finally {sqlsrv_free_stmt($badStmt);}
        }
        // Do not display or trust $short: output buffers after failure are not a valid receipt.
    } finally {
        if (!sqlsrv_configure('WarningsReturnAsErrors',$oldWarnings)) {s152Failure('Восстановление настройки диагностики');}
    }
    $truncated=false;
    foreach ($bad['diagnostics'] as $diagnostic) {
        if ($diagnostic['SQLSTATE']==='01004') {$truncated=true;}
    }
    $saved=s152Read($db,"SELECT label FROM $q WHERE id=?",[1])[0]['label'];
    $rows=[['Полная квитанция',$receipt],['PHP-тип квитанции',get_debug_type($receipt)],['Вместимость правильного параметра','NVARCHAR(80)'],['Слишком короткий параметр','NVARCHAR(4)'],['Обрезание распознано по SQLSTATE','01004'],['Значение ошибочного OUT','не используется'],['Исходная этикетка в базе',$saved]];
    $checks=['valid_finished'=>$valid['done'],'unicode_complete'=>$receipt==='Получено: Кабель №7',
        'valid_string'=>is_string($receipt),'small_buffer_failed'=>$bad['done']===false,
        'truncation_diagnostic'=>$truncated,'original_label_intact'=>$saved==='Кабель №7',
        'warning_policy_restored'=>sqlsrv_get_config('WarningsReturnAsErrors')===$oldWarnings];
    $observations=['complete_receipt'=>$receipt,'valid_results'=>$valid['sets'],
        'too_small'=>['done'=>$bad['done'],'diagnostics'=>$bad['diagnostics']],
        'original_label'=>$saved,'small_output_omitted'=>'Недостоверный OUT после ошибки не читается в отчёт.'];

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

s152Report('sqlsrv152-param-text-buffer','Вернуть кириллицу полностью и распознать короткий буфер',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Полная квитанция — Получено: Кабель №7
PHP-тип квитанции — string
Вместимость правильного параметра — NVARCHAR(80)
Слишком короткий параметр — NVARCHAR(4)
Обрезание распознано по SQLSTATE — 01004
Значение ошибочного OUT — не используется
Исходная этикетка в базе — Кабель №7

Самостоятельная программа включает lab.php и README.txt. Она создаёт свою небольшую таблицу и процедуру SQL Server, вызывает процедуру с параметрами, сохраняет result.html/result.json/metrics.json и удаляет только собственные учебные объекты. Процедура создаётся одним SQL-вызовом без GO: GO — команда внешнего клиента, а не часть отправляемого T-SQL.

Фактический результат программы · Данные результата JSON.

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

Для этого управляемого отказа WarningsReturnAsErrors временно включён и затем восстановлен. SQLSTATE 01004 сообщает о потере части данных; его нельзя заглушить настройкой и считать текст полным. Не увеличивайте всё автоматически до MAX: потоковые типы не поддерживаются как OUT. Большой документ возвращайте SELECT и читайте отдельно. Если сама процедура уже сузила данные до NVARCHAR(4), клиентский размер 80 исходный текст не восстановит. Веб-вывод квитанции всё равно требует HTML-экранирования; кодировка не заменяет его.

При возврате большого XML через OUT разработчик столкнулся с обрезанием длинного документа. Исходная ситуация на Stack Overflow. Размер результата и способ его передачи — часть интерфейса процедуры. Здесь короткая квитанция имеет явную вместимость NVARCHAR(80); большой XML или файл лучше вернуть SELECT и читать как результат/поток. Исторический порог из обсуждения не объявляется универсальным лимитом текущего драйвера.

Слишком короткий параметр самой SQL-процедуры незаметно обрезал входную строку ещё до последующей обработки. Исходная ситуация на Stack Overflow. Проверять обе стороны: размер SQL-параметра и контракт привязки PHP. Увеличение клиентского буфера не восстановит уже обрезанный сервером текст. Для отказа от слишком длинного ввода проверка должна видеть исходное значение до сужения типа.

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

Увеличьте только размер клиентской привязки с 4 до 80, оставив серверную процедуру прежней. Какая проверка теперь подтверждает исправление?

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

Нужны завершение всех результатов и строгое совпадение всей строки «Получено: Кабель №7», а не просто отсутствие исключения. Сравнение strlen с 80 бессмысленно: это разные единицы размера.

Передать точную сумму и большой номер без округления

При сверке документов нужно передать точную сумму с четырьмя знаками после точки и внешний номер 9223372036854775806. Здесь цифры важнее возможности немедленно сложить значения оператором + в PHP. Получим оба OUT-параметра строками и сохраним этот контракт в JSON: кавычки защитят номер от округления у получателя с ограниченной точностью чисел.

Как движутся значения
  1. SQL хранит total в DECIMAL(19,4), external_id в BIGINT
  2. PHP задаёт строковый выходной тип для обоих параметров
  3. Процедура выбирает одну запись по id
  4. После завершения total и externalId остаются точными строками
  5. JSON сохраняет цифры и четыре десятичных знака

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

$total = '0';
$binding = [&$total, SQLSRV_PARAM_OUT,
    SQLSRV_PHPTYPE_STRING('UTF-8'), SQLSRV_SQLTYPE_DECIMAL(19, 4)];
// После вызова: '123456789012345.6789', без преобразования во float.

DECIMAL(19,4) означает 19 десятичных цифр всего, из них 4 после точки. SQLSRV_PHPTYPE_STRING управляет представлением в PHP; SQLSRV_SQLTYPE_DECIMAL описывает серверную сторону. Для BIGINT серверный тип указывается отдельно, даже если PHP получает строку. На 64-битном PHP выбранный BIGINT помещается в int, но передача строкой также сохраняет его в JSON-потребителях с меньшей точностью чисел. В полном коде проверяется не только тип, но и совпадение всех цифр с независимым чтением таблицы.

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

НаблюдениеРезультат
Сумма DECIMAL(19,4)123456789012345.6789
PHP-тип суммыstring
Номер BIGINT9223372036854775806
PHP-тип номераstring
JSON для передачи{"total":"123456789012345.6789","external_id":"9223372036854775806"}
Передать точную сумму и большой номер без округленияСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s152Runtime();
try {

    $config=s152Config();$db=s152Connect($config);$runtime=s152Runtime($db,$config);

    function finishContract152($stmt): array {
        $sets=[];
        for ($result=0;$result<16;$result++) {
            $fields=sqlsrv_num_fields($stmt);
            if ($fields===false) {
                $errors=sqlsrv_errors(SQLSRV_ERR_ALL);
                return ['done'=>false,'sets'=>$sets,'diagnostics'=>s152Diagnostics($errors)];
            }
            if ($fields>0) {
                $rows=[];
                while (true) {
                    $row=sqlsrv_fetch_array($stmt,SQLSRV_FETCH_ASSOC);
                    if ($row===null) {break;}
                    if ($row===false) {
                        $errors=sqlsrv_errors(SQLSRV_ERR_ALL);
                        return ['done'=>false,'sets'=>$sets,'diagnostics'=>s152Diagnostics($errors)];
                    }
                    $rows[]=$row;
                }
                $sets[]=$rows;
            }
            $next=sqlsrv_next_result($stmt);
            if ($next===false) {
                $errors=sqlsrv_errors(SQLSRV_ERR_ALL);
                return ['done'=>false,'sets'=>$sets,'diagnostics'=>s152Diagnostics($errors)];
            }
            if ($next===null) {return ['done'=>true,'sets'=>$sets,'diagnostics'=>[]];}
        }
        throw new RuntimeException('Учебная процедура неожиданно вернула больше 16 результатов.');
    }

    $name=s152OwnTable($db,$owned,'exact','(id int NOT NULL PRIMARY KEY,total decimal(19,4) NOT NULL,external_id bigint NOT NULL)');
    $q=s152Qualified($name);
    $inputTotal='123456789012345.6789';$inputId='9223372036854775806';
    s152Command($db,"INSERT INTO $q(id,total,external_id) VALUES(1,?,?)",[
        [$inputTotal,SQLSRV_PARAM_IN,SQLSRV_PHPTYPE_STRING('UTF-8'),SQLSRV_SQLTYPE_DECIMAL(19,4)],
        [$inputId,SQLSRV_PARAM_IN,SQLSRV_PHPTYPE_STRING('UTF-8'),SQLSRV_SQLTYPE_BIGINT]]);
    $procName=s152OwnProcedure($db,$owned,'exact_receipt',"@id int,@total decimal(19,4) OUTPUT,@externalId bigint OUTPUT AS BEGIN SET NOCOUNT ON;
        SELECT @total=total,@externalId=external_id FROM $q WHERE id=@id;
        IF @@ROWCOUNT<>1 THROW 50002,N'Запись не найдена',1;
        SELECT @id AS receipt_id; END");
    $proc=s152Qualified($procName);$total='0';$externalId='0';
    $params=[1,[&$total,SQLSRV_PARAM_OUT,SQLSRV_PHPTYPE_STRING('UTF-8'),SQLSRV_SQLTYPE_DECIMAL(19,4)],
        [&$externalId,SQLSRV_PARAM_OUT,SQLSRV_PHPTYPE_STRING('UTF-8'),SQLSRV_SQLTYPE_BIGINT]];
    $stmt=s152Query($db,"{CALL $proc(?,?,?)}",$params);
    try {$result=finishContract152($stmt);} finally {sqlsrv_free_stmt($stmt);}
    if (!$result['done']) {throw new RuntimeException('Точные выходные значения не получены.');}
    $payload=json_encode(['total'=>$total,'external_id'=>$externalId],JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR);
    $decoded=json_decode($payload,true,512,JSON_THROW_ON_ERROR);
    $saved=s152Read($db,"SELECT total,external_id FROM $q WHERE id=?",[1])[0];
    $rows=[['Сумма DECIMAL(19,4)',$total],['PHP-тип суммы',get_debug_type($total)],['Номер BIGINT',$externalId],['PHP-тип номера',get_debug_type($externalId)],['JSON для передачи',$payload]];
    $checks=['all_results_finished'=>$result['done'],'decimal_exact'=>$total===$inputTotal,
        'bigint_exact'=>$externalId===$inputId,'both_strings'=>is_string($total)&&is_string($externalId),
        'json_preserves_contract'=>$decoded===['total'=>$inputTotal,'external_id'=>$inputId],
        'readback_agrees'=>$saved===['total'=>$inputTotal,'external_id'=>$inputId],
        'receipt_selected'=>$result['sets']===[[['receipt_id'=>1]]]];
    $observations=['output'=>['total'=>$total,'external_id'=>$externalId],'database'=>$saved,'json'=>$payload];

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

s152Report('sqlsrv152-param-exact-output','Передать точную сумму и большой номер без округления',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Сумма DECIMAL(19,4) — 123456789012345.6789
PHP-тип суммы — string
Номер BIGINT — 9223372036854775806
PHP-тип номера — string
JSON для передачи — {"total":"123456789012345.6789","external_id":"9223372036854775806"}

Самостоятельная программа включает lab.php и README.txt. Она создаёт свою небольшую таблицу и процедуру SQL Server, вызывает процедуру с параметрами, сохраняет result.html/result.json/metrics.json и удаляет только собственные учебные объекты. Процедура создаётся одним SQL-вызовом без GO: GO — команда внешнего клиента, а не часть отправляемого T-SQL.

Фактический результат программы · Данные результата JSON.

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

Строковый формат не превращает + в точную десятичную арифметику. Для расчётов используйте SQL DECIMAL либо подходящую десятичную библиотеку PHP; не приводите сумму к float и не применяйте JSON_NUMERIC_CHECK к этому контракту. Не меняйте масштаб 4 на 2 без принятого правила округления. OUT-параметры с NULL, DateTime или потоком не поддерживаются данным контрактом драйвера: для таких данных возвращайте строку результата SELECT и выбирайте подходящий способ чтения.

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

Измените входную сумму на 123.4000. Что нужно сохранить при получении и передаче?

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

При неизменном DECIMAL(19,4) ожидается строка 123.4000, а не float123.4. Проверяйте полное строковое значение и наличие кавычек в JSON; формат для отображения пользователю можно построить отдельно.

Константы SQLSRV по назначению и функции описания типов

Константа — готовое имя настройки, заданное расширением. SQLSRV_PARAM_OUT читается как намерение «получить значение», а не как случайное число 4. Эти имена доступны только при загруженном sqlsrv. Здесь сохранены все группы официальной страницы констант и отмечены расхождения её написаний с проверенным SQLSRV 5.13.3. Это справочник имён и выбора; наличие имени не означает, что любой тип разрешён во всех направлениях параметра.

Как выбрать нужную группу
  1. Фильтровать строку по id → значение IN
  2. Получить итог в переменную → PARAM_OUT + PHPTYPE + SQLTYPE
  3. Прочитать строку SELECT → FETCH и при необходимости SCROLL
  4. Разобрать отказ → ERR, настройки журнала → LOG
  5. Выбрать изоляцию соединения → TXN

Не все элементы этого справочника — обычные константы. SQLSRV_SQLTYPE_NVARCHAR без скобок — код типа для сравнения, а SQLSRV_SQLTYPE_NVARCHAR('80') — функция, формирующая описание с размером для привязки. SQLSRV_PHPTYPE_STRING и SQLSRV_PHPTYPE_STREAM в 5.13.3 являются функциями, а не одноимёнными обычными константами. Поэтому defined() и function_exists() отвечают на разные вопросы.

// Короткие диагностические фрагменты, без соединения с базой.
var_dump(defined('SQLSRV_PARAM_OUT'));             // true при загруженном sqlsrv
var_dump(function_exists('SQLSRV_PHPTYPE_STRING')); // true
$utf8 = SQLSRV_PHPTYPE_STRING('UTF-8');
$size80 = SQLSRV_SQLTYPE_NVARCHAR('80');
// Для сравнения metadata['Type']: SQLSRV_SQLTYPE_NVARCHAR без ('80').

Направление параметра

ИмяДля чего выбирается
SQLSRV_PARAM_INPHP → SQL; например, номер выбранного счёта.
SQLSRV_PARAM_OUTSQL → привязанная переменная PHP; итог после завершения всех результатов.
SQLSRV_PARAM_INOUTВход и выход в одной переменной; запрос резерва становится остатком.

Представление значения в PHP

ИмяДля чего выбирается
SQLSRV_PHPTYPE_INTЦелое число в диапазоне PHP int.
SQLSRV_PHPTYPE_FLOATЧисло с плавающей точкой; не контракт точных денежных цифр.
SQLSRV_PHPTYPE_DATETIMEОбъект DateTime при чтении; не поддерживаемый OUT-параметр.
SQLSRV_PHPTYPE_NULLМаркер PHP NULL, присутствует в 5.13.3. Не означает разрешение использовать NULL как OUT.
SQLSRV_PHPTYPE_TABLEМаркер табличного параметра TVP в современных версиях; требует отдельного SQL-типа и структуры данных, не произвольного PHP-массива.

Кодировка строк и потоков

ИмяДля чего выбирается
SQLSRV_ENC_BINARYБайты без текстового преобразования: содержимое файла.
SQLSRV_ENC_CHARСистемная кодовая страница; для переносимой кириллицы задайте UTF-8 явно.

Вид массива строки результата

ИмяДля чего выбирается
SQLSRV_FETCH_ASSOCКлючи — названия/AS-псевдонимы столбцов.
SQLSRV_FETCH_NUMERICКлючи 0,1,… по порядку SELECT.
SQLSRV_FETCH_BOTHОба набора ключей; режим по умолчанию у fetch_array, не две строки результата.

Диагностика последнего вызова

ИмяДля чего выбирается
SQLSRV_ERR_ALLОшибки и предупреждения; сохраните сразу после неудачного вызова.
SQLSRV_ERR_ERRORSТолько ошибки.
SQLSRV_ERR_WARNINGSТолько предупреждения, например обрезание данных.

Nullable в метаданных столбца

ИмяДля чего выбирается
SQLSRV_NULLABLE_NOСтолбец не допускает NULL. В 5.13.3 значение 0.
SQLSRV_NULLABLE_YESСтолбец допускает NULL. В 5.13.3 значение 1; это не означает, что текущая строка содержит NULL.
SQLSRV_NULLABLE_UNKNOWNДрайвер не определил допустимость; значение 2, не разрешение записывать NULL.

Подсистемы журнала: LogSubsystems

ИмяДля чего выбирается
SQLSRV_LOG_SYSTEM_OFFОтключить журнал подсистем.
SQLSRV_LOG_SYSTEM_INITИнициализация расширения.
SQLSRV_LOG_SYSTEM_CONNОперации подключения.
SQLSRV_LOG_SYSTEM_STMTРабота statement.
SQLSRV_LOG_SYSTEM_UTILСлужебная обработка ошибок и предупреждений.
SQLSRV_LOG_SYSTEM_ALLВсе подсистемы; диагностический режим, а не обязательная настройка сайта.

Уровни журнала: LogSeverity

ИмяДля чего выбирается
SQLSRV_LOG_SEVERITY_ERRORЗаписывать ошибки.
SQLSRV_LOG_SEVERITY_WARNINGЗаписывать предупреждения.
SQLSRV_LOG_SEVERITY_NOTICEЗаписывать уведомления.
SQLSRV_LOG_SEVERITY_ALLВсе три уровня. Отбор журнала не меняет успешность самого SQL-запроса.

Изоляция соединения: TransactionIsolation

ИмяДля чего выбирается
SQLSRV_TXN_READ_UNCOMMITTEDДопускает чтение неподтверждённых изменений; не универсальное средство ускорения.
SQLSRV_TXN_READ_COMMITTEDНе читать неподтверждённые изменения; поведение зависит также от настройки версионирования базы.
SQLSRV_TXN_REPEATABLE_READПовторное чтение уже прочитанных строк в транзакции защищено от их изменения; фантомы ещё возможны.
SQLSRV_TXN_SERIALIZABLEЗащищает также прочитанный диапазон; возможны дополнительные блокировки.
SQLSRV_TXN_SNAPSHOTСнимок для транзакции; требуется разрешение snapshot в базе и обработка конфликтов записи.

Тип курсора: опция Scrollable

ИмяДля чего выбирается
SQLSRV_CURSOR_FORWARDДвижение вперёд; обычное последовательное чтение без полного клиентского буфера.
SQLSRV_CURSOR_STATICПрокрутка по снимку результата; подходит для ограниченного набора.
SQLSRV_CURSOR_DYNAMICКурсор отражает изменения данных; доступность операций отличается от STATIC.
SQLSRV_CURSOR_KEYSETСостав ключей фиксируется, значения существующих строк могут меняться.
SQLSRV_CURSOR_CLIENT_BUFFEREDРезультат буферизуется на клиенте; занимает память, ограничен ClientBufferMaxKBSize.

Позиция строки при прокрутке

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

SQL-типы: константы для описания и сравнения

ИмяДля чего выбирается
SQLSRV_SQLTYPE_BIGINTБольшое целое: внешние номера; точный строковый OUT разобран выше.
SQLSRV_SQLTYPE_BITЛогический признак в SQL:0/1 и, если разрешено, NULL.
SQLSRV_SQLTYPE_TINYINTНебольшое неотрицательное целое.
SQLSRV_SQLTYPE_SMALLINTЦелое малого диапазона.
SQLSRV_SQLTYPE_INTОбычный SQL int, например количество или id учебной записи.
SQLSRV_SQLTYPE_DECIMALТочные десятичные данные; для привязки используйте DECIMAL(p,s).
SQLSRV_SQLTYPE_NUMERICСиноним decimal; для привязки используйте NUMERIC(p,s).
SQLSRV_SQLTYPE_FLOATПриближённое число двойной точности.
SQLSRV_SQLTYPE_REALПриближённое число меньшей точности.
SQLSRV_SQLTYPE_MONEYДенежный SQL-тип с четырьмя десятичными знаками.
SQLSRV_SQLTYPE_SMALLMONEYДенежный SQL-тип меньшего диапазона.
SQLSRV_SQLTYPE_CHARСтрока фиксированной длины; при привязке CHAR(n).
SQLSRV_SQLTYPE_VARCHARСтрока переменной длины; при привязке VARCHAR(n) или VARCHAR('max').
SQLSRV_SQLTYPE_NCHARUnicode фиксированной длины; при привязке NCHAR(n).
SQLSRV_SQLTYPE_NVARCHARUnicode переменной длины; при привязке NVARCHAR(n) или NVARCHAR('max').
SQLSRV_SQLTYPE_TEXTСтарый текстовый тип; для нового хранения предпочтителен varchar(max).
SQLSRV_SQLTYPE_NTEXTСтарый Unicode-тип; для нового хранения предпочтителен nvarchar(max).
SQLSRV_SQLTYPE_BINARYФиксированные байты; при привязке BINARY(n).
SQLSRV_SQLTYPE_VARBINARYПеременные байты; при привязке VARBINARY(n) или VARBINARY('max').
SQLSRV_SQLTYPE_IMAGEСтарый бинарный тип; современная альтернатива varbinary(max), имя не ограничивает данные картинками.
SQLSRV_SQLTYPE_DATEКалендарная дата без времени.
SQLSRV_SQLTYPE_TIMEВремя суток.
SQLSRV_SQLTYPE_DATETIMEСтарый тип даты и времени со своей точностью и диапазоном.
SQLSRV_SQLTYPE_SMALLDATETIMEДата и время с точностью до минуты.
SQLSRV_SQLTYPE_DATETIME2Дата и время с расширенными точностью/диапазоном.
SQLSRV_SQLTYPE_DATETIMEOFFSETДата и время с числовым смещением часового пояса.
SQLSRV_SQLTYPE_TIMESTAMPБинарный rowversion для контроля изменения строки, не дата/время.
SQLSRV_SQLTYPE_UNIQUEIDENTIFIERSQL GUID; не обычный числовой id.
SQLSRV_SQLTYPE_XMLXML-значение SQL Server; большой документ удобнее вернуть набором SELECT.
SQLSRV_SQLTYPE_UDTПользовательский SQL-тип: контракт зависит от созданного на сервере типа.
SQLSRV_SQLTYPE_TABLEТабличный параметр TVP, присутствует в 5.13.3; это не имя обычной таблицы и не возможность OUT-массива.

Функции, собирающие описание типа

При корректных аргументах эти вызовы возвращают целочисленное описание типа для драйвера, а не значение из таблицы. size в проверенной сборке 5.13.3 передаётся строкой; precision и scale — целыми. Используйте имена и вызовы, а не сохраняйте внутренние числовые коды в конфигурацию. Недопустимое описание или неверный PHP-тип аргумента — ошибка привязки; при strict_types передача числа 80 вместо строки'80' в размерный конструктор вызывает TypeError до SQL-запроса.

СигнатураПримерПараметры и границы
SQLSRV_PHPTYPE_STRING($encoding)SQLSRV_PHPTYPE_STRING('UTF-8')Строка PHP. Кодировка обязательна: UTF-8, SQLSRV_ENC_CHAR либо SQLSRV_ENC_BINARY.
SQLSRV_PHPTYPE_STREAM($encoding)SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY)Поток PHP для файла/большого столбца. Это не поддерживаемый OUT-параметр.
SQLSRV_SQLTYPE_BINARY($size)SQLSRV_SQLTYPE_BINARY('16')1…8000 байт; фиксированный размер, без max.
SQLSRV_SQLTYPE_VARBINARY($size)SQLSRV_SQLTYPE_VARBINARY('max')1…8000 байт либо строка max для большого бинарного значения.
SQLSRV_SQLTYPE_CHAR($size)SQLSRV_SQLTYPE_CHAR('12')1…8000 байт; число отображаемых символов зависит от кодировки.
SQLSRV_SQLTYPE_VARCHAR($size)SQLSRV_SQLTYPE_VARCHAR('80')1…8000 байт либо max; UTF-8 символ может занимать несколько байт.
SQLSRV_SQLTYPE_NCHAR($size)SQLSRV_SQLTYPE_NCHAR('12')1…4000 пар байтов UTF-16; фиксированная длина.
SQLSRV_SQLTYPE_NVARCHAR($size)SQLSRV_SQLTYPE_NVARCHAR('80')1…4000 пар байтов UTF-16 либо max. Это не число байтов строки PHP.
SQLSRV_SQLTYPE_DECIMAL($precision,$scale)SQLSRV_SQLTYPE_DECIMAL(19,4)precision 1…38; scale 0…precision. Четыре знака дробной части в этом примере.
SQLSRV_SQLTYPE_NUMERIC($precision,$scale)SQLSRV_SQLTYPE_NUMERIC(12,0)Те же правила точности/масштаба; scale 0 допустим.

Значение max разрешено для VARCHAR, NVARCHAR и VARBINARY, но не делает их поддерживаемыми OUT-параметрами. Для файлов используйте поток входного параметра и чтение потока столбца. Строка 'UTF-8' — имя кодировки, а не отсутствующая константа SQLSRV_ENC_UTF8. Наличие PHPTYPE_TABLE/SQLTYPE_TABLE обозначает отдельный интерфейс TVP; подробный запуск табличных параметров здесь не подменяется передачей произвольного массива.

Уточнения официального списка для установленного драйвера

Что можно встретить в документацииЧто использовать и почему
SQLSRV_TXN_READ_SERIALIZABLE в PHP ManualВ 5.13.3 существует SQLSRV_TXN_SERIALIZABLE. Вариант с READ отсутствует; не копируйте его в код.
SQLSRV_CURSOR_BUFFERED в PHP ManualВ 5.13.3 существует SQLSRV_CURSOR_CLIENT_BUFFERED; короткое имя отсутствует.
Курсоры и ENC помечены как intВ 5.13.3 это строковые значения. Сравнивайте с именем константы и не навязывайте ей int.
Числа NULLABLE_YES/NO в таблице MicrosoftИсходник и работающий драйвер: NO=0, YES=1, UNKNOWN=2. Имена сохраняют смысл; сравнение с константой надёжнее переписанного числа.
scale начинается с 1 в таблице типов MicrosoftДля decimal/numeric допустимо 0…precision. DECIMAL(19,0) также проверен в сборке 5.13.3.
PHPTYPE_NULL, PHPTYPE_TABLE и SQLTYPE_TABLE отсутствуют в старом списке PHP ManualЭти три имени есть в проверенной сборке. Здесь они сохранены отдельно с назначением и ограничениями.

Список сверён с 76 зарегистрированными константами и 10 функциями описания типов установленного SQLSRV 5.13.3; это не утверждение, что все методы расширения уже разобраны отдельными учебными сценариями. Результаты наличия имён получены из самого расширения, спорные написания дополнительно сопоставлены с регистрацией символов Microsoft 5.13.3. На другой версии сначала проверьте расширение, а затем свойство/режим, который нужен задаче.

Практические продолжения: выбор строк, курсоры и next_result; настройки, журнал и память клиентского буфера; границы транзакций. Не увеличивайте ClientBufferMaxKBSize ради SELECT* всей большой таблицы: состав столбцов, WHERE, сортировка и ограничение числа строк должны соответствовать экрану ещё в SQL.

От выходного параметра к устойчивой операции сайта

Теперь у вызова процедуры есть проверяемый ответ: сумма совпадает со строками счёта, остаток — с таблицей, квитанция сохранена целиком, точные цифры не прошли через float. Значение OUT имеет смысл только вместе с успешным завершением результатов; «statement создан» ещё не подтверждает готовность ответа.

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

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

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

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

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

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