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

Дерево руководства
Глава 295 · Начало PDO_SQLSRV → самостоятельные проекты
PDO_SQLSRV: каталог приложения, квитанция импорта и границы команды
Полный DSN и базовые константы; от одного SELECT до Unicode-каталога, OUTPUT, временной таблицы и подтверждённого таймаута.
От запроса приложения к каталогу и квитанции
Приложению нужен каталог доступных товаров, поставщику — квитанция с присвоенными номерами, складу — сводка упаковок. PDO_SQLSRV позволяет получить эти результаты через общий интерфейс PDO и SQL Server. Здесь мы пройдём от одного SELECT к четырём самостоятельным проектам: каждый даёт читаемый HTML, данные JSON и полезный файл.
- PHP: фильтр «Монтаж», номер страницы и лимит 2
- PDO + расширение pdo_sqlsrv → Microsoft ODBC → выбранная база SQL Server
- SQL: выбрать и упорядочить нужные строки; PHP: прочитать ответ
- Результат для человека: 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.dllREADME каждого 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 |
|---|---|
| $dsn | sqlsrv: с Server и выбранными разрешёнными ключами. |
| $username, $password | SQL 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. |
| ConnectionPooling | 1/true или 0/false: участие в пуле соединений. Возможности и управление зависят от ОС/ODBC; это не обещание сохранённого состояния PHP-объекта. |
| Encrypt | 1/true или 0/false: политика шифрования соединения. Задавайте её явно с учётом используемого ODBC и транспорта. |
| TrustServerCertificate | 1/true доверяет сертификату без обычной проверки, 0/false требует проверки. Включение обхода не исправляет цепочку доверия или имя сервера. |
| Failover_Partner | Настроенный сервер/экземпляр партнёра зеркалирования. Не случайная запасная DSN; есть ограничения ОС и сочетания с MultiSubnetFailover. |
| MultipleActiveResultSets | 0/false отключает, 1/true включает MARS: несколько активных результатов на соединении. Не превращает обработку PHP в параллельную и не отменяет закрытие результатов. |
| QuotedId | 1/true — правила SQL-92 для идентификаторов в кавычках; 0/false — прежний режим T-SQL. Не способ экранирования значений. |
| TraceOn | 1/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_UNCOMMITTED | DSN 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_UTF8 | UTF-8; обычная кодировка PDO_SQLSRV по умолчанию. |
| SQLSRV_ENCODING_DEFAULT | Контекстный выбор: при открытии SYSTEM, при prepare — кодировка соединения. Не синоним UTF8 во всех местах. |
| SQLSRV_ATTR_QUERY_TIMEOUT | Целые неотрицательные секунды; 0 означает отсутствие лимита. Соединение/prepare/statement задают соответствующую область. |
| SQLSRV_ATTR_DIRECT_QUERY | true — прямое исполнение, 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_TYPE | DateTime вместо обычных строк даты/времени; настройка 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; следующий запрос страницы возвращает пустой список, а не ошибку.
- Свои строки: CAB/Монтаж/9; CLP/Монтаж/4; TAP/Монтаж/0; BOX/Тара/2
- WHERE оставляет CAB и CLP → ORDER BY id → OFFSET 0 / FETCH 2
- Три поля в 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, пустой строки и текста.
- incoming.json: S-31/Насос/2/null; S-32/Шланг/0/""; S-33/Клапан/5/Склад Б
- bindValue → INSERT OUTPUT → ID каждой позиции → closeCursor → следующий execute
- 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 |
Самостоятельный проект, исходник и архив
<?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 содержит сгруппированные количества.
- Одно PDO-соединение → собственная #таблица: CAB/3, CAB/4, CLP/5
- GROUP BY sku → COUNT упаковок + SUM единиц: CAB/2/7, CLP/1/5
- 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 создаёт контролируемую задержку без остановки сервера.
- Своя строка: Готовая поставка / qty=6
- Команда WAITFOR 2 секунды с лимитом 1 → ожидаемый отказ и сохранённая диагностика
- Освободить 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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


