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

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

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

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

Глава 295 · Начало PDO_SQLSRV → самостоятельные проекты

PDO_SQLSRV: каталог приложения, квитанция импорта и границы команды

Полный DSN и базовые константы; от одного SELECT до Unicode-каталога, OUTPUT, временной таблицы и подтверждённого таймаута.

От запроса приложения к каталогу и квитанции

Приложению нужен каталог доступных товаров, поставщику — квитанция с присвоенными номерами, складу — сводка упаковок. PDO_SQLSRV позволяет получить эти результаты через общий интерфейс PDO и SQL Server. Здесь мы пройдём от одного SELECT к четырём самостоятельным проектам: каждый даёт читаемый HTML, данные JSON и полезный файл.

Кто выполняет работу
  1. PHP: фильтр «Монтаж», номер страницы и лимит 2
  2. PDO + расширение pdo_sqlsrv → Microsoft ODBC → выбранная база SQL Server
  3. SQL: выбрать и упорядочить нужные строки; PHP: прочитать ответ
  4. Результат для человека: CAB: 9; CLP: 4 → catalog.csv

Соединение — объект PHP, который представляет доступ к конкретной базе. PDOStatement — отдельный объект команды и её результата. Ни один из них не является строкой товара. Таблица хранит строки со столбцами; SELECT описывает, какие значения получить. Массив после fetch уже находится в памяти PHP и может использоваться после освобождения результата.

$db = new PDO(
    'sqlsrv:Server=(localdb)\AENPHPManual177;Database=phpmanual177;Encrypt=0',
    null,
    null,
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$statement = $db->query("SELECT N'Каталог готов' AS message");
$row = $statement->fetch(PDO::FETCH_ASSOC);
if (!is_array($row)) { throw new RuntimeException('Нет ожидаемой строки.'); }
echo $row['message'];
$statement->closeCursor();
$statement = null;
$db = null;

После подготовки отдельного LocalDB по README эта минимальная запись выводит «Каталог готов». new PDO создаёт соединение; строка sqlsrv: выбирает драйвер. null/null здесь означает вход текущего Windows-пользователя. В SQL буква N перед строкой обозначает Unicode-литерал; AS message задаёт имя столбца. Стрелка → в схеме означает передачу данных, а оператор -> в PHP вызывает метод объекта. FETCH_ASSOC возвращает массив с ключом message, [] читает его значение; closeCursor освобождает результат, присваивание null убирает нашу ссылку на объект.

Encrypt=0 в этой записи относится только к собственной локальной named-pipe среде LocalDB. Для удалённого сервера ниже показан отдельный профиль с TLS и проверкой сертификата. Это не настройка, которую следует переносить на рабочий сайт. Полные проекты используют тот же реальный PDO, а lab.php лишь готовит изолированное соединение, собственные имена и отчёт.

Функция строки каталогаРеальные данные примера
sku — короткий код позицииCAB, CLP, TAP, BOX
title — название для человекаКабель O'Reilly, Клемма, Лента, Короб
category — фильтрМонтаж или Тара
qty — целое количество9, 4, 0, 2
Условие страницыcategory=Монтаж и qty>0; id задаёт порядок
ВыгрузкаТолько sku/title/qty для CAB и CLP

В начале PDO разобран сам интерфейс; процедурный SQLSRV показывает другую модель вызовов. sqlsrv_connect возвращает resource и использует sqlsrv_* функции, а PDO_SQLSRV работает через PDO/PDOStatement. Установка одного расширения не включает второе, и их настройки и возвращаемые PHP-типы нельзя механически переносить.

Установка PDO_SQLSRV: PHP, ODBC и свой сервер

Перед первым запросом нужны четыре согласованных компонента: PHP с PDO, расширение pdo_sqlsrv, Microsoft ODBC Driver и доступный SQL Server. Отдельно нужны имя базы и права учётной записи. Наличие SQL Server Management Studio либо рабочего sqlsrv_connect не подтверждает загрузку PDO_SQLSRV в том PHP, который обслуживает приложение.

СлойЧто проверить
PHP и его процессВерсия, x64/x86, TS/NTS, CLI или веб-SAPI, загруженный php.ini. Планировщик и веб-сервер могут использовать другую копию.
PDO_SQLSRVМодуль должен совпадать с PHP по совместимой версии, архитектуре и thread safety. Для этой главы закреплена связка PHP 8.5.11 NTS x64 / PDO_SQLSRV 5.13.3.
ODBCУстановлен совместимый Microsoft ODBC Driver. Это системная клиентская зависимость, не PHP-модуль и не PDO_ODBC.
SQL ServerПроекты используют отдельный SQL Server 2022 Express LocalDB и собственную базу; сведения JSON относятся к фактическому запуску, а не к любому серверу.
echo PHP_VERSION, "\n";
var_dump(extension_loaded('pdo_sqlsrv'));
var_dump(in_array('sqlsrv', PDO::getAvailableDrivers(), true));

Последние две проверки должны дать true в нужном процессе. Они подтверждают загрузку драйвера, но ещё не вход на сервер. Имена расширения pdo_sqlsrv и драйвера PDO sqlsrv различаются. Если PDO не загружен, сначала исправляют его подключение: вызывать методы отсутствующего класса нельзя.

Windows

Выберите официальный архив Microsoft для своей версии PHP; в этой связке используется php_pdo_sqlsrv_85_nts_x64.dll. Положите DLL в extension_dir отдельной копии PHP, подключите её через extension в используемом php.ini и установите соответствующий Microsoft ODBC Driver и требуемый Microsoft Visual C++ runtime. Не выбирайте NTS только потому, что запускается CLI: сверяйте реальный Thread Safety. PDO должен загружаться раньше PDO_SQLSRV, если собран отдельным модулем.

; Пример строки php.ini для выбранной Windows-сборки
extension=php_pdo_sqlsrv_85_nts_x64.dll

README каждого ZIP содержит конкретные команды создания AENPHPManual177, базы и отдельного конфигурационного файла. LocalDB работает от владельца Windows-процесса; веб-пул может иметь другую учётную запись и не видеть этот экземпляр. Успешный CLI-запуск не заменяет проверку веб-SAPI. Полная матрица совместимости Microsoft важнее исторической надписи «SQL Server 2005+» на краткой странице PHP Manual.

Linux и macOS

PDO_SQLSRV доступен отдельно от Windows. PHP Manual указывает PECL; актуальная инструкция Microsoft также предлагает PIE и описывает зависимости для конкретной ОС. Сначала установите поддержанные PHP/заголовки и Microsoft ODBC, затем соберите или установите подходящее расширение и включите extension=pdo_sqlsrv.so в конфигурации нужного SAPI. При отдельной сборке PDO порядок загрузки сохраняется. После изменения php.ini веб-процессу требуется перечитать конфигурацию. LocalDB — Windows-среда; на Linux/macOS нужен доступный SQL Server по сети, собственные реквизиты и TLS. Windows-архив проекта не объявляется проверкой этих вариантов.

Пошаговая установка для Linux и macOS · Официальный выпуск 5.13.3 · Разобрать различия PHP, расширения и ODBC

Если появилось could not find driver, проверяют загрузку PHP-модуля и список PDO, а не меняют пароль. Если драйвер загружен, но конструктор сообщает отказ, следующий шаг — адрес, транспорт, сервер, выбранная база, TLS и вход. Не публикуйте phpinfo или полный конфигурационный файл посетителям.

Полный DSN, аргументы конструктора и виды конфигурации

DSN — строка выбора драйвера и назначения соединения. Её prefix sqlsrv: отделён двоеточием, пары ключ=значение разделены точкой с запятой. Server задаёт сервер, Database — базу внутри него; строку собирает доверенная конфигурация приложения. Логин и пароль SQL-аутентификации передаются отдельными аргументами, параметры SELECT сюда не помещаются.

$dsn = 'sqlsrv:Server=db.example.test,1433;Database=shop;'
    . 'Encrypt=1;TrustServerCertificate=0;LoginTimeout=5;APP=CatalogReader';
$db = new PDO($dsn, $user, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::SQLSRV_ATTR_ENCODING => PDO::SQLSRV_ENCODING_UTF8,
]);

Это сетевой шаблон, а не адрес учебного стенда. db.example.test и shop заменяют реальными доверенными настройками; сертификат должен соответствовать адресу. Вход возвращает объект PDO; отказ конструктора выбрасывает PDOException даже при ERRMODE_SILENT. Он не возвращает объект исключения как успешное соединение. Массив четвёртого аргумента содержит PDO-атрибуты с именованными константами; строка DSN содержит ключи Microsoft. Поддержка установки после открытия зависит от свойства.

Аргумент new PDOСмысл в PDO_SQLSRV
$dsnsqlsrv: с Server и выбранными разрешёнными ключами.
$username, $passwordSQL login/password либо null/пустые значения для подходящего контекста Windows-аутентификации. Способ входа должен соответствовать серверу и ОС.
$optionsАссоциативный массив поддержанных PDO и драйверных атрибутов; не произвольный массив sqlsrv_connect.
Успех / отказPDO / выброшенный PDOException. После работы освобождают statements и все ссылки на соединение.

Все ключи страницы PDO_SQLSRV DSN

КлючЗначение и практическая граница
ServerИмя сервера, server\instance или server,port; для LocalDB — (localdb)\имя. Это назначение соединения.
DatabaseИмя базы; при пропуске действует база входа по умолчанию. Для приложения лучше выбирать её явно.
APPИмя приложения для диагностики соединений и трассировки. Не является правом доступа.
WSIDИмя рабочей станции для диагностики; не использовать как доказательство личности клиента.
LoginTimeoutЧисло секунд ожидания открытия. В DSN оно является частью строки; не ограничивает длительность SELECT.
ConnectionPooling1/true или 0/false: участие в пуле соединений. Возможности и управление зависят от ОС/ODBC; это не обещание сохранённого состояния PHP-объекта.
Encrypt1/true или 0/false: политика шифрования соединения. Задавайте её явно с учётом используемого ODBC и транспорта.
TrustServerCertificate1/true доверяет сертификату без обычной проверки, 0/false требует проверки. Включение обхода не исправляет цепочку доверия или имя сервера.
Failover_PartnerНастроенный сервер/экземпляр партнёра зеркалирования. Не случайная запасная DSN; есть ограничения ОС и сочетания с MultiSubnetFailover.
MultipleActiveResultSets0/false отключает, 1/true включает MARS: несколько активных результатов на соединении. Не превращает обработку PHP в параллельную и не отменяет закрытие результатов.
QuotedId1/true — правила SQL-92 для идентификаторов в кавычках; 0/false — прежний режим T-SQL. Не способ экранирования значений.
TraceOn1/true включает ODBC-трассировку, 0/false отключает. Диагностический журнал может содержать чувствительные сведения.
TraceFileПуть файла трассировки в окружении клиента, с подходящими правами записи. Не файл результата SELECT.
TransactionIsolationОдно из пяти PDO::SQLSRV_TXN_* ниже. Выбранный режим требует поддержки и, для SNAPSHOT, настройки базы.

Формы адресации: sqlsrv:Server=localhost;Database=shop для стандартного локального сервера; Server=localhost,1433 для порта; Server=host\instance для именованного экземпляра; Server=tenant.database.windows.net для Azure SQL. Исторический пример PHP.net добавляет @ServerId к SQL-логину Azure, но это не правило для всех современных способов входа. Нужны требования конкретной службы и выбранной аутентификации.

Точка с запятой — часть грамматики DSN: необработанное значение из формы может добавить другой ключ. В проектах сервер и база взяты из закрытого собственного профиля, а разрешённые варианты проверены. Значения со специальными символами требуют правил connection string используемого драйвера; нельзя заменять их urlencode или SQL-кавычками. Пароли не включаются в публичную строку и ошибки.

Дополнения современных Microsoft Drivers

Базовые 14 ключей выше не исчерпывают современный драйвер. Driver выбирает установленный ODBC; ApplicationIntent и MultiSubnetFailover относятся к маршрутизации/доступности; ConnectRetryCount и ConnectRetryInterval — к восстановлению разорванного соединения. Authentication и AccessToken задают поддержанный вид Microsoft Entra-входа; токен нельзя сочетать с логином/паролем/Authentication. ColumnEncryption и KeyStore* требуют отдельной инфраструктуры Always Encrypted. Language влияет на сообщения сервера. Полная таблица Microsoft показывает версии, ОС, допустимые сочетания и ограничения. Наши LocalDB-проекты не моделируют облачный вход, failover, токены или шифрование столбцов.

Не переносите в PDO DSN параметры CharacterSet, ReturnDatesAsStrings, FormatDecimals, DecimalPlaces, UID или PWD из sqlsrv_connect. В PDO для соответствующих задач применяются атрибуты и аргументы конструктора. У сетевого проекта доверие сертификату, права входа и ограничение времени — три разных договора. Пример ожидания SQL проверяет только последний на уровне команды.

Константы базовой страницы и настройки выбранного драйвера

Драйверные константы появляются только после загрузки PDO_SQLSRV. Сначала убедитесь, что PDO::ATTR_DRIVER_NAME равен sqlsrv; числовой атрибут другого драйвера не получает тот же смысл. Ниже сохранён весь набор из основной страницы PHP.net, а затем показаны дополнительные группы выбранной версии.

Полное имя PDO::Значение для приложения
SQLSRV_TXN_READ_UNCOMMITTEDDSN TransactionIsolation: допускаются незавершённые изменения других транзакций; подходит не для каждого отчёта.
SQLSRV_TXN_READ_COMMITTEDНе читать незавершённые изменения; детали чтения зависят и от настроек SQL Server.
SQLSRV_TXN_REPEATABLE_READСохранить повторяемость прочитанных строк; возможны новые подходящие строки.
SQLSRV_TXN_SNAPSHOTВерсионное состояние для транзакции; база должна разрешать SNAPSHOT.
SQLSRV_TXN_SERIALIZABLEСамый строгий из этих режимов, включая защиту диапазона; возможна большая конкуренция за блокировки.
SQLSRV_ENCODING_BINARYБайты без перекодирования: настройка параметра/столбца/statement. Не допускается как encoding всего PDO-соединения.
SQLSRV_ENCODING_SYSTEMСистемная кодовая страница Windows; непредставимые символы могут заменяться ?. Нельзя обещать сохранность произвольного Unicode.
SQLSRV_ENCODING_UTF8UTF-8; обычная кодировка PDO_SQLSRV по умолчанию.
SQLSRV_ENCODING_DEFAULTКонтекстный выбор: при открытии SYSTEM, при prepare — кодировка соединения. Не синоним UTF8 во всех местах.
SQLSRV_ATTR_QUERY_TIMEOUTЦелые неотрицательные секунды; 0 означает отсутствие лимита. Соединение/prepare/statement задают соответствующую область.
SQLSRV_ATTR_DIRECT_QUERYtrue — прямое исполнение, false — подготовленное. Значение выбирают для будущей команды; не меняют уже подготовленную команду.

В PHP.net пять TXN-констант подписаны как int, однако исходник 5.13.3 регистрирует их строками. Для DSN используйте саму константу, не выдуманное числовое соответствие. Сравнение режимов и реальные границы снимка находятся в главе транзакций. Эта таблица объясняет все пять вариантов, но не выдаёт одиночный запуск за проверку всех конкурентных режимов.

$dsn = 'sqlsrv:Server=db.example.test;Database=shop;Encrypt=1;'
    . 'TrustServerCertificate=0;TransactionIsolation='
    . PDO::SQLSRV_TXN_READ_COMMITTED;

Какие настройки не следует смешивать

Группа выбранной версииМесто и смысл
SQLSRV_ATTR_ENCODINGКлюч атрибута для значений ENCODING_*; connection/prepare/statement/bind имеют разные границы допустимости.
SQLSRV_ATTR_FETCHES_NUMERIC_TYPEВыдача поддержанных числовых SQL-типов как PHP int/float. ATTR_STRINGIFY_FETCHES может снова потребовать строки; точные DECIMAL и большие числа разбираются отдельно.
SQLSRV_ATTR_FETCHES_DATETIME_TYPEDateTime вместо обычных строк даты/времени; настройка statement может переопределить connection.
SQLSRV_ATTR_FORMAT_DECIMALS и SQLSRV_ATTR_DECIMAL_PLACESПредставление десятичных строк и числа знаков money, а не изменение хранимой точности. DECIMAL_PLACES действует при включённом FORMAT_DECIMALS.
SQLSRV_ATTR_CURSOR_SCROLL_TYPEПри CURSOR_SCROLL выбирает SQLSRV_CURSOR_STATIC, DYNAMIC, KEYSET или BUFFERED; это разные источники данных и стоимость хранения.
SQLSRV_CURSOR_STATIC / DYNAMIC / KEYSET / BUFFEREDСерверный снимок / серверные изменения / ключевой набор / клиентский буфер. CURSOR_SCROLL без уточнения выбирает STATIC.
SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZEПредел клиентского буфера в КБ; не лимит числа строк SQL. Память сокращают также проекцией и фильтром.
SQLSRV_PARAM_OUT_DEFAULT_SIZEСпециальный размер -1 при привязке выходных параметров; не произвольный размер входного текста.
SQLSRV_ATTR_DATA_CLASSIFICATIONЗапрос метаданных классификации чувствительных столбцов; требуется соответствующая серверная поддержка. Не средство авторизации и не классификатор PHP-строк.
SQLSRV_ATTR_BATCH_ERROR_CONTINUEДополнение pin 5.13.3 для продолжения обработки ошибок batch. Error marker не становится успешной строкой; возможен переход с нулём столбцов. Не включаем как общий способ скрыть ошибки.

Практические настройки чисел, дат, двоичных значений, курсоров и памяти разобраны в результатах PDO_SQLSRV. OUT/INOUT и несколько ответов команды — в процедуре и жизненном цикле результатов. Карточки общих PDO::getAttribute(), PDO::setAttribute() и PDOStatement::setAttribute() объясняют вызовы; конкретную поддержку проверяют по драйверу.

DIRECT_QUERY и ATTR_EMULATE_PREPARES — разные настройки. Первая выбирает способ исполнения SQL Server, вторая — эмуляцию подготовки PDO. Привязка параметров сохраняется в прямом пути сводки упаковок. Нельзя делать вывод «direct значит SQL-инъекция» или «prepare гарантирует заранее проверенный SQL»: драйвер может обнаружить ошибку только на execute. BATCH_ERROR_CONTINUE влияет также на дочитывание прежнего результата; ответственность за учёт ошибок остаётся у приложения.

Выдать страницу каталога с нужными полями и явными типами параметров

Менеджеру нужен короткий список доступных монтажных товаров. В базе есть и нулевой остаток, и другая категория, но в CSV должны попасть только две подходящие позиции. SQL выбирает строки и поля до передачи в PHP; следующий запрос страницы возвращает пустой список, а не ошибку.

Путь данных и полученный результат
  1. Свои строки: CAB/Монтаж/9; CLP/Монтаж/4; TAP/Монтаж/0; BOX/Тара/2
  2. WHERE оставляет CAB и CLP → ORDER BY id → OFFSET 0 / FETCH 2
  3. Три поля в PHP → catalog.csv; OFFSET 2 → []

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

$statement = $db->prepare('SELECT sku, title FROM products
    WHERE category = ? ORDER BY id OFFSET ? ROWS FETCH NEXT ? ROWS ONLY');
$statement->bindValue(1, 'Монтаж', PDO::PARAM_STR);
$statement->bindValue(2, 0, PDO::PARAM_INT);
$statement->bindValue(3, 2, PDO::PARAM_INT);
$statement->execute();
$row = $statement->fetch(PDO::FETCH_ASSOC);
echo is_array($row) ? $row['title'] : 'Нет подходящих товаров';
$statement->closeCursor();

Знак ? обозначает значение. Номера bindValue начинаются с 1; PARAM_INT передаёт offset и limit именно как целые числа. OFFSET 0 пропускает ноль строк, FETCH NEXT 2 ограничивает ответ двумя. ORDER BY id задаёт однозначный порядок. prepare создаёт команду, execute выполняет её, fetch читает одну строку. Полный проект дочитывает короткую страницу, закрывает результат и повторяет ту же команду для offset=2.

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

НаблюдениеРезультат
ФильтрМонтаж; qty > 0
Размер страницы2
ТоварыCAB: 9; CLP: 4
Название первой позицииКабель O'Reilly
Следующая страницапустая
Файлcatalog.csv
Самостоятельный проект, исходник и архив
Выдать страницу каталога с нужными полями и явными типами параметровСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p177Config();$db=p177Connect($config);$runtime=p177Runtime($db);

function appCsv177(string $name,array $records): void {
    $file=fopen(__DIR__.'/'.$name,'wb');
    if ($file===false) {throw new RuntimeException('Не удалось открыть свой CSV.');}
    try {foreach ($records as $record) {
        if (fputcsv($file,$record,',','"','',"\n")===false) {
            throw new RuntimeException('Не удалось записать CSV.');
        }
    }} finally {fclose($file);}
}

    $name=p177OwnTable($db,$owned,'catalog','(id int PRIMARY KEY,sku varchar(12) NOT NULL,
        title nvarchar(80) NOT NULL,category nvarchar(30) NOT NULL,qty int NOT NULL)');
    $table=p177Qualified($name);$inserted=0;
    foreach ([[1,'CAB',"Кабель O'Reilly",'Монтаж',9],[2,'CLP','Клемма','Монтаж',4],
              [3,'TAP','Лента','Монтаж',0],[4,'BOX','Короб','Тара',2]] as $item) {
        $inserted+=p177Command($db,'INSERT INTO '.$table.' (id,sku,title,category,qty) VALUES (?,?,?,?,?)',$item);
    }
    $category='Монтаж';$offset=0;$limit=2;
    if ($offset<0 || $limit<1 || $limit>20) {throw new InvalidArgumentException('Недопустимая страница.');}
    $statement=$db->prepare('SELECT sku,title,qty FROM '.$table.'
        WHERE category=? AND qty>0 ORDER BY id OFFSET ? ROWS FETCH NEXT ? ROWS ONLY',
        [PDO::ATTR_EMULATE_PREPARES=>false]);
    $raw=[];$catalog=[];$nextPage=[];
    try {
        $statement->bindValue(1,$category,PDO::PARAM_STR);
        $statement->bindValue(2,$offset,PDO::PARAM_INT);
        $statement->bindValue(3,$limit,PDO::PARAM_INT);
        $statement->execute();
        while (($row=$statement->fetch(PDO::FETCH_ASSOC))!==false) {
            if (!is_array($row)) {throw new RuntimeException('Ожидалась строка каталога.');}
            $raw[]=$row;$catalog[]=['sku'=>$row['sku'],'title'=>$row['title'],'qty'=>(int)$row['qty']];
        }
        $statement->closeCursor();
        $statement->bindValue(2,2,PDO::PARAM_INT);$statement->execute();
        $nextPage=p177Rows($statement);
    } finally {p177Release($statement);}
    $expected=[['sku'=>'CAB','title'=>"Кабель O'Reilly",'qty'=>9],['sku'=>'CLP','title'=>'Клемма','qty'=>4]];
    $records=[['sku','title','qty']];foreach($catalog as $row) {$records[]=array_values($row);}
    appCsv177('catalog.csv',$records);
    $rows=[['Фильтр','Монтаж; qty > 0'],['Размер страницы',2],['Товары','CAB: 9; CLP: 4'],
        ['Название первой позиции',$catalog[0]['title']],['Следующая страница','пустая'],['Файл','catalog.csv']];
    $checks=['four_fixture_rows'=>$inserted===4,'exact_catalog'=>$catalog===$expected,
        'two_rows'=>count($catalog)===2,'projected_columns'=>array_keys($raw[0])===['sku','title','qty'],
        'unicode_apostrophe'=>$catalog[0]['title']==="Кабель O'Reilly",'positive_stock'=>$catalog[1]['qty']===4,
        'empty_next_page'=>$nextPage===[],'request_released'=>$statement===null,
        'no_manual_transaction'=>$db->inTransaction()===false,'artifact_written'=>is_file(__DIR__.'/catalog.csv')];
    $observations=['filter'=>['category'=>$category,'qty_min_exclusive'=>0],'offset'=>0,'limit'=>$limit,
        'raw_rows'=>$raw,'raw_qty_type'=>get_debug_type($raw[0]['qty']),'catalog'=>$catalog,'next_page'=>$nextPage];

} finally {

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

p177Report('pdo177-filtered-catalog','Выдать страницу каталога с нужными полями и явными типами параметров',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Фильтр — Монтаж; qty > 0
Размер страницы — 2
Товары — CAB: 9; CLP: 4
Название первой позиции — Кабель O'Reilly
Следующая страница — пустая
Файл — catalog.csv

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

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

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

OFFSET/FETCH требует SQL Server 2012+. Большое смещение может дорого обходиться серверу; для длинной ленты рассматривают продолжение по ключу. Уникальная сортировка делает порядок определённым, но не замораживает меняющиеся данные между запросами страниц. Поле category не является именем таблицы или столбца. Массовое execute([$offset,$limit]) без явных типов не подменяет PARAM_INT. Числа в каталоге приведены к int намеренно: это небольшой складской остаток, не деньги или BIGINT. Исходные типы сохранены отдельно в наблюдениях.

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

Добавьте третью доступную монтажную позицию с id=5. Что должны содержать первая и вторая страницы?

Разбор

При прежнем ORDER BY первая содержит CAB и CLP, вторая — новую позицию. Увеличение лимита меняет размер выдачи; фильтр qty>0 продолжает исключать TAP. Проверить нужно состав обеих страниц, а не только count первой.

Принять поставку и связать номера базы со строками входного JSON

Поставщик знает номера S-31, S-32 и S-33, а база назначает собственные ID. После импорта нужно вернуть точное соответствие, не угадывая последний номер всей таблицы. incoming.json хранит три исходные позиции; receipt.json связывает их с сохранёнными строками и сохраняет различие null, пустой строки и текста.

Путь данных и полученный результат
  1. incoming.json: S-31/Насос/2/null; S-32/Шланг/0/""; S-33/Клапан/5/Склад Б
  2. bindValue → INSERT OUTPUT → ID каждой позиции → closeCursor → следующий execute
  3. commit → SELECT по id → receipt.json с соответствием S-31→1, S-32→2, S-33→3

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

$statement = $db->prepare('INSERT INTO deliveries (supplier_ref, title)
    OUTPUT inserted.id, inserted.supplier_ref VALUES (?, ?)');
$statement->execute(['S-31', 'Насос Мини']);
$receipt = $statement->fetch(PDO::FETCH_ASSOC);
if (!is_array($receipt)) { throw new RuntimeException('Нет строки OUTPUT.'); }
echo $receipt['supplier_ref'], ' → ', $receipt['id'];
$statement->closeCursor();

IDENTITY в полной таблице назначает номер; inserted — специальное имя новых значений в OUTPUT. Обычный INSERT не обязан возвращать читаемую строку, поэтому fetch применяется к явно объявленному OUTPUT. Полный пример готовит одну команду, меняет привязанные значения для каждой позиции, освобождает её результаты и фиксирует всю поставку. Только затем он читает сохранённые строки и записывает квитанцию.

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

НаблюдениеРезультат
Принято строк3
Связь поставщика и базыS-31 → 1; S-32 → 2; S-33 → 3
Название второй позицииШланг O'Reilly
Примечанияnull; пустая строка; Склад Б
Повторное чтениесовпадает с OUTPUT
Файлreceipt.json
Самостоятельный проект, исходник и архив
Принять поставку и связать номера базы со строками входного JSONСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p177Config();$db=p177Connect($config);$runtime=p177Runtime($db);

    $input=json_decode(file_get_contents(__DIR__.'/incoming.json'),true,512,JSON_THROW_ON_ERROR);
    if (!is_array($input) || count($input)!==3) {throw new InvalidArgumentException('Ожидалась небольшая поставка.');}
    foreach($input as $item) {
        if (!is_array($item) || !isset($item['supplier_ref'],$item['title'],$item['qty'])
            || !array_key_exists('note',$item) || !is_string($item['supplier_ref'])
            || $item['supplier_ref']==='' || strlen($item['supplier_ref'])>12 || !is_string($item['title'])
            || $item['title']==='' || !is_int($item['qty']) || $item['qty']<0
            || !($item['note']===null || is_string($item['note']))) {throw new InvalidArgumentException('Неверная строка поставки.');}
    }
    $name=p177OwnTable($db,$owned,'incoming','(id int IDENTITY(1,1) PRIMARY KEY,
        supplier_ref varchar(12) NOT NULL UNIQUE,title nvarchar(80) NOT NULL,qty int NOT NULL,note nvarchar(80) NULL)');
    $table=p177Qualified($name);$returned=[];$rawReturned=[];$statement=null;
    $normalize=static fn(array $r):array=>['id'=>(int)$r['id'],'supplier_ref'=>$r['supplier_ref'],
        'title'=>$r['title'],'qty'=>(int)$r['qty'],'note'=>$r['note']];
    p177Begin($db);
    try {
        $statement=$db->prepare('INSERT INTO '.$table.' (supplier_ref,title,qty,note)
            OUTPUT inserted.id,inserted.supplier_ref,inserted.title,inserted.qty,inserted.note VALUES (?,?,?,?)');
        foreach($input as $item) {
            $statement->bindValue(1,$item['supplier_ref'],PDO::PARAM_STR);
            $statement->bindValue(2,$item['title'],PDO::PARAM_STR);
            $statement->bindValue(3,$item['qty'],PDO::PARAM_INT);
            $statement->bindValue(4,$item['note'],$item['note']===null?PDO::PARAM_NULL:PDO::PARAM_STR);
            $statement->execute();$row=$statement->fetch(PDO::FETCH_ASSOC);
            if (!is_array($row) || $statement->fetch(PDO::FETCH_ASSOC)!==false) {
                throw new RuntimeException('Ожидалась одна строка OUTPUT.');
            }
            $rawReturned[]=$row;$returned[]=$normalize($row);
            $statement->closeCursor();
        }
        p177Release($statement);p177Commit($db);
    } catch (Throwable $failure) {
        p177Release($statement);p177Rollback($db);throw $failure;
    }
    $stored=array_map($normalize,p177Read($db,'SELECT id,supplier_ref,title,qty,note FROM '.$table.' ORDER BY id'));
    $receipt=['accepted'=>count($stored),'items'=>$stored];
    file_put_contents(__DIR__.'/receipt.json',json_encode($receipt,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR)."\n");
    $rows=[['Принято строк',3],['Связь поставщика и базы','S-31 → 1; S-32 → 2; S-33 → 3'],
        ['Название второй позиции',$stored[1]['title']],['Примечания','null; пустая строка; Склад Б'],
        ['Повторное чтение','совпадает с OUTPUT'],['Файл','receipt.json']];
    $checks=['three_rows'=>count($stored)===3,'own_ids'=>array_column($stored,'id')===[1,2,3],
        'supplier_mapping'=>array_column($stored,'supplier_ref')===['S-31','S-32','S-33'],
        'exact_titles'=>array_column($stored,'title')===array_column($input,'title'),
        'quantities'=>array_column($stored,'qty')===[2,0,5],'null_note'=>$stored[0]['note']===null,
        'empty_note'=>$stored[1]['note']==='','text_note'=>$stored[2]['note']==='Склад Б',
        'output_matches_stored'=>$returned===$stored,'request_released'=>$statement===null,
        'transaction_finished'=>$db->inTransaction()===false,
        'artifact_roundtrip'=>json_decode(file_get_contents(__DIR__.'/receipt.json'),true,512,JSON_THROW_ON_ERROR)===$receipt];
    $observations=['input'=>$input,'raw_output'=>$rawReturned,'receipt'=>$receipt,
        'confirmation'=>'OUTPUT consumed, transaction committed, stored rows read again'];

} finally {

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

p177Report('pdo177-import-receipt','Принять поставку и связать номера базы со строками входного JSON',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Принято строк — 3
Связь поставщика и базы — S-31 → 1; S-32 → 2; S-33 → 3
Название второй позиции — Шланг O'Reilly
Примечания — null; пустая строка; Склад Б
Повторное чтение — совпадает с OUTPUT
Файл — receipt.json

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

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

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

OUTPUT сам по себе ещё не подтверждает commit: SQL Server может выдать строки и затем откатить команду при ошибке. При любом отказе квитанция не принимается. ID 1–3 относятся только к новой таблице; IDENTITY допускает пропуски. Для таблиц с включёнными триггерами и сложных OUTPUT INTO действуют дополнительные ограничения SQL Server; эта собственная таблица их не содержит. lastInsertId в современном PDO_SQLSRV существует, но его необязательный аргумент с драйвера 5.0 означает sequence, а не таблицу, как в старых версиях. MAX(id) не определяет автора вставки.

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

Сделайте supplier_ref третьей позиции равным S-31. Какая квитанция допустима после отказа UNIQUE?

Разбор

Успешной квитанции нет: ошибка должна прервать поставку и привести к rollback всей транзакции. Нельзя выдавать первые две OUTPUT-строки как сохранённую поставку. Для рабочего импорта отдельно проектируют протокол повторной отправки и уникальные внешние ключи.

Свести упаковки во временной таблице одного соединения

Нужно объединить три упаковки из двух складов в две позиции отправки. Промежуточные строки нужны только этой операции; постоянную таблицу импорта создавать не требуется. Локальная #таблица хранит рабочий набор в контексте одного соединения, а staging.csv содержит сгруппированные количества.

Путь данных и полученный результат
  1. Одно PDO-соединение → собственная #таблица: CAB/3, CAB/4, CLP/5
  2. GROUP BY sku → COUNT упаковок + SUM единиц: CAB/2/7, CLP/1/5
  3. staging.csv → DROP собственной #таблицы → подтверждённое отсутствие

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

$options = [PDO::SQLSRV_ATTR_DIRECT_QUERY => true];
$create = $db->prepare('CREATE TABLE #packing (sku varchar(12), units int)', $options);
$create->execute();
$create->closeCursor();
$insert = $db->prepare('INSERT INTO #packing VALUES (?, ?)', $options);
$insert->execute(['CAB', 3]);
$insert->closeCursor();
$read = $db->prepare('SELECT sku, units FROM #packing', $options);
$read->execute();
print_r($read->fetch(PDO::FETCH_ASSOC));
$read->closeCursor();
$db->exec('DROP TABLE #packing');

Символ # задаёт локальную временную таблицу SQL Server. DIRECT_QUERY=true выбирает прямое исполнение и сохраняет нужный контекст между командами; prepare здесь всё равно возвращает PDOStatement. Маркеры и bindValue остаются: direct не означает вставку пользовательского текста в SQL. Полный проект регистрирует случайное собственное имя, явно включает режим при CREATE, INSERT, SELECT и DROP и не переключает PDO-соединение между ними.

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

НаблюдениеРезультат
Исходных упаковок3
Кабель2 упаковки; 7 единиц
Клемма1 упаковка; 5 единиц
Временный набор после выгрузкиудалён
Файлstaging.csv
Самостоятельный проект, исходник и архив
Свести упаковки во временной таблице одного соединенияСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p177Config();$db=p177Connect($config);$runtime=p177Runtime($db);

function appCsv177(string $name,array $records): void {
    $file=fopen(__DIR__.'/'.$name,'wb');
    if ($file===false) {throw new RuntimeException('Не удалось открыть свой CSV.');}
    try {foreach ($records as $record) {
        if (fputcsv($file,$record,',','"','',"\n")===false) {
            throw new RuntimeException('Не удалось записать CSV.');
        }
    }} finally {fclose($file);}
}

    $name=p177OwnName($db,$owned,'TEMP_TABLE','staging');$table=p177Qualified($name);
    $direct=[PDO::SQLSRV_ATTR_DIRECT_QUERY=>true];$statement=null;
    try {
        $statement=$db->prepare('CREATE TABLE '.$table.' (line_no int PRIMARY KEY,sku varchar(12) NOT NULL,
            units int NOT NULL,warehouse nvarchar(30) NOT NULL)',$direct);
        $statement->execute();p177Release($statement);
        $created=p177ObjectExists($db,'TEMP_TABLE',$name);
        $statement=$db->prepare('INSERT INTO '.$table.' (line_no,sku,units,warehouse) VALUES (?,?,?,?)',$direct);
        $affected=0;
        foreach([[1,'CAB',3,'Север'],[2,'CAB',4,'Юг'],[3,'CLP',5,'Север']] as $item) {
            $statement->bindValue(1,$item[0],PDO::PARAM_INT);
            $statement->bindValue(2,$item[1],PDO::PARAM_STR);
            $statement->bindValue(3,$item[2],PDO::PARAM_INT);
            $statement->bindValue(4,$item[3],PDO::PARAM_STR);
            $statement->execute();$affected+=$statement->rowCount();$statement->closeCursor();
        }
        p177Release($statement);
        $raw=p177Read($db,'SELECT sku,COUNT(*) AS packages,SUM(units) AS units FROM '.$table.'
            GROUP BY sku ORDER BY sku',[],$direct);
        $summary=array_map(static fn(array $r):array=>['sku'=>$r['sku'],'packages'=>(int)$r['packages'],'units'=>(int)$r['units']],$raw);
        $warehouses=p177Read($db,'SELECT warehouse FROM '.$table.' WHERE line_no=1',[],$direct);
        $records=[['sku','packages','units']];foreach($summary as $row) {$records[]=array_values($row);}
        appCsv177('staging.csv',$records);
        $statement=$db->prepare('DROP TABLE '.$table,$direct);$statement->execute();p177Release($statement);
        $absent=!p177ObjectExists($db,'TEMP_TABLE',$name);
    } finally {p177Release($statement);}
    $rows=[['Исходных упаковок',3],['Кабель','2 упаковки; 7 единиц'],['Клемма','1 упаковка; 5 единиц'],
        ['Временный набор после выгрузки','удалён'],['Файл','staging.csv']];
    $checks=['temp_created'=>$created,'three_inserted'=>$affected===3,
        'exact_summary'=>$summary===[['sku'=>'CAB','packages'=>2,'units'=>7],['sku'=>'CLP','packages'=>1,'units'=>5]],
        'two_groups'=>count($summary)===2,'unicode_preserved'=>$warehouses[0]['warehouse']==='Север',
        'unit_total'=>array_sum(array_column($summary,'units'))===12,
        'temp_removed_explicitly'=>$absent,'request_released'=>$statement===null,
        'artifact_written'=>is_file(__DIR__.'/staging.csv')];
    $observations=['input'=>[['CAB',3,'Север'],['CAB',4,'Юг'],['CLP',5,'Север']],
        'direct_query'=>true,'parameterized_insert'=>true,'raw_summary'=>$raw,'summary'=>$summary,
        'created_in_same_connection'=>$created,'absent_after_drop'=>$absent];

} finally {

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

p177Report('pdo177-direct-staging','Свести упаковки во временной таблице одного соединения',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Исходных упаковок — 3
Кабель — 2 упаковки; 7 единиц
Клемма — 1 упаковка; 5 единиц
Временный набор после выгрузки — удалён
Файл — staging.csv

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

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

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

Временная таблица не является PHP-массивом, не передаётся другому соединению и не сохраняет импорт между HTTP-запросами. DIRECT_QUERY выбирают до подготовки команды: менять его у уже подготовленного statement для нового execute нельзя. Этот режим отличается от ATTR_EMULATE_PREPARES. Если вызываемая хранимая процедура сама создаёт временные таблицы, Microsoft отдельно рекомендует exec; это другой путь. Для долговременного приёма поставок нужна постоянная staging-таблица с бизнес-ключом и контролем повторов. В проекте показан положительный путь direct; отказ другого режима намеренно не воспроизводится.

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

Добавьте ещё одну упаковку CAB на 2 единицы. Какие числа изменятся в выгрузке?

Разбор

У CAB будет 3 упаковки и 9 единиц, общий итог станет 14. COUNT считает упаковки, SUM — их содержимое. Сортировка двух групп не должна зависеть от случайного порядка INSERT.

Ограничить ожидание отчёта и подтвердить следующий полезный запрос

Страница отчёта не должна бесконечно ждать SQL. Проект задаёт лимит конкретной команды, сохраняет безопасную диагностику отказа и выполняет другой короткий запрос. deadline.csv сообщает, что задержанный отчёт не получен, но готовую поставку удалось прочитать. WAITFOR создаёт контролируемую задержку без остановки сервера.

Путь данных и полученный результат
  1. Своя строка: Готовая поставка / qty=6
  2. Команда WAITFOR 2 секунды с лимитом 1 → ожидаемый отказ и сохранённая диагностика
  3. Освободить statement → другой SELECT с лимитом 3 → Готовая поставка: 6 → deadline.csv

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

$statement = $db->prepare("WAITFOR DELAY '00:00:02'; SELECT 1 AS ready",
    [PDO::SQLSRV_ATTR_QUERY_TIMEOUT => 1]);
try {
    $statement->execute();
} catch (PDOException $error) {
    echo 'Отчёт не готов в отведённое время';
} finally {
    $statement->closeCursor();
}

SQLSRV_ATTR_QUERY_TIMEOUT задаёт секунды запроса: 1 здесь ограничивает двухсекундный WAITFOR. Это не LoginTimeout открытия соединения и не лимит времени PHP. Минимальный фрагмент показывает место обработки исключения; полный файл сопоставляет SQLSTATE и код именно ожидаемого таймаута, измеряет время и затем получает title/qty. Реальное время наблюдается в JSON, но не обещается точное срабатывание до миллисекунды.

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

НаблюдениеРезультат
Лимит команды1 секунда
Задержанный отчёттаймаут
Следующий запросГотовая поставка: 6
Файлdeadline.csv
Самостоятельный проект, исходник и архив
Ограничить ожидание отчёта и подтвердить следующий полезный запросСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p177Config();$db=p177Connect($config);$runtime=p177Runtime($db);

function appCsv177(string $name,array $records): void {
    $file=fopen(__DIR__.'/'.$name,'wb');
    if ($file===false) {throw new RuntimeException('Не удалось открыть свой CSV.');}
    try {foreach ($records as $record) {
        if (fputcsv($file,$record,',','"','',"\n")===false) {
            throw new RuntimeException('Не удалось записать CSV.');
        }
    }} finally {fclose($file);}
}

    $name=p177OwnTable($db,$owned,'deadline','(id int PRIMARY KEY,title nvarchar(80) NOT NULL,qty int NOT NULL)');
    $table=p177Qualified($name);p177Command($db,'INSERT INTO '.$table.' VALUES (?,?,?)',[1,'Готовая поставка',6]);
    $statement=$db->prepare("WAITFOR DELAY '00:00:02'; SELECT title,qty FROM ".$table.' WHERE id=1',
        [PDO::SQLSRV_ATTR_QUERY_TIMEOUT=>1]);
    $started=hrtime(true);
    try {$timeout=p177Capture(static function() use ($statement): void {
        $statement->execute();while($statement->fetch(PDO::FETCH_ASSOC)!==false) {}
    });} finally {$elapsedMs=(hrtime(true)-$started)/1000000;p177Release($statement);}
    $actual=p177Read($db,'SELECT title,qty FROM '.$table.' WHERE id=?',[1],
        [PDO::SQLSRV_ATTR_QUERY_TIMEOUT=>3]);
    $record=['title'=>$actual[0]['title'],'qty'=>(int)$actual[0]['qty']];
    appCsv177('deadline.csv',[
        ['operation','status','qty'],['delayed_report','timeout',''],['bounded_report','ready',$record['qty']]]);
    $rows=[['Лимит команды','1 секунда'],['Задержанный отчёт','таймаут'],
        ['Следующий запрос',$record['title'].': '.$record['qty']],['Файл','deadline.csv']];
    $checks=['timeout_reported'=>$timeout['failed']===true,
        'timeout_diagnosed'=>$timeout['state']==='HYT00' && $timeout['code']===0,
        'bounded_wait'=>$elapsedMs>=500 && $elapsedMs<10000,
        'one_record'=>count($actual)===1,'exact_title'=>$record['title']==='Готовая поставка',
        'exact_quantity'=>$record['qty']===6,'request_released'=>$statement===null,
        'no_manual_transaction'=>$db->inTransaction()===false,'artifact_written'=>is_file(__DIR__.'/deadline.csv')];
    $observations=['query_timeout_seconds'=>1,'requested_delay_seconds'=>2,'timeout'=>$timeout,
        'elapsed_ms'=>round($elapsedMs,3),'recovery_timeout_seconds'=>3,'next_query'=>$record,
        'transaction_started'=>false,'automatic_retries'=>0];

} finally {

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

p177Report('pdo177-query-deadline','Ограничить ожидание отчёта и подтвердить следующий полезный запрос',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Лимит команды — 1 секунда
Задержанный отчёт — таймаут
Следующий запрос — Готовая поставка: 6
Файл — deadline.csv

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

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

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

Таймаут отдельного SQL не доказывает недоступность сервера. Он не равен rollback всей бизнес-операции; здесь транзакция не открывается и задержанная команда ничего не меняет. В рабочей записи после таймаута нужно разобрать состояние операции и допустимость повтора, прежде чем отправлять её снова. Автоматического повтора в проекте нет. Ноль отключает ограничение запроса; отрицательные значения недопустимы. Обработчик не публикует внутренние пути или полное сообщение ODBC.

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

Почему нельзя после любого PDOException немедленно повторять INSERT с теми же данными?

Разбор

Причиной может быть неверный SQL, нарушение ограничения или неопределённый для приложения результат записи. Повтор способен создать дубль. Сначала нужны классификация ошибки, бизнес-ключ операции и явная политика восстановления; безопасный следующий SELECT в проекте не доказывает безопасность повторной записи.

Параметры, ошибки и подтверждение полезного результата

В вопросе о пагинации число в execute-массиве не стало нужным SQL-параметром OFFSET/FETCH. Решение нашего каталога — явный PARAM_INT и неизменный SQL. Замена ? на :name сама по себе не задаёт тип. Именованные и позиционные маркеры выбирают последовательно; в одном запросе их не смешивают.

bindValue сохраняет переданное значение, bindParam связывает переменную по ссылке для последующего execute. У PDO_SQLSRV длина bindParam имеет особый смысл: ненулевая длина или SQLSRV_PARAM_OUT_DEFAULT_SIZE в проверяемом исходнике переводит параметр в OUT; PARAM_INPUT_OUTPUT с размером задаёт INOUT. Поэтому длину не добавляют к обычному IN-параметру просто как «предел формы». Для строки формы проверяют вход, а ограничения хранилища задаёт схема. Подробности PDOStatement::bindParam() и выходных параметров процедуры дополняют эту границу.

Похожая потребность в обсуждении номера вставки — получить не только SQL-совет, но и понятную связь INSERT с fetch. Наша квитанция использует собственные данные и OUTPUT, затем commit и повторное чтение. Старое утверждение вопроса о недоступности lastInsertId не переносится на PDO_SQLSRV 5.13.3; контракт Microsoft описывает его и изменение смысла аргумента в версии 5.0.

Во вопросе о времени подключения настройку MySQL пытались перенести на SQLSRV. В этой главе LoginTimeout принадлежит DSN, QUERY_TIMEOUT — исполняемой команде. PHP max_execution_time и ограничения веб-сервера образуют ещё один слой; увеличение одного лимита не исправляет другой. Измерение в result.json проекта ожидания не обещает диагностику отказа всей сети.

НаблюдениеЧто оно означает и что делать
Драйвер не загруженНет нужного PHP-модуля в текущем SAPI; проверять PHP/ini/ABI, а не SQL таблицы.
Конструктор выбросил PDOExceptionСоединение не получено: разбирать адрес, транспорт, TLS, доступность базы и вход. Не выдавать raw DSN или пароль.
prepare вернул statement, execute отказалSQL мог ещё не проверяться сервером. Сохранить диагностику именно выполняемой команды.
fetch вернул false при успешном чтенииРезультат закончился или подходящих строк нет; NULL внутри найденной строки — другой случай.
rowCount равен 0Для DML не затронуты строки; не считать это автоматически ошибкой SQL. Для SELECT нужен договор курсора или отдельный COUNT(*).
Получены OUTPUT-строкиЕсть промежуточный результат команды; до завершения операции и commit нельзя подтверждать поставку.
Таймаут / поздняя ошибка при чтении или закрытииПроверить команду, транзакцию и незавершённые результаты. closeCursor способен дочитывать ответы и обнаружить поздний отказ.

PDOException::errorInfo содержит SQLSTATE, код драйвера и сообщение; PDO::errorInfo относится к соединению, PDOStatement::errorInfo — к statement. Снимайте ошибку до других вызовов и публикуйте безопасную причину, не реквизиты. ERRMODE_EXCEPTION задан явно: старые страницы Microsoft показывают SILENT как default, тогда как в PHP 8 общий default PDO — EXCEPTION. Успех и предупреждение также различаются, поэтому нельзя считать любой SQLSTATE не 00000 безусловным отказом всех данных.

Наличие MARS не снимает обязанности освобождать statements. closeCursor не закрывает PDO и не делает rollback; ссылка на statement может удерживать соединение после $db=null. Нельзя доказать reuse пула двумя равными числами ресурса или одним CLI-процессом. Эти проекты выключают pooling в своей среде и не заявляют измерение его производительности. Следующая глава связывает результаты, транзакцию и видимость другим соединением.

Попробуйте разделить ответственность на четыре строки: что проверяет форма, что гарантирует тип параметра PDO, что запрещает схема таблицы, что гарантирует commit. Например, для qty=0 число является допустимым int, схема может его хранить, а WHERE qty>0 не показывает товар в доступном каталоге. Ни один из этих шагов не заменяет остальные.

От приложения к точным данным и согласованным изменениям

Теперь приложение выбирает ограниченный набор строк, возвращает квитанцию вставки, сводит временные данные и не ждёт команду бесконечно. Дальше типы и результаты PDO_SQLSRV сохранят смысл даты, точной суммы и двоичного файла; транзакции и жизненный цикл объяснят резерв, снимок и несколько ответов процедуры. Общие контракты PDO остаются справочной основой; примеры процедурного SQLSRV полезны для сравнения, но не подменяют эти реальные вызовы PDO.

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

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

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

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

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