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

Дерево руководства
Глава 289 · От знакомства с PDO к связанным операциям CUBRID
PDO_CUBRID: каталог, резервирование, несколько результатов и время ожидания
Подключение PDO_CUBRID и четыре полезных приложения: ограниченный каталог, атомарный резерв, составная квитанция и настоящий конфликт блокировки.
От задачи приложения к PDO и PDOStatement
Представьте небольшой складской сервис. Посетителю нужен каталог доступных товаров, сотруднику — подтверждение резерва, интеграции — понятная квитанция. База хранит данные, а PHP должен получить только нужные поля и строки, выполнить связанную операцию целиком и объяснить отказ. PDO_CUBRID позволяет решать эти задачи через интерфейс PDO, сохраняя особенности CUBRID.
- Профиль warehouse выбирает адрес базы и категорию «склад»
- PDO — открытое соединение; PDOStatement — подготовленный SELECT с параметрами
- 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 сохраняет нулевую цену и различает отсутствие примечания от пустого текста.
- Профиль warehouse → category=склад, limit=2
- 4 строки → WHERE: склад и qty>0 → ORDER BY price_cents,id
- 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 | Данные и действие |
|---|---|
| До beginTransaction | CAB=5, CLIP=2; новая заявка хочет 4 и 3. |
| Первый UPDATE | Внутри транзакции CAB временно равен 1. |
| Второй UPDATE | Условие qty>=3 не выполнено; rowCount=0. SQL успешен, заявка не принимается. |
| rollBack | CAB снова 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 становится понятной квитанцией обеих попыток.
- До заявки: CAB=7, CLIP=3
- R-41: UPDATE 2 и 1 → commit → CAB=5, CLIP=2
- 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-41 | CAB=5; CLIP=2 |
| Заявка R-42 | отклонена: не хватает хомутов |
| Остаток после R-42 | CAB=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_INSTANCE | 1. Для строк возможны dirty/non-repeatable/phantom, а для таблицы — неповторяемое чтение её схемы. Самый слабый из описанных исторических уровней. |
| PDO::TRAN_COMMIT_CLASS_COMMIT_INSTANCE | 2. Dirty read исключён; non-repeatable и phantom для строк допускаются. |
| PDO::TRAN_REP_CLASS_UNCOMMIT_INSTANCE | 3. В старом описании назван default: повторяемая схема таблицы, но dirty/non-repeatable/phantom строк возможны. Это не default сервера11.3. |
| PDO::TRAN_REP_CLASS_COMMIT_INSTANCE | 4. Dirty read исключён; non-repeatable и phantom возможны, схема таблицы защищена от изменения во время чтения. |
| PDO::TRAN_REP_CLASS_REP_INSTANCE | 5. Исторический текст исключает dirty/non-repeatable, допускает phantom. Современную snapshot-семантику нужно читать отдельно. |
| PDO::TRAN_SERIALIZABLE | 6. Историческое описание обещает отсутствие перечисленных аномалий. Нельзя переносить это обещание на новую 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.
- SELECT позиций → 2 строки; SELECT проблем → 0 строк, но 1 колонка
- UPDATE хомутов → 1 изменённая строка; итоговый SELECT → 11
- 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 |
Самостоятельный проект, исходник и архив
<?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 остаётся прежним.
- Child: BEGIN → qty=99 без commit → сообщает «строка занята»
- Parent: lock=500 мс/query=3 с → отказ; затем lock=3000 мс/query=1 с → отказ
- 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 с выполнения запроса |
| Коды PDO | 4294967221; 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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


