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

Дерево руководства
Глава 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 с реальными строками.
- PHP: параметры «Монтаж», подготовка команды и чтение результата
- PDO_ODBC: методы PDO → вызовы ODBC
- Менеджер ODBC: выбирает установленный драйвер по DSN
- Драйвер SQL Server: передаёт SQL своему серверу
- База: фильтр строк → CAB 7, TAP 4 → PHP → route.json
ODBC — интерфейс подключения к разным базам. Менеджер ODBC находит нужный драйвер и управляет его вызовами; сам драйвер знает протокол конкретной СУБД. Поэтому слова «ODBC установлен» недостаточно: нужны расширение PHP, подходящий менеджер, драйвер базы и доступ к самой базе. На Windows менеджер входит в систему; unixODBC и iODBC — другие варианты. PDO_ODBC также поддерживает соединение с семейством IBM DB2 через DB2 CLI. Наличие одного варианта не доказывает готовность остальных.
| Что выбираем | Что это означает в проектах |
|---|---|
| PHP и PDO_ODBC | PHP 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 — «какие значения искать»; это разные входы.
| Вид маршрута | Запись | Где находятся настройки |
|---|---|---|
| Каталогизированный DSN | odbc: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 и аргументы конструктора: четыре независимых случая
| Что уже указано в DSN | PHP 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_LIBRARY | ATTR_USE_CURSOR_LIBRARY |
| ODBC_SQL_USE_IF_NEEDED | SQL_USE_IF_NEEDED |
| ODBC_SQL_USE_DRIVER | SQL_USE_DRIVER |
| ODBC_SQL_USE_ODBC | SQL_USE_ODBC |
| ODBC_ATTR_ASSUME_UTF8 | ATTR_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_NAME | odbc: имя расширения PDO, не имя SQL Server ODBC-драйвера. |
| ATTR_CLIENT_VERSION | ODBC-Win32: разновидность библиотеки PDO_ODBC. |
| ATTR_SERVER_VERSION / ATTR_SERVER_INFO | 16.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.
- 4 строки: CAB 7, CLP 0, TAP 4, BOX 2
- category = Монтаж и qty > 0; TOP (2), ORDER BY id
- Прямое подключение → CAB 7, TAP 4
- FILEDSN → те же CAB 7, TAP 4
- 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 |
Самостоятельный проект, исходник и архив
<?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.
- supplier.csv: Кабель/апостроф, qty 0, NULL, пустой текст, «0», ✓
- Разбор пяти полей и явное состояние примечания
- NVARCHAR + ATTR_ASSUME_UTF8=true → четыре параметризованные INSERT
- commit → новый SELECT и UNICODE(title)=1050
- 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 |
| Первый символ в SQL | 1050 |
| Байты названия | 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. Вторая последовательная вставка показывает, почему «последняя запись таблицы» — другое понятие.
- R-42: Кабель O'Reilly × 3 → INSERT ... OUTPUT → id 1001
- commit подтверждает R-42
- Позже R-43: Лента × 8 → id 1002
- SELECT WHERE id=1001 AND client_ref=R-42 → прежняя заявка
- 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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


