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

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

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

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

Глава 182 · После первого запроса к MongoDB

События команд MongoDB. Как выяснить, сохранился ли заказ

Прослеживаем путь заказа от вызова PHP до результата MongoDB. Разбираем подтверждённую запись, конфликт номера, ноль совпадений, локальный отказ и безопасную сводку для оператора.

Как команда превращается в событие и результат для человека

Сотрудник поддержки видит «заказ принят», а покупатель сообщает, что отправил форму дважды. Сначала узнаем, что именно ответила MongoDB. Затем посмотрим, какой документ действительно лежит в коллекции. Для самых первых шагов пригодятся подключение PHP к MongoDB и первый поиск для экрана.

$manager->executeCommand($db, new MongoDB\Driver\Command(['ping'=>1]));

Эта строка обращается к MongoDB по уже созданному соединению или открывает его при необходимости. ping просит сервер ответить. Прикладной мониторинг (APM) позволяет узнать, какие команды отправил драйвер при работе приложения. Чтобы получить уведомления, создайте объект наблюдателя и передайте его методу addSubscriber до обращения к базе. Полный работающий вариант для одного ping приведён в следующем примере.

Кто что делает
  1. Приложение вызывает executeCommand или executeBulkWrite
  2. Драйвер вызывает commandStarted и даёт requestId
  3. Драйвер получает ответ и вызывает commandSucceeded либо commandFailed с тем же requestId
  4. Приложение разбирает результат операции и решает, что показать человеку
ВопросГде искать ответЧто увидим у повторного R-17
Пытались ли что-то отправить?CommandStartedEventНачался insert
Ответила ли команда?CommandSucceededEvent и CommandFailedEventПришёл нормальный ответ insert
Сработала ли запись?Ответ writeErrors и исключение BulkWriteExceptionЭлемент отклонён с кодом 11000
Что теперь показать?Прочитать нужные поля заказаПервоначальный статус «принят», количество 2

Для своего набора метрик используйте CommandSubscriber. Subscriber — его общий родительский интерфейс. Подписчика можно зарегистрировать глобально для процесса или только у используемого Manager. Глобальную подписку и её снятие разбираем в соседней главе. События живут во время работы подписчика. Истории прошлых запросов после регистрации в них нет.

Для начала сохраняйте только имя разрешённой команды и временную запись с requestId; при любом завершении удаляйте запись. Не сохраняйте весь объект события: в отправленном фильтре или полученном документе могут находиться личные данные. Если не найден интерфейс CommandSubscriber, проверьте extension_loaded("mongodb") и phpversion("mongodb") именно в том процессе PHP, где работает приложение. Как сопоставить PHP CLI и PHP веб-сервера.

Время из события относится к одной команде. Например, экран делает несколько find с фильтром по номеру. Разные номера можно объединить в заранее заданную категорию «поиск по _id», не записывая сами значения. При начале свяжите категорию с requestId. При успехе прибавьте duration к сумме микросекунд, увеличьте число успешных команд и очистите requestId. При отказе ведите отдельный счётчик и тоже очищайте requestId.

// В сводке только по успешным find одной заранее выбранной категории.
$meanMicros = $completed === 0 ? null : $sumMicros / $completed;

Если два успешных find условно заняли 120 и 280 мкс, сумма равна 400, среднее — 200 мкс. Это пример арифметики, фактические задержки каждого запуска иные. Долгое чтение курсора может выполнить дополнительные getMore. Практические разборы: границы времени команды и порции курсора и getMore. Официальный урок PHP по APM.

Узнать, ответил ли сервер на ping

Сервис открыл страницу диагностики. Самая маленькая проверка — попросить текущий сервер ответить на ping. Запрос не изменяет заказы. Подключённый к этому Manager наблюдатель фиксирует начало и ответ.

Путь выполнения
  1. Открыть окно наблюдения для ping
  2. Отправить ping и получить ok = 1
  3. Связать два уведомления по requestId
  4. Отдельно прочитать неизменившийся R-1

Короткий фрагмент

$cursor = $manager->executeCommand($db, new MongoDB\Driver\Command(['ping' => 1]));
echo $cursor->toArray()[0]->ok; // 1

Что увидит читатель

Что проверилиРезультат
Командаping
Ответ сервера1
Событиеsucceeded
Незавершённых пар0

Откуда получился такой результат

PHP вызывает executeCommand. Расширение само вызывает commandStarted у подписчика, затем commandSucceeded, когда пришёл успешный ответ. Число 1 в ok относится к этой команде. В обычном коде достаточно прочитать ответ; пара событий нужна тому, кто измеряет обращения.

Новый Manager создаётся лениво. В учебной программе стенд уже подготовлен до подключения наблюдателя. Быстрый ping в этот момент не измеряет построение всей веб-страницы, запись заказа или доступность сервера в будущем.

Полная самостоятельная программа и архив
Узнать, ответил ли сервер на pingСамостоятельный пример
<?php
declare(strict_types=1);
use MongoDB\Driver\Manager;
use MongoDB\Driver\Exception\{BulkWriteException,CommandException};
require __DIR__.'/ev135-helper.php';

[$rows,$checks,$runtime,$counts,$board] = ev135Lab(static function(Manager $manager,string $db,string $ns): array {
    $seed=ev135Put($manager,$ns,[['_id'=>'R-1','state'=>'принят']]);
    $trace=new Ev135Trace($db,['ping']);
    $reply=ev135Observe($manager,$trace,fn()=>ev135Command($manager,$db,['ping'=>1]));
    $record=$trace->single();
    $order=ev135Order($manager,$ns,'R-1');
    $rows=[['Команда',$record['command']],['Ответ сервера',(int)$reply[0]['ok']],['Событие',$record['terminal']],['Незавершённых пар',$trace->pendingCount()]];
    $checks=['one_fixture'=>$seed===1,'server_ok'=>$reply[0]['ok']==1,'one_start'=>$trace->started===1,
        'same_request'=>$record['linked'],'succeeded'=>$record['terminal']==='succeeded','duration_observed'=>$record['micros']>=0,
        'order_unchanged'=>$order['state']==='принят'];
    return [$rows,$checks,[]];
});
ev135Report('mongodb135-events-first-pair', 'Узнать, ответил ли сервер на ping', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts,$board);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
Команда — ping
Ответ сервера — 1
Событие — succeeded
Незавершённых пар — 0

Самостоятельный запуск PHP с реальной MongoDB. После чтения итогового состояния программа удаляет собственную коллекцию.

Открыть HTML с результатом запуска · Скачать результат в JSON

Попробуйте самостоятельно. Измените разрешённый тип команды наблюдателя с ping на find. Сам ping по-прежнему ответит 1, но проверка ровно одной наблюдаемой пары обнаружит неверный фильтр.

Приложение не видит интерфейс CommandSubscriber. В разделе показано, как проверить установленное расширение в используемом SAPI до подключения наблюдателя. Attempted to load interface CommandSubscriber на Stack Overflow.

Подтвердить первую запись заказа отдельно от сетевого события

Покупатель добавил две единицы товара и отправил заказ R-17. Чтобы показать ему подтверждение, приложению нужно понять, сколько документов появилось, и какой статус отобразить.

Путь выполнения
  1. Собрать один документ R-17
  2. Отправить его командой insert
  3. Получить число созданных документов
  4. Снова прочитать статус и количество по номеру

Короткий фрагмент

$bulk = new MongoDB\Driver\BulkWrite();
$bulk->insert(['_id' => 'R-17', 'state' => 'принят', 'quantity' => 2]);
$result = $manager->executeBulkWrite($ns, $bulk);
echo $result->getInsertedCount(); // 1

Что увидит читатель

Что проверилиРезультат
Командаinsert
Результат командыsucceeded
Создано заказов1
Статус в базепринят
Количество2

Откуда получился такой результат

insert() у объекта BulkWrite подготавливает операцию в памяти. executeBulkWrite() отправляет её. Событие команды, число в WriteResult и поля, которые позже прочитал экран заказа, описывают три разных наблюдения. Повторное чтение здесь сделано для обучения.

Количество команд не равно количеству товаров или документов. В один insert может попасть несколько документов; драйвер также может разделить большой пакет на несколько команд.

Полная самостоятельная программа и архив
Подтвердить первую запись заказа отдельно от сетевого событияСамостоятельный пример
<?php
declare(strict_types=1);
use MongoDB\Driver\Manager;
use MongoDB\Driver\Exception\{BulkWriteException,CommandException};
require __DIR__.'/ev135-helper.php';

[$rows,$checks,$runtime,$counts,$board] = ev135Lab(static function(Manager $manager,string $db,string $ns): array {
    $trace=new Ev135Trace($db,['insert']);
    $inserted=ev135Observe($manager,$trace,fn()=>ev135Put($manager,$ns,[['_id'=>'R-17','state'=>'принят','quantity'=>2]]));
    $record=$trace->single();
    $order=ev135Order($manager,$ns,'R-17');
    $rows=[['Команда',$record['command']],['Результат команды',$record['terminal']],['Создано заказов',$inserted],['Статус в базе',$order['state']],['Количество',$order['quantity']]];
    $checks=['insert_command'=>$record['command']==='insert','complete_pair'=>$record['linked'],'succeeded'=>$record['terminal']==='succeeded',
        'no_item_errors'=>$record['writeErrors']===0,'one_insert'=>$inserted===1,'stored_id'=>$order['_id']==='R-17','stored_quantity'=>$order['quantity']===2];
    return [$rows,$checks,[]];
});
ev135Report('mongodb135-events-accepted-order', 'Подтвердить первую запись заказа отдельно от сетевого события', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts,$board);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
Команда — insert
Результат команды — succeeded
Создано заказов — 1
Статус в базе — принят
Количество — 2

Самостоятельный запуск PHP с реальной MongoDB. После чтения итогового состояния программа удаляет собственную коллекцию.

Открыть HTML с результатом запуска · Скачать результат в JSON

Попробуйте самостоятельно. Увеличьте quantity с 2 до 3 и измените соответствующее ожидание в checks. Счётчик созданных документов останется равным 1.

Найти отказ повторной записи в успешном ответе insert

Форма повторно отправила R-17 с количеством 9. В журнале команда зелёная, но на экране заказа осталось 2. Найдём конкретное место, в котором MongoDB сообщила об отказе.

Путь выполнения
  1. Записать первоначальный заказ с количеством 2
  2. Попытаться вставить ещё один с тем же _id
  3. Прочитать writeErrors из ответа и код элемента из исключения
  4. Открыть R-17 по его номеру

Короткий фрагмент

// Внутри commandSucceeded для нашей команды insert.
$items = $event->getReply()->writeErrors ?? [];
echo count($items);       // 1
echo $items[0]->code;     // 11000 для повторного _id

Что увидит читатель

Что проверилиРезультат
Завершение insertsucceeded
Ошибок элементов1
Код ошибки11000
Остался статуспринят
Осталось единиц2

Откуда получился такой результат

Сервер обработал команду insert и прислал полноценный ответ. Внутри ответа writeErrors содержит отказ отдельного элемента. PHP-драйвер предоставляет его прикладному коду через BulkWriteException. Пара событий показывает доставку команды, а чтение заказа подтверждает, что повтор его не заменил.

Отчёт, который считает бизнес-ошибки только в commandFailed(), пропустит этот конфликт. В многодокументном пакете проверяйте результат каждого затронутого элемента и возможный частичный успех.

Полная самостоятельная программа и архив
Найти отказ повторной записи в успешном ответе insertСамостоятельный пример
<?php
declare(strict_types=1);
use MongoDB\Driver\Manager;
use MongoDB\Driver\Exception\{BulkWriteException,CommandException};
require __DIR__.'/ev135-helper.php';

[$rows,$checks,$runtime,$counts,$board] = ev135Lab(static function(Manager $manager,string $db,string $ns): array {
    $original=ev135Put($manager,$ns,[['_id'=>'R-17','quantity'=>2,'state'=>'принят']]);
    $trace=new Ev135Trace($db,['insert']);
    $itemCode=0;
    try {
        ev135Observe($manager,$trace,fn()=>ev135Put($manager,$ns,[['_id'=>'R-17','quantity'=>9,'state'=>'повтор']]));
    } catch (BulkWriteException $error) {
        $itemCode=$error->getWriteResult()->getWriteErrors()[0]->getCode();
    }
    $record=$trace->single();
    $order=ev135Order($manager,$ns,'R-17');
    $rows=[['Завершение insert',$record['terminal']],['Ошибок элементов',$record['writeErrors']],['Код ошибки',$itemCode],['Остался статус',$order['state']],['Осталось единиц',$order['quantity']]];
    $checks=['original_created'=>$original===1,'server_received_insert'=>$record['command']==='insert',
        'command_succeeded'=>$record['terminal']==='succeeded','one_write_error'=>$record['writeErrors']===1,
        'duplicate_item'=>$itemCode===11000 && $record['code']===$itemCode,'same_state'=>$order['state']==='принят','same_quantity'=>$order['quantity']===2];
    return [$rows,$checks,[]];
});
ev135Report('mongodb135-events-duplicate-reply', 'Найти отказ повторной записи в успешном ответе insert', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts,$board);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
Завершение insert — succeeded
Ошибок элементов — 1
Код ошибки — 11000
Остался статус — принят
Осталось единиц — 2

Самостоятельный запуск PHP с реальной MongoDB. После чтения итогового состояния программа удаляет собственную коллекцию.

Открыть HTML с результатом запуска · Скачать результат в JSON

Попробуйте самостоятельно. Измените номер второго документа на R-18. При таком вводе конфликт исчезнет, и вместо обработки дубликата нужно показать ещё один созданный заказ.

Разработчик хотел найти в истории неудачные записи с дублирующимся номером. Раздел показывает, где ошибка элемента действительно видна при живом вызове и какие сведения надо сохранить заранее. MongoDB view failed commands на Stack Overflow.

Различить неизвестную команду и ошибку элемента записи

В модуле отчётности ошиблись в имени команды. Покупательский заказ уже существует и не требует повторной отправки. Разработчику нужно выяснить, где произошёл отказ.

Путь выполнения
  1. Создать контрольный заказ R-61
  2. Послать заведомо неизвестную учебную команду
  3. Сопоставить код события и исключения
  4. Считать состояние R-61 независимо

Короткий фрагмент

public function commandFailed(MongoDB\Driver\Monitoring\CommandFailedEvent $event): void {
    $this->lastCode = $event->getError()->getCode();
}

Что увидит читатель

Что проверилиРезультат
ОтправленоphpmanMissingCommand
Событиеfailed
Код в commandFailed59
Код у вызывающего кода59
R-61 после отказаготов

Откуда получился такой результат

При неизвестном имени сервер возвращает ошибку команды. Драйвер вызывает commandFailed и передаёт вызывающему коду исключение. Обработчик освобождает ячейку requestId и на этом пути. В отчёт поступают только разрешённое имя и код 59.

Не помещайте полный текст getError()->getMessage() в публичный журнал. Транспортный сбой также может породить failed, и одного факта отказа недостаточно, чтобы назвать причину.

Полная самостоятельная программа и архив
Различить неизвестную команду и ошибку элемента записиСамостоятельный пример
<?php
declare(strict_types=1);
use MongoDB\Driver\Manager;
use MongoDB\Driver\Exception\{BulkWriteException,CommandException};
require __DIR__.'/ev135-helper.php';

[$rows,$checks,$runtime,$counts,$board] = ev135Lab(static function(Manager $manager,string $db,string $ns): array {
    $seed=ev135Put($manager,$ns,[['_id'=>'R-61','state'=>'готов']]);
    $name='phpmanMissingCommand';
    $trace=new Ev135Trace($db,[$name]);
    $exceptionCode=0;
    try {
        ev135Observe($manager,$trace,fn()=>ev135Command($manager,$db,[$name=>1]));
    } catch (CommandException $error) {$exceptionCode=$error->getCode();}
    $record=$trace->single();
    $order=ev135Order($manager,$ns,'R-61');
    $rows=[['Отправлено',$record['command']],['Событие',$record['terminal']],['Код в commandFailed',$record['code']],['Код у вызывающего кода',$exceptionCode],['R-61 после отказа',$order['state']]];
    $checks=['seed'=>$seed===1,'named_command'=>$record['command']===$name,'failed'=>$record['terminal']==='failed',
        'same_request'=>$record['linked'],'observed_code'=>$record['code']===59,'matching_exception'=>$exceptionCode===$record['code'],
        'slot_released'=>$trace->pendingCount()===0,'data_intact'=>$order['state']==='готов'];
    return [$rows,$checks,[]];
});
ev135Report('mongodb135-events-server-failed', 'Различить неизвестную команду и ошибку элемента записи', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts,$board);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
Отправлено — phpmanMissingCommand
Событие — failed
Код в commandFailed — 59
Код у вызывающего кода — 59
R-61 после отказа — готов

Самостоятельный запуск PHP с реальной MongoDB. После чтения итогового состояния программа удаляет собственную коллекцию.

Открыть HTML с результатом запуска · Скачать результат в JSON

Попробуйте самостоятельно. Откройте предыдущий пример и сравните 59 и 11000. В каком случае сработал commandFailed и в каком ошибка относится к одному элементу?

Понять, почему успешный update оставил статус прежним

Оператор открыл старую вкладку и нажал «Отменить». За это время заказ отправили. Вместе с номером проверяем старый статус, чтобы не отменить отправленную посылку.

Путь выполнения
  1. Записать текущий статус «отправлен»
  2. Попробовать отменить только заказ в статусе «новый»
  3. Прочитать matched и modified
  4. Обновить экран из базы

Короткий фрагмент

$bulk->update(['_id'=>'R-8', 'state'=>'новый'], ['$set'=>['state'=>'отменён']]);
$result = $manager->executeBulkWrite($ns, $bulk);
echo $result->getMatchedCount(); // 0: состояние уже другое

Что увидит читатель

Что проверилиРезультат
Завершение updatesucceeded
Подошло заказов0
Изменено заказов0
Текущий статусотправлен

Откуда получился такой результат

При upsert=false фильтр с нулём совпадений штатно завершает update. Для решения оператора существенен matchedCount. Если matched=1 и modified=0, документ подошёл, но новое значение уже совпадало со старым. Здесь matched=0, и читать текущую карточку полезнее, чем показывать сообщение о сетевом сбое.

Нулевое число совпадений само по себе не различает отсутствующий номер и изменившийся статус. Повторное чтение с фильтром только по _id различает эти два случая в текущем состоянии базы.

Полная самостоятельная программа и архив
Понять, почему успешный update оставил статус прежнимСамостоятельный пример
<?php
declare(strict_types=1);
use MongoDB\Driver\Manager;
use MongoDB\Driver\Exception\{BulkWriteException,CommandException};
require __DIR__.'/ev135-helper.php';

[$rows,$checks,$runtime,$counts,$board] = ev135Lab(static function(Manager $manager,string $db,string $ns): array {
    $seed=ev135Put($manager,$ns,[['_id'=>'R-8','state'=>'отправлен']]);
    $trace=new Ev135Trace($db,['update']);
    $result=ev135Observe($manager,$trace,fn()=>ev135Update($manager,$ns,['_id'=>'R-8','state'=>'новый'],['state'=>'отменён']));
    $record=$trace->single();
    $order=ev135Order($manager,$ns,'R-8');
    $matched=$result->getMatchedCount();
    $modified=$result->getModifiedCount();
    $rows=[['Завершение update',$record['terminal']],['Подошло заказов',$matched],['Изменено заказов',$modified],['Текущий статус',$order['state']]];
    $checks=['seed'=>$seed===1,'command_name'=>$record['command']==='update','command_succeeded'=>$record['terminal']==='succeeded',
        'no_write_error'=>$record['writeErrors']===0,'stale_filter'=>$matched===0,'no_change'=>$modified===0,'current_status'=>$order['state']==='отправлен'];
    return [$rows,$checks,[]];
});
ev135Report('mongodb135-events-no-match', 'Понять, почему успешный update оставил статус прежним', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts,$board);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
Завершение update — succeeded
Подошло заказов — 0
Изменено заказов — 0
Текущий статус — отправлен

Самостоятельный запуск PHP с реальной MongoDB. После чтения итогового состояния программа удаляет собственную коллекцию.

Открыть HTML с результатом запуска · Скачать результат в JSON

Попробуйте самостоятельно. Смените начальный статус на «новый». Проверьте, что matched и modified стали равны 1, а в карточке показано «отменён».

Увидеть, дошло ли количество из формы до базы

Сервис принимает целое количество от 1 до 20. Отрицательное значение можно отклонить до обращения в MongoDB и подсказать пользователю, что исправить. Отправим два значения в одну и ту же функцию.

Путь выполнения
  1. Передать −3
  2. Отказать до executeBulkWrite и прочитать прежнее количество 2
  3. Передать 3 через тот же код
  4. Увидеть один update и заново прочитать 3

Короткий фрагмент

if (!is_int($quantity) || $quantity < 1 || $quantity > 20) {
    return null;
}
// Только после этой проверки подготавливаем update.

Что увидит читатель

Что проверилиРезультат
Команд для −30
Количество после отказа2
Команд для 31
Изменено заказов1
Количество после записи3

Откуда получился такой результат

Возврат null обозначает в этой функции отказ проверки. Никакая команда update не начиналась. Положительный контроль показывает, что при допустимом вводе тот же наблюдатель и тот же тип фильтра действительно видят запись.

Ноль событий вне контролируемого примера не доказывает ошибку формы: подписчик мог быть снят или подключён к другому Manager. Обычная HTML-форма присылает строки; сначала отдельно проверьте и преобразуйте ввод, только затем применяйте этот контракт целого количества.

Полная самостоятельная программа и архив
Увидеть, дошло ли количество из формы до базыСамостоятельный пример
<?php
declare(strict_types=1);
use MongoDB\Driver\Manager;
use MongoDB\Driver\Exception\{BulkWriteException,CommandException};
require __DIR__.'/ev135-helper.php';

[$rows,$checks,$runtime,$counts,$board] = ev135Lab(static function(Manager $manager,string $db,string $ns): array {
    $seed=ev135Put($manager,$ns,[['_id'=>'R-4','state'=>'новый','quantity'=>2]]);
    $saveQuantity=static function(mixed $raw) use ($manager,$ns): ?int {
        if (!is_int($raw) || $raw < 1 || $raw > 20) {return null;}
        return ev135Update($manager,$ns,['_id'=>'R-4'],['quantity'=>$raw])->getModifiedCount();
    };
    $first=new Ev135Trace($db,['update']);
    $bad=ev135Observe($manager,$first,fn()=>$saveQuantity(-3));
    $before=ev135Order($manager,$ns,'R-4');
    $second=new Ev135Trace($db,['update']);
    $good=ev135Observe($manager,$second,fn()=>$saveQuantity(3));
    $record=$second->single();
    $after=ev135Order($manager,$ns,'R-4');
    $rows=[['Команд для −3',$first->started],['Количество после отказа',$before['quantity']],['Команд для 3',$second->started],['Изменено заказов',$good],['Количество после записи',$after['quantity']]];
    $checks=['seed'=>$seed===1,'rejected_input'=>$bad===null,'no_command'=>$first->started===0 && $first->records===[],
        'unchanged'=>$before['quantity']===2,'valid_command'=>$record['command']==='update' && $record['terminal']==='succeeded',
        'changed_one'=>$good===1,'quantity_now'=>$after['quantity']===3];
    return [$rows,$checks,[]];
});
ev135Report('mongodb135-events-local-validation', 'Увидеть, дошло ли количество из формы до базы', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts,$board);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
Команд для −3 — 0
Количество после отказа — 2
Команд для 3 — 1
Изменено заказов — 1
Количество после записи — 3

Самостоятельный запуск PHP с реальной MongoDB. После чтения итогового состояния программа удаляет собственную коллекцию.

Открыть HTML с результатом запуска · Скачать результат в JSON

Попробуйте самостоятельно. Передайте 21 и 20. Первое значение сохраняет старое количество, второе создаёт один update. Проверьте обе ветви в интерфейсе.

Ограничить наблюдение одним действием в воркере

Долгоживущий PHP-воркер обрабатывает несколько задач подряд. Если подписчик с прошлой задачи остаётся активным, чужие чтения попадут в предыдущий отчёт. Здесь задаём и границу времени, и лимит сохраняемых пар.

Путь выполнения
  1. Включить подписчика для find с лимитом одной пары
  2. Прочитать два разных заказа внутри окна
  3. Снять подписчика в finally
  4. Выполнить третье чтение и увидеть прежний счётчик

Короткий фрагмент

$manager->addSubscriber($observer);
try {
    $firstOrder = ev135Order($manager, $ns, 'L-1');
} finally {
    $manager->removeSubscriber($observer);
}

Что увидит читатель

Что проверилиРезультат
Чтений внутри окна2
Сохранено пар1
Пропущено из-за лимита1
Новых начал после отписки0
Прочитали после отпискиL-1

Откуда получился такой результат

started считает все подходящие начала в окне. Чтобы расход памяти оставался ограниченным, после лимита мы прибавляем omitted и больше не записываем requestId. Третье чтение действительно возвращает L-1. Счётчик подписчика к этому моменту уже не меняется.

Удалять нужно тот же экземпляр подписчика у того же Manager. Если снимете новый объект, первый продолжит получать события. Глобальная подписка имеет другую область действия и разбирается в соседней главе.

Полная самостоятельная программа и архив
Ограничить наблюдение одним действием в воркереСамостоятельный пример
<?php
declare(strict_types=1);
use MongoDB\Driver\Manager;
use MongoDB\Driver\Exception\{BulkWriteException,CommandException};
require __DIR__.'/ev135-helper.php';

[$rows,$checks,$runtime,$counts,$board] = ev135Lab(static function(Manager $manager,string $db,string $ns): array {
    $seed=ev135Put($manager,$ns,[['_id'=>'L-1','state'=>'готов'],['_id'=>'L-2','state'=>'готов']]);
    $trace=new Ev135Trace($db,['find'],1);
    [$a,$b]=ev135Observe($manager,$trace,fn()=>[ev135Order($manager,$ns,'L-1'),ev135Order($manager,$ns,'L-2')]);
    $seenBefore=$trace->started;
    $c=ev135Order($manager,$ns,'L-1');
    $record=$trace->records[0];
    $rows=[['Чтений внутри окна',$trace->started],['Сохранено пар',count($trace->records)],['Пропущено из-за лимита',$trace->omitted],
        ['Новых начал после отписки',$trace->started-$seenBefore],['Прочитали после отписки',$c['_id']]];
    $checks=['two_fixtures'=>$seed===2,'both_inside'=>$a['_id']==='L-1' && $b['_id']==='L-2',
        'two_starts'=>$seenBefore===2,'bounded'=>count($trace->records)===1 && $trace->omitted===1,
        'paired'=>$record['linked'] && $record['terminal']==='succeeded','pending_clear'=>$trace->pendingCount()===0,
        'detached'=>$trace->started===$seenBefore && $c['_id']==='L-1'];
    return [$rows,$checks,[]];
});
ev135Report('mongodb135-events-unsubscribe', 'Ограничить наблюдение одним действием в воркере', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts,$board);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
Чтений внутри окна — 2
Сохранено пар — 1
Пропущено из-за лимита — 1
Новых начал после отписки — 0
Прочитали после отписки — L-1

Самостоятельный запуск PHP с реальной MongoDB. После чтения итогового состояния программа удаляет собственную коллекцию.

Открыть HTML с результатом запуска · Скачать результат в JSON

Попробуйте самостоятельно. Установите лимит 2. Оба чтения в окне должны попасть в отчёт. За пределами окна число новых начал по-прежнему равно нулю.

Прочитать отчёт по пяти действиям с одним заказом

Сотрудник поддержки разбирает историю R-17. На одной доске нужны и действие человека, и отправленная команда, и фактически сохранённые данные. Четыре обращения к базе в пяти окнах не означают, что четыре заказа были созданы.

Путь выполнения
  1. Принять R-17 и сверить состояние
  2. Повторить отправку и увидеть код 11000
  3. Испытать фильтр состояния и заведомо неверное количество
  4. Сопоставить отказ команды 59 с последним чтением R-17

Короткий фрагмент

$row = [
    'action' => $action,                   // название кнопки
    'command' => $command,                 // завершение наблюдаемой команды
    'outcome' => $outcome,                 // число записей или причина отказа
    'stored' => $order['state'],           // прочитано отдельно по _id
];

Что увидит читатель

Что проверилиРезультат
Действий на экране5
Начатых команд в пяти окнах4
Создано заказов1
Последнее состояниепринят
Количество в базе2

Откуда получился такой результат

У каждой строки собственное короткое окно наблюдения. После его закрытия программа отдельно читает именно R-17, чтобы служебная проверка не попала в счётчик действия. Дубликат требует сверки номера, устаревший фильтр — обновления карточки, неверное количество — исправления ввода. При неизвестной команде исправляют код вызывающего модуля.

В реальном сервисе повторное чтение может увидеть действие другого клиента между записью и чтением. Эта доска описывает последовательный изолированный запуск. В рабочем журнале оставляйте разрешённые категории и числа, а доступ к карточке заказа контролируйте отдельно.

ДействиеСобытие командыРезультат действияЧто прочитано из базы
Принять заказinsert → succeededСоздано документов: 1принят · 2 шт.
Повторить отправкуinsert → succeededОшибок записи: 1, код 11000принят · 2 шт.
Отметить доставленнымupdate → succeededПодошло документов: 0принят · 2 шт.
Ввести −3Не отправленаКоличество вне диапазонапринят · 2 шт.
Вызвать неизвестную командуphpmanMissingCommand → failedКод отказа: 59принят · 2 шт.

В том же архиве в папке web/ есть отдельная локальная веб-форма поиска. Прочитайте web-README.md в корне архива, затем откройте форму в браузере и введите A-101 (товар «Лампа <мини>»), A-999 (нет такого товара), затем «лампа» (неверный формат). Сравните количество команд find с числом выведенных товаров. Так можно увидеть разницу между пустой выборкой и отказом до обращения к базе.

Полная самостоятельная программа и архив
Прочитать отчёт по пяти действиям с одним заказомСамостоятельный пример
<?php
declare(strict_types=1);
use MongoDB\Driver\Manager;
use MongoDB\Driver\Exception\{BulkWriteException,CommandException};
require __DIR__.'/ev135-helper.php';

[$rows,$checks,$runtime,$counts,$board] = ev135Lab(static function(Manager $manager,string $db,string $ns): array {
    $board=[];
    $append=static function(string $action,Ev135Trace $trace,string $outcome) use (&$board,$manager,$ns): void {
        $signal=$trace->started===0 ? null : $trace->single();
        $command=$signal===null ? 'Не отправлена' : $signal['command'].' → '.$signal['terminal'];
        $order=ev135Order($manager,$ns,'R-17');
        $stored=$order===null ? 'Заказ отсутствует' : $order['state'].' · '.$order['quantity'].' шт.';
        $board[]=compact('action','command','outcome','stored');
    };
    $doc=['_id'=>'R-17','state'=>'принят','quantity'=>2];
    $one=new Ev135Trace($db,['insert']);
    $created=ev135Observe($manager,$one,fn()=>ev135Put($manager,$ns,[$doc]));
    $append('Принять заказ',$one,'Создано документов: '.$created);

    $two=new Ev135Trace($db,['insert']);
    $duplicateCode=0;
    try {ev135Observe($manager,$two,fn()=>ev135Put($manager,$ns,[array_replace($doc,['quantity'=>7])]));}
    catch (BulkWriteException $error) {$duplicateCode=$error->getWriteResult()->getWriteErrors()[0]->getCode();}
    $reply=$two->single();
    $append('Повторить отправку',$two,'Ошибок записи: '.$reply['writeErrors'].', код '.$duplicateCode);

    $three=new Ev135Trace($db,['update']);
    $matched=ev135Observe($manager,$three,fn()=>ev135Update($manager,$ns,['_id'=>'R-17','state'=>'отправлен'],['state'=>'доставлен']))->getMatchedCount();
    $append('Отметить доставленным',$three,'Подошло документов: '.$matched);

    $four=new Ev135Trace($db,['update']);
    $quantity=-3;
    $guard=ev135Observe($manager,$four,static function() use ($quantity,$manager,$ns): string {
        if ($quantity<1 || $quantity>20) {return 'Количество вне диапазона';}
        $changed=ev135Update($manager,$ns,['_id'=>'R-17'],['quantity'=>$quantity])->getModifiedCount();
        return 'Изменено документов: '.$changed;
    });
    $append('Ввести −3',$four,$guard);

    $five=new Ev135Trace($db,['phpmanMissingCommand']);
    $serverCode=0;
    try {ev135Observe($manager,$five,fn()=>ev135Command($manager,$db,['phpmanMissingCommand'=>1]));}
    catch (CommandException $error) {$serverCode=$error->getCode();}
    $append('Вызвать неизвестную команду',$five,'Код отказа: '.$serverCode);

    $final=ev135Order($manager,$ns,'R-17');
    $rows=[['Действий на экране',count($board)],['Начатых команд в пяти окнах',$one->started+$two->started+$three->started+$four->started+$five->started],
        ['Создано заказов',$created],['Последнее состояние',$final['state']],['Количество в базе',$final['quantity']]];
    $checks=['one_persisted'=>$created===1,'duplicate'=>$duplicateCode===11000 && $reply['writeErrors']===1,
        'stale_filter'=>$matched===0,'local_refusal'=>$four->started===0 && $guard==='Количество вне диапазона',
        'wire_refusal'=>$five->single()['code']===$serverCode && $serverCode===59,'five_readbacks'=>count($board)===5 && array_unique(array_column($board,'stored'))===['принят · 2 шт.'],
        'final_quantity'=>$final['quantity']===2,'final_state'=>$final['state']==='принят'];
    return [$rows,$checks,$board];
});
ev135Report('mongodb135-events-operator-board', 'Прочитать отчёт по пяти действиям с одним заказом', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts,$board);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
Действий на экране — 5
Начатых команд в пяти окнах — 4
Создано заказов — 1
Последнее состояние — принят
Количество в базе — 2

Самостоятельный запуск PHP с реальной MongoDB. После чтения итогового состояния программа удаляет собственную коллекцию.

Открыть HTML с результатом запуска · Скачать результат в JSON

Попробуйте самостоятельно. Откройте HTML-результат. Найдите две строки с одинаковым insert → succeeded и разными исходами. Какая колонка позволяет отличить впервые созданный заказ от повторной отправки?

Разработчику нужно увидеть команды, которые реально уходят из приложения. Учебная доска сопоставляет известное действие интерфейса с разрешённым именем команды и отдельно проверяет данные. Logging MongoDB queries for debugging на Stack Overflow.

Куда идти по результату наблюдения

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

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

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

MongoDB\Driver\Monitoring\SubscriberТип, который принимает драйвер при регистрации наблюдателя.

ext-mongodb 1.3.0 и новее · mongodb

interface MongoDB\Driver\Monitoring\Subscriber

Кто вызывает и какое значение доступно

Это общий интерфейс без собственных методов. Принимающий Subscriber метод может зарегистрировать разные виды наблюдателей. В прикладном классе реализуют подходящий дочерний контракт. Для команд это CommandSubscriber, для сообщений библиотеки — LogSubscriber, для событий обнаружения серверов — SDAMSubscriber.

$manager->addSubscriber($observer);
try {
    $reply = $manager->executeCommand($db, new MongoDB\Driver\Command(['ping'=>1]));
} finally {
    $manager->removeSubscriber($observer);
}

Передайте готовый объект при регистрации, сохраните его и передайте этот же экземпляр при снятии. Подписка через Manager относится к этому менеджеру, а глобальная регистрация действует шире. Выбор влияет на то, попадут ли в статистику соседние задачи.

Для команд откройте CommandSubscriber, для сообщений и наблюдения за узлами — журнал и описание топологии.

Проверить на собственном запуске

Снять один экземпляр после чтения заказов.

Проверить регистрацию на одном ping.

MongoDB\Driver\Monitoring\CommandSubscriberПолучать уведомления до команды, после ответа и при отказе.

ext-mongodb 1.3.0 и новее · mongodb

interface MongoDB\Driver\Monitoring\CommandSubscriber extends MongoDB\Driver\Monitoring\Subscriber

Кто вызывает и какое значение доступно

Класс приложения реализует три публичных метода. Каждый получает один объект своего типа и возвращает void. Когда вызывается обычная операция Manager, драйвер сам вызывает подходящий метод. Обратный вызов может изменить состояние вашего наблюдателя, например увеличить счётчик.

use MongoDB\Driver\Monitoring\{CommandSubscriber, CommandStartedEvent, CommandSucceededEvent, CommandFailedEvent};
final class CommandCounts implements CommandSubscriber {
    public int $started=0, $succeeded=0, $failed=0;
    public function commandStarted(CommandStartedEvent $e): void {$this->started++;}
    public function commandSucceeded(CommandSucceededEvent $e): void {$this->succeeded++;}
    public function commandFailed(CommandFailedEvent $e): void {$this->failed++;}
}

Не отправляйте из callback новый запрос через наблюдаемый Manager: этот запрос сам может вызвать подписчика. Записывайте минимум сведений в памяти, а результирующие значения передавайте потребителю после окна наблюдения. На 2.x типы void обязательны; в 1.15 появились переходные возвращаемые типы.

Контракты отдельных методов: commandStarted(), commandSucceeded(), commandFailed(). События: CommandStartedEvent, CommandSucceededEvent, CommandFailedEvent.

Проверить на собственном запуске

Увидеть вызов на одной команде.

Сравнить допустимое действие и ранний return.

MongoDB\Driver\Monitoring\CommandStartedEventОпределить, какая команда началась, и запомнить её идентификатор.

ext-mongodb 1.3.0 и новее · mongodb

final class MongoDB\Driver\Monitoring\CommandStartedEvent

Кто вызывает и какое значение доступно

Драйвер создаёт объект и передаёт его в commandStarted. Из имени команды можно узнать, начался ли find, insert или update. Сохраните requestId до завершения. operationId группирует связанные команды операции. Это разные уровни, ни один из них не является номером заказа.

// Внутри commandStarted.
if ($event->getCommandName() === 'insert') {
    $this->pending[$event->getRequestId()] = 'insert';
}

command содержит объект команды. Даже фильтр по одному полю может содержать телефон или адрес. Используйте список разрешённых имён и полей, а для статистики сохраняйте только необходимое. Начало запроса само по себе не доказывает ни ответ сервера, ни изменение данных.

В ext-mongodb с 2.3.0 у событий есть публичные свойства только для чтения. Общие поля — host и port, commandName и databaseName, строковые operationId и requestId, необязательные serviceId (ObjectId для балансировщика) и serverConnectionId (целое число, если известно). У события начала добавляется command (объект).

Открыть подробные карточки методов для этой версии драйвера: getHost(), getPort(), getCommandName(), getDatabaseName(), getOperationId(), getRequestId(), getServiceId(), getServerConnectionId(), getCommand(). Исторический getServer() удалён в 2.0. Для адреса узла используйте host и port.

Проверить на собственном запуске

Увидеть отправку insert с одним документом.

Ограничить число начатых find в памяти.

MongoDB\Driver\Monitoring\CommandSucceededEventСопоставить ответ с началом команды и проверить результат записи.

ext-mongodb 1.3.0 и новее · mongodb

final class MongoDB\Driver\Monitoring\CommandSucceededEvent

Кто вызывает и какое значение доступно

Объект передаётся в commandSucceeded. reply содержит документ ответа, duration — длительность команды в микросекундах, включая отправку и получение ответа. requestId позволяет удалить соответствующую запись pending. Ошибка отдельного документа в writeErrors совместима с успешным завершением самой команды.

// Внутри commandSucceeded для отслеживаемого insert.
$id = $event->getRequestId();
$errors = $event->getReply()->writeErrors ?? [];
$count = count($errors);
unset($this->pending[$id]);

Для update прочитайте matched и modified из WriteResult, для пакетной записи проверьте также отдельные ошибки и подтверждение записи. На код приложения, формирование HTML и показ страницы время duration не распространяется.

В ext-mongodb с 2.3.0 у событий есть публичные свойства только для чтения. Общие поля — host и port, commandName и databaseName, строковые operationId и requestId, необязательные serviceId (ObjectId для балансировщика) и serverConnectionId (целое число, если известно). У события успеха добавляются duration (int) и reply (объект).

Открыть подробные карточки методов для этой версии драйвера: getHost(), getPort(), getCommandName(), getDatabaseName(), getOperationId(), getRequestId(), getServiceId(), getServerConnectionId(), getDurationMicros(), getReply(). Исторический getServer() удалён в 2.0. Для адреса узла используйте host и port.

Проверить на собственном запуске

Увидеть succeeded и 11000 у одного insert.

Увидеть succeeded, хотя фильтр не подошёл.

MongoDB\Driver\Monitoring\CommandFailedEventПолучить ошибку самой команды и завершить начатое наблюдение.

ext-mongodb 1.3.0 и новее · mongodb

final class MongoDB\Driver\Monitoring\CommandFailedEvent

Кто вызывает и какое значение доступно

Драйвер передаёт событие в commandFailed. error даёт исключение, reply — документ ответа об отказе, если сервер предоставил диагностические поля. В getError()->getCode() у неизвестной команды в примере находится 59. Продолжительность измеряется в микросекундах.

// Внутри commandFailed.
$id = $event->getRequestId();
$this->lastFailureCode = $event->getError()->getCode();
unset($this->pending[$id]);

При потере связи не следует рассчитывать на полезный документ серверного ответа. Нельзя по событию failed решать, безопасен ли повтор записи: это зависит от операции и её подтверждения. Необработанные сообщения могут включать лишние сведения. Удаляйте pending и на этом выходе.

В ext-mongodb с 2.3.0 у событий есть публичные свойства только для чтения. Общие поля — host и port, commandName и databaseName, строковые operationId и requestId, необязательные serviceId (ObjectId для балансировщика) и serverConnectionId (целое число, если известно). У события отказа добавляются duration (int), error (исключение) и reply (объект).

Открыть подробные карточки методов для этой версии драйвера: getHost(), getPort(), getCommandName(), getDatabaseName(), getOperationId(), getRequestId(), getServiceId(), getServerConnectionId(), getDurationMicros(), getError(), getReply(). Исторический getServer() удалён в 2.0. Для адреса узла используйте host и port.

Проверить на собственном запуске

Показать отказ команды 59 и прежний заказ.

Различить отказ сервера, конфликт записи и ошибку ввода.

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

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

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

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

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