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

Дерево руководства
Глава 119 · После обычного SELECT, mysqli_result и обработки исключений
MySQLi: асинхронные запросы, готовность ответа и граница ожидания
MYSQLI_ASYNC, poll и reap на настоящих SELECT/UPDATE: два соединения, SQL-ошибка, reject, общий deadline и безопасное завершение pending-ответа.
Зачем разделять отправку, ожидание и получение
Async нужен, когда PHP полезно продолжить свою работу, пока MySQL готовит ответ, или ждать несколько независимых соединений. Это не команда «ускорить SQL» и не возможность послать сколько угодно запросов в одну сессию. Разберём шесть состояний на маленьких результатах: отправка, готовность, чтение, отказ, свободное соединение и исчерпанное ожидание.
- Свободно: можно отправить SQL
- Отправили MYSQLI_ASYNC: ответ ещё принадлежит этой операции
- poll: готово к чтению или пока 0
- reap: таблица, true или ошибка
- Прочитали/освободили нужный результат → новая команда
| Понятие | Значение |
|---|---|
| Соединение | Одна сессия PHP ↔ MySQL с собственным порядком команд |
| Pending | Запрос отправлен, ответ ещё не получен |
| Готовность | Драйвер может читать ответ; успех SQL ещё не доказан |
| Deadline | Одна конечная граница ожидания всей операции |
| Сбор результата | reap забирает ответ, fetch читает строки, free освобождает память |
Если нужен один обычный запрос с пользовательскими значениями, начните с подготовленного SQL. У mysqli_stmt::execute нет флага MYSQLI_ASYNC: нельзя добавлять этот флаг к prepare/execute и ожидать тот же механизм. Здесь все SQL фиксированы автором, внешнего ввода нет. Справочник MySQLi сохраняет отдельные контракты методов.
Запустить самостоятельный пример
Архив содержит example.php, aq112-helper.php и README. Нужны 64-битный PHP 8.2+ с mysqli/mysqlnd, MySQL 8.4 и отдельная локальная учебная база. Настройте PHPMAN_MYSQL_HOST, PHPMAN_MYSQL_PORT, PHPMAN_MYSQL_DATABASE, PHPMAN_MYSQL_USER и PHPMAN_MYSQL_PASSWORD по первому подключению. Контрольный стенд: PHP 8.5.11, MySQL 8.4.13, 127.0.0.1:33070. Выполните php example.php, откройте result.html.
Каждый пример создаёт только свои TEMPORARY-таблицы или выполняет SELECT констант. Таблицы исчезают при закрытии соединений в finally. result.json содержит строки отчёта, metrics.json — версии, счётчики poll/reap и проверки, без пароля. Короткие SLEEP до 0.04 секунды служат учебной задержкой, не измерением производительности.
Один общий deadline и четыре результата poll
В helper read/error собираются заново, потому что poll их изменяет. false проверяется строго отдельно от 0. Для ожидаемого reject есть специальный пример ниже; в обычном учёте pending он означает нарушение состояния. Готовые read/error объединяются без повторов. Нельзя получать один ожидающий ответ дважды.
$deadline = hrtime(true) + 2_000_000_000;
while ($pending) {
$remaining = $deadline - hrtime(true);
if ($remaining <= 0) {
throw new RuntimeException('Бюджет ожидания исчерпан');
}
$read = $error = array_values($pending);
$reject = [];
$sliceUs = min(50000, max(1, intdiv($remaining, 1000)));
$count = mysqli_poll($read, $error, $reject, 0, $sliceUs);
if ($count === false) { /* сбой poll: завершить с ошибкой */ }
if ($reject) { /* исправить реестр pending; не делать reap */ }
if ($count === 0) { continue; }
// Только уникальные готовые соединения из read/error:
// reap → прочитать/освободить → убрать из pending.
}Это схема цикла с комментариями вместо обработчиков, не отдельная запускаемая программа. Полные исполняемые ветки находятся в helper каждого ZIP. Микросекунда — миллионная доля секунды, наносекунда — миллиардная; 50000 микросекунд здесь означают максимум 0.05 секунды одного poll.
Отправить SELECT и дождаться конкретного ответа
В интерфейсе нужно вывести два товара с остатком. Отправка запроса и получение таблицы — разные события. Сначала получим привычные строки, но явно пройдём путь «отправили → дождались готовности → забрали ответ».
Исходные данные
| ID | Товар | Остаток |
|---|---|---|
| 1 | Чай | 7 |
| 2 | Кофе | 4 |
| 3 | Мёд | 0 |
- SELECT выбирает id,title,qty и не более 2 товаров
- query(..., MYSQLI_ASYNC) отправляет SQL
- poll сообщает, какие соединения готовы к чтению
- reap_async_query получает mysqli_result
- fetch_all читает две строки, free освобождает результат
Что получится
| ID | Товар | Остаток |
|---|---|---|
| 1 | Чай | 7 |
| 2 | Кофе | 4 |
Главные вызовы
$db->query(
'SELECT id,title,qty FROM stock WHERE qty>0 ORDER BY id LIMIT 2',
MYSQLI_ASYNC | MYSQLI_STORE_RESULT
);
// Запрос отправлен; таблицы результата здесь ещё нет.
$read = $error = [$db];
$reject = [];
$ready = mysqli_poll($read, $error, $reject, 0, 50000);
// Только для соединения из готового read/error:
// $result = $db->reap_async_query();
// Полный ограниченный цикл разобран ниже и включён в архив.Здесь выделена основная операция для объяснения. Подключение, подготовка данных, полный цикл и обработка результата находятся в самостоятельной программе ниже.
MYSQLI_ASYNC — флаг режима вызова query. MYSQLI_STORE_RESULT выбирает буферизованный ответ; символ | объединяет флаги. Сразу после отправки объекта с найденными строками ещё нет. mysqli_poll получает списки соединений по ссылке и изменяет их. Затем reap_async_query забирает ответ готового соединения; после SELECT это mysqli_result, который можно прочитать.
Один poll с 50000 микросекундами не является общим таймаутом. Он может вернуть 0: за этот промежуток никто не готов. Если повторять такие вызовы бесконечно, весь сценарий тоже будет ждать бесконечно. Поэтому helper один раз получает конечную монотонную границу deadline и перед каждым следующим ожиданием проверяет остаток бюджета. В данном наборе он равен двум секундам; это пример политики, не универсальное время для вашего приложения.
Обычный синхронный SELECT часто проще. Здесь мы осознанно изучаем состояния соединения. Нужные поля и LIMIT остаются необходимыми независимо от async: этот флаг не исправляет выборку всей базы.
Границы и ошибки
mysqlnd обязателен. poll может сообщить готовность сокета до получения полного большого ответа; reap с буферизацией, обработка строк и планировщик ОС добавляют время. Граница нашего цикла запрещает новые ожидания после deadline, но не является жёстким общим сетевым таймером для произвольного запроса. Для этого нужны также настройки драйвера/сервера и ограниченный результат.
Полная самостоятельная программа и файлы
<?php
declare(strict_types=1);
require __DIR__.'/aq112-helper.php';
$db=aq112Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->client_info,'server'=>$db->server_info];
$others=[];$details=[];
try {
aq112Stock($db);
$deadline=aq112Now()+2_000_000_000;
$sent=$db->query('SELECT id,title,qty FROM mysqli112_async_stock WHERE qty>0 ORDER BY id LIMIT 2',MYSQLI_ASYNC|MYSQLI_STORE_RESULT);
$trace=aq112Trace();
$result=aq112Wait($db,$deadline,$trace);
if(!$result instanceof mysqli_result){throw new RuntimeException('SELECT must yield a result set');}
$found=aq112Rows($result);$rows=[];
foreach($found as $row){$rows[]=[(int)$row['id'],$row['title'],(int)$row['qty']];}
$checks=['dispatch_accepted'=>$sent===true,'expected_stock'=>$rows===[[1,'Чай',7],[2,'Кофе',4]],'single_reap'=>$trace['reap_calls']===1];
$details=['trace'=>$trace,'budget_ns'=>2_000_000_000,'result_mode'=>'MYSQLI_STORE_RESULT','client_row_limit'=>2];
} finally {
foreach($others as $connection){$connection->close();}
$db->close();
}
aq112Report('mysqli112-async-first-select', ['ID', 'Товар', 'Остаток'], $rows, $checks, $runtime, $details);
echo 'mysqli112-async-first-select: saved result.html and result.json
';
Ожидаемый результат
mysqli112-async-first-select: saved result.html and result.json
Запустите один ZIP и откройте result.html: получите конкретную таблицу выше. aq112-helper.php и README уже внутри; предыдущие примеры выполнять не нужно.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте сами. Измените WHERE на qty>=5: в ответе должен остаться только «Чай», 7. Если вернулось 0 готовых соединений, объясните, почему это ещё не означает пустую таблицу товаров.
Практический вопрос: Что меняется в read/error/reject и почему timeout poll не ограничивает весь запрос. Автор не понимает назначение массивов и ожидает, что время одного mysqli_poll само ограничит длительность операции. Наш пример отделяет одну проверку готовности от общего монотонного срока ожидания и пересоздаёт массивы на каждой итерации. Обсуждение задаёт ситуацию; правила сверены с PHP, программа написана отдельно.
Собрать две независимые сводки без второго запроса в занятую сессию
Панель показывает общий остаток товаров и число оплаченных заказов. Эти два расчёта независимы. Чтобы отправить оба, не ожидая первого ответа в PHP, используем два отдельных соединения и храним, какой ответ ещё не получен.
Исходные данные
| Источник | Данные | Ожидаемый итог |
|---|---|---|
| Товары | Чай 7, кофе 4, мёд 0 | 11 |
| Заказы | 101 paid, 102 paid, 103 draft | 2 оплаченных |
- Соединение stock: свой SELECT суммы
- Соединение orders: свой SELECT количества
- pending содержит оба запроса
- Полученный ответ убирается из pending ровно один раз
- Экран выводит stock и orders в заданном порядке, независимо от порядка готовности
Что получится
| Сводка | Значение |
|---|---|
| Общий остаток | 11 |
| Оплаченных заказов | 2 |
Главные вызовы
$stockConnection->query($stockSql, MYSQLI_ASYNC);
$ordersConnection->query($ordersSql, MYSQLI_ASYNC);
// В pending остаются только запросы без полученного ответа.
// Перед каждым poll заново строим read/error из pending.
// Готовые соединения: reap → прочитать → free → убрать из pending.Здесь выделена основная операция для объяснения. Подключение, подготовка данных, полный цикл и обработка результата находятся в самостоятельной программе ниже.
У одного соединения есть последовательность команд и ответов. Для независимой второй команды в этом примере открыто второе соединение. В каждой его учебной TEMPORARY-таблице находятся только относящиеся к нему данные. Два подключения не делают общими сессионные таблицы или транзакции.
pending хранит объекты по spl_object_id, names связывает их с назначением. На каждой итерации read/error заново строятся из ещё ожидающих соединений. После poll объединяем готовые read/error и убираем дубли по объекту: нельзя получать ответ с одного соединения дважды. После получения и освобождения ответа соответствующий ключ удаляется из pending. Выходной порядок задаём именами сводок, не угадываем, какой сокет первым станет готов.
Короткие SLEEP(0.02) и SLEEP(0.01) нужны только для демонстрации ожидания. Порядок завершения сохраняется в metrics.json, но не проверяется как неизменный. Эти задержки не доказывают ускорения: async позволяет перекрыть ожидания клиента, а серверу всё равно нужно выполнить каждый запрос.
Границы и ошибки
Параллельные соединения расходуют ресурсы и не дают единого снимка данных автоматически. Для зависимых команд, например «создать заказ → добавить его позиции», этот приём без транзакционного проекта не подходит. Не открывайте соединение на каждую строку вместо JOIN или ограниченного пакетного запроса. Сначала исправьте форму SQL.
Полная самостоятельная программа и файлы
<?php
declare(strict_types=1);
require __DIR__.'/aq112-helper.php';
$db=aq112Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->client_info,'server'=>$db->server_info];
$others=[];$details=[];
try {
aq112Stock($db);
$other=aq112Connect();$others[]=$other;
$other->query('CREATE TEMPORARY TABLE mysqli112_async_orders (id INT PRIMARY KEY,status VARCHAR(12) NOT NULL) ENGINE=InnoDB');
$other->query("INSERT INTO mysqli112_async_orders VALUES (101,'paid'),(102,'paid'),(103,'draft')");
$deadline=aq112Now()+2_000_000_000;
$db->query('SELECT SLEEP(0.02) AS waited,SUM(qty) AS total_qty FROM mysqli112_async_stock',MYSQLI_ASYNC|MYSQLI_STORE_RESULT);
$other->query("SELECT SLEEP(0.01) AS waited,COUNT(*) AS paid_count FROM mysqli112_async_orders WHERE status='paid'",MYSQLI_ASYNC|MYSQLI_STORE_RESULT);
$pending=[spl_object_id($db)=>$db,spl_object_id($other)=>$other];
$names=[spl_object_id($db)=>'stock',spl_object_id($other)=>'orders'];
$answers=[];$completionOrder=[];$trace=aq112Trace();
while($pending){
foreach(aq112Ready($pending,$deadline,$trace) as $connection){
$key=spl_object_id($connection);$trace['reap_calls']++;
$result=$connection->reap_async_query();
if(!$result instanceof mysqli_result){throw new RuntimeException('Summary SELECT must return rows');}
$answer=aq112Rows($result)[0];
$answers[$names[$key]]=$answer;$completionOrder[]=$names[$key];unset($pending[$key]);
}
}
$rows=[['Общий остаток',(int)$answers['stock']['total_qty']],['Оплаченных заказов',(int)$answers['orders']['paid_count']]];
$checks=['two_distinct_connections'=>$db->thread_id!==$other->thread_id,'both_summaries'=>$rows===[['Общий остаток',11],['Оплаченных заказов',2]],'reaped_each_once'=>$trace['reap_calls']===2 && count($completionOrder)===2,'no_pending_connections'=>$pending===[]];
$details=['trace'=>$trace,'completion_order'=>$completionOrder,'budget_ns'=>2_000_000_000,'speedup_claimed'=>false];
} finally {
foreach($others as $connection){$connection->close();}
$db->close();
}
aq112Report('mysqli112-async-two-connections', ['Сводка', 'Значение'], $rows, $checks, $runtime, $details);
echo 'mysqli112-async-two-connections: saved result.html and result.json
';
Ожидаемый результат
mysqli112-async-two-connections: saved result.html and result.json
Запустите один ZIP и откройте result.html: получите конкретную таблицу выше. aq112-helper.php и README уже внутри; предыдущие примеры выполнять не нужно.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте сами. Добавьте в учебные заказы ещё один paid: итог должен стать 3. Уберите искусственные SLEEP: содержимое результата останется тем же, а порядок готовности по-прежнему нельзя закреплять в логике экрана.
Практический вопрос: Можно ли передать MYSQLI_ASYNC подготовленному выражению. Нужен подготовленный асинхронный запрос. Здесь явно показано различие API: execute не принимает режим ASYNC; наличие параллельного ожидания не оправдывает склеивание пользовательских значений в SQL. Обсуждение задаёт ситуацию; правила сверены с PHP, программа написана отдельно.
Получить подтверждение UPDATE и число изменённых строк
Нужно списать две упаковки чая, если остаток позволяет. Сервер не возвращает таблицу строк для UPDATE; нужен признак завершения, число изменённых записей и сохранённый остаток.
Исходные данные
| Товар | Было | Списание | Должно остаться |
|---|---|---|---|
| Чай, ID 1 | 7 | 2 | 5 |
- Отправка UPDATE возвращает принятие команды
- poll ждёт готовности ответа
- reap возвращает true, а не таблицу
- Сразу сохраняем affected_rows = 1
- Следующий SELECT подтверждает qty = 5
Что получится
| Что проверяем | Значение |
|---|---|
| Отправка принята | true |
| Ответ reap | true |
| Изменено строк | 1 |
| Остаток чая | 5 |
Главные вызовы
$db->query(
'UPDATE stock SET qty=qty-2 WHERE id=1 AND qty>=2',
MYSQLI_ASYNC
);
// После готовности poll:
$answer = $db->reap_async_query(); // true при успешном UPDATE
$changed = $db->affected_rows; // сохранить до следующего SQL
// fetch_assoc() и free() у true вызывать нельзя.Здесь выделена основная операция для объяснения. Подключение, подготовка данных, полный цикл и обработка результата находятся в самостоятельной программе ниже.
reap_async_query имеет несколько видов результата: mysqli_result для запроса с набором строк, true для успешной команды без такого набора, false при отказе без строгих исключений. Поэтому одинаково вызывать fetch_assoc или free для любого «успешного результата» нельзя. В нашем UPDATE проверяем именно $answer===true.
affected_rows принадлежит соединению и описывает последний выполненный запрос. Сохраняем его до проверочного SELECT. Значение 1 подтверждает изменение одной строки, а новое qty=5 подтверждает предметный результат. true само по себе не означает, что условие WHERE нашло строку: при недостаточном остатке изменённых строк может быть 0.
Условие qty>=2 входит в UPDATE, поэтому решение о допустимости списания принимает сервер в той же команде. Здесь показан только один запрос и его ответ. Для заказа с несколькими связанными изменениями используйте подходящую транзакционную схему.
Границы и ошибки
Режим MYSQLI_CLIENT_FOUND_ROWS меняет трактовку некоторых счётчиков UPDATE; в этом примере он не включён. Нулевое число изменений требует бизнес-разбора, а не автоматического повторения списания. Если связь оборвалась до получения ответа, нельзя по отсутствию подтверждения заключать, что запись точно не изменилась.
Полная самостоятельная программа и файлы
<?php
declare(strict_types=1);
require __DIR__.'/aq112-helper.php';
$db=aq112Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->client_info,'server'=>$db->server_info];
$others=[];$details=[];
try {
aq112Stock($db);
$deadline=aq112Now()+2_000_000_000;
$sent=$db->query('UPDATE mysqli112_async_stock SET qty=qty-2 WHERE id=1 AND qty>=2',MYSQLI_ASYNC);
$trace=aq112Trace();$answer=aq112Wait($db,$deadline,$trace);
$changed=$db->affected_rows;
$stored=aq112Rows($db->query('SELECT id,title,qty FROM mysqli112_async_stock WHERE id=1'))[0];
$rows=[['Отправка принята',$sent===true?'true':'false'],['Ответ reap',$answer===true?'true':'не true'],['Изменено строк',(int)$changed],['Остаток чая',(int)$stored['qty']]];
$checks=['update_dispatch'=>$sent===true,'update_has_no_result_object'=>$answer===true,'exactly_one_changed'=>$changed===1,'remaining_quantity'=>(int)$stored['qty']===5];
$details=['trace'=>$trace,'affected_rows_saved_before_followup'=>$changed,'updated_id'=>(int)$stored['id']];
} finally {
foreach($others as $connection){$connection->close();}
$db->close();
}
aq112Report('mysqli112-async-update-result', ['Что проверяем', 'Значение'], $rows, $checks, $runtime, $details);
echo 'mysqli112-async-update-result: saved result.html and result.json
';
Ожидаемый результат
mysqli112-async-update-result: saved result.html and result.json
Запустите один ZIP и откройте result.html: получите конкретную таблицу выше. aq112-helper.php и README уже внутри; предыдущие примеры выполнять не нужно.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте сами. Замените списание на 9 и условие на qty>=9: успешный ответ UPDATE должен сопровождаться affected_rows=0 и остатком 7. Не показывайте пользователю «списано 9» только потому, что reap вернул true.
Различить успешную отправку и ошибку SQL при получении
В запросе опечатка: missing_quantity вместо qty. Отправка прошла, но сервер не может выполнить такой SELECT. Нужна диагностика именно полученного ответа, без бесконечного ожидания «успеха».
Исходные данные
| Есть в таблице | Написали в SELECT | Ошибка |
|---|---|---|
| id,title,qty | missing_quantity | 1054 / 42S22 |
- query ASYNC принимает отправку и возвращает true
- poll сообщает готовность чтения
- reap читает серверный пакет ошибки
- Строгий режим выбрасывает mysqli_sql_exception
- Ожидаемую ошибку разбираем; затем правильный SELECT возвращает 7
Что получится
| Событие | Результат |
|---|---|
| Отправка принята | true |
| Код ошибки reap | 1054 |
| SQLSTATE | 42S22 |
| Следующий правильный запрос | 7 |
Главные вызовы
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$db->query('SELECT missing_quantity FROM stock', MYSQLI_ASYNC);
// После готовности poll:
try {
$result = $db->reap_async_query();
} catch (mysqli_sql_exception $error) {
// 1054 / 42S22: в нашем учебном SQL нет такой колонки.
// Исправляем запрос; успешная отправка не означала успешный SQL.
}Здесь выделена основная операция для объяснения. Подключение, подготовка данных, полный цикл и обработка результата находятся в самостоятельной программе ниже.
Сообщение «готово к чтению» не равно «SQL успешен». Пакет с ошибкой тоже можно прочитать. Поэтому не нужно ждать, что любая SQL-ошибка обязательно окажется только в массиве error у poll: конкретный код узнаём при reap. В примере перехватывается именно ожидаемый код 1054; неожиданный код повторно выбрасывается, а не маскируется удачным отчётом.
MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT превращает отказ в mysqli_sql_exception. getCode даёт код MySQL, getSqlState — SQLSTATE. Мы сохраняем только эти значения в учебном результате, не показываем посетителю текст SQL/подключения. После получения ошибочного ответа соединение больше не имеет этого pending-пакета и выполняет корректный SELECT.
Повторить тот же ошибочный SQL десять раз бессмысленно: имя колонки от этого не появится. Ошибку схемы/запроса, отсутствие строк и истёкшее ожидание обрабатывают по разным правилам. В рабочем журнале связывайте событие с операцией, сохраняя безопасные диагностические поля.
Границы и ошибки
Пример намеренно проверяет ошибку колонки, не сетевой обрыв. При потере соединения нельзя автоматически считать объект пригодным к повторному использованию. false и исключение также зависят от режима отчёта об ошибках; этот пакет задаёт строгий режим явно.
Полная самостоятельная программа и файлы
<?php
declare(strict_types=1);
require __DIR__.'/aq112-helper.php';
$db=aq112Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->client_info,'server'=>$db->server_info];
$others=[];$details=[];
try {
aq112Stock($db);
$deadline=aq112Now()+2_000_000_000;
$sent=$db->query('SELECT missing_quantity FROM mysqli112_async_stock',MYSQLI_ASYNC);
$trace=aq112Trace();$caught=null;
try {aq112Wait($db,$deadline,$trace);}
catch(mysqli_sql_exception $error){
if($error->getCode()!==1054){throw $error;}
$caught=['code'=>$error->getCode(),'sqlstate'=>$error->getSqlState()];
}
$next=aq112Rows($db->query('SELECT qty FROM mysqli112_async_stock WHERE id=1'))[0];
$rows=[['Отправка принята',$sent===true?'true':'false'],['Код ошибки reap',$caught['code']??0],['SQLSTATE',$caught['sqlstate']??'нет'],['Следующий правильный запрос',(int)$next['qty']]];
$checks=['send_did_not_prove_sql_success'=>$sent===true,'expected_unknown_column'=>$caught===['code'=>1054,'sqlstate'=>'42S22'],'exception_occurs_on_reap'=>$trace['reap_calls']===1,'connection_reusable_after_error'=>(int)$next['qty']===7];
$details=['trace'=>$trace,'error'=>$caught,'public_error_message_contains_sql'=>false];
} finally {
foreach($others as $connection){$connection->close();}
$db->close();
}
aq112Report('mysqli112-async-sql-error', ['Событие', 'Результат'], $rows, $checks, $runtime, $details);
echo 'mysqli112-async-sql-error: saved result.html and result.json
';
Ожидаемый результат
mysqli112-async-sql-error: saved result.html and result.json
Запустите один ZIP и откройте result.html: получите конкретную таблицу выше. aq112-helper.php и README уже внутри; предыдущие примеры выполнять не нужно.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте сами. Исправьте missing_quantity на qty и ограничьте строку WHERE id=1. Ожидаемый результат станет mysqli_result со значением 7; ветка исключения не должна срабатывать.
Не принимать свободное соединение за готовый ответ
В реестре опроса случайно остались два соединения, хотя запрос отправлен только в одно. Если трактовать свободное соединение как готовый результат или снова и снова его опрашивать, легко получить ошибку состояния.
Исходные данные
| Соединение | Состояние до poll |
|---|---|
| active | Отправлен SELECT с ответом 42 |
| idle | Подключено, но async-запроса нет |
- В read намеренно попадают active и idle
- poll возвращает idle в reject
- Для reject не вызывается reap
- Ответ active читается один раз: 42
- Свободное idle выполняет обычный новый SELECT: 9
Что получится
| Событие | Значение |
|---|---|
| Свободных соединений в reject | 1 |
| Активный SELECT | 42 |
| Новый SQL на свободном соединении | 9 |
Главные вызовы
$read = [$activeConnection, $idleConnection];
$error = [$activeConnection];
$reject = [];
$count = mysqli_poll($read, $error, $reject, 0, 50000);
// idleConnection попадёт в reject: ожидающего async-запроса нет.
// Для reject не вызываем reap_async_query().
// На следующих шагах опрашиваем только настоящий pending.Здесь выделена основная операция для объяснения. Подключение, подготовка данных, полный цикл и обработка результата находятся в самостоятельной программе ниже.
reject означает, что у соединения нет ожидающего асинхронного запроса. Это не массив «SQL завершился с ошибкой». Свободный объект может быть исправен: после исключения из списка ожидания мы выполняем на нём новый обычный SELECT и получаем 9.
Первый опрос намеренно содержит и реальный pending, и idle. Так проверяется отбраковка без запуска poll на заведомо пустом наборе работы. На следующих шагах остаётся только активный запрос. Если pending вообще пуст, цикл должен завершиться до poll. Подставлять в массив закрытый mysqli-объект или подавлять предупреждение знаком @ не нужно.
В helper для нормального пути неожиданный reject — ошибка собственного учёта pending. В этом учебном сценарии он ожидаем и обработан отдельно. Если poll вернул 0, проверка reject всё равно нужна: отсутствие готового ответа не отменяет обнаруженное неверное состояние списка.
Границы и ошибки
read/error/reject передаются по ссылке; после вызова это уже результаты опроса, а не полный исходный список. Нельзя бесконечно переиспользовать их без восстановления из pending. Не пытайтесь выполнять reap для каждого исходного соединения независимо от того, куда оно попало.
Полная самостоятельная программа и файлы
<?php
declare(strict_types=1);
require __DIR__.'/aq112-helper.php';
$db=aq112Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->client_info,'server'=>$db->server_info];
$others=[];$details=[];
try {
$idle=aq112Connect();$others[]=$idle;
$deadline=aq112Now()+2_000_000_000;
$db->query('SELECT SLEEP(0.02) AS waited,42 AS report_value',MYSQLI_ASYNC|MYSQLI_STORE_RESULT);
$read=[$db,$idle];$error=[$db];$reject=[];
$count=mysqli_poll($read,$error,$reject,0,50000);
if($count===false){throw new RuntimeException('Initial mixed poll failed');}
$rejectedIdle=count($reject)===1 && $reject[0]===$idle;
$trace=aq112Trace();
$ready=in_array($db,array_merge($read,$error),true);
if($ready){$trace['reap_calls']++;$result=$db->reap_async_query();}
else{$result=aq112Wait($db,$deadline,$trace);}
if(!$result instanceof mysqli_result){throw new RuntimeException('Expected active SELECT result');}
$active=aq112Rows($result)[0];
$idleAnswer=aq112Rows($idle->query('SELECT 9 AS next_value'))[0];
$rows=[['Свободных соединений в reject',count($reject)],['Активный SELECT',(int)$active['report_value']],['Новый SQL на свободном соединении',(int)$idleAnswer['next_value']]];
$checks=['only_idle_rejected'=>$rejectedIdle,'no_reap_on_idle'=>$trace['reap_calls']===1,'active_answer'=>(int)$active['report_value']===42,'idle_connection_works'=>(int)$idleAnswer['next_value']===9];
$details=['first_poll_count'=>$count,'first_read_count'=>count($read),'first_error_count'=>count($error),'first_reject_count'=>count($reject),'trace'=>$trace,'rejected_connection_reaped'=>false];
} finally {
foreach($others as $connection){$connection->close();}
$db->close();
}
aq112Report('mysqli112-async-idle-reject', ['Событие', 'Значение'], $rows, $checks, $runtime, $details);
echo 'mysqli112-async-idle-reject: saved result.html and result.json
';
Ожидаемый результат
mysqli112-async-idle-reject: saved result.html and result.json
Запустите один ZIP и откройте result.html: получите конкретную таблицу выше. aq112-helper.php и README уже внутри; предыдущие примеры выполнять не нужно.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте сами. Уберите idle из исходного read: reject должен стать пустым. Затем уберите оба соединения из pending приложения: правильное действие — закончить цикл, а не делать ещё один пустой poll.
Остановить ожидание и явно завершить оставшийся ответ
Общий бюджет ожидания исчерпан. Нужно выйти управляемо и решить судьбу уже отправленного запроса. Если просто отправить следующий SQL в ту же занятую сессию, проблема останется.
Исходные данные
| Событие | Условие |
|---|---|
| Первая команда | Короткий SELECT с будущим ответом 21 |
| Бюджет ожидания | Уже истёк до входа в helper |
| Выбранная политика | Отдельно дочитать малый ответ и освободить его |
| Следующий SQL | Вернуть 22 только после завершения первого |
- Сравнение deadline с hrtime замечает исчерпанный бюджет
- В этом бюджете не выполняется ни poll, ни reap
- Отправленный SQL не объявляется отменённым
- Отдельный ограниченный этап получает 21 и освобождает ответ
- Только затем новое обращение возвращает 22
Что получится
| Этап | Результат |
|---|---|
| Истёкший бюджет обработан | да |
| poll в истёкшем бюджете | 0 |
| Отдельно дочитанный ответ | 21 |
| Следующий запрос после освобождения | 22 |
Главные вызовы
$deadline = hrtime(true) - 1; // специально уже истёк
// Ожидание возвращает управляемый отказ, но SQL сам не отменяется.
// Нельзя сразу отправлять новый SQL в соединение с pending-ответом.
// Выбранная здесь политика: отдельный бюджет на дочитывание:
// poll готовности → reap → прочитать → free → следующий запрос.
// Другая политика сервиса: закрыть соединение и не переиспользовать его.Здесь выделена основная операция для объяснения. Подключение, подготовка данных, полный цикл и обработка результата находятся в самостоятельной программе ниже.
Мы специально задаём deadline=hrtime(true)-1. Это воспроизводимая проверка ветки отказа: она не зависит от того, успел ли компьютер за 10 миллисекунд обогнать сервер. Счётчики подтверждают ноль poll и reap в уже истёкшем бюджете. Такая проверка не утверждает, что мы измерили таймаут сети.
Выход из цикла ожидания сам не отправляет команду отмены MySQL. В этом примере явно выбрано дочитывание: отдельному этапу дан собственный бюджет, после готовности выполняются reap, чтение и free. Это завершение протокола, а не повтор исходной бизнес-операции. Альтернативный договор сервиса — отказаться от соединения и не возвращать его в пул; это тоже не доказательство мгновенной отмены серверной работы.
hrtime измеряет монотонное время и не меняется от перевода часов. В 64-битном PHP числовая форма — наносекунды. Общий deadline вычисляют один раз для операции. Если на каждой итерации присваивать «сейчас + две секунды», предел всё время отодвигается и перестаёт ограничивать ожидание.
Границы и ошибки
Результаты здесь малы и задержка сервера не более 0.04 секунды. Для произвольной большой выдачи один poll не ограничивает длительность последующего reap/буферизации. Нужны отдельные сетевые и серверные таймауты, ограничение размера ответа и политика отмены/проверки исхода записи. Не повторяйте INSERT или списание только из-за истечения клиентского ожидания.
Полная самостоятельная программа и файлы
<?php
declare(strict_types=1);
require __DIR__.'/aq112-helper.php';
$db=aq112Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->client_info,'server'=>$db->server_info];
$others=[];$details=[];
try {
$db->query('SELECT SLEEP(0.04) AS waited,21 AS report_value',MYSQLI_ASYNC|MYSQLI_STORE_RESULT);
$waiting=aq112Trace();$expired=aq112Now()-1;$deadlineCaught=false;
try {aq112Wait($db,$expired,$waiting);}
catch(Aq112Deadline $error){$deadlineCaught=true;}
$pendingStillOwned=true;
$cleanup=aq112Trace();$answer=aq112Wait($db,aq112Now()+2_000_000_000,$cleanup);
if(!$answer instanceof mysqli_result){throw new RuntimeException('Expected pending SELECT result');}
$late=aq112Rows($answer)[0];$pendingStillOwned=false;
$next=aq112Rows($db->query('SELECT 22 AS next_value'))[0];
$rows=[['Истёкший бюджет обработан',$deadlineCaught?'да':'нет'],['poll в истёкшем бюджете',$waiting['poll_calls']],['Отдельно дочитанный ответ',(int)$late['report_value']],['Следующий запрос после освобождения',(int)$next['next_value']]];
$checks=['expired_deadline_path'=>$deadlineCaught,'no_extra_poll_after_deadline'=>$waiting['poll_calls']===0 && $waiting['reap_calls']===0,'late_answer_collected'=>(int)$late['report_value']===21,'one_cleanup_reap'=>$cleanup['reap_calls']===1,'safe_reuse'=>$pendingStillOwned===false && (int)$next['next_value']===22];
$details=['waiting_trace'=>$waiting,'cleanup_trace'=>$cleanup,'deadline_test'=>'already expired by construction','automatic_server_cancellation_claimed'=>false,'new_business_sql_sent_before_drain'=>false];
} finally {
foreach($others as $connection){$connection->close();}
$db->close();
}
aq112Report('mysqli112-async-deadline-recovery', ['Этап', 'Результат'], $rows, $checks, $runtime, $details);
echo 'mysqli112-async-deadline-recovery: saved result.html and result.json
';
Ожидаемый результат
mysqli112-async-deadline-recovery: saved result.html and result.json
Запустите один ZIP и откройте result.html: получите конкретную таблицу выше. aq112-helper.php и README уже внутри; предыдущие примеры выполнять не нужно.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте сами. Для безопасного SELECT замените истёкший deadline на hrtime(true)+2_000_000_000: ожидается обычный ответ 21 без ветки отказа. Для платежа такую замену нельзя превращать в автоматический повтор: сначала нужна проверка исхода и идемпотентность операции.
Практический вопрос: Commands out of sync после нескольких асинхронных команд. Несколько команд на одном подключении оставляют непрочитанный ответ. Наш пример сохраняет владение ожидающим ответом даже после прикладного timeout, получает его, освобождает результат и только затем повторно использует соединение. Обсуждение задаёт ситуацию; правила сверены с PHP, программа написана отдельно.
Выбрать нужный механизм без лишней сложности
Для одного параметризованного запроса обычно достаточно prepared execute и обычного чтения. Async применяют с явным владельцем pending, общим бюджетом и политикой исхода ошибки. Независимые соединения не заменяют согласование изменений, а фоновые задания требуют отдельной очереди/воркера. Удобство API не отменяет проектирование SQL под экран.
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


