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

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

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

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

Глава 289 · От знакомства с PDO к связанным операциям CUBRID

PDO_CUBRID: каталог, резервирование, несколько результатов и время ожидания

Подключение PDO_CUBRID и четыре полезных приложения: ограниченный каталог, атомарный резерв, составная квитанция и настоящий конфликт блокировки.

От задачи приложения к PDO и PDOStatement

Представьте небольшой складской сервис. Посетителю нужен каталог доступных товаров, сотруднику — подтверждение резерва, интеграции — понятная квитанция. База хранит данные, а PHP должен получить только нужные поля и строки, выполнить связанную операцию целиком и объяснить отказ. PDO_CUBRID позволяет решать эти задачи через интерфейс PDO, сохраняя особенности CUBRID.

Один запрос каталога: кто за что отвечает
  1. Профиль warehouse выбирает адрес базы и категорию «склад»
  2. PDO — открытое соединение; PDOStatement — подготовленный SELECT с параметрами
  3. CUBRID применяет WHERE и ORDER BY: TAG → CAB; PHP пишет эти две строки в CSV

PDO — общий набор классов PHP для обращения к разным СУБД. PDO_CUBRID — конкретный драйвер, который реализует этот интерфейс для CUBRID. Объект PDO представляет соединение, а PDOStatement — отдельный запрос и его результат. Это не таблицы и не копии всей базы. Native cubrid_* из предыдущих глав — другое API: его ресурсы нельзя передать методам PDO.

$db = new PDO($dsn, $user, $password);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$statement = $db->query('SELECT 2 + 3 AS total');
$row = $statement->fetch(PDO::FETCH_ASSOC);
echo (int) $row['total']; // 5
$statement->closeCursor();
$statement = null;
$db = null;

Переменные $dsn, $user и $password сначала получают настройки из закрытой конфигурации; следующий раздел показывает их форму. new создаёт соединение, стрелка -> вызывает метод конкретного объекта, :: обращается к именованной константе класса. query отправляет фиксированный SQL. fetch(PDO::FETCH_ASSOC) получает одну строку как массив: ключ total соответствует SQL-псевдониму AS total. closeCursor освобождает результат, null убирает ссылку. Минимальный результат — число 5; четыре самостоятельных проекта ниже развивают этот же путь на собственных данных.

В каталоге результатом станет CSV, в резервировании — две квитанции с подтверждением и отказом, в пакете результатов — JSON нескольких разделов, а в конкурирующих запросах — отчёт о том, какой предел остановил ожидание. Сначала разберите общую модель PDO и смысл транзакции, если эти понятия встретились впервые; текущая глава объясняет именно правила драйвера CUBRID.

Установка и границы проверенной версии

Для запуска нужны три совместимые части: PHP с PDO core, драйвер PDO_CUBRID и доступный CUBRID Broker/сервер. Наличие native-расширения cubrid ещё не добавляет драйвер в PDO. Проверка PDO::getAvailableDrivers() должна содержать строку cubrid; успешная загрузка модуля не доказывает правильность DSN, пароля или прав.

print_r(PDO::getAvailableDrivers());
echo extension_loaded('pdo_cubrid') ? 'PDO_CUBRID загружен' : 'Нет PDO_CUBRID';

Официальная страница относит PDO_CUBRID к PECL и описывает установку через обычную сборку PECL-расширения. Для описанного там варианта CUBRID DBMS должен быть установлен на машине сборки PHP; configure принимает --with-pdo-cubrid=/path/to/CUBRID[,shared], а путь по умолчанию берётся из переменной CUBRID. Руководство не предоставляет готовую Windows DLL и отсылает к сборке Windows и build-guide.html пакета. Это исторические условия страницы, а не обещание доступности готового бинарника для любого сегодняшнего PHP.

Компонент стендаЗачем он нужен и как ограничена проверка
PHP 7.4.33 CLI + PDO coreОдин ABI для PHP и модулей. Отдельный процесс и php.ini; PHP сайта не меняется.
PDO_CUBRID v11.2.0.0002Закреплён commit 802ee27b2b34d682f2533db494c70712cff9ee0e, PHP7-ветка исходников. Совместимость PHP8 из этого не следует.
CCI d1ab94fab746429f356679667f924e726473fa02Закреплённый подмодуль драйвера; библиотека связи с брокером.
CUBRID 11.3.5 / своя UTF-8 базаСвои конфигурация, пароль, таблицы и LOB-каталог. Серверные DDL и специфичные результаты зависят от версии.
# Краткая схема сборки; полный воспроизводимый рецепт — README проекта.
/path/to/php74/bin/phpize
./configure --with-php-config=/path/to/php74/bin/php-config --with-pdo-cubrid
make
# В собственном php.ini порядок загрузки:
extension=/path/to/pdo.so
extension=/path/to/pdo_cubrid.so

В закреплённом репозитории config.m4 собирает собственный CCI и выбирает cubrid_driver7.c/cubrid_statement7.c для PHP7. Поэтому короткая историческая команда с путём CUBRID и рецепт конкретного source checkout не обязаны совпадать буквально. README каждого ZIP содержит полный отдельный Linux/WSL-рецепт: исходники, проверка хешей, PDO core той же версии, submodule, конфигурация, запуск и остановка. Если PHP не загрузил модуль, сначала сравните ABI, архитектуру, phpize/php-config и порядок модулей; не меняйте случайно php.ini рабочего сайта.

Эти проекты выполняются через CLI и настоящий PDO_CUBRID. Они не проверяют веб-аутентификацию, HTTP-пул или PHP-FPM. Исторический PHP нужен для воспроизводимого исследования данного порта драйвера; на новый production-проект его не предлагают переносить без отдельного выбора поддерживаемого стека.

Полный DSN: адрес базы, пользователь и профиль

DSN — строка адреса источника данных. Она отвечает на вопрос «к какой базе подключиться», а имя пользователя и пароль — «от чьего имени». Разделение помогает держать секрет вне маршрута и журнала. Для PDO_CUBRID официальный формат существует с PECL PDO_CUBRID 8.3.0.0001.

$dsn = 'cubrid:host=127.0.0.1;port=33175;dbname=phpmanual175';
$db = new PDO($dsn, $user, $password);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
ЭлементСмысл и пример
cubrid:Префикс выбирает драйвер PDO_CUBRID. Это не URL native connect_with_url.
host=127.0.0.1Адрес сервера/брокера, доступный текущему процессу. localhost и сетевой адрес выбирают по развёртыванию.
port=33175Порт учебного Broker, не master-порт базы. Рабочее значение берут из своей конфигурации.
dbname=phpmanual175Имя базы на этом сервере. Нельзя подставлять произвольный ввод клиента.
;Разделитель элементов DSN; это не разделитель SQL-команд в данном контексте.
$user, $passwordВторой и третий аргументы конструктора. Не включайте настоящий секрет в пример, Git или результат HTML.

Указывайте все три элемента явно. Закреплённый parser знает host/port/dbname и проверяет число разобранных элементов; значения-заготовки localhost/55300/demodb в C-исходнике не являются договором вашего приложения. Параметр charset из PDO_MYSQL нельзя молча приписать этому DSN: в официальной форме PDO_CUBRID его нет. Кодировка базы и текстовые данные должны быть согласованы отдельно; каталог проверяет кириллицу, апостроф, NULL и пустой текст.

У конструктора PDO есть четвёртый аргумент options, но поддержка опций зависит от драйвера. PHP7-порт этой версии перебирает driver_options как строковые пары для CCI URL. Поэтому здесь конструктор получает три аргумента, а общий режим ошибок задаётся после подключения setAttribute. Пример с числовыми PDO-ключами из другого драйвера нельзя считать автоматически совместимым. Пароль также не следует собирать вручную в native CCI URL.

$profiles = [
    'warehouse' => ['dsn' => 'cubrid:host=db.example.test;port=33000;dbname=shop'],
    'archive' => ['dsn' => 'cubrid:host=archive.example.test;port=33000;dbname=history'],
];
$name = 'warehouse'; // уже проверенный выбор приложения
if (!array_key_exists($name, $profiles)) {
    throw new InvalidArgumentException('Неизвестный профиль');
}
$db = new PDO($profiles[$name]['dsn'], $user, $password);

Вторая иллюстрация показывает маршрутизацию приложения: логическое имя выбирает заранее известный DSN. Домены example.test здесь демонстрационные и не вызываются проектом. В самостоятельном ZIP разрешена только собственная лабораторная база; выбранный профиль задаёт категорию и лимит каталога. Разрешение выбрать профиль и SQL-права пользователя — разные проверки.

Неудача подключения вызывает PDOException, в том числе до установки ERRMODE_EXCEPTION. Клиенту достаточно понятного сообщения «каталог временно недоступен»; полную диагностику сохраняют в закрытый журнал, исключая пароль. В публичном result проекта остаются безопасные признаки ошибки, SQLSTATE и код драйвера, а не сырая строка с реквизитами.

Выбрать каталог по доверенному профилю и выгрузить нужные товары

Сотруднику нужен небольшой каталог доступных складских товаров для заказа. Имена профилей задаёт приложение; пользователь выбирает warehouse, а не присылает произвольный DSN или SQL. База сразу возвращает две нужные позиции в порядке цены. CSV сохраняет нулевую цену и различает отсутствие примечания от пустого текста.

Путь данных и полученный результат
  1. Профиль warehouse → category=склад, limit=2
  2. 4 строки → WHERE: склад и qty>0 → ORDER BY price_cents,id
  3. TAG: 0 коп., 5 шт.; CAB: 12500 коп., 7 шт. → catalog.csv

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

$statement = $db->prepare(
    'SELECT sku,title FROM products WHERE category=? AND qty>0 ORDER BY price_cents,id LIMIT 2'
);
$statement->execute(['склад']);
$first = $statement->fetch(PDO::FETCH_ASSOC);
if ($first !== false) {
    echo $first['sku'], ': ', $first['title'];
}
$statement->closeCursor();

В минимальном фрагменте $db — уже открытый PDO, products — ваша таблица. prepare создаёт PDOStatement, знак ? обозначает одно значение, execute передаёт «склад», fetch читает строку с ключами sku/title. Для данных полного проекта первая строка — TAG: Этикетка. Полный файл создаёт свои данные и передаёт LIMIT как PDO::PARAM_INT. Значения фильтруются в SQL; нормализация двух числовых полей в PHP нужна для модели приложения, а не для сортировки огромной выгрузки.

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

НаблюдениеРезультат
Профильwarehouse
Порядок товаровTAG, CAB
Этикетка: цена, коп.0
Кабель: остаток7
Примечанияпустая строка; SQL NULL
Результатcatalog.csv
Самостоятельный проект, исходник и архив
Выбрать каталог по доверенному профилю и выгрузить нужные товарыСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p175Config();$db=p175Connect();$runtime=p175Runtime($db);

function writeCsv175(string $filename,array $rows): void {
    $output='';
    foreach($rows as $row) {
        $quoted=array_map(static function($value):string {return '"'.str_replace('"','""',(string)$value).'"';},$row);
        $output.=implode(';',$quoted)."\n";
    }
    if (file_put_contents(__DIR__.'/'.$filename,$output)!==strlen($output)) {throw new RuntimeException('CSV не записан целиком.');}
}

    $profiles=['warehouse'=>['category'=>'склад','limit'=>2]];
    $profileName='warehouse';
    if (!array_key_exists($profileName,$profiles)) {throw new InvalidArgumentException('Неизвестный профиль каталога.');}
    $profile=$profiles[$profileName];
    $name=p175CreateTable($db,$owned,'catalog',[
        ['id'=>1,'sku'=>'CAB','title'=>"Кабель O'Reilly",'category'=>'склад','price_cents'=>12500,'qty'=>7,'note'=>null],
        ['id'=>2,'sku'=>'ADP','title'=>'Розетка','category'=>'склад','price_cents'=>2500,'qty'=>0,'note'=>'ожидается'],
        ['id'=>3,'sku'=>'TAG','title'=>'Этикетка','category'=>'склад','price_cents'=>0,'qty'=>5,'note'=>''],
        ['id'=>4,'sku'=>'BOX','title'=>'Коробка','category'=>'упаковка','price_cents'=>900,'qty'=>9,'note'=>null],
    ]);
    $table=p175Qualified($name);
    $statement=$db->prepare('SELECT sku,title,price_cents,qty,note FROM '.$table
        .' WHERE category=? AND qty>0 ORDER BY price_cents,id LIMIT ?');
    try {
        $statement->bindValue(1,$profile['category'],PDO::PARAM_STR);
        $statement->bindValue(2,$profile['limit'],PDO::PARAM_INT);
        $statement->execute();$raw=p175Rows($statement);
    } finally {p175Release($statement);}
    $catalog=[];$csv=[['sku','title','price_cents','qty','note_state']];
    foreach($raw as $row) {
        $item=['sku'=>$row['sku'],'title'=>$row['title'],'price_cents'=>(int)$row['price_cents'],
            'qty'=>(int)$row['qty'],'note'=>$row['note']];
        $catalog[]=$item;
        $csv[]=[$item['sku'],$item['title'],$item['price_cents'],$item['qty'],
            $item['note']===null?'null':($item['note']===''?'empty':'text')];
    }
    writeCsv175('catalog.csv',$csv);
    $rows=[['Профиль','warehouse'],['Порядок товаров','TAG, CAB'],['Этикетка: цена, коп.',0],
        ['Кабель: остаток',7],['Примечания','пустая строка; SQL NULL'],['Результат','catalog.csv']];
    $checks=['native_pdo_driver'=>$db->getAttribute(PDO::ATTR_DRIVER_NAME)==='cubrid',
        'sql_limited'=>count($catalog)===2,'stable_order'=>array_column($catalog,'sku')===['TAG','CAB'],
        'unicode_apostrophe'=>$catalog[1]['title']==="Кабель O'Reilly",'zero_price'=>$catalog[0]['price_cents']===0,
        'empty_distinct'=>$catalog[0]['note']==='','null_distinct'=>$catalog[1]['note']===null,
        'out_of_stock_excluded'=>!in_array('ADP',array_column($catalog,'sku'),true),
        'other_category_excluded'=>!in_array('BOX',array_column($catalog,'sku'),true),
        'artifact_written'=>is_file(__DIR__.'/catalog.csv')];
    $observations=['profile'=>$profileName,'catalog'=>$catalog,
        'raw_numeric_types'=>array_map(static function(array $r):array {return [gettype($r['price_cents']),gettype($r['qty'])];},$raw),
        'statement_released'=>$statement===null];

} finally {

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

p175Report('pdoc175-catalog-route','Выбрать каталог по доверенному профилю и выгрузить нужные товары',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Профиль — warehouse
Порядок товаров — TAG, CAB
Этикетка: цена, коп. — 0
Кабель: остаток — 7
Примечания — пустая строка; SQL NULL
Результат — catalog.csv

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

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

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

DSN выбирается конфигурацией сервера. Имя профиля не даёт пользователю права менять базу; список допустимых профилей не заменяет авторизацию. SELECT * передал бы лишние поля, а отсутствие WHERE/LIMIT — лишние строки. Полученные малые строки можно преобразовать в CSV, но большой каталог следует ограничивать и сортировать в SQL. Поведение типов PDO зависит от драйвера: ноль проверяется после явного преобразования, SQL NULL — строго через === null.

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

Добавьте профиль packaging для упаковки с лимитом 1. Какой товар должен попасть в CSV?

Разбор

Профиль должен задавать category=упаковка и limit=1 в доверенном массиве. SQL остаётся тем же; результат — BOX, Коробка, 900 копеек, 9 единиц. Не добавляйте имя категории конкатенацией внутрь SQL.

Одна операция: режим, подтверждение и отмена

Autocommit означает, что отдельная завершённая SQL-команда фиксируется автоматически. В PDO_CUBRID он включён при обычном новом подключении. Когда заявку образуют несколько зависимых изменений, beginTransaction временно отключает его; commit подтверждает работу, rollBack отменяет её. Возврат к исходному режиму после этих методов относится к настройке драйвера, а не ко всем возможным ручным SQL-командам.

Этап заявки R-42Данные и действие
До beginTransactionCAB=5, CLIP=2; новая заявка хочет 4 и 3.
Первый UPDATEВнутри транзакции CAB временно равен 1.
Второй UPDATEУсловие qty>=3 не выполнено; rowCount=0. SQL успешен, заявка не принимается.
rollBackCAB снова 5, CLIP по-прежнему 2; частичного резерва нет.

PDO::inTransaction() показывает состояние, которое отслеживает PDO. При последовательности beginTransaction→commit/rollBack оно полезно для обработки исключения. Ручной SQL COMMIT, DDL с неявной фиксацией и ручное переключение режима могут разойтись с этим учётом. Не стройте вложенные транзакции из нескольких beginTransaction: определите один участок, который начинает и завершает операцию.

$db->beginTransaction();
try {
    // Все связанные UPDATE выполняются через этот же $db.
    // Проверяем rowCount и бизнес-условия после каждого изменения.
    $db->commit();
} catch (Throwable $failure) {
    if ($db->inTransaction()) { $db->rollBack(); }
    throw $failure;
}

Исключение не означает, что приложению уже предоставлена подходящая завершённая транзакция. catch явно отменяет свою незавершённую работу, после чего решается, что показать пользователю. Резерв двух позиций показывает и commit, и бизнес-отказ без SQL-ошибки. Квитанция из пакета SQL применяет ту же границу к нескольким разным результатам.

Официальная страница содержит неудачно сформулированное примечание о pending work при переключении autocommit. В source v11.2.0.0002 ветка setter выполняет cci_end_tran(COMMIT), когда меняется прежний режим false; переход false→true поэтому нельзя использовать как отмену. В реальной подготовке setter вернул true, но getAttribute(ATTR_AUTOCOMMIT) остался false; последующее наблюдение подтвердило сохранённую ожидавшую правку. Это отдельная особенность PHP7-порта: успешный setter нельзя выдавать за доказательство нового режима. А beginTransaction при уже вручную отключённом режиме в этой реализации также имеет ветку commit ожидающей работы. Здесь обычные операции начинаются из исходного true и используют методы PDO, без смешивания двух стилей.

Нельзя переносить на CUBRID объяснение конкретного поведения MySQL из старого ответа. Вопрос о begin и commit на разных PDO раскрывает общую ошибку владения соединением; вопрос об автоматическом rollback при исключении — необходимость явного завершения операции. Наши решения связывают все команды с одним $db и проверяют реальные сохранённые остатки.

Резерв двух позиций: подтвердить обе или отменить обе

Одна заявка требует кабель и хомут. Если второго товара не хватает, нельзя оставить списанный первый товар. Полный пример сначала успешно резервирует 2 кабеля и 1 хомут, а затем отменяет заявку на 4 кабеля и 3 хомута. CSV становится понятной квитанцией обеих попыток.

Путь данных и полученный результат
  1. До заявки: CAB=7, CLIP=3
  2. R-41: UPDATE 2 и 1 → commit → CAB=5, CLIP=2
  3. R-42: CAB временно 1, CLIP не изменён → rollback → CAB=5, CLIP=2

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

$db->beginTransaction();
try {
    $statement = $db->prepare('UPDATE products SET qty=qty-? WHERE sku=? AND qty>=?');
    $statement->execute([2, 'CAB', 2]);
    if ($statement->rowCount() !== 1) {
        throw new RuntimeException('Недостаточно кабеля');
    }
    $statement->closeCursor();
    $db->commit();
} catch (Throwable $failure) {
    if ($db->inTransaction()) { $db->rollBack(); }
    throw $failure;
}

beginTransaction, запрос и commit используют один и тот же $db. Условие qty>=? проверяется вместе с уменьшением, а rowCount сообщает число изменённых строк. Значение 0 означает бизнес-отказ, хотя SQL выполнен без синтаксической ошибки. Малый фрагмент резервирует одну позицию; в полном проекте та же транзакция охватывает две. finally для результата и явный rollback не оставляют работу до неопределённого конца запроса.

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

НаблюдениеРезультат
Заявка R-41принята: 2 кабеля и 1 хомут
Остаток после R-41CAB=5; CLIP=2
Заявка R-42отклонена: не хватает хомутов
Остаток после R-42CAB=5; CLIP=2
Другое соединение подтверждаетда
Результатreservation.csv
Самостоятельный проект, исходник и архив
Резерв двух позиций: подтвердить обе или отменить обеСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p175Config();$db=p175Connect();$runtime=p175Runtime($db);

function writeCsv175(string $filename,array $rows): void {
    $output='';
    foreach($rows as $row) {
        $quoted=array_map(static function($value):string {return '"'.str_replace('"','""',(string)$value).'"';},$row);
        $output.=implode(';',$quoted)."\n";
    }
    if (file_put_contents(__DIR__.'/'.$filename,$output)!==strlen($output)) {throw new RuntimeException('CSV не записан целиком.');}
}

    $name=p175CreateTable($db,$owned,'reserve',[
        ['id'=>1,'sku'=>'CAB','title'=>'Кабель','category'=>'склад','price_cents'=>12500,'qty'=>7],
        ['id'=>2,'sku'=>'CLIP','title'=>'Хомут','category'=>'склад','price_cents'=>150,'qty'=>3],
    ]);$table=p175Qualified($name);$modes=[];$operations=[];
    $reserve=static function(string $request,int $cables,int $clips) use ($db,$table,&$modes,&$operations):bool {
        $modes[]=$db->getAttribute(PDO::ATTR_AUTOCOMMIT);
        $db->beginTransaction();$modes[]=$db->getAttribute(PDO::ATTR_AUTOCOMMIT);
        $statement=null;
        try {
            $statement=$db->prepare('UPDATE '.$table.' SET qty=qty-? WHERE sku=? AND qty>=?');
            $counts=[];
            foreach([['CAB',$cables],['CLIP',$clips]] as $wanted) {
                $statement->bindValue(1,$wanted[1],PDO::PARAM_INT);
                $statement->bindValue(2,$wanted[0],PDO::PARAM_STR);
                $statement->bindValue(3,$wanted[1],PDO::PARAM_INT);
                $statement->execute();$changed=$statement->rowCount();$counts[]=$changed;
                if ($changed!==1) {
                    p175Release($statement);$db->rollBack();
                    $operations[]=['request'=>$request,'counts'=>$counts,'status'=>'rejected'];
                    $modes[]=$db->getAttribute(PDO::ATTR_AUTOCOMMIT);return false;
                }
                $statement->closeCursor();
            }
            p175Release($statement);$db->commit();
            $operations[]=['request'=>$request,'counts'=>$counts,'status'=>'accepted'];
            $modes[]=$db->getAttribute(PDO::ATTR_AUTOCOMMIT);return true;
        } catch(Throwable $failure) {
            p175Release($statement);if ($db->inTransaction()) {$db->rollBack();}throw $failure;
        }
    };
    $accepted=$reserve('R-41',2,1);
    $afterAccepted=p175Read($db,'SELECT sku,qty FROM '.$table.' ORDER BY id');
    $rejected=$reserve('R-42',4,3);
    $afterRejected=p175Read($db,'SELECT sku,qty FROM '.$table.' ORDER BY id');
    $quantities=static function(array $records):array {return array_map(static function(array $row):int {return (int)$row['qty'];},$records);};
    $a=$quantities($afterAccepted);$b=$quantities($afterRejected);
    $reader=p175Connect();
    try {$confirmed=$quantities(p175Read($reader,'SELECT sku,qty FROM '.$table.' ORDER BY id'));} finally {p175Close($reader);}
    writeCsv175('reservation.csv',[['request','status','CAB_qty','CLIP_qty'],['R-41','accepted',$a[0],$a[1]],['R-42','rejected',$b[0],$b[1]]]);
    $rows=[['Заявка R-41','принята: 2 кабеля и 1 хомут'],['Остаток после R-41','CAB=5; CLIP=2'],
        ['Заявка R-42','отклонена: не хватает хомутов'],['Остаток после R-42','CAB=5; CLIP=2'],
        ['Другое соединение подтверждает',true],['Результат','reservation.csv']];
    $checks=['first_accepted'=>$accepted===true,'second_rejected'=>$rejected===false,'first_counts'=>$operations[0]['counts']===[1,1],
        'second_business_refusal'=>$operations[1]['counts']===[1,0],'commit_visible'=>$a===[5,2],
        'rollback_restored_both'=>$b===[5,2],'fresh_connection_visible'=>$confirmed===[5,2],
        'autocommit_lifecycle'=>$modes===[true,false,true,true,false,true],
        'no_open_transaction'=>$db->inTransaction()===false,'artifact_written'=>is_file(__DIR__.'/reservation.csv')];
    $observations=['requests'=>$operations,'autocommit'=>$modes,'after_accept'=>$afterAccepted,'after_refusal'=>$afterRejected,'fresh_quantities'=>$confirmed];

} finally {

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

p175Report('pdoc175-reservation','Резерв двух позиций: подтвердить обе или отменить обе',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Заявка R-41 — принята: 2 кабеля и 1 хомут
Остаток после R-41 — CAB=5; CLIP=2
Заявка R-42 — отклонена: не хватает хомутов
Остаток после R-42 — CAB=5; CLIP=2
Другое соединение подтверждает — да
Результат — reservation.csv

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

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

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

Транзакция объединяет изменения одного соединения. Вызов новой функции connect для каждого UPDATE создал бы другую границу. Бизнес-правила проверяются условными UPDATE и количеством затронутых строк; атомарность сама не описывает все конкурентные ограничения. Не смешивайте ручное переключение ATTR_AUTOCOMMIT, SQL COMMIT и методы PDO в одной операции. Таблицы создаются заранее, вне транзакции заявки.

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

Замените вторую заявку на 4 кабеля и 2 хомута. Какой остаток станет видим после commit?

Разбор

Обе команды изменят по одной строке, поэтому заявка пройдёт: CAB=1 и CLIP=0. Нулевой остаток допустим; проверка qty>=wanted, а не qty>wanted, разрешает забрать ровно всё доступное количество.

Все атрибуты соединения и исторические уровни изоляции

Атрибут соединения — настройка или характеристика конкретного драйвера. Общие имена PDO не гарантируют поддержку каждой СУБД. Константы PDO_CUBRID появляются при загрузке этого расширения; перед использованием в многодрайверном приложении проверьте $db->getAttribute(PDO::ATTR_DRIVER_NAME) === «cubrid». В примере строковое значение сравнивают обычными PHP-кавычками.

if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) === 'cubrid') {
    $level = $db->getAttribute(PDO::CUBRID_ATTR_ISOLATION_LEVEL);
    // Сопоставить полученное число с документацией своего сервера.
}
Специфичная константаНазначение и предел
PDO::CUBRID_ATTR_ISOLATION_LEVELПолучить или задать уровень изоляции соединения. Менять до прикладной транзакции; шесть исторических имён ниже не означают шесть поддерживаемых современных режимов.
PDO::CUBRID_ATTR_LOCK_TIMEOUTПредел ожидания блокировки. Официальная PDO-страница исторически указывает секунды; проверенная связка PDO/CCI11.2 + сервер11.3 использует миллисекунды, как показано в wait-budget.
PDO::CUBRID_ATTR_MAX_STRING_LENGTHПо официальному контракту только чтение: максимальная длина bit, varbit, char, varchar, nchar, nchar varying через PDO_CUBRID. Это не размер значения выбранной строки. По исходнику PHP7 getter этой версии не возвращает полезное значение через штатную ветку, поэтому по нему не строят ограничение формы.

Вторая задача — задать согласованную политику следующей операции. Следующий фрагмент показывает способ обращения к атрибуту, но не является испытанием всех уровней и аномалий конкурентности:

// После проверки возможностей выбранного сервера и вне незавершённой операции:
$ok = $db->setAttribute(
    PDO::CUBRID_ATTR_ISOLATION_LEVEL,
    PDO::TRAN_REP_CLASS_COMMIT_INSTANCE
);
if ($ok !== true) { throw new RuntimeException('Уровень не принят'); }
$actual = $db->getAttribute(PDO::CUBRID_ATTR_ISOLATION_LEVEL);

Изоляция отвечает за то, какие изменения других транзакций видны чтению. Dirty read — чтение ещё не подтверждённой правки; non-repeatable read — повторный SELECT той же записи увидел другое значение; phantom — повторная выборка по условию получила другой набор строк. Это разные вопросы, а не три названия тайм-аута. В старых именах CLASS относится к схеме таблицы, INSTANCE — к её строкам, не к PHP-классам.

Историческая PDO-константаНомер и описание официальной PDO-страницы
PDO::TRAN_COMMIT_CLASS_UNCOMMIT_INSTANCE1. Для строк возможны dirty/non-repeatable/phantom, а для таблицы — неповторяемое чтение её схемы. Самый слабый из описанных исторических уровней.
PDO::TRAN_COMMIT_CLASS_COMMIT_INSTANCE2. Dirty read исключён; non-repeatable и phantom для строк допускаются.
PDO::TRAN_REP_CLASS_UNCOMMIT_INSTANCE3. В старом описании назван default: повторяемая схема таблицы, но dirty/non-repeatable/phantom строк возможны. Это не default сервера11.3.
PDO::TRAN_REP_CLASS_COMMIT_INSTANCE4. Dirty read исключён; non-repeatable и phantom возможны, схема таблицы защищена от изменения во время чтения.
PDO::TRAN_REP_CLASS_REP_INSTANCE5. Исторический текст исключает dirty/non-repeatable, допускает phantom. Современную snapshot-семантику нужно читать отдельно.
PDO::TRAN_SERIALIZABLE6. Историческое описание обещает отсутствие перечисленных аномалий. Нельзя переносить это обещание на новую MVCC-реализацию только по названию константы.

В руководстве CUBRID 11.3 перечислены READ COMMITTED(4), REPEATABLE READ(5) и SERIALIZABLE(6), default — 4. Подробный раздел уровня5 описывает snapshot, предотвращающий также phantom, но допускающий write skew: две операции по отдельности видят допустимое состояние и вместе нарушают составное правило. Для уровня6 документация прямо оговаривает сохранённое ради совместимости имя и поведение, аналогичное REPEATABLE READ, а не обещание полной сериализуемости. Исторические уровни1–3 сохранены здесь как часть PDO-справки, не как проверенные настройки данного сервера.

CUBRID 11.3: изоляция и MVCC — источник этого версионного различия. В текущем пакете выполняются транзакции и конфликт двух UPDATE, но не матрица всех уровней и аномалий. Условный резерв проверяет реальный остаток; бюджет ожидания решает, когда прекратить ждать, а не заменяет политику согласованности.

Строка, пустой результат, команда и следующий раздел

Результат SQL — не обязательно таблица со строками. SELECT может вернуть колонки, но ни одной строки; UPDATE возвращает число изменений. Несколько команд через ; образуют последовательность таких результатов. PDOStatement::nextRowset() относится к переходу между ними, fetch — к строкам внутри текущего результата.

МетодЧто означает его результат
columnCount()Число колонок текущего результата. У пустого SELECT оно может быть положительным.
fetch(PDO::FETCH_ASSOC)Одна строка либо false в конце; ошибки учитываются выбранным ERRMODE и errorInfo.
rowCount()Для DML — число затронутых строк. Считать все SELECT-строки этим методом переносимо нельзя.
nextRowset()true: новый текущий результат. false: больше нет либо отказ, который различают по исключению/SQLSTATE.
closeCursor(), затем nullЗакончить работу с результатом и отпустить объект запроса; это не commit и не rollback.

При ERRMODE_EXCEPTION отказ драйвера обычно превращается в PDOException. Код всё равно проверяет false-переход с SQLSTATE, потому что конец — нормальное событие. В полном чтении квитанции пустой shortages остаётся отдельным [] и не скрывает следующий UPDATE и итог. Обсуждение пустого первого результата взято как реальная проблема; MySQL stored procedure и её синтаксис не перенесены в CUBRID.

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

Прочитать квитанцию из нескольких SQL-результатов

После пополнения склада нужна квитанция: исходные позиции, возможные отрицательные остатки, результат изменения и итог. Отсутствие отрицательных остатков — полезный пустой раздел, а не конец всей квитанции. Проект читает все четыре результата и сохраняет их смысл в receipt.json.

Путь данных и полученный результат
  1. SELECT позиций → 2 строки; SELECT проблем → 0 строк, но 1 колонка
  2. UPDATE хомутов → 1 изменённая строка; итоговый SELECT → 11
  3. nextRowset=false без ошибки → closeCursor → commit → receipt.json

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

$statement = $db->query('SELECT 5 AS qty; SELECT 11 AS total_qty');
do {
    if ($statement->columnCount() > 0) {
        $rows = $statement->fetchAll(PDO::FETCH_ASSOC);
        // Передать текущий небольшой раздел в отчёт.
    } else {
        $changed = $statement->rowCount();
    }
} while ($statement->nextRowset());
$statement->closeCursor();

Точка с запятой разделяет SQL-команды, а не строки одной таблицы. query/execute делает первый результат текущим. columnCount различает результат с колонками и команду изменения; nextRowset переходит к следующему результату. Первый SELECT вернёт qty=5, второй total_qty=11. Цикл проверяет переход, поэтому пустой массив строк не обрывает чтение. В полном файле после false отдельно проверяется SQLSTATE, а исключение отменяет транзакцию.

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

НаблюдениеРезультат
Разделов квитанции4
Каталог до пополненияCAB=5; CLIP=2
Отрицательных остатков0
Изменено строк1
Всего единиц после пополнения11
Результатreceipt.json
Самостоятельный проект, исходник и архив
Прочитать квитанцию из нескольких SQL-результатовСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p175Config();$db=p175Connect();$runtime=p175Runtime($db);

    $name=p175CreateTable($db,$owned,'receipt',[
        ['id'=>1,'sku'=>'CAB','title'=>'Кабель','category'=>'склад','price_cents'=>12500,'qty'=>5],
        ['id'=>2,'sku'=>'CLIP','title'=>'Хомут','category'=>'склад','price_cents'=>150,'qty'=>2],
    ]);$table=p175Qualified($name);$db->beginTransaction();$statement=null;
    $parts=[];$shape=[];$next=[];
    try {
        $statement=$db->prepare('SELECT sku,qty FROM '.$table.' ORDER BY id LIMIT 2; '
            .'SELECT sku FROM '.$table.' WHERE qty<0 ORDER BY id LIMIT 2; '
            .'UPDATE '.$table.' SET qty=qty+4 WHERE id=2; '
            .'SELECT SUM(qty) AS total_qty FROM '.$table);
        $statement->execute();
        do {
            $columns=$statement->columnCount();$count=$statement->rowCount();
            $shape[]=['columns'=>$columns,'row_count'=>$count];
            $parts[]=$columns>0?p175Rows($statement):$count;
            $more=$statement->nextRowset();$next[]=$more;
            if ($more===false && $statement->errorCode()!=='00000') {throw new RuntimeException('Следующая часть квитанции не получена.');}
            if (count($parts)>4) {throw new RuntimeException('Неожиданная дополнительная часть.');}
        } while($more);
        p175Release($statement);$db->commit();
    } catch(Throwable $failure) {p175Release($statement);if ($db->inTransaction()) {$db->rollBack();}throw $failure;}
    $items=array_map(static function(array $row):array {return ['sku'=>$row['sku'],'qty'=>(int)$row['qty']];},$parts[0]);
    $stored=p175Read($db,'SELECT sku,qty FROM '.$table.' ORDER BY id');$quantities=[];
    foreach($stored as $row) {$quantities[$row['sku']]=(int)$row['qty'];}
    $receipt=['sections'=>[['kind'=>'items','rows'=>$items],['kind'=>'shortages','rows'=>$parts[1]],
        ['kind'=>'restock','changed'=>$parts[2]],['kind'=>'summary','total_qty'=>(int)$parts[3][0]['total_qty']]],
        'committed_quantities'=>$quantities];
    file_put_contents(__DIR__.'/receipt.json',json_encode($receipt,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR)."\n");
    $rows=[['Разделов квитанции',4],['Каталог до пополнения','CAB=5; CLIP=2'],['Отрицательных остатков',0],
        ['Изменено строк',1],['Всего единиц после пополнения',11],['Результат','receipt.json']];
    $checks=['four_parts'=>count($parts)===4,'items_preserved'=>$items===[['sku'=>'CAB','qty'=>5],['sku'=>'CLIP','qty'=>2]],
        'empty_is_a_result'=>$parts[1]===[],'dml_count'=>$parts[2]===1,'summary_after_empty'=>(int)$parts[3][0]['total_qty']===11,
        'column_shapes'=>array_column($shape,'columns')===[2,1,0,1],'transitions'=>$next===[true,true,true,false],
        'commit_readback'=>$quantities===['CAB'=>5,'CLIP'=>6],'statement_released'=>$statement===null,
        'no_open_transaction'=>$db->inTransaction()===false,'artifact_written'=>is_file(__DIR__.'/receipt.json')];
    $observations=['receipt'=>$receipt,'rowset_shape'=>$shape,'nextRowset'=>$next];

} finally {

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

p175Report('pdoc175-receipt-chain','Прочитать квитанцию из нескольких SQL-результатов',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Разделов квитанции — 4
Каталог до пополнения — CAB=5; CLIP=2
Отрицательных остатков — 0
Изменено строк — 1
Всего единиц после пополнения — 11
Результат — receipt.json

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

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

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

Не склеивайте пользовательский ввод в SQL-пакет. Этот batch состоит из фиксированных команд и зарегистрированного имени таблицы; значения приложения в обычных запросах передаются параметрами. rowCount для SELECT зависит от драйвера, поэтому алгоритм не использует его для поиска конца. Несколько SQL-команд сами по себе не создают атомарную операцию: здесь границу задаёт beginTransaction/commit. nextRowset(false) требует отличить окончание от ошибки.

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

Добавьте последним отдельный SELECT COUNT(*) AS positions. Как не перепутать две позиции с 11 единицами товара?

Разбор

Добавьте в формат квитанции новый именованный раздел positions и увеличьте явное допустимое число результатов. COUNT(*) посчитает две строки позиций, SUM(qty) — 11 единиц. Не объединяйте разные показатели в общий безымянный массив значений.

Два предела ожидания и точная диагностика

Тайм-аут нужен для понятного отказа, когда работа заняла больше допустимого времени. Это не отменяет необходимость освободить результат и завершить транзакцию. Предел SQL и предел ожидания блокировки — разные настройки; тайм-аут подключения и лимит выполнения всего PHP-запроса также отдельны.

Настройка в закреплённой версииЕдиница и граница
PDO::ATTR_TIMEOUTЦелое число секунд SQL. Исходный -1 означает отсутствие установленного лимита; 0 и -2 setter отвергает возвратом false. Устанавливается до prepare.
PDO::CUBRID_ATTR_LOCK_TIMEOUTВ проверенной связке CCI 11.2/CUBRID11.3 значение передаётся как миллисекунды ожидания блокировки; 500 — половина секунды. Историческое описание секунд не переносится автоматически.
setAttribute(...), getAttribute(...)Проверить успех setter и прочитать фактическое значение. Поддержка атрибута драйвером не следует из наличия общей PDO-константы.
Конкуренция лимитовПри lock=500/query=3 остановило ожидание блокировки; при lock=3000/query=1 — бюджет SQL. Фактические коды и времена сохраняются.

В driver7.c ATTR_TIMEOUT умножается на 1000 при создании запроса для CCI. Изменение атрибута после prepare не переписывает уже созданный запрос; подготовьте новый после смены политики. CUBRID_ATTR_LOCK_TIMEOUT идёт отдельным вызовом CCI. Проект с двумя соединениями связывает эти настройки с реальной занятой строкой и откатом. Обычный резерв показывает короткую транзакцию, которая не держит блокировки ради ожидания пользователя.

Ограничить ожидание занятого остатка и сохранить причину отказа

Оператор нажал «зарезервировать», пока другой процесс меняет ту же позицию. Интерфейс не должен ждать бесконечно. Проект создаёт двух настоящих участников: первый удерживает свой UPDATE, второй дважды пробует изменить ту же строку с разными пределами. Оба отказа имеют сохранённую причину, оба участника откатывают работу, исходный остаток 3 остаётся прежним.

Путь данных и полученный результат
  1. Child: BEGIN → qty=99 без commit → сообщает «строка занята»
  2. Parent: lock=500 мс/query=3 с → отказ; затем lock=3000 мс/query=1 с → отказ
  3. Parent rollback; child rollback и выход → повторный SELECT qty=3 → wait-budget.csv

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

$db->setAttribute(PDO::CUBRID_ATTR_LOCK_TIMEOUT, 500); // миллисекунды этой версии CCI
$db->setAttribute(PDO::ATTR_TIMEOUT, 3);                // секунды SQL
$statement = $db->prepare('UPDATE products SET qty=qty-1 WHERE id=? AND qty>=1');
$statement->execute([1]);

Два атрибута задают разные пределы. Первый ограничивает ожидание блокировки, второй — выполнение SQL. ATTR_TIMEOUT устанавливается до prepare: драйвер копирует его в подготовленный запрос. Фрагмент сам по себе не создаёт конфликт; полный ZIP запускает отдельного владельца строки и получает подтверждение его готовности по pipe. Зафиксированные миллисекунды измерения доступны в JSON; CSV сохраняет заданные бюджеты и устойчивые причины отказа.

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

НаблюдениеРезультат
Первый предел500 мс ожидания блокировки
Второй предел1 с выполнения запроса
Коды PDO4294967221; 4294967292
Остаток после двух отказов3
Владелец блокировкиоткатил работу и завершился
Результатwait-budget.csv
Самостоятельный проект, исходник и архив
Ограничить ожидание занятого остатка и сохранить причину отказаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
require __DIR__.'/lock-lab.php';
$lockJob=p175LockStartEarly();
try {
    $config=p175Config();$db=p175Connect();$runtime=p175Runtime($db);

function writeCsv175(string $filename,array $rows): void {
    $output='';
    foreach($rows as $row) {
        $quoted=array_map(static function($value):string {return '"'.str_replace('"','""',(string)$value).'"';},$row);
        $output.=implode(';',$quoted)."\n";
    }
    if (file_put_contents(__DIR__.'/'.$filename,$output)!==strlen($output)) {throw new RuntimeException('CSV не записан целиком.');}
}

    $name=p175CreateTable($db,$owned,'budget',[
        ['id'=>1,'sku'=>'CAB','title'=>'Кабель','category'=>'склад','price_cents'=>12500,'qty'=>3],
    ]);$table=p175Qualified($name);
    p175LockBegin($lockJob,$name,$owned);
    $attempts=[];
    foreach([['lock-first',500,3],['query-first',3000,1]] as $budget) {
        $db->setAttribute(PDO::CUBRID_ATTR_LOCK_TIMEOUT,$budget[1]);
        $db->setAttribute(PDO::ATTR_TIMEOUT,$budget[2]);
        $effective=['lock_ms'=>$db->getAttribute(PDO::CUBRID_ATTR_LOCK_TIMEOUT),
            'query_seconds'=>$db->getAttribute(PDO::ATTR_TIMEOUT)];
        $db->beginTransaction();$statement=null;$started=microtime(true);
        try {
            // PDO_CUBRID transfers ATTR_TIMEOUT to CCI when prepare creates this request.
            $statement=$db->prepare('UPDATE '.$table.' SET qty=qty-1 WHERE id=1 AND qty>=1');
            $call=p175Capture(static function() use ($statement) {return $statement->execute();},$statement);
            $elapsed=(microtime(true)-$started)*1000;
            $safe=$call;unset($safe['value']);
            $attempts[]=['budget'=>$budget[0],'effective'=>$effective,'diagnostics'=>$safe,'elapsed_ms'=>round($elapsed,3)];
            if ($call['exception']!==true || $call['sqlstate']!=='HY000') {throw new RuntimeException('Ожидался подтверждённый конфликт блокировки.');}
        } finally {
            p175Release($statement);
            if ($db->inTransaction()) {$db->rollBack();}
        }
    }
    $proof=p175LockStop($lockJob);$lockJob=null;
    $stored=p175Read($db,'SELECT sku,qty FROM '.$table.' WHERE id=1');$qty=(int)$stored[0]['qty'];
    $csv=[['budget','lock_ms','query_seconds','sqlstate','driver_code','qty_after']];
    foreach($attempts as $a) {$csv[]=[$a['budget'],$a['effective']['lock_ms'],$a['effective']['query_seconds'],
        $a['diagnostics']['sqlstate'],$a['diagnostics']['driver_code'],$qty];}
    writeCsv175('wait-budget.csv',$csv);
    $rows=[['Первый предел','500 мс ожидания блокировки'],['Второй предел','1 с выполнения запроса'],
        ['Коды PDO','4294967221; 4294967292'],['Остаток после двух отказов',3],
        ['Владелец блокировки','откатил работу и завершился'],['Результат','wait-budget.csv']];
    $checks=['lock_error'=>$attempts[0]['diagnostics']['driver_code']===4294967221,
        'query_error'=>$attempts[1]['diagnostics']['driver_code']===4294967292,
        'both_exceptions'=>$attempts[0]['diagnostics']['exception']&&$attempts[1]['diagnostics']['exception'],
        'lock_effective'=>$attempts[0]['effective']===['lock_ms'=>500,'query_seconds'=>3],
        'query_effective'=>$attempts[1]['effective']===['lock_ms'=>3000,'query_seconds'=>1],
        'bounded_lock_time'=>$attempts[0]['elapsed_ms']>=200&&$attempts[0]['elapsed_ms']<6000,
        'bounded_query_time'=>$attempts[1]['elapsed_ms']>=700&&$attempts[1]['elapsed_ms']<7000,
        'messages_present'=>$attempts[0]['diagnostics']['message_present']&&$attempts[1]['diagnostics']['message_present'],
        'quantity_unchanged'=>$qty===3,'child_stopped'=>$proof['stopped']===true&&$proof['exit_code']===0,
        'child_rollback'=>($proof['completed']['rolled_back']??null)===true,
        'no_forced_stop'=>$proof['termination_by_parent']===false,
        'no_open_transaction'=>$db->inTransaction()===false,'artifact_written'=>is_file(__DIR__.'/wait-budget.csv')];
    $observations=['attempts'=>$attempts,'stored_row'=>$stored[0],
        'lock_child'=>['pid'=>$proof['pid'],'stopped'=>$proof['stopped'],'exit_code'=>$proof['exit_code'],
            'rolled_back'=>$proof['completed']['rolled_back']]];

} finally {
if (is_array($lockJob)) {p175LockStop($lockJob);$lockJob=null;}
    if ($db instanceof PDO) {try {$removed=p175Cleanup($db,$owned);} finally {p175Close($db);}}
}

p175Report('pdoc175-wait-budget','Ограничить ожидание занятого остатка и сохранить причину отказа',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Первый предел — 500 мс ожидания блокировки
Второй предел — 1 с выполнения запроса
Коды PDO — 4294967221; 4294967292
Остаток после двух отказов — 3
Владелец блокировки — откатил работу и завершился
Результат — wait-budget.csv

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

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

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

Это проверка конкуренции двух соединений, не HTTP и не измерение скорости сайта. Время зависит от планирования ОС, поэтому проверяется разумный диапазон, а не точная миллисекунда. У проверенного PHP7-порта ошибки представлены как 32-битные беззнаковые числа: 4294967221 соответствует серверному -75, 4294967292 — -4; нельзя требовать native-код -75 от PDO. Процесс запускается до первого подключения родителя, получает только собственное имя таблицы и завершается до cleanup. Повтор SQL после тайм-аута требует отдельной политики идемпотентности.

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

Сделайте lock=3000 мс и query=3 с. Можно ли обещать, какой предел сработает первым?

Разбор

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

Ошибки, persistent-ограничение и дальнейшие возможности

Официальная страница оговаривает отсутствие поддержки persistent-соединений. Этот пакет не включает PDO::ATTR_PERSISTENT и не обещает пул или сохранение сессии между HTTP-запросами. Обычный PDO живёт, пока на него или связанные PDOStatement остаются ссылки. Закрывайте результаты, заканчивайте транзакцию и затем отпускайте соединение. Поведение native cubrid_pconnect из другой главы не доказывает persistent PDO.

НаблюдениеРешение приложения
PDOException при new PDOПроверить драйвер, DSN, доступность Broker и реквизиты; не показывать сырой пароль/адрес посетителю.
SQLSTATE не 00000 и код драйвераСохранить диагностику сразу; отменить собственную незавершённую транзакцию.
SELECT успешно вернул []Показать пустой каталог/раздел, не выдавать это за недоступную базу.
UPDATE затронул 0 строкПроверить бизнес-смысл: нет записи, недостаточно остатка, не выполнено условие версии.
Тайм-аут ожиданияПоказать понятный отказ, завершить транзакцию; повторять только по определённой политике и с защитой от дублирования.

PDO::ERRMODE_SILENT оставляет код для явной проверки, WARNING добавляет предупреждение, EXCEPTION выбрасывает PDOException. В PHP7 старое значение по умолчанию отличается от PHP8, поэтому пример всегда задаёт режим явно. errorInfo относится к тому PDO/statement, где произошла операция; после успешного следующего вызова старую ошибку можно потерять. Отдельные переменные с диагностикой лучше многократного чтения текущего состояния.

Официальная возможность PDO_CUBRIDГде продолжить на полезной задаче
DSN, установка и подключениеПолный DSN и каталог по профилю
Autocommit, транзакции, несколько SQL, timeoutзаявка, квитанция и ожидание
Forward-only/scrollable cursor, 14 полей getColumnMetaПревью результата и описание колонок
Все виды PDO::cubrid_schemaПолный контракт схемы; каноническая карточка
BLOB/CLOB как потоки, не native LOB2Вложения и текст
Три атрибута и шесть исторических имён изоляцииПолный каталог атрибутов и версионная граница
SET/MULTISET/SEQUENCE и 18 типов bindParamКомплект и последовательность работ

PDO делает многие имена методов общими, но не превращает специфичный SQL, кодировки, коллекции, schema и единицы ожидания в одинаковые правила всех СУБД. Общий справочник PDO остаётся маршрутом к каждому методу. Следующий практический шаг — построить экран и план импорта по точным метаданным.

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

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

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

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

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