Разрабатывая для клиента программный комплекс на языке PHP (был указан как средство разработки в техническом задании к договору) пришлось столкнуться с проблемой передачи опционального аргумента при вызове метода из COM-объекта.
Создаваемое программное обеспечение должно было решать целый комплекс поставленных задач, в том числе предотвращать повторное подключение к базе данных Microsoft Access (файл *.MDB формата) когда с ней уже шла работа. Логичным и надёжным решением было опросить ADODB-объект о количестве подключённых пользователей[1] и, если их было более одного, то отсоединяться не предпринимая никаких дальнейших действий.
Рассмотрим на простом примере: создадим в Microsoft Access новую базу данных и сохраним её под именем "database.mdb" (создавать таблицы в ней не обязательно - для данного примера в этом нет необходимости). Теперь напишем небольшой код на Visual Basic Script (VBS) для проверки:
' database.vbs Option Explicit Dim db Dim rs Dim i Set db = CreateObject("ADODB.Connection") db.ConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=database.mdb" ' adModeRead (1) db.Mode = 1 db.Open() ' adSchemaProviderSpecific (-1), *missing*, JET_SCHEMA_USERROSTER Set rs = db.OpenSchema(-1, , "{947bb102-5d43-11d1-bdbf-00c04fb92675}") ' manual counting since rs.RecordCount is -1 (not supported) i = 0 While (Not rs.EOF) i = i + 1 rs.MoveNext() WEnd rs.Close() rs = Empty db.Close() db = Empty Wscript.Echo i
Если поместить код выше в текстовый файл с именем "database.vbs" в тот же каталог где лежит созданная ранее база данных "database.mdb", то при запуске через двойной щелчок мышкой в Windows, будет показано информационно окно с цифрой "1". Это значит, что на момент запуска скрипта к базе данных было всего одно подключение (собственно, сам скрипт). Но если открыть базу данных в Access, а уже потом запустить скрипт, то будет показано число "2" (скрипт и Access) - т.е. всё работает, как и должно.
Но вот при попытке перенести этот код на PHP возникла проблема. Если присмотреться к тексту скрипта повнимательнее, то можно заметить, что у метода OpenSchema отсутствует второй аргумент - там ничего нет:
OpenSchema(-1, , "{947bb102-5d43-11d1-bdbf-00c04fb92675}")
В PHP, в отличие от Visual Basic, подобная конструкция невозможна и не поддерживается.
Попытка вписать в это место значения 1, 0, -1, null, true, false и даже array(), а также некоторые другие, вызывала ошибку:
Object or provider is not capable of performing requested operation
Поиск в Интернете по тексту сообщения этой ошибки ничего не дал. Зато подстановка у скрипта VBS вместо пропущенного аргумента любого из перечисленных выше значений повторяло ошибку, из чего стало понятно, что аргумент на самом деле не пустой, и там что-то должно быть.
Немного поискав информацию по Интернету, удалось найти ответ в английской Wikipedia: Missing arguments are actually a particular Error value titled "parameter not found".[2] После этого, наконец-то, стало понятно что именно нужно искать. Следующая найденная ссылка уже содержала подробности: VT_EMPTY - No value was specified. If an optional argument to an Automation method is left blank, do not pass a VARIANT of type VT_EMPTY. Instead, pass a VARIANT of type VT_ERROR with a value of DISP_E_PARAMNOTFOUND.[3]
Наконец-то всё прояснилось - для создания пустого аргумента нужен Variant с типом VT_ERROR и значением DISP_E_PARAMNOTFOUND. Делаем поиск в заголовочных файлах любого компилятора языка C для Windows и находим то что нужно в файле "winerror.h":
// // MessageId: DISP_E_PARAMNOTFOUND // // MessageText: // // Parameter not found. // #define DISP_E_PARAMNOTFOUND _HRESULT_TYPEDEF_(0x80020004L)
Теперь уже в справке по PHP смотрим как создавать и работать с Variant и пробуем:
<?php // database.php $vtMissing = new VARIANT(0x80020004, VT_ERROR);
После чего можно было бы уже праздновать победу, но нет:
Fatal error: Uncaught exception 'com_exception' with message 'Variant type conversion failed: Type mismatch.' in Z:\database.php:2 Stack trace: #0 Z:\database.php(2): variant->variant(2147614724, 10) #1 {main} thrown in Z:\database.php on line 2
И тут выясняется, что в PHP 5, на котором шла разработка, создание Variant с типом VT_ERROR невозможно. Причём, даже последняя версия PHP 7 не могла этого сделать прерываясь той же самой ошибкой. Любые попытки "обмануть", в том числе через создание другого типа, а затем его смену, точно также не помогали:
<?php // database.php $vtMissing = new VARIANT(0x80020004, VT_UINT); // ok $vtMissing = variant_cast($vtMissing, VT_ERROR); // fatal error variant_set_type($vtMissing, VT_ERROR); // also fatal error
Не получалось "обмануть" даже через serialize() похожего типа и unserialize() назад с исправлениями (тут ещё выяснилось, что serialize() для Variant не может корректно работать, поэтому его принудительно запретили применять для этого типа в одной из последних версий PHP 7). Попасть в тупик всего в шаге от решения очень досадно.
Помимо безуспешно исследования Интернета в поисках решения, пришлось также ещё раз вернуться к сохранённому когда-то[4] справочнику PHP 5 и описанию Variant там. Внимание привлёк пример старого и нового кода (незначительно изменён ниже для удобства представления):
<?php // Example #1 Variant example, PHP 4.x style $v = new VARIANT(42); echo 'The type is '.$v->type.PHP_EOL; echo 'The value is '.$v->value.PHP_EOL; // Example #2 Variant example, PHP 5 style $v = new VARIANT(42); echo 'The type is '.variant_get_type($v).PHP_EOL; echo 'The value is '.$v.PHP_EOL;
Т.е. в PHP 4 к типу и значению Variant можно было обращаться как к свойствам объекта. Возможно, что и проверки на соответствие значения типу там не было. Тут же загрузив с официального сайта PHP museum последнюю версию PHP 4 (4.4.9 из архива "php-4.4.9-Win32.zip") - удалось проверить это предположение.
Оказалось что, действительно, на PHP 4 работает даже обычный код "new VARIANT(0x80020004, VT_ERROR)".
В итоге полный текст кода программы на PHP получился следующим:
<?php // database.php // next line works only on PHP 4 (PHP 5, 6 and even 7 are bugged) $vtMissing = new VARIANT(0x80020004, VT_ERROR); $db = new COM('ADODB.Connection'); $db->ConnectionString = 'Provider=Microsoft.Jet.OLEDB.4.0;Data Source=database.mdb'; // adModeRead (1) $db->Mode = 1; $db->Open(); // adSchemaProviderSpecific (-1), *missing*, JET_SCHEMA_USERROSTER $rs = $db->OpenSchema(-1, $vtMissing, '{947bb102-5d43-11d1-bdbf-00c04fb92675}'); // manual counting since rs.RecordCount is -1 (not supported) $i = 0; while (!$rs->EOF) { $rs->MoveNext(); $i++; } $rs->Close(); $rs = null; $db->Close(); $db = null; echo $i;
После чего весь проект был переведён на PHP 4. Из недостатков связанных с понижением версии можно отметить только несущественное замедление работы и необходимость внесения некоторого количества изменений в уже написанный код для его адаптации к более ранней версии PHP.
// PHP 8.2.0+ x32, note that hexdecimal notation do not work anymore in x32 version $vtMissing = new VARIANT(-2147352572, VT_ERROR); // PHP 8.2.0+ x64 $vtMissing = new VARIANT(0x80020004, VT_ERROR); // PHP 8.2.0+ x32 / x64, using new predefined constant $vtMissing = new VARIANT(DISP_E_PARAMNOTFOUND, VT_ERROR); // also you may need to install this for ADODB x64 Provider: // https://www.microsoft.com/en-us/download/details.aspx?id=13255
Сноски и полезные ссылки (отмечены как [*]):
[1] Microsoft Docs: Use ADO to Return a List of Users Connected to a Database
[*] Microsoft Docs: How to determine who is logged on to a database by using Microsoft Jet UserRoster in Access 2000 (архивная копия)
[2] Wikipedia: Variant type
[*] The Old New Thing: One of my favorite error codes: Optional parameter missing
[*] Microsoft Docs: How to pass optional arguments to a method on an ActiveX Control (архивная копия)
[3] Tenouk: The VARIANT and VARIANTARG stucture, union and typedef used in the MFC and IDispatch interface programming
[*] Microsoft Docs: VARIANT Type Constants
[4] PHP: VARIANT (последняя архивная копия, где ещё был пример для PHP 4)
2020.09.30