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

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

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

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

Глава 298 · Начало PDO_ODBC → прикладные данные

PDO_ODBC: маршрут к базе, Unicode-импорт и собственная квитанция

Полная базовая настройка PDO_ODBC, DSN/FILEDSN, Pdo\Odbc и pooling; три полезных проекта на SQL Server через ODBC.

Найти товары в существующей базе и получить полезные файлы

У склада уже есть база, к которой другие программы подключаются через ODBC. PHP-приложению нужны доступные товары, импорт названий поставщика и квитанция с собственным номером заявки. PDO_ODBC позволяет выполнять эти задачи через общий интерфейс PDO и установленный ODBC-драйвер. Начнём с одного ответа SQL, затем сохраним полезные JSON и CSV с реальными строками.

Кто обрабатывает запрос каталога
  1. PHP: параметры «Монтаж», подготовка команды и чтение результата
  2. PDO_ODBC: методы PDO → вызовы ODBC
  3. Менеджер ODBC: выбирает установленный драйвер по DSN
  4. Драйвер SQL Server: передаёт SQL своему серверу
  5. База: фильтр строк → CAB 7, TAP 4 → PHP → route.json

ODBC — интерфейс подключения к разным базам. Менеджер ODBC находит нужный драйвер и управляет его вызовами; сам драйвер знает протокол конкретной СУБД. Поэтому слова «ODBC установлен» недостаточно: нужны расширение PHP, подходящий менеджер, драйвер базы и доступ к самой базе. На Windows менеджер входит в систему; unixODBC и iODBC — другие варианты. PDO_ODBC также поддерживает соединение с семейством IBM DB2 через DB2 CLI. Наличие одного варианта не доказывает готовность остальных.

Что выбираемЧто это означает в проектах
PHP и PDO_ODBCPHP 8.5.11 для Windows, встроенная в поставку DLL PDO_ODBC той же версии.
Менеджер и драйверWin32 ODBC и Microsoft ODBC Driver 18; это два отдельных слоя.
СерверОтдельный SQL Server LocalDB 2022, ProductVersion 16.0.1000.6, вход Windows.
SQL и данныеTOP, NVARCHAR, IDENTITY и OUTPUT относятся к SQL Server, а не к общему языку всех ODBC-баз.
Выходroute.json, imported-items.csv, receipt.csv; каждому проекту соответствуют HTML и JSON с фактическими проверками.

Сначала один маленький запрос

$db = new Pdo\Odbc('odbc:Warehouse', null, null, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$statement = $db->query('SELECT 2 + 3 AS total');
$row = $statement->fetch(PDO::FETCH_ASSOC);
if ($row === false) { throw new RuntimeException('Нет строки результата'); }
echo $row['total']; // 5
$statement->closeCursor();

Warehouse здесь — заранее настроенное имя DSN; его нельзя скопировать как готовый адрес вашего сервера. $db хранит объект соединения, знак = присваивает значение, new вызывает конструктор. Запись → в схеме показывает движение данных, а -> в PHP вызывает метод объекта. query передаёт SQL без пользовательских значений; fetch читает одну строку как массив с ключом total. AS total задаёт имя столбца результата. Даже если число пришло строкой «5», echo покажет 5; проверку PHP-типов нельзя заменять одним внешним видом вывода. Точка с запятой завершает инструкцию.

Пример показывает минимальную форму, а самостоятельные архивы ниже готовят собственные таблицы в отдельной учебной базе и удаляют их после результата. Текущий путь PDO, SQL и массива разбирают введение в базы данных и массивы; параметры вызова и методы дополняют функции и классы и объекты. Здесь сразу объясняется специфическая часть ODBC, а не предполагается знание имени каждого слоя.

Общий справочник PDO остаётся каноническим местом методов. Эта глава расширяет базовое описание PDO_ODBC установкой, всеми параметрами DSN и атрибутами класса; следующая глава о результатах покажет ограничения метаданных, счётчика строк и двоичных значений. Три маршрута чтения ниже дают одинаковый интерфейс PHP, но не обещают переносимость SQL без изменений.

Подготовить PDO_ODBC: Windows, unixODBC, DB2 CLI и generic

Сначала нужно сделать доступным драйвер PDO с именем odbc. Для диагностики важна именно та копия PHP и тот php.ini, которыми работает приложение: CLI, Apache и PHP-FPM могут загружать разные конфигурации. Ошибка «could not find driver» возникает до обращения к серверу. Успешный odbc_connect из другого расширения не подтверждает наличие PDO_ODBC.

var_dump(extension_loaded('pdo_odbc'));
var_dump(in_array('odbc', PDO::getAvailableDrivers(), true));
echo php_ini_loaded_file() ?: 'php.ini не загружен';

Для готового профиля первые две строки должны дать bool(true). Последняя строка предназначена для локальной диагностики: путь к конфигурации не выводят посетителю сайта. Это проверка загрузки расширения, а не успешного входа в базу. В обсуждении «PDO cannot find ODBC driver» одновременно смешались имя DSN, отсутствие префикса odbc: и конфигурация PHP. Разделение этих этапов позволяет исправить точную причину, не менять случайно драйвер другой СУБД.

Windows: расширение PHP и драйвер базы

Официальные Windows-поставки PHP включают php_pdo_odbc.dll, связанную с Windows ODBC Driver Manager. В активном php.ini включите extension=pdo_odbc и корректный extension_dir. Разрядность PHP, менеджера/DSN и ODBC-драйвера должна совпадать; отдельно учитывается разрядность администратора источников данных. Сборка расширения должна соответствовать версии и ABI PHP, включая TS/NTS. Не копируйте случайную DLL от другой версии. Затем установите ODBC-драйвер нужной базы по инструкции его поставщика.

; фрагмент php.ini, а не PHP-код
extension_dir="ext"
extension=pdo_odbc
pdo_odbc.connection_pooling=off

В панели управления можно создать System DSN с нужным сервером и базой и обращаться к нему как odbc:Warehouse. User DSN относится к учётной записи; веб-служба может работать от другой записи и не видеть этот источник. Доступность User DSN зависит от учётной записи, от которой запущен процесс, и её прав. Сам PDO не запрещает использование User DSN. Проект сравнения маршрутов использует полную строку и собственный FILEDSN, поэтому не меняет чужие записи DSN. Для SQL Server установленный Microsoft ODBC Driver не требует установки PHP-расширения PDO_SQLSRV: это отдельный путь, объяснённый в главе PDO_SQLSRV.

Unix: три варианта сборки, перечисленные в PHP Manual

ВариантЧто должно быть установленоФорма configure
ibm-db2Клиент DB2 с заголовками и библиотеками DB2 CLI; не только удалённый сервер.--with-pdo-odbc=ibm-db2,/путь/к/sqllib
unixODBCМенеджер unixODBC, его development-файлы и ODBC-драйвер выбранной СУБД.--with-pdo-odbc=unixODBC,/путь/к/unixODBC
genericДругой совместимый менеджер; имя библиотеки и флаги компиляции/связывания задаются явно.--with-pdo-odbc=generic,dir,libname,ldflags,cflags
# Формы из документации PHP для сборки PHP из исходников:
./configure --with-pdo-odbc=ibm-db2,/opt/IBM/db2/V8.1/
./configure --with-pdo-odbc=unixODBC,/usr/local
./configure --with-pdo-odbc=generic,/usr/local,libname,ldflags,cflags

Это варианты configure, выбираемые при сборке PHP; libname, ldflags и cflags в третьей строке — заменяемые обозначения, а не готовая команда для любой системы. PHP Manual допускает статическую либо shared-сборку. Для ibm-db2 документирован путь по умолчанию /home/db2inst1/sqllib, для unixODBC — /usr/local. В production выбирают актуальные пути установленного клиента. Упоминание V8.1, DB2 Universal Database, Cloudscape/Apache Derby и клиента DB2 Express-C относится к историческому объяснению поддержанного семейства, а не к рекомендации установить старый продукт. В этой поставке учебных проектов Unix/DB2 не исполнялись; Windows-результаты не подтверждают их настройку.

После сборки нужно настроить драйвер и каталог DSN самого менеджера; одна опция configure не создаёт запись Warehouse. Значение PDO_ODBC_TYPE показывает вариант библиотеки, с которым собран PDO_ODBC: документация перечисляет unixODBC, iODBC, generic; проверенная Windows-сборка возвращает Win32. PDO::ATTR_CLIENT_VERSION в ней равен ODBC-Win32. Это не версия Microsoft ODBC Driver 18. Версию конкретного драйвера получают из его средств диагностики и установленного пакета; PDO::getAttribute() не превращается в универсальный источник сведений обо всех слоях.

Упражнение: если odbc есть в PDO::getAvailableDrivers(), но DSN не найден, нужно ли заново собирать PHP? Сначала проверьте префикс, имя источника, разрядность и учётную запись процесса, затем наличие соответствующего ODBC-драйвера. При отсутствии odbc исправляют загрузку расширения; при ошибке связи уже проверяют профиль сервера. Подготовка отдельного стенда, точные команды и удаление только собственных объектов приведены в README каждого архива.

Выбрать DSN, строку подключения или FILEDSN без путаницы реквизитов

DSN — описание маршрута к источнику данных. PDO сначала видит префикс odbc:, а оставшуюся часть передаёт выбранному пути ODBC. Пользователь формы выбирает логический профиль из разрешённого списка, например warehouse-demo; он не вводит произвольные DRIVER, SERVER или путь .dsn. Профиль отвечает на вопрос «куда подключаться», а параметры SQL — «какие значения искать»; это разные входы.

Вид маршрутаЗаписьГде находятся настройки
Каталогизированный DSNodbc:WarehouseИсточник предварительно зарегистрирован менеджером ODBC или каталогом DB2 CLI.
Строка без каталогаodbc:DRIVER={ODBC Driver 18 for SQL Server};Server=...;Database=...Пары ключ=значение разрешает конкретный ODBC-драйвер.
Файловый источникodbc:FILEDSN=C:\app\warehouse.dsnМенеджер читает секцию [ODBC] файла; файл не содержит движок или сами данные.

В php-src 8.5.11 путь определяется наличием знака = в части после префикса: строка параметров идёт через SQLDriverConnect без диалогового окна, простое имя — через SQLConnect. Поэтому пропущенный odbc: не исправляется наличием System DSN. FILEDSN — ключ менеджера ODBC внутри обычного DSN; это не механизм PDO uri:, не URL и не подключение к базе, расположенной в файле .dsn.

$dsn = 'odbc:DRIVER={ODBC Driver 18 for SQL Server};'
    . 'Server=db.example.test;Database=warehouse;'
    . 'Encrypt=Yes;TrustServerCertificate=No';
$db = new Pdo\Odbc($dsn, $user, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    Pdo\Odbc::ATTR_ASSUME_UTF8 => true,
]);

Здесь $user и $password приходят из закрытой конфигурации, не из опубликованного исходника. Конструктор принимает string $dsn, ?string $username=null, ?string $password=null, ?array $options=null. Для Windows Integrated Authentication вместо SQL-логина профиль задаёт Trusted_Connection=Yes и передаёт null/null; процессу всё равно нужны права. В собственных LocalDB-примерах соединение идёт по локальному named pipe с явно заданным Encrypt=No. Этот частный выбор нельзя переносить на удалённый сервер: ODBC Driver 18 обычно включает шифрование по умолчанию, и удалённое подключение должно проверять сертификат. Параметры Encrypt и TrustServerCertificate принадлежат выбранному драйверу SQL Server.

UID, PWD и аргументы конструктора: четыре независимых случая

Что уже указано в DSNPHP 8.4 и новее
Нет UID и PWDНепустой или пустой username/password, если они не null, передаются как отдельные реквизиты; null оставляет соответствующий аргумент не заданным.
Только UIDИмя берётся из DSN; отдельный username игнорируется. Отдельный password по-прежнему применяется, если не null.
Только PWDПароль берётся из DSN; отдельный password игнорируется. Отдельный username по-прежнему применяется, если не null.
Оба ключаОба отдельных аргумента игнорируются; значения DSN имеют приоритет.

Важны две границы PHP 8.4. Раньше наличие любого из UID или PWD приводило к игнорированию обоих отдельных аргументов; теперь подавляется только соответствующий. Кроме того, отдельный пароль "" раньше не добавлялся, а теперь передаётся как пустой PWD=. Для прежнего поведения «не задавать пароль» передайте null. Пустой пароль и отсутствующий пароль — разные намерения; ни одно не является универсальным способом включить доверенный вход. Не создавайте DSN с дублированными ключами в надежде на исправление в конце строки.

// Только имя уже есть в DSN: в PHP 8.4+ password берётся из аргумента.
$db = new Pdo\Odbc('odbc:DRIVER={Vendor Driver};UID=reader', null, $password);
// Для настроенного каталога можно оставить реквизиты незаданными:
$db = new Pdo\Odbc('odbc:Warehouse', null, null);

Первый фрагмент иллюстрирует приоритет, а Vendor Driver обозначает реальный драйвер, который нужно установить и дополнить его обязательными параметрами. Конкретное имя базы не конкатенируют из URL. PHP автоматически оформляет добавляемые реквизиты для ODBC-строки, но произвольная самостоятельная сборка DRIVER/FILEDSN/APP и других значений остаётся ответственностью приложения. Фигурные скобки в ODBC помогают заключать специальные значения; это не экранирование SQL и не замена параметрам prepare.

Что лежит в FILEDSN

[ODBC]
DRIVER=ODBC Driver 18 for SQL Server
SERVER=db.example.test
DATABASE=warehouse
Encrypt=Yes
TrustServerCertificate=No

Так выглядит файл настроек, не PHP. Для обычного shareable FILEDSN секция [ODBC] содержит DRIVER и параметры соединения. Microsoft также описывает unshareable-файл с DSN, который ссылается на локальный каталог. Оба варианта нуждаются в установленном драйвере. В проекте используется только первый, собственный временный файл. Если FILEDSN и DSN одновременно находятся в строке SQLDriverConnect, порядок ключей влияет на выбор; обычные параметры строки могут переопределить значения файла. Надёжнее выбрать одну понятную форму, чем полагаться на скрытые приоритеты.

Не помещайте пароль в общедоступный .dsn и не считайте расширение файла защитой секрета. Механизм SAVEFILE менеджера не сохраняет пароль автоматически, но вручную записанный PWD всё равно будет обычным текстом. Путь из запроса пользователя может направить приложение к чужому файлу или серверу. В проекте маршрутов файл создаётся исключительно, сверяется с разрешённым профилем, второе соединение закрывается, затем файл удаляется. В импорте тот же профиль остаётся неизменным, а меняются только значения SQL.

Другие примеры официальной страницы и их границы

// Зарегистрированное имя — не обязательно SQL Server:
$dsn = 'odbc:testdb';
// Пример семейства DB2; реквизиты передаются отдельно:
$dsn = 'odbc:DRIVER={IBM DB2 ODBC DRIVER};HOSTNAME=db.example.test;'
    . 'PORT=50000;DATABASE=SAMPLE;PROTOCOL=TCPIP';
// Историческая форма для установленного Access-драйвера:
$dsn = 'odbc:Driver={Microsoft Access Driver (*.mdb)};Dbq=C:\\db.mdb;Uid=Admin';

PHP Manual показывает каталог, полную DB2-строку с UID/PWD и Access с Dbq. Здесь пароль вынесен из строки, а смысл всех вариантов сохранён. Эти фрагменты объясняют форму, не утверждают наличие DB2 или Access в стенде и не рекомендуют старый формат .mdb для нового сервиса. Наборы HOSTNAME/PROTOCOL и Dbq зависят от драйвера. PDO_ODBC не имеет собственного универсального списка параметров Server, Database, таймаута и кодировки для всех СУБД.

Упражнение: конфигурация содержит PWD, а программа передала другой пароль вторым отдельным реквизитом. Какой используется? В PHP 8.4+ — PWD из DSN. Уберите дублирование и оставьте один источник секрета. Если пароль должен быть не указан, передайте null, а не пустую строку. Проверяйте это при переносе старой конфигурации на PHP 8.4+, даже если SQL приложения не менялся.

Класс Pdo\Odbc и все его настройки

Начиная с PHP 8.4 класс Pdo\Odbc представляет соединение PDO_ODBC. Он наследует PDO; отдельного набора новых методов у него нет. Полезное отличие — явно названный драйвер и место для его констант. new PDO("odbc:...") создаёт обычный объект PDO, а new Pdo\Odbc(...) — конкретный класс; статическая фабрика PDO::connect(...) с доступным odbc возвращает Pdo\Odbc. Драйвер проверяют через ATTR_DRIVER_NAME, а не по предположению о написании переменной.

$db = PDO::connect($dsn, $user, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    Pdo\Odbc::ATTR_USE_CURSOR_LIBRARY => Pdo\Odbc::SQL_USE_DRIVER,
    Pdo\Odbc::ATTR_ASSUME_UTF8 => true,
]);
echo get_class($db);                         // Pdo\Odbc в выбранном профиле
echo $db->getAttribute(PDO::ATTR_DRIVER_NAME); // odbc

Статическая запись PDO::connect вызывает фабрику класса; запись $db->getAttribute читает настройку уже созданного соединения. Ошибка открытия выбрасывает PDOException независимо от выбранного ERRMODE. Для последующих операций проект явно задаёт ERRMODE_EXCEPTION. Полная общая сигнатура конструктора и унаследованные методы находятся в PDO::__construct(), PDO::connect() и справочнике PDO. Присутствие метода в унаследованном списке не означает, что конкретный драйвер реализует каждую возможность.

Все пять констант класса

Константа Pdo\Odbc::Назначение и значение настройки
ATTR_USE_CURSOR_LIBRARYКлюч атрибута подключения: выбирает участие ODBC Cursor Library, передаётся в options конструктора до открытия.
SQL_USE_IF_NEEDEDЗначение ATTR_USE_CURSOR_LIBRARY: менеджер использует библиотеку при необходимости. Это документированный выбор по умолчанию.
SQL_USE_DRIVERЗначение ATTR_USE_CURSOR_LIBRARY: работать с курсорами самого ODBC-драйвера, не подключая cursor library.
SQL_USE_ODBCЗначение ATTR_USE_CURSOR_LIBRARY: всегда использовать ODBC Cursor Library. Не универсальный режим улучшения результатов.
ATTR_ASSUME_UTF8Ключ настройки с bool-значением. На Windows включает обработку текста как UTF-8 для соответствующего Unicode-пути; false по умолчанию оставляет преобразование драйверу.

Числовые значения констант — детали API, поэтому в проектах написаны имена. ATTR_USE_CURSOR_LIBRARY — выбор библиотеки при подключении; PDO::ATTR_CURSOR и параметры fetch определяют поведение конкретного statement. Они не взаимозаменяемы. Microsoft помечает ODBC Cursor Library как устаревающее направление Windows и рекомендует для новой разработки курсоры драйвера. В учебном профиле явно выбран SQL_USE_DRIVER. Возможность прокрутки и её цену проверяет предпросмотр страниц; выбор библиотеки сам по себе не делает любой драйвер прокручиваемым.

Новые имена и совместимость со старым PHP

Старое имя PDO::, deprecated с PHP 8.5Замена Pdo\Odbc:: с PHP 8.4
ODBC_ATTR_USE_CURSOR_LIBRARYATTR_USE_CURSOR_LIBRARY
ODBC_SQL_USE_IF_NEEDEDSQL_USE_IF_NEEDED
ODBC_SQL_USE_DRIVERSQL_USE_DRIVER
ODBC_SQL_USE_ODBCSQL_USE_ODBC
ODBC_ATTR_ASSUME_UTF8ATTR_ASSUME_UTF8

Старые имена не объявляются удалёнными: PHP 8.5 помечает их устаревшими. Для кода на PHP 8.4+ используйте новые. Если библиотека действительно поддерживает более ранний PHP, выбирайте совместимую ветвь после проверки class_exists; не обращайтесь к несуществующему классу без проверки. Общая PDO_ODBC-страница сохраняет исторические сведения PECL PDO_ODBC ≥ 0.1.0; это не указание скачивать отдельный старый PECL-пакет вместо соответствующего расширения современной поставки PHP.

UTF-8: точная область настройки

PHP Manual описывает ATTR_ASSUME_UTF8 как Windows-путь преобразования текста UTF-16 → UTF-8 при true, с передачей конвертации драйверу при false. В исходнике php-8.5.11 проверка Unicode-пути относится к SQL_WCHAR, SQL_WVARCHAR и SQL_WLONGVARCHAR; потому в наших SQL Server-таблицах используются NCHAR/NVARCHAR, а не обещание исправить любой CHAR/VARCHAR. Входные PHP-строки должны уже быть UTF-8. Атрибут не устанавливает кодировку источника CSV, не меняет collation базы и не гарантирует идентичного поведения unixODBC/DB2.

$db->setAttribute(Pdo\Odbc::ATTR_ASSUME_UTF8, true);
$statement = $db->prepare('SELECT sku FROM products WHERE title = ?');
$statement->execute(["Кабель O'Reilly"]);
$row = $statement->fetch(PDO::FETCH_ASSOC);
if ($row !== false) { echo $row['sku']; }
$statement->closeCursor();

В импорте поставщика проверяются Unicode и апостроф при INSERT и при поиске, а квитанция возвращает название вместе с ID. Это два применения той же настройки, а не обещание смены кодировки сервера. Проверенная подготовка с true сохранила NVARCHAR точно; выключение на этой Windows-машине дало байты системной кодовой страницы. Такие байты не публикуются как UTF-8 JSON. На другом профиле кодовая страница может быть иной — проверять нужно сохранённые символы, а не угадывать «всегда CP1251».

Что унаследованный интерфейс не обещает

Вызов или атрибутКонтракт выбранного профиля
ATTR_DRIVER_NAMEodbc: имя расширения PDO, не имя SQL Server ODBC-драйвера.
ATTR_CLIENT_VERSIONODBC-Win32: разновидность библиотеки PDO_ODBC.
ATTR_SERVER_VERSION / ATTR_SERVER_INFO16.00.1000 / Microsoft SQL Server через SQLGetInfo. ProductVersion SQL Server отдельно равен 16.0.1000.6; строки не исправляют под одну форму.
ATTR_CONNECTION_STATUS / ATTR_TIMEOUTВ php-src 8.5.11 не реализованы этим getter; проверенный вызов дал IM001. Общее наличие константы PDO не создаёт поддержку.
quote / lastInsertIdВ таблице методов драйвера нет реализации; проверенный профиль дал IM001. Для значений используйте параметры, для ID — SQL-контракт своей СУБД.
getColumnMeta / rowCount / LOBДоступность, состав метаданных, счётчик и представление байтов требуют отдельных правил результата; они разобраны в следующей главе.

Упражнение: нужно ограничить время запроса, а PDO::ATTR_TIMEOUT оказался недоступен. Не подставляйте SQLSRV_ATTR_QUERY_TIMEOUT из другого расширения. Найдите документированный механизм конкретного ODBC-драйвера/СУБД и задайте отдельную политику восстановления после отказа. Общий интерфейс PDO облегчает устройство приложения, но не скрывает всех возможностей сервера.

Пул ODBC и настройки процесса: strict, relaxed, off и DB2INSTANCE

Пул соединений уменьшает работу при повторном открытии уже подходящего соединения. Это настройка слоя ODBC, а не кэш SQL-строк и не гарантия повторного использования одной сессии. Короткий CLI-проект не измеряет экономию пула. В наших архивных примерах pooling явно выключен; поэтому сравнение строк двух подключений не выдаётся за эксперимент с повторным использованием.

Директива php.iniОфициальное значение/областьСмысл
pdo_odbc.connection_poolingПо умолчанию strict; таблица PHP Manual помечает INI_ALL, но описание ограничивает настройку php.ini.strict — строго совпадающие атрибуты; relaxed — допускаются похожие; off или пустая строка — отключение.
pdo_odbc.db2_instance_nameПо умолчанию NULL; INI_SYSTEM; deprecated.Для DB2 CLI на Unix задаёт экземпляр через DB2INSTANCE, чтобы найти клиентские библиотеки и каталог. На Windows не действует.

Противоречие таблицы и текста pooling важно разрешать по исходнику: php-src 8.5.11 читает параметр при инициализации модуля, прежде чем создавать ODBC environment. Планируйте его как настройку запуска процесса в php.ini и перезапускайте соответствующий CLI/web-worker после изменения; не обещайте переключить уже созданный пул через ini_set. Настройка действует на весь процесс, а не на один объект $db, и может затронуть другие PHP-модули, использующие те же ODBC-библиотеки.

; Выбор одного режима перед запуском процесса:
pdo_odbc.connection_pooling=strict
; relaxed допустим только после оценки разделения приложений
; off отключает pooling; пустое значение тоже отключает

; Историческая DB2 CLI-настройка, только если нужен именно этот вариант:
; pdo_odbc.db2_instance_name=db2inst1

В strict соединения подбираются по одинаковым атрибутам. В relaxed соединения с похожими атрибутами могут попасть разным виртуальным хостам; официальный Manual не рекомендует relaxed на общем сервере. Учитывайте также process-wide эффект DB2INSTANCE: это не настройка отдельного запроса или отдельной базы внутри одного PHP-процесса. Для новой DB2-конфигурации директива db2_instance_name не рекомендуется и объявлена к удалению в будущем; подготовьте окружение DB2 штатным способом до запуска PHP. Она не настраивает SQL Server.

PDO::ATTR_PERSISTENT и ODBC pooling — разные механизмы. Завершение переменной PDO, завершение request и возврат физического соединения в пул имеют разные границы. До возврата соединения завершайте транзакции и освобождайте statements; не оставляйте режим работы следующему пользователю. Глава жизненного цикла показывает завершение запросов и транзакций, а общий PDO::__construct() объясняет persistent-option. Эти проекты не содержат HTTP-worker и не проверяют межзапросное повторное использование.

Два практических решения: для переносимого диагностического архива явно выбрать off и исключить влияние чужого пула; для долгоживущего сервиса выбрать strict до старта workers и отдельно измерить свою нагрузку. Ни один режим не меняет SQL-права. Упражнение: почему два виртуальных хоста нельзя изолировать одним лишь разным именем переменной PDO? Они могут жить в одном процессе и делить общий ODBC environment; изоляцию задают конфигурация, процессы и права, а не имена PHP-переменных.

Один каталог через строку подключения и собственный FILEDSN

Приложение склада должно открывать один и тот же каталог при двух вариантах развёртывания: настройки находятся в строке PHP либо в отдельном ODBC-файле. Сравним выбранные товары, а не только успешное создание объекта. Системный реестр DSN не изменяется: проект создаёт и затем удаляет только свой lab178.dsn.

Путь данных и полученный результат
  1. 4 строки: CAB 7, CLP 0, TAP 4, BOX 2
  2. category = Монтаж и qty > 0; TOP (2), ORDER BY id
  3. Прямое подключение → CAB 7, TAP 4
  4. FILEDSN → те же CAB 7, TAP 4
  5. route.json: выбранные товары и совпадение маршрутов

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

$db = new Pdo\Odbc('odbc:FILEDSN=C:\\app\\warehouse.dsn', null, null, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    Pdo\Odbc::ATTR_ASSUME_UTF8 => true,
]);
$query = $db->prepare('SELECT TOP (2) sku, title FROM products
    WHERE category = ? AND qty > 0 ORDER BY id');
$query->execute(['Монтаж']);
$items = $query->fetchAll(PDO::FETCH_ASSOC);

new создаёт соединение; Pdo\Odbc — имя класса драйвера. Строка после odbc:FILEDSN= указывает файл настроек, а не файл базы. null означает, что отдельные имя пользователя и пароль не переданы. Массив последнего аргумента содержит атрибуты PDO. prepare получает постоянный SQL, execute — значение вместо ?. TOP (2) и ORDER BY id задают размер и порядок результата на сервере. В полном проекте точное содержимое собственного FILEDSN дополнительно сверяется с выбранным профилем.

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

НаблюдениеРезультат
Профильwarehouse-demo
Маршрутыстрока подключения; FILEDSN
КаталогCAB: Кабель — 7; TAP: Лента — 4
Одинаковые строкида
Временный FILEDSN удалёнда
Файлroute.json
Самостоятельный проект, исходник и архив
Один каталог через строку подключения и собственный FILEDSNСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p178Config();$db=p178Connect($config);$runtime=p178Runtime($db);

    $name=p178OwnTable($db,$owned,'route','(id int PRIMARY KEY,sku varchar(12) NOT NULL,
        title nvarchar(80) NOT NULL,category nvarchar(30) NOT NULL,qty int NOT NULL)');
    $table=p178Qualified($name);
    foreach ([[1,'CAB','Кабель','Монтаж',7],[2,'CLP','Клемма','Монтаж',0],
              [3,'TAP','Лента','Монтаж',4],[4,'BOX','Короб','Тара',2]] as $item) {
        p178Command($db,'INSERT INTO '.$table.' (id,sku,title,category,qty) VALUES (?,?,?,?,?)',$item);
    }
    $sql='SELECT TOP (2) sku,title,qty FROM '.$table.' WHERE category=? AND qty>0 ORDER BY id';
    $direct=p178Read($db,$sql,['Монтаж']);
    $filePath=__DIR__.'/lab178.dsn';$fileOwned=false;$fileHandle=null;$fileDb=null;
    $fromFile=[];$fileClass=null;$fileDatabase=null;
    try {
        $fileHandle=fopen($filePath,'x+b');
        if ($fileHandle===false) {throw new RuntimeException('Собственный FILEDSN уже существует или недоступен.');}
        $fileOwned=true;$fileText=p178FileDsnText($config);
        if (fwrite($fileHandle,$fileText)!==strlen($fileText)) {throw new RuntimeException('FILEDSN записан не полностью.');}
        fclose($fileHandle);$fileHandle=null;
        $fileDb=p178FileDsnConnect($config,$filePath);
        p178AssertDatabase($fileDb);
        $fileClass=get_class($fileDb);
        $fileDatabase=p178Read($fileDb,'SELECT DB_NAME() AS database_name')[0]['database_name'];
        $fromFile=p178Read($fileDb,$sql,['Монтаж']);
    } finally {
        try {p178Close($fileDb);} finally {
            if (is_resource($fileHandle)) {fclose($fileHandle);$fileHandle=null;}
            if ($fileOwned && is_file($filePath) && !unlink($filePath)) {
                throw new RuntimeException('Не удалось удалить собственный FILEDSN.');
            }
        }
    }
    $catalog=[];
    foreach ($direct as $row) {$catalog[]=['sku'=>$row['sku'],'title'=>$row['title'],'qty'=>(int)$row['qty']];}
    $expected=[['sku'=>'CAB','title'=>'Кабель','qty'=>7],['sku'=>'TAP','title'=>'Лента','qty'=>4]];
    $document=['profile'=>'warehouse-demo','driver'=>$db->getAttribute(PDO::ATTR_DRIVER_NAME),
        'routes'=>['connection-string','file-dsn'],'same_catalog'=>$direct===$fromFile,'items'=>$catalog];
    $encoded=json_encode($document,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)."\n";
    if (file_put_contents(__DIR__.'/route.json',$encoded)!==strlen($encoded)) {throw new RuntimeException('Отчёт не записан.');}
    $rows=[['Профиль','warehouse-demo'],['Маршруты','строка подключения; FILEDSN'],
        ['Каталог','CAB: Кабель — 7; TAP: Лента — 4'],['Одинаковые строки',$direct===$fromFile],
        ['Временный FILEDSN удалён',!is_file($filePath)],['Файл','route.json']];
    $checks=['four_seed_rows'=>(int)p178Read($db,'SELECT COUNT(*) AS total FROM '.$table)[0]['total']===4,
        'two_matching_rows'=>count($catalog)===2,'exact_catalog'=>$catalog===$expected,
        'same_catalog'=>$direct===$fromFile,'projected_columns'=>array_keys($direct[0])===['sku','title','qty'],
        'driver_is_odbc'=>$document['driver']==='odbc','file_class'=>$fileClass==='Pdo\\Odbc',
        'same_database'=>$fileDatabase==='phpmanual178','file_removed'=>!is_file($filePath),
        'second_connection_closed'=>$fileDb===null,'no_transaction'=>$db->inTransaction()===false,
        'artifact_written'=>is_file(__DIR__.'/route.json')];
    $observations=['routes'=>$document['routes'],'direct_rows'=>$direct,'file_rows'=>$fromFile,
        'raw_qty_type'=>get_debug_type($direct[0]['qty']),'catalog'=>$catalog,'file_class'=>$fileClass,
        'odbc_library'=>PDO_ODBC_TYPE,'driver_client'=>$db->getAttribute(PDO::ATTR_CLIENT_VERSION)];

} finally {

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

p178Report('pdo178-profile-route','Один каталог через строку подключения и собственный FILEDSN',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Профиль — warehouse-demo
Маршруты — строка подключения; FILEDSN
Каталог — CAB: Кабель — 7; TAP: Лента — 4
Одинаковые строки — да
Временный FILEDSN удалён — да
Файл — route.json

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

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

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

Одинаковый результат не доказывает повторное использование физического соединения или скорость пула. FILEDSN не устанавливает драйвер и не предоставляет права на базу. В опубликованном JSON нет DSN, пути, имени компьютера и реквизитов. Путь FILEDSN берётся из собственного каталога проекта; параметр формы не выбирает файл подключения. Копирование .dsn на другую машину требует соответствующего драйвера и доступного сервера.

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

Добавьте правило: выбирать доступные товары категории «Тара». Что поменяется в маршруте и в результате?

Разбор

Оставьте SQL и оба способа подключения прежними, передайте «Тара» вместо «Монтаж». Получится BOX: Короб — 2. Если изменить одновременно драйвер или сервер, это уже дополнительная проверка совместимости, а не только смена значения фильтра.

Импортировать русский каталог и проверить сохранённые символы

Поставщик прислал CSV в UTF-8. Названия должны остаться правильными и для другого приложения, которое прочитает SQL Server. Поэтому недостаточно получить обратно похожую строку: проверим поиск по названию и серверный код первого символа. Отдельное поле note_state сохраняет отличие SQL NULL от пустого текста при выгрузке в CSV.

Путь данных и полученный результат
  1. supplier.csv: Кабель/апостроф, qty 0, NULL, пустой текст, «0», ✓
  2. Разбор пяти полей и явное состояние примечания
  3. NVARCHAR + ATTR_ASSUME_UTF8=true → четыре параметризованные INSERT
  4. commit → новый SELECT и UNICODE(title)=1050
  5. imported-items.csv сохраняет note_state и все четыре строки

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

$db->setAttribute(Pdo\Odbc::ATTR_ASSUME_UTF8, true);
$statement = $db->prepare('INSERT INTO products (title, note) VALUES (?, ?)');
$statement->bindValue(1, "Кабель O'Reilly", PDO::PARAM_STR);
$statement->bindValue(2, null, PDO::PARAM_NULL);
$statement->execute();
$statement->closeCursor();

В этой Windows-сборке true включает путь UTF-8 ↔ Unicode для широких SQL-типов; столбцы title и note имеют тип NVARCHAR. Атрибут задаётся до prepare. ? обозначает значение, поэтому апостроф в O'Reilly не закрывает SQL-строку. bindValue передаёт текст или настоящий null с соответствующим типом. Один prepared statement принимает четыре набора данных. Транзакция завершает небольшой импорт целиком; затем новый SELECT проверяет сохранённые строки.

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

НаблюдениеРезультат
Принято строк4
НазваниеКабель O'Reilly
Первый символ в SQL1050
Байты названияPHP UTF-8: 21; SQL NVARCHAR: 30
ПримечанияNULL; пустая строка; текст 0; Проверено ✓
Файлimported-items.csv
Самостоятельный проект, исходник и архив
Импортировать русский каталог и проверить сохранённые символыСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p178Config();$db=p178Connect($config);$runtime=p178Runtime($db);

function appCsv178(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=p178OwnTable($db,$owned,'unicode','(id int PRIMARY KEY,sku varchar(16) NOT NULL UNIQUE,
        title nvarchar(80) NOT NULL,qty int NOT NULL,note nvarchar(120) NULL)');
    $table=p178Qualified($name);$input=fopen(__DIR__.'/supplier.csv','rb');$items=[];
    if ($input===false) {throw new RuntimeException('Нет исходного CSV.');}
    try {
        if (fgetcsv($input,0,';','"','')!==['sku','title','qty','note_state','note']) {
            throw new RuntimeException('Неожиданные заголовки поставщика.');
        }
        while (($fields=fgetcsv($input,0,';','"',''))!==false) {
            if (count($fields)!==5) {throw new RuntimeException('Ожидаются пять полей.');}
            [$sku,$title,$qtyText,$state,$noteText]=$fields;
            $qty=filter_var($qtyText,FILTER_VALIDATE_INT,['options'=>['min_range'=>0]]);
            if ($sku==='' || $title==='' || $qty===false || !in_array($state,['NULL','EMPTY','TEXT'],true)
                || ($state!=='TEXT' && $noteText!=='')) {throw new RuntimeException('Неверная строка поставщика.');}
            $items[]=['sku'=>$sku,'title'=>$title,'qty'=>$qty,'note'=>$state==='NULL'?null:$noteText];
        }
    } finally {fclose($input);}
    $oldUtf8=$db->getAttribute(Pdo\Odbc::ATTR_ASSUME_UTF8);$statement=null;
    try {
        $db->setAttribute(Pdo\Odbc::ATTR_ASSUME_UTF8,true);
        p178Begin($db);
        $statement=$db->prepare('INSERT INTO '.$table.' (id,sku,title,qty,note) VALUES (?,?,?,?,?)');
        foreach ($items as $index=>$item) {
            $statement->bindValue(1,$index+1,PDO::PARAM_INT);
            $statement->bindValue(2,$item['sku'],PDO::PARAM_STR);
            $statement->bindValue(3,$item['title'],PDO::PARAM_STR);
            $statement->bindValue(4,$item['qty'],PDO::PARAM_INT);
            $statement->bindValue(5,$item['note'],$item['note']===null?PDO::PARAM_NULL:PDO::PARAM_STR);
            $statement->execute();$statement->closeCursor();
        }
        p178Release($statement);p178Commit($db);
    } finally {p178Release($statement);p178Rollback($db);$db->setAttribute(Pdo\Odbc::ATTR_ASSUME_UTF8,$oldUtf8);}
    $raw=p178Read($db,'SELECT sku,title,qty,note FROM '.$table.' WHERE id BETWEEN 1 AND 4 ORDER BY id');
    $saved=[];$records=[['sku','title','qty','note_state','note']];
    foreach ($raw as $row) {
        $saved[]=['sku'=>$row['sku'],'title'=>$row['title'],'qty'=>(int)$row['qty'],'note'=>$row['note']];
        $state=$row['note']===null?'NULL':($row['note']===''?'EMPTY':'TEXT');
        $records[]=[$row['sku'],$row['title'],(int)$row['qty'],$state,$row['note']??''];
    }
    $found=p178Read($db,'SELECT sku FROM '.$table.' WHERE title=?',["Кабель O'Reilly"]);
    $stored=p178Read($db,'SELECT UNICODE(title) AS first_codepoint,DATALENGTH(title) AS stored_bytes
        FROM '.$table.' WHERE sku=?',['K-11'])[0];
    appCsv178('imported-items.csv',$records);
    $rows=[['Принято строк',count($saved)],['Название', $saved[0]['title']],['Первый символ в SQL', (int)$stored['first_codepoint']],
        ['Байты названия','PHP UTF-8: '.strlen($saved[0]['title']).'; SQL NVARCHAR: '.(int)$stored['stored_bytes']],
        ['Примечания','NULL; пустая строка; текст 0; Проверено ✓'],['Файл','imported-items.csv']];
    $checks=['four_rows'=>count($saved)===4,'exact_roundtrip'=>$saved===$items,
        'unicode_apostrophe'=>$saved[0]['title']==="Кабель O'Reilly",'lookup_by_title'=>$found===[['sku'=>'K-11']],
        'server_first_character'=>(int)$stored['first_codepoint']===1050,'server_utf16_bytes'=>(int)$stored['stored_bytes']===30,
        'php_utf8_bytes'=>strlen($saved[0]['title'])===21,'sql_null'=>$saved[0]['note']===null,
        'empty_string'=>$saved[1]['note']==='','text_zero'=>$saved[2]['note']==='0',
        'checkmark'=>$saved[3]['note']==='Проверено ✓','zero_quantity'=>$saved[1]['qty']===0,
        'mode_restored'=>$db->getAttribute(Pdo\Odbc::ATTR_ASSUME_UTF8)===$oldUtf8,
        'request_released'=>$statement===null,'committed'=>$db->inTransaction()===false,
        'artifact_written'=>is_file(__DIR__.'/imported-items.csv')];
    $observations=['input'=>$items,'raw_rows'=>$raw,'saved_rows'=>$saved,'raw_qty_type'=>get_debug_type($raw[0]['qty']),
        'storage_probe'=>['first_codepoint'=>(int)$stored['first_codepoint'],'stored_bytes'=>(int)$stored['stored_bytes']],
        'php_title_bytes'=>strlen($saved[0]['title']),'assume_utf8'=>$oldUtf8];

} finally {

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

p178Report('pdo178-unicode-import','Импортировать русский каталог и проверить сохранённые символы',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Принято строк — 4
Название — Кабель O'Reilly
Первый символ в SQL — 1050
Байты названия — PHP UTF-8: 21; SQL NVARCHAR: 30
Примечания — NULL; пустая строка; текст 0; Проверено ✓
Файл — imported-items.csv

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

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

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

fgetcsv разбирает разделители и кавычки, но не определяет и не исправляет кодировку. Входной файл проекта уже UTF-8. ATTR_ASSUME_UTF8 — настройка PDO_ODBC для Windows, а не команда смены кодировки базы; для обычного VARCHAR и других менеджеров результат нельзя переносить автоматически. Флаг не исправит ранее повреждённые данные. strlen считает байты PHP-строки, DATALENGTH — байты SQL-представления: 21 и 30 описывают одно название, а не две длины для проверки размера поля. Пакет из четырёх строк не является загрузчиком неограниченного файла.

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

Добавьте строку с qty=0 и примечанием «0». Как сохранить оба значения и не превратить их в отсутствие данных?

Разбор

Оставьте проверку $qty===false, а не if (!$qty), и note_state=TEXT. Для SQL NULL используйте только явную метку NULL. Не применяйте empty() к этим полям: она считает строку «0» и число 0 пустыми. После импорта подтвердите значения отдельным SELECT.

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

После добавления заявки сайт должен показать номер именно этой записи. Поиск самого большого id может выбрать чужую заявку. Получим номер из INSERT через OUTPUT, сохраним его отдельно, подтвердим транзакцию и прочитаем запись по этому номеру вместе с известным client_ref. Вторая последовательная вставка показывает, почему «последняя запись таблицы» — другое понятие.

Путь данных и полученный результат
  1. R-42: Кабель O'Reilly × 3 → INSERT ... OUTPUT → id 1001
  2. commit подтверждает R-42
  3. Позже R-43: Лента × 8 → id 1002
  4. SELECT WHERE id=1001 AND client_ref=R-42 → прежняя заявка
  5. receipt.csv: 1001, R-42, 3, committed

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

$statement = $db->prepare('INSERT INTO orders (client_ref, title, qty)
    OUTPUT INSERTED.id VALUES (?, ?, ?)');
$statement->execute(['R-42', "Кабель O'Reilly", 3]);
$row = $statement->fetch(PDO::FETCH_ASSOC);
if ($row === false) { throw new RuntimeException('Нет номера вставки'); }
$id = (int) $row['id'];
$statement->closeCursor();

OUTPUT INSERTED.id — синтаксис SQL Server: команда возвращает значение только что вставленной строки как результат SELECT. fetch читает эту строку; false означает, что её нет. Приведение к int здесь допустимо потому, что схема использует SQL int и небольшие номера. Минимальная запись работает в режиме autocommit; полный проект явно начинает и подтверждает транзакции, дочитывает ответ и лишь затем формирует квитанцию. Общий PDO::lastInsertId не является универсальной реализацией для каждого ODBC-драйвера.

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

НаблюдениеРезультат
ЗаявкаR-42
Собственный номер1001
ТоварКабель O'Reilly × 3
Позднейшая заявкаR-43: 1002
Подтверждениеcommit и SELECT по id + client_ref
Файлreceipt.csv
Самостоятельный проект, исходник и архив
Выдать квитанцию о своей записи, даже если позже появилась другаяСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p178Config();$db=p178Connect($config);$runtime=p178Runtime($db);

function appCsv178(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=p178OwnTable($db,$owned,'receipt','(id int IDENTITY(1001,1) PRIMARY KEY,
        client_ref varchar(20) NOT NULL UNIQUE,title nvarchar(80) NOT NULL,qty int NOT NULL)');
    $table=p178Qualified($name);$statement=null;$own=null;$later=null;
    try {
        p178Begin($db);
        $statement=$db->prepare('INSERT INTO '.$table.' (client_ref,title,qty)
            OUTPUT INSERTED.id,INSERTED.client_ref,INSERTED.title,INSERTED.qty VALUES (?,?,?)');
        $statement->bindValue(1,'R-42',PDO::PARAM_STR);
        $statement->bindValue(2,"Кабель O'Reilly",PDO::PARAM_STR);
        $statement->bindValue(3,3,PDO::PARAM_INT);
        $statement->execute();$own=$statement->fetch(PDO::FETCH_ASSOC);$ownEnd=$statement->fetch(PDO::FETCH_ASSOC);
        if (!is_array($own) || $ownEnd!==false) {throw new RuntimeException('Ожидался один номер своей вставки.');}
        $statement->closeCursor();p178Commit($db);
        // Другая заявка появляется позже. Она не меняет номер уже полученной квитанции.
        p178Begin($db);
        $statement->bindValue(1,'R-43',PDO::PARAM_STR);
        $statement->bindValue(2,'Лента',PDO::PARAM_STR);
        $statement->bindValue(3,8,PDO::PARAM_INT);
        $statement->execute();$later=$statement->fetch(PDO::FETCH_ASSOC);$laterEnd=$statement->fetch(PDO::FETCH_ASSOC);
        if (!is_array($later) || $laterEnd!==false) {throw new RuntimeException('Ожидался номер второй заявки.');}
        p178Release($statement);p178Commit($db);
    } finally {p178Release($statement);p178Rollback($db);}
    $saved=p178Read($db,'SELECT id,client_ref,title,qty FROM '.$table.' WHERE id=? AND client_ref=?',[(int)$own['id'],'R-42']);
    if (count($saved)!==1) {throw new RuntimeException('Своя запись не найдена после commit.');}
    $receipt=['id'=>(int)$saved[0]['id'],'client_ref'=>$saved[0]['client_ref'],
        'title'=>$saved[0]['title'],'qty'=>(int)$saved[0]['qty'],'status'=>'committed'];
    appCsv178('receipt.csv',[array_keys($receipt),array_values($receipt)]);
    $rows=[['Заявка',$receipt['client_ref']],['Собственный номер',$receipt['id']],
        ['Товар',$receipt['title'].' × '.$receipt['qty']],['Позднейшая заявка','R-43: '.(int)$later['id']],
        ['Подтверждение','commit и SELECT по id + client_ref'],['Файл','receipt.csv']];
    $checks=['own_id'=>(int)$own['id']===1001,'own_reference'=>$own['client_ref']==='R-42',
        'later_id'=>(int)$later['id']===1002,'different_rows'=>$own['id']!==$later['id'],
        'own_eof'=>$ownEnd===false,'later_eof'=>$laterEnd===false,'one_readback'=>count($saved)===1,
        'exact_receipt'=>$receipt===['id'=>1001,'client_ref'=>'R-42','title'=>"Кабель O'Reilly",'qty'=>3,'status'=>'committed'],
        'output_matches_readback'=>$own===$saved[0],'both_rows_persisted'=>(int)p178Read($db,'SELECT COUNT(*) AS total FROM '.$table)[0]['total']===2,
        'request_released'=>$statement===null,'committed'=>$db->inTransaction()===false,
        'artifact_written'=>is_file(__DIR__.'/receipt.csv')];
    $observations=['own_output'=>$own,'later_output'=>$later,'readback'=>$saved[0],
        'raw_id_type'=>get_debug_type($own['id']),'receipt'=>$receipt,'later_insert_is_sequential'=>true];

} finally {

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

p178Report('pdo178-own-receipt','Выдать квитанцию о своей записи, даже если позже появилась другая',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Заявка — R-42
Собственный номер — 1001
Товар — Кабель O'Reilly × 3
Позднейшая заявка — R-43: 1002
Подтверждение — commit и SELECT по id + client_ref
Файл — receipt.csv

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

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

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

Это две последовательные заявки, а не тест конкурентной нагрузки. OUTPUT может отдать строки до последующей ошибки или отката: видимый номер сам по себе не является подтверждением сохранения. Не публикуйте квитанцию до успешного commit. Порядок OUTPUT для многострочной вставки не обещан; используйте бизнес-ключ для сопоставления. IDENTITY допускает пропуски и не заменяет отдельные правила нумерации документов. В проверенном PDO_ODBC 8.5.11 lastInsertId не реализован и сообщает IM001; OUTPUT относится к SQL Server, а другие СУБД требуют своего SQL-контракта.

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

Разрешите пользователю повторить запрос с тем же client_ref. Достаточно ли ещё раз выполнить INSERT?

Разбор

UNIQUE(client_ref) отвергнет вторую запись с тем же ключом. Для повторяемого запроса спроектируйте правило: при подтверждённом конфликте читать прежнюю запись по client_ref, проверять совпадение данных и возвращать прежнюю квитанцию. Не перехватывайте все ошибки как «запись уже есть»; ошибки связи и нарушения других ограничений требуют отдельной обработки.

Отличить ошибку маршрута, повреждение текста и неподтверждённую запись

Разделяйте готовность маршрута и успех бизнес-операции. Загруженный PDO_ODBC не подтверждает установленный ODBC-драйвер; успешное подключение не подтверждает права INSERT; полученный ID не подтверждает commit. Видимый итог проекта — выбранные товары, сохранённые символы или квитанция, а не только созданный объект PDO.

ГраницаЧто проверить
PDO не находит драйверПрефикс odbc:, PDO::getAvailableDrivers(), активный php.ini и наличие pdo_odbc. Это ещё не ошибка пароля.
ODBC не находит DSN/драйверИмя источника, разрядность, драйвер и учётная запись процесса. Типичные SQLSTATE менеджера: IM002, IM003, IM014; конкретный текст зависит от платформы.
Не удалось подключиться / войтиСервер, маршрут сети и реквизиты; возможны 08001 или 28000. Не публикуйте сообщение с адресами и путями без проверки.
Не поддержан метод/атрибутIM001 или HYC00 относятся к возможностям слоя драйвера; не исправляются приведением результата к нужному PHP-типу.
SQL выполнен, строк нетfetch возвращает false при конце результата; пустой список — допустимый ответ фильтра. Ошибки при ERRMODE_EXCEPTION ловят как исключения.
Вернулась строка с NULL/0/пустым текстомПроверяйте строго === null, === false, === "" и собственную схему. Не заменяйте все значения проверкой empty().
Получен ответ INSERTЗакончите чтение statement, подтвердите транзакцию и перечитайте свою запись до обещания пользователю.

Для ожидаемого отказа сохраняйте SQLSTATE и код из PDOException::errorInfo сразу в catch. Полный errorInfo содержит текст ODBC, который может раскрывать настройки. В публичном отчёте нужен безопасный тип отказа; исходное сообщение оставляют в закрытом журнале. PDO::errorInfo относится к соединению, PDOStatement::errorInfo — к конкретной команде. Общие PDO::errorInfo(), PDOStatement::errorInfo() и исключения объясняют структуру и порядок обработки.

В вопросе о Unicode через PDO_ODBC приложение читало свой текст, но другие средства показывали повреждённые символы. Старые обходы с ручным hex не являются правилом для PHP 8.5. Наш импорт использует документированную Windows-настройку, NVARCHAR, привязку и отдельную SQL-проверку UNICODE. Успешный круг «записал → прочитал» дополнен проверкой того, что действительно хранится в базе.

В обсуждении получения номера ODBC-вставки исходная СУБД и предложенные SQL-решения различались. Нельзя перенести MySQL LAST_INSERT_ID(), MAX(id) или общий совет PDO::lastInsertId на любой ODBC-маршрут. Наша квитанция опирается на документированный OUTPUT SQL Server и свою бизнес-ссылку. Последующий SELECT не ищет «последнюю строку базы» и не обещает защиту от всех повторов HTTP-запроса.

Параметры prepare защищают границы значений, но не являются способом передать имя таблицы, поле ORDER BY или DSN. Выбирайте такие элементы из собственной разрешённой схемы. Новый класс Pdo\Odbc не меняет права, не шифрует автоматически любой удалённый профиль и не превращает специфический SQL Server-синтаксис в переносимый SQL. При смене драйвера отдельно проверяют текст, точные числа, бинарные данные, курсоры, границы транзакций и обработку отказов. Именно этим продолжением служат контракты результатов и жизненный цикл операций.

Продолжить от маршрута к типам и целой операции

Теперь выбранный маршрут выдаёт товары, импорт сохраняет смысл текста и NULL, а квитанция связана со своей записью. Дальше выгрузка точных значений и метаданных объяснит PHP-строки, размеры, NULL и байты. Транзакции, процедура и восстановление запроса свяжут несколько действий в подтверждённую операцию. Общие методы PDO остаются общей основой, а прямой драйвер PDO_SQLSRV — отдельным выбором для SQL Server с другим набором атрибутов.

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

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

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

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

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