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

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

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

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

Глава 69 · После запросов, функций и транзакций Firebird

Firebird: событие после commit, обновление данных и остановка подписки

Настоящие события между процессами: ожидание, commit против rollback, пакетное обновление и освобождение подписки.

Понять, почему уведомление не является строкой таблицы

Один процесс изменил остаток товара, другому пора обновить экран или начать работу. Постоянно выгружать всю таблицу ради проверки изменения не нужно. Firebird может отправить именованный сигнал после commit. Получатель затем читает ровно то сохранённое состояние, которое ему требуется. Здесь увидим Cable с остатками 3, 7 и 11, а в пакетной операции — ещё Clamp с остатком 4.

Сигнал и данные идут разными путями
  1. Отправитель: UPDATE выбранной строки
  2. POST_EVENT задаёт имя, например FB158_CHANGED
  3. commit подтверждает запись и позволяет доставить уведомление
  4. Получатель узнаёт только имя события
  5. Новый SELECT с нужными полями и фильтром получает данные
  6. PHP показывает реальный новый остаток, затем завершает подписку

Событие похоже на звонок «данные могли измениться». Сам звонок не несёт коробку с товарами и не хранится для человека, которого нет дома. У аналогии есть граница: это сигнал нативного клиента Firebird, а не сообщение браузеру. Чтобы обновить браузер, приложение отдельно доставляет данные через HTTP, SSE или WebSocket; эта глава учит связи между PHP-процессами и базой, без обещания автоматического обновления страницы.

Для первых шагов нужны соединение, таблица и SELECT, commit и rollback и параметры функции и return. Серверная команда POST_EVENT находится внутри PSQL-блока либо триггера, а PHP получает уведомление через функции этой главы. Не путайте функцию callback с запросом SQL: первая выполняется в PHP, второй — в Firebird.

Есть два способа: ждать с остановкой текущего PHP-кода или зарегистрировать callback. Ресурс регистрации позже освобождает ibase_free_event_handler. События не гарантируют доставку исторических заданий отключённому клиенту и не являются долговечной очередью. Для важных операций сохраняют данные работы в транзакции; сигнал помогает проснуться, но не заменяет эти данные.

Каждый проект создаёт одну собственную синтетическую базу и два настоящих дочерних PHP-процесса. Listener получает события, sender меняет строки и подтверждает транзакции. Внешний контроллер ограничивает время и останавливает только свои процессы. Перед SQL оба child сверяют токен владельца PHPMAN158_OWNER; после завершения удаляется только база этого запуска. Публичные HTML/JSON содержат предметные наблюдения, служебные PID и журналы остаются локальными.

Разбудить обработчик и показать новый остаток товара

Небольшой складской обработчик ожидает изменения каталога. Было три кабеля, другой процесс сохранил одиннадцать. Нужно разбудить обработчик и показать именно новый остаток. Событие передаёт только имя FB158_CHANGED; число 11 получаем отдельным SELECT после подтверждения записи.

Как получится этот результат
  1. Сохранённая строка: ID 1, Cable, QTY 3
  2. Получатель вызывает ibase_wait_event и ждёт
  3. Отправитель выполняет один UPDATE: QTY = 11, затем commit
  4. POST_EVENT FB158_CHANGED и commit будят получателя
  5. wait_event возвращает имя; новый SELECT возвращает Cable / 11
  6. Внешний контроллер подтверждает завершение обоих процессов

Самый короткий шаг

$name = ibase_wait_event($db, 'FB158_CHANGED');
if ($name === false) {
    throw new RuntimeException('Ожидание завершилось ошибкой');
}
$result = ibase_query($db, 'SELECT ID, TITLE, QTY FROM LAB_ITEMS WHERE ID = ?', 1);

$db — уже открытое соединение. Строка в кавычках — точное имя подписки. Вызов останавливает выполнение этого PHP-процесса до уведомления: следующий SELECT начнётся после возврата функции. Параметр 1 ограничивает чтение одним нужным товаром; он не извлекается из события. Результат запроса затем читают через ibase_fetch_assoc и освобождают. Полная программа читает два заданных товара для сопоставления, а не весь каталог.

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

НаблюдениеРезультат
Товар до изменения1 / Cable / остаток 3
Полученное имя событияFB158_CHANGED
Строка нового SELECT1 / Cable / остаток 11
Предметных UPDATE отправителя1
Получатель подтвердил пробуждениеда
Два учебных процесса остановленыда
Полный самостоятельный проект и архив
Разбудить обработчик и показать новый остаток товараСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$owned=[];$removed=[];$db=$service=null;
try {
    $config=f158Config();$service=f158Service();$runtime=f158Runtime($service);
    require __DIR__.'/events-lab.php';
    $database=f158CreateDatabase($service,$owned,'ev_wait');
    $events=f158EventScenario($database,'wait');
    $listener=$events['listener'];$sender=$events['sender'];

    $items=$listener['items'];
    $rows=[['Товар до изменения','1 / Cable / остаток 3'],['Полученное имя события',$listener['event']],
        ['Строка нового SELECT',$items[0]['ID'].' / '.$items[0]['TITLE'].' / остаток '.$items[0]['QTY']],
        ['Предметных UPDATE отправителя',$sender['business_updates']],['Получатель подтвердил пробуждение',$sender['ack_received']],
        ['Два учебных процесса остановлены',$events['all_children_stopped']]];
    $checks=['event_name'=>$listener['event']==='FB158_CHANGED','fresh_stock'=>$items===[['ID'=>1,'TITLE'=>'Cable','QTY'=>11],['ID'=>2,'TITLE'=>'Clamp','QTY'=>0]],
        'one_business_update'=>$sender['business_updates']===1,'wake_up_ack'=>$sender['ack_received']===true,
        'no_native_error'=>$listener['error_code']===false&&$listener['error_message_present']===false,
        'children_stopped'=>$events['all_children_stopped']===true,'two_children'=>$events['process_count']===2];
    $observations=['events'=>$events,'initial_item'=>['ID'=>1,'TITLE'=>'Cable','QTY'=>3],
        'registration_ack_is_not_exposed'=>true,'repeated_notification_is_not_repeated_update'=>true];

} finally {
    if(is_resource($db)){f158Close($db);}
    if(is_resource($service)){f158Detach($service);}
    $removed=f158Cleanup($owned);
}

f158Report('fb158-event-wait','Разбудить обработчик и показать новый остаток товара',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Товар до изменения — 1 / Cable / остаток 3
Полученное имя события — FB158_CHANGED
Строка нового SELECT — 1 / Cable / остаток 11
Предметных UPDATE отправителя — 1
Получатель подтвердил пробуждение — да
Два учебных процесса остановлены — да

Распакуйте полный архив в отдельную папку и выполните example.php по README.txt. events-lab.php управляет двумя процессами, events-child.php содержит настоящий SQL и вызовы событий; lab.php создаёт и удаляет только собственную базу. result.html показывает конкретные строки, result.json — фактические наблюдения, metrics.json — проверки. Файлы events-process.json, events-listener.json и events-sender.json сохраняют локальное подтверждение исполнения. Повторно запускайте только отдельный учебный проект, не направляйте его на рабочую базу.

Открыть наглядный результат · Посмотреть наблюдения JSON.

Границы и разбор отказов

У функции нет параметра timeout. Лаборатория ограничивает время жизни отдельного получателя снаружи: 20 секунд на сценарий и до двух секунд на остановку. Это не тайм-аут Firebird API. У wait_event нет доступного PHP-сигнала «нативная регистрация закончена»: отметка READY ставится непосредственно перед вызовом. Поэтому отправитель меняет остаток ровно один раз, а затем ограниченно повторяет только сигнал о уже сохранённом состоянии до ACK. Такой приём пригоден для идемпотентного обновления представления и не означает гарантированную доставку каждого заказа. Само ожидание не должно удерживать старую snapshot-транзакцию: перед ожиданием она завершена, после сигнала начинается новое чтение.

Клиентам нужно обновлять представление базы без непрерывного чтения большого объёма строк. Исходная задача на Stack Overflow. Событие будит получателя, а выбранные поля и фильтр нового SQL определяют полезный результат. Сам сигнал не содержит прежних и новых значений.

Попробуйте изменить сценарий

Измените сохраняемый остаток с 11 на 12. Где нужно изменить ожидаемый результат, а где имя события оставляют прежним?

Разбор решения

Измените UPDATE отправителя в events-child.php и соответствующую проверку/строку результата example.php. Имя FB158_CHANGED обозначает вид изменения, оно не кодирует число. Повторное уведомление не должно повторять увеличение остатка; сохраняйте бизнес-операцию отдельно от пробуждения.

Отличить отменённую запись от подтверждённого изменения

Оператор ввёл 99 единиц, затем отменил операцию. Другой экран не должен показывать отменённый остаток как принятый складом. Сначала совместно откатим UPDATE и POST_EVENT, прочитаем прежнее значение 3; затем сохраним 7 и получим уведомление. Эта последовательность показывает практический смысл границы транзакции.

Как получится этот результат
  1. Callback-подписка зарегистрирована; исходный Cable = 3
  2. UPDATE QTY = 99 + POST_EVENT находятся в одной транзакции
  3. rollback → в базе по-прежнему 3; сигнал не опубликован как commit
  4. UPDATE QTY = 7 + POST_EVENT → commit
  5. Callback отмечает имя; основной код делает новый SELECT → 7
  6. Подписка освобождается до закрытия соединения

Самый короткий шаг

ibase_query($db, 'UPDATE LAB_ITEMS SET QTY = ? WHERE ID = ?', 7, 1);
ibase_query($db, "EXECUTE BLOCK AS BEGIN POST_EVENT 'FB158_CHANGED'; END");
ibase_commit($db);

Обе команды идут через одно соединение и его текущую транзакцию. EXECUTE BLOCK — анонимный серверный блок PSQL; POST_EVENT — его команда отправки события, а не встроенная PHP-функция. В этом кратком фрагменте показана последовательность; полный запускаемый проект проверяет отказы каждого вызова и явно завершает транзакции. Для получения уведомления заранее нужен второй процесс с регистрацией обработчика.

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

НаблюдениеРезультат
Попытка внутри отменённой транзакцииCable / остаток 99
SELECT после rollbackCable / остаток 3
Уведомления в окне после rollback0
SELECT после commitCable / остаток 7
Подтверждённое событие полученода
Подписка освобожденада
Полный самостоятельный проект и архив
Отличить отменённую запись от подтверждённого измененияСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$owned=[];$removed=[];$db=$service=null;
try {
    $config=f158Config();$service=f158Service();$runtime=f158Runtime($service);
    require __DIR__.'/events-lab.php';
    $database=f158CreateDatabase($service,$owned,'ev_commit');
    $events=f158EventScenario($database,'commit');
    $listener=$events['listener'];$sender=$events['sender'];

    $afterRollback=$listener['rollback_items'];$items=$listener['items'];$seen=$listener['seen'];
    $rows=[['Попытка внутри отменённой транзакции','Cable / остаток 99'],['SELECT после rollback',$afterRollback[0]['TITLE'].' / остаток '.$afterRollback[0]['QTY']],
        ['Уведомления в окне после rollback',$listener['rollback_callbacks']],['SELECT после commit',$items[0]['TITLE'].' / остаток '.$items[0]['QTY']],
        ['Подтверждённое событие получено',$listener['callback_count']>=1],['Подписка освобождена',$listener['freed']]];
    $checks=['rollback_kept_stock'=>$afterRollback===[['ID'=>1,'TITLE'=>'Cable','QTY'=>3],['ID'=>2,'TITLE'=>'Clamp','QTY'=>0]],
        'rollback_window_silent'=>$listener['rollback_callbacks']===0,'committed_stock'=>$items===[['ID'=>1,'TITLE'=>'Cable','QTY'=>7],['ID'=>2,'TITLE'=>'Clamp','QTY'=>0]],
        'callback_received'=>$listener['callback_count']>=1,'callback_arguments'=>array_unique(array_column($seen,'name'))===['FB158_CHANGED']&&!in_array(false,array_column($seen,'connection_resource'),true),
        'handler_freed'=>$listener['freed']===true,'children_stopped'=>$events['all_children_stopped']===true,'two_children'=>$events['process_count']===2];
    $observations=['events'=>$events,'attempted_uncommitted_stock'=>99,'rollback_observation_window_ms'=>100,
        'business_rule'=>'Изменение и POST_EVENT принадлежат одной подтверждаемой транзакции'];

} finally {
    if(is_resource($db)){f158Close($db);}
    if(is_resource($service)){f158Detach($service);}
    $removed=f158Cleanup($owned);
}

f158Report('fb158-event-commit','Отличить отменённую запись от подтверждённого изменения',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Попытка внутри отменённой транзакции — Cable / остаток 99
SELECT после rollback — Cable / остаток 3
Уведомления в окне после rollback — 0
SELECT после commit — Cable / остаток 7
Подтверждённое событие получено — да
Подписка освобождена — да

Распакуйте полный архив в отдельную папку и выполните example.php по README.txt. events-lab.php управляет двумя процессами, events-child.php содержит настоящий SQL и вызовы событий; lab.php создаёт и удаляет только собственную базу. result.html показывает конкретные строки, result.json — фактические наблюдения, metrics.json — проверки. Файлы events-process.json, events-listener.json и events-sender.json сохраняют локальное подтверждение исполнения. Повторно запускайте только отдельный учебный проект, не направляйте его на рабочую базу.

Открыть наглядный результат · Посмотреть наблюдения JSON.

Границы и разбор отказов

Событие от POST_EVENT доставляется после успешного commit соответствующей транзакции. Нельзя использовать его как отчёт о промежуточном ходе ещё не подтверждённого длинного изменения. В эксперименте ноль callback наблюдается в ограниченном окне после согласованного rollback; это измерение дополняет документированный контракт commit, а не доказывает отсутствие сетевого сообщения бесконечным ожиданием. Callback только фиксирует имя и признак ресурса, предметный SELECT выполняется позже в основном коде. Сетевое отключение или неработающий listener не откатывает уже подтверждённую запись.

Разработчик хотел немедленно получать сообщения из ещё выполняющейся процедуры. Исходная задача на Stack Overflow. Разбираем границу commit и показываем отменённую и принятую запись отдельно. В обсуждении нет принятого ответа; правило берётся из документации POST_EVENT, а не придумывается по вопросу.

Попробуйте изменить сценарий

Если один бизнес-процесс вызывает POST_EVENT, но забывает commit, какое значение должен показывать соседний экран?

Разбор решения

Прежнее подтверждённое значение. Соседнее подключение не должно принимать незавершённую запись за итог. Исправление — явно выбрать нужную границу транзакции; нельзя просто отправлять внешнее уведомление раньше commit и выдавать его за подтверждение записи.

Обновить две строки по сигналу, не считать callback за товар

Складская операция меняет сразу два товара. В той же транзакции сервер три раза вызывает одно имя события. Экрану нужны два новых остатка, а не предположение «пришло три callback — значит, изменились три товара». Получатель воспринимает сигнал как просьбу перечитать свой небольшой набор данных; запрос сразу ограничивает поля и ID.

Как получится этот результат
  1. До операции: Cable 3, Clamp 0
  2. Два UPDATE: Cable 7, Clamp 4
  3. Три POST_EVENT с одинаковым именем → один commit
  4. Доставка может объединять уведомления; точное количество callback не является контрактом
  5. SELECT ID,TITLE,QTY WHERE ID IN (1,2) ORDER BY ID
  6. Экран получает Cable 7 и Clamp 4

Самый короткий шаг

function catalogChanged($eventName, $connection) {
    $GLOBALS['catalogNeedsRefresh'] = true;
    return true;
}
$subscription = ibase_set_event_handler($db, 'catalogChanged', 'FB158_CHANGED');

callback — функция, которую драйвер вызовет при уведомлении. Здесь она только отмечает необходимость обновления. У неё два аргумента: имя и ресурс соединения; нет аргументов «строка товара» или «число обновлённых записей». После обработки флага основной код выполняет ограниченный SELECT. Полный пример дополнительно сохраняет фактическое количество callback в наблюдениях, но не требует, чтобы оно было равно двум или трём.

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

НаблюдениеРезультат
До измененияCable / 3; Clamp / 0
После нового SELECTCable / 7; Clamp / 4
Изменено товаров отправителем2
POST_EVENT в одном commit3
Получен хотя бы один callbackда
Callback передал сами строкинет
Порядок SQL задан явнода
Полный самостоятельный проект и архив
Обновить две строки по сигналу, не считать callback за товарСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$owned=[];$removed=[];$db=$service=null;
try {
    $config=f158Config();$service=f158Service();$runtime=f158Runtime($service);
    require __DIR__.'/events-lab.php';
    $database=f158CreateDatabase($service,$owned,'ev_batch');
    $events=f158EventScenario($database,'batch');
    $listener=$events['listener'];$sender=$events['sender'];

    $items=$listener['items'];$seen=$listener['seen'];
    $rows=[['До изменения','Cable / 3; Clamp / 0'],['После нового SELECT',$items[0]['TITLE'].' / '.$items[0]['QTY'].'; '.$items[1]['TITLE'].' / '.$items[1]['QTY']],
        ['Изменено товаров отправителем',$sender['committed_business_updates']],['POST_EVENT в одном commit',$sender['post_statements_first_commit']],
        ['Получен хотя бы один callback',$listener['callback_count']>=1],['Callback передал сами строки',$listener['payload_exposed']],
        ['Порядок SQL задан явно',True]];
    $checks=['selected_rows'=>$items===[['ID'=>1,'TITLE'=>'Cable','QTY'=>7],['ID'=>2,'TITLE'=>'Clamp','QTY'=>4]],
        'two_business_rows'=>$sender['committed_business_updates']===2,'three_posts'=>$sender['post_statements_first_commit']===3,
        'some_notification'=>$listener['callback_count']>=1,'only_named_signal'=>array_unique(array_column($seen,'name'))===['FB158_CHANGED'],
        'no_payload'=>$listener['payload_exposed']===false&&$listener['counter_argument_exposed']===false,
        'handler_freed'=>$listener['freed']===true,'children_stopped'=>$events['all_children_stopped']===true,'two_children'=>$events['process_count']===2];
    $observations=['events'=>$events,'selection'=>['fields'=>['ID','TITLE','QTY'],'ids'=>[1,2],'order_by'=>'ID'],
        'callback_count_is_not_a_business_row_count'=>true,'exact_callback_batching_is_not_asserted'=>true];

} finally {
    if(is_resource($db)){f158Close($db);}
    if(is_resource($service)){f158Detach($service);}
    $removed=f158Cleanup($owned);
}

f158Report('fb158-event-batch','Обновить две строки по сигналу, не считать callback за товар',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
До изменения — Cable / 3; Clamp / 0
После нового SELECT — Cable / 7; Clamp / 4
Изменено товаров отправителем — 2
POST_EVENT в одном commit — 3
Получен хотя бы один callback — да
Callback передал сами строки — нет
Порядок SQL задан явно — да

Распакуйте полный архив в отдельную папку и выполните example.php по README.txt. events-lab.php управляет двумя процессами, events-child.php содержит настоящий SQL и вызовы событий; lab.php создаёт и удаляет только собственную базу. result.html показывает конкретные строки, result.json — фактические наблюдения, metrics.json — проверки. Файлы events-process.json, events-listener.json и events-sender.json сохраняют локальное подтверждение исполнения. Повторно запускайте только отдельный учебный проект, не направляйте его на рабочую базу.

Открыть наглядный результат · Посмотреть наблюдения JSON.

Границы и разбор отказов

Firebird ведёт внутренние счётчики событий, однако этот PHP callback их не возвращает. Нельзя обещать ровно один вызов на commit или ровно один на строку: группировка доставки не даёт такого договора. Реализация interbase 3.0.1 при обработке буфера выбирает первое подходящее имя; это ещё одна причина не строить на ней журнал всех изменений. Для надёжной обработки заданий сохраняют задания или outbox в таблице в той же транзакции, а событие используют только для пробуждения; при запуске worker перечитывает сохранённые задания. Здесь показывается обновление текущего состояния двух товаров, а не реализация очереди.

Несколько изменённых строк ошибочно сопоставили с количеством уведомлений. Исходная задача на Stack Overflow. Разделяем выполнение строкового триггера, внутренний счётчик Firebird и число вызовов PHP callback. Не обещаем фиксированную группировку доставки.

Попробуйте изменить сценарий

Добавьте в учебный каталог третий товар, который не относится к текущему экрану. Должен ли запрос обновления списка загружать его?

Разбор решения

Нет: состав запроса определяется тем, что нужно показать. Оставьте выбранные поля, нужный фильтр и ORDER BY на стороне SQL. Событие не повод выполнить SELECT * по всей базе и затем сортировать большой массив в PHP. Для другого экрана сформируйте другой ограниченный запрос.

Отключить обработчик, сохранив уже подтверждённые изменения

Временный экран или завершившийся worker больше не должен получать уведомления. Сначала он увидит Cable с остатком 7, затем явно освободит свою подписку. Другой процесс после этого сохранит 11 и отправит событие. Новое значение останется в базе, но этот обработчик больше не вызовется. Освобождение ресурса подписки и отмена бизнес-транзакции — разные действия.

Как получится этот результат
  1. Подписка активна → commit Cable 7 → callback
  2. Основной код читает Cable 7
  3. free_event_handler(handle) → true; handle = null
  4. Отправитель сохраняет Cable 11 и подтверждает транзакцию
  5. Данная подписка не принимает новый callback
  6. Обычный SELECT всё равно видит 11; соединение закрывается после освобождения обработчика

Самый короткий шаг

if (ibase_free_event_handler($subscription) !== true) {
    throw new RuntimeException('Не удалось отменить подписку');
}
$subscription = null;

$subscription — ресурс, ранее возвращённый set_event_handler. Это не $db и не результат SELECT. После успеха переменную обнуляют, чтобы не передавать устаревший handle повторно. Подписка держит связь с соединением, поэтому порядок завершения явный: остановить приём событий, освободить результаты чтения, завершить свою транзакцию и закрыть соединение.

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

НаблюдениеРезультат
До отмены подпискиCable / остаток 7
ibase_free_event_handler завершился успешнода
Отправитель затем сохранилCable / остаток 11
Число callback после освобождения не вырослода
Отмена подписки откатила записьнет
Два учебных процесса остановленыда
Полный самостоятельный проект и архив
Отключить обработчик, сохранив уже подтверждённые измененияСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$owned=[];$removed=[];$db=$service=null;
try {
    $config=f158Config();$service=f158Service();$runtime=f158Runtime($service);
    require __DIR__.'/events-lab.php';
    $database=f158CreateDatabase($service,$owned,'ev_release');
    $events=f158EventScenario($database,'release');
    $listener=$events['listener'];$sender=$events['sender'];

    $before=$listener['committed_items'];$after=$listener['items'];
    $rows=[['До отмены подписки',$before[0]['TITLE'].' / остаток '.$before[0]['QTY']],['ibase_free_event_handler завершился успешно',$listener['freed']],
        ['Отправитель затем сохранил',$after[0]['TITLE'].' / остаток '.$after[0]['QTY']],
        ['Число callback после освобождения не выросло',$listener['callbacks_after_free']===$listener['callback_count']],
        ['Отмена подписки откатила запись',False],['Два учебных процесса остановлены',$events['all_children_stopped']]];
    $checks=['first_committed_row'=>$before===[['ID'=>1,'TITLE'=>'Cable','QTY'=>7],['ID'=>2,'TITLE'=>'Clamp','QTY'=>0]],
        'handler_freed'=>$listener['freed']===true,'at_least_one_before_free'=>$listener['callback_count']>=1,
        'no_callback_after_free'=>$listener['callbacks_after_free']===$listener['callback_count'],
        'data_persisted_after_free'=>$after===[['ID'=>1,'TITLE'=>'Cable','QTY'=>11],['ID'=>2,'TITLE'=>'Clamp','QTY'=>0]],
        'sender_wrote_twice'=>$sender['committed_business_updates']===2&&$sender['post_after_release']===true,
        'children_stopped'=>$events['all_children_stopped']===true,'two_children'=>$events['process_count']===2];
    $observations=['events'=>$events,'after_release_observation_window_ms'=>100,
        'release_is_not_rollback'=>true,'handler_released_before_connection'=>true];

} finally {
    if(is_resource($db)){f158Close($db);}
    if(is_resource($service)){f158Detach($service);}
    $removed=f158Cleanup($owned);
}

f158Report('fb158-event-release','Отключить обработчик, сохранив уже подтверждённые изменения',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
До отмены подписки — Cable / остаток 7
ibase_free_event_handler завершился успешно — да
Отправитель затем сохранил — Cable / остаток 11
Число callback после освобождения не выросло — да
Отмена подписки откатила запись — нет
Два учебных процесса остановлены — да

Распакуйте полный архив в отдельную папку и выполните example.php по README.txt. events-lab.php управляет двумя процессами, events-child.php содержит настоящий SQL и вызовы событий; lab.php создаёт и удаляет только собственную базу. result.html показывает конкретные строки, result.json — фактические наблюдения, metrics.json — проверки. Файлы events-process.json, events-listener.json и events-sender.json сохраняют локальное подтверждение исполнения. Повторно запускайте только отдельный учебный проект, не направляйте его на рабочую базу.

Открыть наглядный результат · Посмотреть наблюдения JSON.

Границы и разбор отказов

Нельзя проверять освобождение вторым вызовом free на том же ресурсе: контракт требует действующий handler, а реализация 3.0.1 небезопасна для некоторых устаревших ресурсов. Проверка is_resource сама по себе не восстанавливает владение уже освобождённым handle. Callback, вернувший false, прекращает дальнейшую регистрацию, однако свою ссылку на ресурс всё равно завершают контролируемо. Успех free не отменяет commit отправителя и не закрывает чужие соединения. В архиве обе роли действительно завершаются; только после этого родитель удаляет собственную учебную базу.

Попробуйте изменить сценарий

Что произойдёт, если обработчик больше не нужен, но программа лишь перестанет читать свой флаг и не освободит ресурс?

Разбор решения

Подписка продолжит принадлежать процессу и соединению до последующего освобождения. Это ненужные уведомления и неясное владение. Сохраните handle, завершайте его в finally, присваивайте null после успешного free и отдельно завершайте транзакцию. Для отмены неподтверждённых данных нужен rollback, а не free_event_handler.

Справочник ожидания, регистрации и освобождения событий

ФункцияКогда нужна
ibase_wait_eventДождаться именованного события и затем прочитать нужные сохранённые данные.
ibase_set_event_handlerЗарегистрировать callback для сигнала после commit и сохранить handle подписки.
ibase_free_event_handlerОсвободить свою подписку до закрытия её соединения.

Выбирать сохранённые данные источником истины

Практическая граница теперь видна: rollback оставляет Cable = 3; commit позволяет прочитать 7; снятие подписки не мешает другой транзакции сохранить 11. Для пакетного изменения мы читаем Cable 7 и Clamp 4, а не выводим количество callback как число изменённых строк. В JSON сохранено фактическое количество вызовов, которое может зависеть от группировки доставки.

Далее свяжите события с владением соединениями и очисткой состояния worker, параметрами ограниченного запроса и транзакцией записи. Если нельзя потерять работу при остановке, сначала проектируют сохраняемую таблицу заданий и восстановление чтения после перезапуска. Уведомление остаётся ускорением пробуждения, а база — источником фактов.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

ibase_wait_eventДождаться именованного события и затем прочитать нужные сохранённые данные.

Исторически PHP 5 и PHP 7 до 7.4; далее внешнее расширение. Проверяемый профиль interbase 3.0.1 / PHP 8.2. · interbase

ibase_wait_event(resource $connection, string $event_name, string ...$event_names): string|false

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

ibase_wait_event(string $event_name, string ...$event_names): string|false
ibase_wait_event(resource $connection, string $event_name, string ...$event_names): string|false
Часть вызоваЧто передать и получить
connectionНеобязательное действующее соединение с нужной базой. Без него используется последнее открытое соединение; в приложении с несколькими базами передавайте явно.
event_name и следующие именаОдно или несколько имён, отдельными строковыми аргументами. PHP Manual указывает максимум 15. Совпадение точное, включая регистр; строку массива целиком вместо аргументов не передают.
ВозвратИмя события, которое разбудило ожидание. Это не массив всех событий, не число изменённых строк и не JSON. При отказе реализация возвращает false и может выдавать предупреждение.
Срок ожиданияПараметра timeout нет. Дальнейший PHP-код данного процесса не выполняется, пока вызов не вернётся. Отказ по соединению не следует принимать за обычное пробуждение.

POST_EVENT вызывается на сервере в PSQL, например в триггере или EXECUTE BLOCK, и публикует событие после commit. Длина имени в Firebird ограничена 127 байтами, а не 127 произвольными многобайтными символами. Имя выбирают коротким и стабильным. Регистр важен: catalog_changed и CATALOG_CHANGED — разные подписки. Транзакция связывает запись и её сигнал.

Применение 1: разбудить обновление конкретного товара

$event = ibase_wait_event($db, 'CATALOG_CHANGED');
if ($event === false) {
    $errorCode = ibase_errcode();
    throw new RuntimeException('Ожидание каталога не выполнено');
}
// Начинаем свежее чтение нужного представления, а не SELECT * по всей базе.
$result = ibase_query($db, 'SELECT ID, TITLE, QTY FROM LAB_ITEMS WHERE ID = ?', 1);

Здесь соединение и таблица уже подготовлены; результат SELECT ещё нужно прочитать и освободить. Полный проект с двумя процессами показывает исходные 3 и полученные 11, сохраняет событие и доказывает, что предметный UPDATE был один. Это полезно для worker, которому нужно обновить свой кэш или сформировать представление после сохранения.

Применение 2: одно представление зависит от двух видов изменений

$name = ibase_wait_event($db, 'PRICE_CHANGED', 'STOCK_CHANGED');
if ($name === false) {
    throw new RuntimeException('Не удалось дождаться изменения');
}
// Любое из двух имён означает, что выбранная карточка могла устареть.
$result = ibase_query($db, 'SELECT ID, TITLE, QTY FROM LAB_ITEMS WHERE ID = ?', $itemId);

Это контекстный вариант для заранее выбранного ID. Нельзя трактовать возвращённое PRICE_CHANGED как доказательство, что STOCK_CHANGED за это время не происходило: функция вернула одно имя, а не полный журнал. Для представления можно перечитать весь небольшой нужный набор выбранных полей. Если нужны все задания, их сохраняют в таблице, читают при старте и после пробуждения; потеря сигнала тогда не равна потере самого задания.

События не сохраняются как очередь для будущего клиента. Если commit произошёл до регистрации или клиент был отключён, он не получает исторический журнал при подключении. Поэтому запуск worker начинается с чтения сохранённой работы, а событие служит подсказкой для следующего чтения. В API ожидания есть также гонка между внешней отметкой готовности и внутренней регистрацией; проект явно показывает её ограниченное решение, а не обещает встроенный тайм-аут.

Вызов не подходит для бесконечного ожидания внутри обычного HTTP-запроса страницы: он занимает worker и зависит от лимитов веб-сервера. Для отдельного процесса проектируют остановку и повторное подключение. Альтернатива в этой же главе — регистрация callback, но она также не превращает события в надёжную очередь. Ошибки считывают сразу через ibase_errcode и ibase_errmsg, до следующего успешного вызова драйвера.

Историческая пометка PHP Manual «PHP 5, PHP 7 < 7.4» относится к расширению прежних поставок PHP. Здесь используется отдельное interbase 3.0.1 с PHP 8.2.35 NTS x64 и Firebird 5.0.4. Наличие похожего имени в PDO или другом драйвере не означает наличие этих функций. Настройка окружения и все файлы находятся в README архива. Реальные события проверяются между двумя PHP-процессами; это не вывод заранее подготовленной строки.

ibase_set_event_handlerЗарегистрировать callback для сигнала после commit и сохранить handle подписки.

Исторически PHP 5 и PHP 7 до 7.4; далее внешнее расширение. Проверяемый профиль interbase 3.0.1 / PHP 8.2. · interbase

ibase_set_event_handler(resource $connection, callable $callback, string $event_name, string ...$event_names): resource|false

Если процессу нужно продолжать управлять своей работой, ему можно зарегистрировать функцию обратного вызова. ibase_set_event_handler сохраняет подписку, а при событии драйвер вызовет вашу функцию. Самый понятный обработчик лишь помечает данные как устаревшие; SQL и запись результата выполняет основной код после этого.

ibase_set_event_handler(callable $callback, string $event_name, string ...$event_names): resource|false
ibase_set_event_handler(resource $connection, callable $callback, string $event_name, string ...$event_names): resource|false
Параметр или результатПолный смысл
connectionНеобязательное подключение. Без него документация использует последнее открытое; в примерах оно всегда явно задано.
callbackВызываемая PHP-функция. Драйвер передаёт ей имя события и ресурс соединения, в таком порядке.
event_name, event_namesОдно или несколько отдельных строковых имён; базовая документация указывает до 15. Не массив строк как один аргумент.
Результат регистрацииРесурс обработчика, который нужно сохранить для free_event_handler. false означает неудачную регистрацию; используйте строгое сравнение и сразу снимайте диагностику.
Результат callbackСтрого false прекращает дальнейшую обработку этой подписки; другие возвращённые значения игнорируются. true удобно показывает намерение продолжать.
Чего в callback нетНет строки таблицы, ID операции, payload или аргумента со счётчиком. Число callback не заменяет число обработанных товаров.
Что входит в обработчик
  1. Отправитель: UPDATE + POST_EVENT, затем commit
  2. Firebird сообщает подписке: имя события
  3. Драйвер вызывает callback(имя, ресурс соединения)
  4. Callback отмечает необходимость работы и возвращается
  5. Основной код делает новый ограниченный SELECT; handle хранится до free

Применение 1: обновить складское представление после commit

$GLOBALS['refreshCatalog'] = false;
function onCatalogChange($eventName, $connection) {
    $GLOBALS['refreshCatalog'] = true;
    return true;
}
$handler = ibase_set_event_handler($db, 'onCatalogChange', 'CATALOG_CHANGED');
if ($handler === false) {
    throw new RuntimeException('Подписка не установлена');
}

Это регистрация, а не полный цикл worker. Основной код позднее проверит флаг, выберет свежие данные, обработает остановку и освободит handler. В самостоятельном сравнении rollback и commit есть два настоящих подключения, синхронизация готовности, конкретные 3 → 7 и завершение ресурсов. Преждевременный сигнал о ещё не принятом значении 99 не становится полезным результатом.

Применение 2: прекратить одноразовую подписку после сигнала

$GLOBALS['reportMayBeReady'] = false;
function reportReadyOnce($eventName, $connection) {
    $GLOBALS['reportMayBeReady'] = true;
    return false;
}
$handler = ibase_set_event_handler($db, 'reportReadyOnce', 'REPORT_CHANGED');
// Основной код проверяет сохранённый статус отчёта через SELECT.
// Когда handler больше не нужен, он отдельно освобождает ресурс в finally.

Одноразовость означает прекращение уведомлений, а не доказательство завершения любого отчёта. Имя не несёт ID: проверяйте нужный отчёт по сохранённому ключу. Возврат false отменяет дальнейшую обработку; сохранённую ссылку на ресурс в конце всё равно нужно освободить. Полный пример явного снятия подписки показывает другой управляемый способ: free после обработки первого подтверждённого состояния.

Ограничения именно реализации interbase 3.0.1

Исходник драйвера отличает необязательное соединение по типу первого аргумента. Поэтому короткая форма с замыканием или массивом callable без явного соединения не равнозначна общему обещанию callable в документации; передавайте $db первым. Также цикл регистрации данной версии сохраняет не более 13 имён при явном соединении (14 в короткой строковой форме), хотя PHP Manual сообщает 15. Это замечание по прочитанному исходнику, а не отдельный испытанный максимум. Наши работающие проекты подписаны на одно имя.

Обработка нативного буфера в этой версии выбирает первое имя с ненулевым внутренним счётчиком; PHP эти счётчики не получает. Не используйте её как перечислитель всех изменений. У Firebird нет гарантии нужной приложению группировки уведомлений: три POST могут прийти иначе, чем три вызова функции. Практика с двумя товарами и тремя POST_EVENT сохраняет фактическое число callback отдельно и проверяет нужные строки базы, а не удобное предполагаемое количество вызовов.

Callback вызывается нативным клиентом асинхронно. Держите его коротким, не выполняйте в нём длительный ввод-вывод, SQL на том же ресурсе и освобождение самого активного обработчика. В примере callback лишь сохраняет простые наблюдения; операции с ресурсами делает основной код. Реальный запуск подтверждает конкретный профиль, но не автоматически все сочетания PHP, NTS/TS, клиентских библиотек и режимов сервера. Отдельный listener и внешний предел времени делают эту лабораторию контролируемой.

Историческая пометка PHP Manual «PHP 5, PHP 7 < 7.4» относится к расширению прежних поставок PHP. Здесь используется отдельное interbase 3.0.1 с PHP 8.2.35 NTS x64 и Firebird 5.0.4. Наличие похожего имени в PDO или другом драйвере не означает наличие этих функций. Настройка окружения и все файлы находятся в README архива. Реальные события проверяются между двумя PHP-процессами; это не вывод заранее подготовленной строки.

ibase_free_event_handlerОсвободить свою подписку до закрытия её соединения.

Исторически PHP 5 и PHP 7 до 7.4; далее внешнее расширение. Проверяемый профиль interbase 3.0.1 / PHP 8.2. · interbase

ibase_free_event_handler(resource $event): bool

Когда экран закрывается или worker завершает задачу, ему нужно снять собственную подписку. ibase_free_event_handler освобождает handler и прекращает вызовы связанного с ним callback. Она не отменяет UPDATE, не выполняет rollback и не удаляет само имя события из базы для всех остальных клиентов.

ibase_free_event_handler(resource $event): bool
ЧастьЧто означает
eventДействующий ресурс, ранее возвращённый ibase_set_event_handler. Ресурс соединения, результат SELECT или строка имени события не подходят.
trueОбработчик успешно освобождён; связанная подписка больше не должна вызывать callback. Сохранённую переменную присваивают null.
false / предупреждениеОсвобождение не подтверждено. Сразу получите диагностику, не выдавайте успех заранее и не проверяйте повторным free устаревшего handle.
Порядок владенияСначала освободить обработчик, затем завершить свои результаты/транзакцию и закрыть соединение. Ссылка обработчика связана с его подключением.

Применение 1: подписка существует ровно на время работы

$handler = ibase_set_event_handler($db, 'onCatalogChange', 'CATALOG_CHANGED');
if ($handler === false) {
    throw new RuntimeException('Нет подписки');
}
try {
    // Здесь ограниченный цикл приложения, работа с флагом обновления и остановкой.
} finally {
    if (ibase_free_event_handler($handler) !== true) {
        throw new RuntimeException('Подписка не освобождена');
    }
    $handler = null;
}

callback onCatalogChange определён в предыдущей карточке. finally выполняет завершение и при выходе из блока, и при исключении; проект должен также сохранять причину исходной ошибки. Это контекстный шаблон владения, а полный запускаемый пример имеет настоящий отправитель: сначала он сохраняет 7, затем после free сохраняет 11. Результат показывает 11 в SQL и отсутствие прироста callback этой подписки.

Применение 2: переключить worker на другой канал

if (ibase_free_event_handler($oldHandler) !== true) {
    throw new RuntimeException('Старая подписка не остановлена');
}
$oldHandler = null;
$newHandler = ibase_set_event_handler($db, 'onCatalogChange', 'OTHER_CATALOG_CHANGED');
if ($newHandler === false) {
    throw new RuntimeException('Новая подписка не установлена');
}
// Сразу перечитать сохранённое состояние нового каталога через SELECT.

Это не атомарная замена каналов. Между освобождением и регистрацией существует промежуток; события не являются сохраняемой очередью. После смены подписки перечитывают текущее состояние или сохранённые задания, а не ожидают, что новое соединение получит прошлые сигналы. Для надёжного учёта операций нужна таблица состояния и правило повторного чтения.

В interbase 3.0.1 нельзя экспериментировать с повторным вызовом на уже освобождённом ресурсе: внутренний fetch такого ресурса не во всех ветках безопасно обработан. Обнуляйте свою переменную и не храните её копии как якобы действующий handler. Не закрывайте соединение раньше события и не передавайте handle между PHP-процессами. Каждый процесс в лаборатории освобождает только собственные ресурсы.

После возврата false из callback дальнейшая доставка прекращается, но владение сохранённым ресурсом всё равно нужно завершить. Если целью является отмена данных, переходите к ibase_rollback; если обычное закрытие соединения — к ibase_close. Для создания новой подписки служит ibase_set_event_handler, а для синхронного ожидания без сохранённого handler — ibase_wait_event.

Историческая пометка PHP Manual «PHP 5, PHP 7 < 7.4» относится к расширению прежних поставок PHP. Здесь используется отдельное interbase 3.0.1 с PHP 8.2.35 NTS x64 и Firebird 5.0.4. Наличие похожего имени в PDO или другом драйвере не означает наличие этих функций. Настройка окружения и все файлы находятся в README архива. Реальные события проверяются между двумя PHP-процессами; это не вывод заранее подготовленной строки.

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

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

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

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

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