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

Дерево руководства
Глава 183 · После первого поиска документа
MongoDB: журнал расширения, состояние узлов и ответы оператору
Как включить диагностику одного интервала, отличить состояние узла от судьбы заявки и объяснить пустой результат, повторный номер или отказ соединения.
Выбрать между событиями команд, журналом и наблюдением за узлами
Сотрудник вводит O-509 и видит пустой экран. Нужно сообщить ему одно из двух: «Сверьте номер» или «Сейчас база недоступна». Если чтение дошло до MongoDB и вернуло ноль документов, верен первый ответ. Если клиент вообще не выбрал сервер, сказать, есть ли заказ, пока нельзя. Посмотреть ответ для O-509 и сравнить с недоступной точкой.
- В форме введено O-509
- PHP просит MongoDB найти документ по _id
- Драйвер выбрал узел и получил ответ без документов
- Экран сообщает «Сверьте номер»
Мониторинг позволяет подписаться на происходящее внутри драйвера. Подписчик — ваш объект PHP. Вы подключаете его заранее, а драйвер вызывает подходящий метод объекта при событии. Наблюдение полезно, когда нужно узнать, каким путём программа пришла к ответу.
| Что требуется узнать | Какое семейство выбрать | Что оно даст |
|---|---|---|
| Как завершилась команда? | CommandSubscriber | commandStarted отмечает начало, commandSucceeded или commandFailed — соответствующее завершение. Бизнес-ошибки отдельных строк иногда есть и в успешном ответе команды. |
| Какой узел открыт и как меняется представление клиента? | SDAMSubscriber | Открытие, изменение, закрытие узла и всей топологии. Начало, успех и отказ проверки связи (heartbeat). |
| Сообщало ли расширение что-либо за этот интервал? | LogSubscriber | Уровень, имя компонента и текст. Можно накапливать только числа. |
Как устроены события команд · Общий интерфейс Subscriber · Как читать снимок одного сервера · Как получить снимок топологии · Официальный раздел Monitoring.
Включить и снять подписку на время одной задачи
Чтобы временно включить диагностику, заведите переменную $logger и сохраните в ней объект. Сначала добавьте его в реестр расширения, затем выполните чтение и снимите именно этот экземпляр. В блок finally PHP входит и после ошибки внутри try.
use MongoDB\Driver\Monitoring as Mon;
$logger = new SafeTally();
Mon\addSubscriber($logger);
try {
$ticket = readTicket(freshObserverManager($uri), $namespace, 'O-101');
} finally {
Mon\removeSubscriber($logger);
}
echo $ticket[0]['state']; // готоваuse … as Mon даёт короткое имя пространству имён, в котором объявлены обе глобальные функции. Обе принимают MongoDB\Driver\Monitoring\Subscriber и возвращают void. Двойное добавление того же экземпляра не дублирует регистрацию, удаление отсутствующей регистрации ничего не меняет. При ошибке разбора аргументов документация указывает InvalidArgumentException. Обе глобальные функции доступны с ext-mongodb 1.3.0.
Регистрация у одного Manager подходит CommandSubscriber и SDAMSubscriber (метод доступен с 1.10.0). Если один такой объект зарегистрирован и глобально, и у этого Manager, каждое его событие доставляется объекту один раз. LogSubscriber регистрируется глобально; попытка передать его Manager::addSubscriber() завершается InvalidArgumentException.
Глобальное добавление · Глобальное снятие · Подписка у одного Manager · Обе функции в официальном справочнике.
Что хранит минимальный приёмник журнала
В архиве примера откройте observability-lab.php. В классе SafeTally шесть позиций массива изначально содержат целый ноль. Если драйвер вызывает log() с уровнем 5, выражение ++$this->levels[5] увеличивает именно последнюю ячейку. Методу ничего не требуется возвращать. Свободный текст даже временно не сохраняется в состоянии объекта.
public function log(int $level, string $domain, string $message): void
{
if (isset($this->levels[$level])) {
++$this->levels[$level];
} else {
++$this->unknown;
}
}В скачиваемом ZIP команды для запуска находятся в README.txt. Стенд использует самостоятельный локальный mongod, ext-mongodb 2.5.5 и Composer-библиотеку 2.5.0. Каждый запуск создаёт одну временную коллекцию и отдельно проверяет её удаление. Клиент наблюдения открывается с disableClientPersistence=true для нового цикла обнаружения. Отладочный поток mongodb.debug выключен. Используются вымышленные заявки. В открытый отчёт входят только отобранные значения, счётчики и исход конкретного запроса.
Включить счётчик журнала и открыть заявку
На тестовом рабочем месте надо проверить, подключён ли сбор диагностики, и при этом показать оператору его заявку.
Исходные данные
В коллекции заявка O-101 с полями state и note. Перед открытием нового клиента PHP регистрирует объект SafeTally.
- Вызов readTicket читает только _id и state.
- Расширение само вызывает log() на зарегистрированном объекте.
- Колонка «Записи драйвера» показывает, получил ли сборщик хоть один вызов.
Короткий фрагмент
Mon\addSubscriber($logger);
try {
$found = readTicket(freshObserverManager($uri), $namespace, 'O-101');
} finally {
Mon\removeSubscriber($logger);
}Результат на экране
O-101 готова. Счётчик получил записи расширения.
Открыть результат и измерения · Те же строки в JSON.
| Заявка | Состояние | Записи драйвера |
|---|---|---|
| O-101 | готова | получены |
Как прочитать результат
У результата две независимые части. Поле state приходит из записи в MongoDB. Счётчик приходит из PHP-расширения и не содержит описания заказа.
Что ещё учитывать
Частота и уровни событий меняются между версиями и запусками. Одно сообщение не соответствует одному запросу. Конкретные числа находятся под таблицей.
Полная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/observability-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager, Query, BulkWrite, ServerDescription, TopologyDescription};
use MongoDB\Driver\Monitoring as Mon;
runObserverLesson('mongodb135-observe-first-log', 'Включить счётчик журнала и открыть заявку', ['Заявка', 'Состояние', 'Записи драйвера'],
function (Collection $orders,string $uri,string $namespace): array {
$orders->insertOne(['_id'=>'O-101','state'=>'готова','note'=>'учебная заметка']);
$logger=new SafeTally();
$found=withLogs($logger,fn()=>readTicket(freshObserverManager($uri),$namespace,'O-101'));
$ticket=$found[0] ?? [];
$checks['ticket_read'] = $ticket===['_id'=>'O-101','state'=>'готова'];
$checks['driver_called_logger'] = $logger->total()>0;
$checks['known_levels'] = count($logger->levels)===6 && $logger->unknown===0;
$rows=[[$ticket['_id'],$ticket['state'],$logger->total()>0?'получены':'пока пусто']];
return [$rows,$checks,'O-101 готова. Счётчик получил записи расширения.',
array_combine(['ERROR','CRITICAL','WARNING','MESSAGE','INFO','DEBUG'],$logger->levels)];
}
);
Ожидаемый результат
O-101 готова. Счётчик получил записи расширения.
CLI-программа подключается к учебной базе, сопоставляет живые наблюдения с данными заявки и записывает result.html и result.json.
Попробуйте изменить
Добавьте ещё одно поле в исходную заявку. Убедитесь, что проекция и таблица по-прежнему показывают только два выбранных поля.
Как получить диагностику драйвера, не перепутав место и способ записи? Подключить глобального LogSubscriber и сохранить только числовую статистику наблюдаемых уровней. Постановка задачи на Stack Overflow.
Отделить предупреждения от обычного потока
Дежурная смена хочет сразу видеть число записей уровня WARNING и более серьёзных, сохраняя отдельную сумму остального потока.
Исходные данные
Массив levels имеет шесть целочисленных счётчиков, первоначально везде нули. При реальном чтении O-102 каждый обратный вызов увеличивает одну ячейку.
- По коду уровня 0, 1 или 2 сообщение относится к очереди разбора.
- Остальные три счётчика дают объём обычной диагностики.
- Программа сравнивает сумму двух очередей с общим числом входящих вызовов.
Короткий фрагмент
$urgent = array_sum(array_slice($logger->levels, 0, 3));
$routine = array_sum(array_slice($logger->levels, 3));Результат на экране
O-102 собрана. Сообщения распределены по двум очередям.
Открыть результат и измерения · Те же строки в JSON.
| Уровень или заявка | Куда попадает результат |
|---|---|
| 0–2 | Разбор предупреждений |
| 3–5 | Диагностика |
| O-102 | собрана |
Как прочитать результат
Число 0 в очереди предупреждений означает, что за этот интервал таких вызовов не наблюдалось. Именно статус «собрана» прочитан из базы.
Что ещё учитывать
Порог выбирает приложение. DEBUG может быть частым, а появление WARNING само по себе не устанавливает причину проблемы с заявкой.
Полная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/observability-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager, Query, BulkWrite, ServerDescription, TopologyDescription};
use MongoDB\Driver\Monitoring as Mon;
runObserverLesson('mongodb135-observe-severity-filter', 'Отделить предупреждения от обычного потока', ['Уровень или заявка', 'Куда попадает результат'],
function (Collection $orders,string $uri,string $namespace): array {
$orders->insertOne(['_id'=>'O-102','state'=>'собрана']);
$logger=new SafeTally();
$ticket=withLogs($logger,fn()=>readTicket(freshObserverManager($uri),$namespace,'O-102'))[0];
$levels=[Mon\LogSubscriber::LEVEL_ERROR,Mon\LogSubscriber::LEVEL_CRITICAL,Mon\LogSubscriber::LEVEL_WARNING,
Mon\LogSubscriber::LEVEL_MESSAGE,Mon\LogSubscriber::LEVEL_INFO,Mon\LogSubscriber::LEVEL_DEBUG];
$urgent=$logger->priorityCount();
$routine=array_sum(array_slice($logger->levels,3));
$checks['driver_enumeration'] = $levels===[0,1,2,3,4,5];
$checks['partition_complete'] = $urgent+$routine===$logger->total() && $logger->total()>0;
$checks['ticket_visible'] = $ticket['state']==='собрана';
$rows=[['0–2','Разбор предупреждений'],['3–5','Диагностика'],[$ticket['_id'],$ticket['state']]];
return [$rows,$checks,'O-102 собрана. Сообщения распределены по двум очередям.',
['Получено уровней 0–2'=>$urgent,'Получено уровней 3–5'=>$routine]];
}
);
Ожидаемый результат
O-102 собрана. Сообщения распределены по двум очередям.
CLI-программа подключается к учебной базе, сопоставляет живые наблюдения с данными заявки и записывает result.html и result.json.
Попробуйте изменить
Сузьте первую очередь до ERROR и CRITICAL. Измените обе границы так, чтобы сумма очередей по-прежнему совпадала с общим количеством.
Закрыть окно сбора между двумя заданиями
Обработчик очереди переходит от заказа к заказу в одном процессе. Его временный сборщик должен прекратить работу после первого задания.
Исходные данные
Есть два экземпляра SafeTally. Контрольный остаётся подписанным при обоих чтениях.
- Первый сборщик регистрируется дважды. Разности двух счётчиков за одно и то же окно совпадают.
- Два снятия регистрации одного объекта закрывают окно.
- Второе чтение успешно; новые вызовы видит контрольный сборщик.
Короткий фрагмент
Mon\addSubscriber($window);
Mon\addSubscriber($window); // тот же экземпляр
Mon\removeSubscriber($window);Результат на экране
Окно диагностики O-201 закрыто до чтения O-202.
Открыть результат и измерения · Те же строки в JSON.
| Задание | Состояние | Временный приёмник |
|---|---|---|
| O-201 | принята | окно открыто |
| O-202 | отгружена | окно закрыто |
Как прочитать результат
Для снятия подписки сохраняют сам экземпляр, который зарегистрировали. Второй объект того же класса имеет свою регистрацию.
Что ещё учитывать
Окно относится к периоду работы PHP. Если в нём действовали другие клиенты, их сообщения тоже могут попасть в глобальный счётчик.
Полная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/observability-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager, Query, BulkWrite, ServerDescription, TopologyDescription};
use MongoDB\Driver\Monitoring as Mon;
runObserverLesson('mongodb135-observe-subscribe-window', 'Закрыть окно сбора между двумя заданиями', ['Задание', 'Состояние', 'Временный приёмник'],
function (Collection $orders,string $uri,string $namespace): array {
$orders->insertMany([['_id'=>'O-201','state'=>'принята'],['_id'=>'O-202','state'=>'отгружена']]);
$window=new SafeTally();
$control=new SafeTally();
Mon\addSubscriber($control);
try {
Mon\addSubscriber($window);
Mon\addSubscriber($window);
$oldW=$window->total();
$oldC=$control->total();
$first=readTicket(freshObserverManager($uri),$namespace,'O-201')[0];
$bothW=$window->total()-$oldW;
$bothC=$control->total()-$oldC;
Mon\removeSubscriber($window);
Mon\removeSubscriber($window);
$closed=$window->total();
$stillOpen=$control->total();
$second=readTicket(freshObserverManager($uri),$namespace,'O-202')[0];
$checks['one_delivery_per_registration'] = $bothW>0 && $bothW===$bothC;
$checks['closed_window'] = $closed===$window->total() && $control->total()>$stillOpen;
} finally {
Mon\removeSubscriber($window);
Mon\removeSubscriber($control);
}
$checks['two_reads'] = $first['state']==='принята' && $second['state']==='отгружена';
$rows=[[$first['_id'],$first['state'],'окно открыто'],[$second['_id'],$second['state'],'окно закрыто']];
return [$rows,$checks,'Окно диагностики O-201 закрыто до чтения O-202.',
['События за первое окно'=>$bothW,'События контрольного приёмника за второе окно'=>$control->total()-$stillOpen]];
}
);
Ожидаемый результат
Окно диагностики O-201 закрыто до чтения O-202.
CLI-программа подключается к учебной базе, сопоставляет живые наблюдения с данными заявки и записывает result.html и result.json.
Попробуйте изменить
Перенесите закрытие окна после второго чтения и проверьте разницу между итоговыми значениями двух приёмников.
Определить, какому клиенту принадлежит наблюдение
В worker работают два клиента. Нужно разобраться в состоянии первого, сохранив общую статистику расширения.
Исходные данные
Левый Manager получает SDAM-наблюдатель, правый Manager его не получает. Оба обращаются к одной учебной базе.
- Читаем O-301 левым клиентом и запоминаем число переходов.
- Открываем второй клиент и читаем O-302. Локальная сумма не увеличивается, глобальная увеличивается.
- Попытка зарегистрировать чистый LogSubscriber у одного Manager приводит к InvalidArgumentException.
Короткий фрагмент
$left->addSubscriber($observer);
try {
$one = readTicket($left, $namespace, 'O-301');
$two = readTicket($right, $namespace, 'O-302');
} finally {
$left->removeSubscriber($observer);
}Результат на экране
Оба клиента прочли заявки. Адресный наблюдатель видел клиента А.
Открыть результат и измерения · Те же строки в JSON.
| Клиент | Номер | Состояние | Подписка на изменения |
|---|---|---|---|
| Клиент А | O-301 | выдана | SDAM подключён |
| Клиент Б | O-302 | упакована | SDAM не подключён |
Как прочитать результат
Адресную диагностику состояния клиента задаёт регистрация SDAM. В log() нет Manager или идентификатора команды, поэтому общую строку журнала нельзя приписать заказу.
Что ещё учитывать
Числа за окно ничего не говорят об источнике каждой отдельной строки журнала. Для конкретной команды существуют начатое и терминальное события APM.
Полная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/observability-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager, Query, BulkWrite, ServerDescription, TopologyDescription};
use MongoDB\Driver\Monitoring as Mon;
runObserverLesson('mongodb135-observe-client-scope', 'Определить, какому клиенту принадлежит наблюдение', ['Клиент', 'Номер', 'Состояние', 'Подписка на изменения'],
function (Collection $orders,string $uri,string $namespace): array {
$orders->insertMany([['_id'=>'O-301','state'=>'выдана'],['_id'=>'O-302','state'=>'упакована']]);
$logger=new SafeTally();
$scope=new ChangeObserver();
[$a,$b,$changes,$more]=withLogs($logger,function() use ($logger,$scope,$uri,$namespace) {
$left=freshObserverManager($uri);
return observeChanges($left,$scope,function() use ($left,$logger,$scope,$uri,$namespace) {
$a=readTicket($left,$namespace,'O-301')[0];
$changes=$scope->nodeChanges+$scope->fleetChanges;
$old=$logger->total();
$b=readTicket(freshObserverManager($uri),$namespace,'O-302')[0];
return [$a,$b,$changes,$logger->total()-$old];
});
});
$rejected=false;
$another=freshObserverManager($uri);
try { $another->addSubscriber($logger); }
catch (MongoDB\Driver\Exception\InvalidArgumentException $expected) { $rejected=true; }
$checks['only_left_observed'] = $changes>0 && $scope->nodeChanges+$scope->fleetChanges===$changes;
$checks['global_observed_second_window'] = $more>0;
$checks['local_logger_rejected'] = $rejected;
$checks['business_values'] = $a['state']==='выдана' && $b['state']==='упакована';
$rows=[['Клиент А',$a['_id'],$a['state'],'SDAM подключён'],['Клиент Б',$b['_id'],$b['state'],'SDAM не подключён']];
return [$rows,$checks,'Оба клиента прочли заявки. Адресный наблюдатель видел клиента А.',
['Изменений у клиента А'=>$changes,'Глобальных сообщений за второе окно'=>$more]];
}
);
Ожидаемый результат
Оба клиента прочли заявки. Адресный наблюдатель видел клиента А.
CLI-программа подключается к учебной базе, сопоставляет живые наблюдения с данными заявки и записывает result.html и result.json.
Попробуйте изменить
Подключите второй экземпляр ChangeObserver к правому клиенту и сравните локальные показания после каждого чтения.
Можно ли по одному журналу получить перечень конкретных операций клиента? Сопоставить область глобального журнала с адресной подпиской SDAM у отдельного Manager, получить результат каждой прикладной проверки отдельно. Постановка задачи на Stack Overflow.
Прочитать две строки паспорта подключения
При подключении новой смены инженер видит два разных названия на одинаковом адресе. Как подписать каждый показатель на экране?
Исходные данные
Одно прямое подключение к самостоятельному mongod. Драйвер узнаёт об узле, когда программа впервые читает O-401.
- ServerChangedEvent даёт снимок конкретного сервера.
- TopologyChangedEvent даёт картину всех известных этому клиенту серверов.
- Сравниваем внутренний идентификатор двух событий и отдельно выводим ответ запроса.
Короткий фрагмент
$after = $event->getNewDescription();
$type = $after->getType();
$before = $event->getPreviousDescription()->getType();Результат на экране
Узел Standalone, топология Single. O-401 в пути.
Открыть результат и измерения · Те же строки в JSON.
| Что описываем | Что обнаружено |
|---|---|
| Узел | Standalone |
| Топология | Single |
| O-401 | в пути |
Как прочитать результат
Standalone — роль процесса mongod. Single — модель прямого соединения. На других конфигурациях эти понятия тоже следует подписывать отдельно.
Что ещё учитывать
RTT имеет единицу миллисекунд и может отсутствовать. Это показатель обмена с узлом, а не длительность загрузки заказа.
Полная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/observability-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager, Query, BulkWrite, ServerDescription, TopologyDescription};
use MongoDB\Driver\Monitoring as Mon;
runObserverLesson('mongodb135-observe-node-and-fleet', 'Прочитать две строки паспорта подключения', ['Что описываем', 'Что обнаружено'],
function (Collection $orders,string $uri,string $namespace): array {
$orders->insertOne(['_id'=>'O-401','state'=>'в пути']);
$reader=freshObserverManager($uri);
$events=new ChangeObserver();
$ticket=observeChanges($reader,$events,fn()=>readTicket($reader,$namespace,'O-401'))[0];
$node=$events->node;
$fleet=$events->fleet;
$checks['known_local_node'] = $node['local'] && $node['to']===ServerDescription::TYPE_STANDALONE;
$checks['single_topology'] = $fleet['to']===TopologyDescription::TYPE_SINGLE && $fleet['servers']===1;
$checks['event_access_paths_agree'] = $node['properties_agree'] && $fleet['properties_agree'];
$checks['snapshots_related'] = $events->sameGroup();
$checks['latency_is_optional'] = $node['valid_rtt'];
$checks['ticket_verified'] = $ticket['state']==='в пути';
$rows=[['Узел',$node['to']],['Топология',$fleet['to']],[$ticket['_id'],$ticket['state']]];
return [$rows,$checks,'Узел Standalone, топология Single. O-401 в пути.',
['Известно узлов'=>$fleet['servers'],'RTT измерения, мс'=>$node['rtt_ms']]];
}
);
Ожидаемый результат
Узел Standalone, топология Single. O-401 в пути.
CLI-программа подключается к учебной базе, сопоставляет живые наблюдения с данными заявки и записывает result.html и result.json.
Попробуйте изменить
Сравните число объектов getServers() и число строк при чтении отсутствующего номера. Это размеры двух разных наборов.
Отличить пустую карточку от недоступной базы
Оператору прислали номер O-509. Пустой результат запроса сам по себе легко принять за отказ сервиса.
Исходные данные
Учебная очередь содержит O-501. Сначала запрашивается отсутствующая O-509, затем существующая O-501.
- Первый executeQuery завершается успешно, курсор содержит ноль строк.
- Второй запрос возвращает состояние известной заявки.
- Последний снимок топологии показывает, что драйвер видел подходящий для чтения сервер.
Короткий фрагмент
$matches = readTicket($reader, $namespace, 'O-509');
if ($matches === []) {
echo 'Сверьте номер заявки';
}Результат на экране
O-509 не найдена. Контрольная O-501 передана курьеру.
Открыть результат и измерения · Те же строки в JSON.
| Введённый номер | Совпадений | Что показать |
|---|---|---|
| O-509 | 0 | Сверить номер |
| O-501 | 1 | передана курьеру |
Как прочитать результат
Сопоставление законченного чтения с фильтром определяет, какой текст увидит оператор. Снимок объясняет, какой путь был известен драйверу в момент наблюдения.
Что ещё учитывать
Вывод «не найдена» относится к данному фильтру и моменту чтения. После вставки другой программой ответ может измениться.
Полная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/observability-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager, Query, BulkWrite, ServerDescription, TopologyDescription};
use MongoDB\Driver\Monitoring as Mon;
runObserverLesson('mongodb135-observe-absent-ticket', 'Отличить пустую карточку от недоступной базы', ['Введённый номер', 'Совпадений', 'Что показать'],
function (Collection $orders,string $uri,string $namespace): array {
$orders->insertOne(['_id'=>'O-501','state'=>'передана курьеру']);
$reader=freshObserverManager($uri);
$events=new ChangeObserver();
[$missing,$present]=observeChanges($reader,$events,function() use ($reader,$namespace) {
return [readTicket($reader,$namespace,'O-509'),readTicket($reader,$namespace,'O-501')];
});
$checks['route_discovered'] = $events->fleet['readable']===true;
$checks['missing_completed'] = $missing===[];
$checks['control_ticket_valid'] = count($present)===1 && $present[0]['state']==='передана курьеру';
$rows=[['O-509',count($missing),$missing===[]?'Сверить номер':'Открыть карточку'],
[$present[0]['_id'],count($present),$present[0]['state']]];
return [$rows,$checks,'O-509 не найдена. Контрольная O-501 передана курьеру.',
['Успешные проверки узла'=>$events->heartbeatOK,'Изменения топологии'=>$events->fleetChanges]];
}
);
Ожидаемый результат
O-509 не найдена. Контрольная O-501 передана курьеру.
CLI-программа подключается к учебной базе, сопоставляет живые наблюдения с данными заявки и записывает result.html и result.json.
Попробуйте изменить
Поменяйте фильтр первого запроса на O-501. Проследите, какая ветка сообщения теперь сработает.
Объяснить отказ вставки при доступном узле
Форма регистрации сообщает о повторном номере. Нужно решить, исправлять ли подключение или показать сотруднику уже созданную заявку.
Исходные данные
У существующей O-601 состояние «принята». Повторная вставка пытается сохранить такой же _id со значением «новая».
- Топология содержит узел, пригодный для записи.
- Сервер проверяет уникальность _id. Ошибка элемента записи имеет код 11000.
- Программа отдельно читает сохранённое состояние и пересчитывает документы в учебной коллекции.
Короткий фрагмент
$issues = $error->getWriteResult()->getWriteErrors();
$code = $issues[0]->getCode();
$saved = readTicket($reader, $namespace, 'O-601');Результат на экране
O-601 уже зарегистрирована. Сохранённое состояние — принята.
Открыть результат и измерения · Те же строки в JSON.
| Проверка | Фактическое значение |
|---|---|
| Есть узел для записи | true |
| Код элемента | 11000 |
| O-601 | принята |
Как прочитать результат
Доступный маршрут отвечает на вопрос, куда обращаться. Приняла ли база конкретную запись, показывают ответ команды, ошибки элементов и контрольное чтение.
Что ещё учитывать
В примере один элемент и один уникальный ключ _id. Для партии следует разбирать каждый индекс ошибки и полученный результат.
Полная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/observability-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager, Query, BulkWrite, ServerDescription, TopologyDescription};
use MongoDB\Driver\Monitoring as Mon;
runObserverLesson('mongodb135-observe-write-receipt', 'Объяснить отказ вставки при доступном узле', ['Проверка', 'Фактическое значение'],
function (Collection $orders,string $uri,string $namespace): array {
$orders->insertOne(['_id'=>'O-601','state'=>'принята']);
$reader=freshObserverManager($uri);
$events=new ChangeObserver();
[$code,$ticket]=observeChanges($reader,$events,function() use ($reader,$namespace) {
$batch=new BulkWrite();
$batch->insert(['_id'=>'O-601','state'=>'новая']);
$code=null;
try { $reader->executeBulkWrite($namespace,$batch); }
catch (MongoDB\Driver\Exception\BulkWriteException $error) {
$issues=$error->getWriteResult()->getWriteErrors();
$code=isset($issues[0]) ? $issues[0]->getCode() : null;
if ($code!==11000) { throw $error; }
}
return [$code,readTicket($reader,$namespace,'O-601')[0]];
});
$writable=$events->fleet['writable'];
$checks['route_to_writer'] = $writable===true;
$checks['id_conflict'] = $code===11000;
$checks['old_state_preserved'] = $ticket['state']==='принята' && $orders->countDocuments([])===1;
$rows=[['Есть узел для записи',$writable],['Код элемента',$code],[$ticket['_id'],$ticket['state']]];
return [$rows,$checks,'O-601 уже зарегистрирована. Сохранённое состояние — принята.',
['Сохранено документов'=>(int)$orders->countDocuments([])]];
}
);
Ожидаемый результат
O-601 уже зарегистрирована. Сохранённое состояние — принята.
CLI-программа подключается к учебной базе, сопоставляет живые наблюдения с данными заявки и записывает result.html и result.json.
Попробуйте изменить
Попробуйте отдельную вставку с другим _id и запросите обе заявки. Измените ожидаемое количество документов.
Выбрать сообщение оператору при отказе соединения
Что писать сотруднику, если драйвер не смог найти доступный сервер, а оператор уже ждёт ответа по заказу?
Исходные данные
Первый клиент обращается к зарезервированному программой локальному порту без слушающего сервера. Второй — к учебному mongod с O-701.
- Первое чтение заканчивается исключением при выборе сервера. Программа отмечает, что статус заявки пока неизвестен.
- Обработчик отдельно отмечает неудачную heartbeat-проверку первой точки.
- В рабочей базе независимый клиент успешно читает заявку и получает её состояние.
Короткий фрагмент
try {
$found = readTicket($reader, $namespace, 'O-701');
} catch (MongoDB\Driver\Exception\ConnectionTimeoutException $failure) {
echo 'Сейчас не удалось загрузить заявку';
}Результат на экране
Первое чтение не выполнено. Рабочая база вернула O-701 — ожидает выдачи.
Открыть результат и измерения · Те же строки в JSON.
| Куда обратились | Что известно | Сообщение или действие |
|---|---|---|
| Учебная недоступная точка | Чтение не выполнено | Проверить подключение |
| Рабочая точка | O-701 | ожидает выдачи |
Как прочитать результат
Для первого запроса состояние заявки осталось неизвестным. У другой попытки есть положительный результат и источник. Подмена исключения пустым массивом дала бы человеку ложный ответ «заявки нет».
Что ещё учитывать
Две независимые точки нужны для сравнения. Это не автоматическое переключение сервиса и не испытание отказоустойчивости кластера.
Полная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/observability-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager, Query, BulkWrite, ServerDescription, TopologyDescription};
use MongoDB\Driver\Monitoring as Mon;
runObserverLesson('mongodb135-observe-probe-decision', 'Выбрать сообщение оператору при отказе соединения', ['Куда обратились', 'Что известно', 'Сообщение или действие'],
function (Collection $orders,string $uri,string $namespace): array {
$orders->insertOne(['_id'=>'O-701','state'=>'ожидает выдачи']);
$socket=stream_socket_server('tcp://127.0.0.1:0',$errno,$errstr,STREAM_SERVER_BIND);
if ($socket===false) { throw new RuntimeException('Cannot reserve private probe port'); }
$failed=new ChangeObserver();
$timedOut=false;
try {
$endpoint=stream_socket_get_name($socket,false);
if (!is_string($endpoint) || !preg_match('/^127\.0\.0\.1:[0-9]+$/D',$endpoint)) {
throw new RuntimeException('Expected owned loopback socket');
}
$bad=freshObserverManager('mongodb://'.$endpoint.'/?directConnection=true&retryWrites=false');
try { observeChanges($bad,$failed,fn()=>readTicket($bad,$namespace,'O-701')); }
catch (MongoDB\Driver\Exception\ConnectionTimeoutException $expected) { $timedOut=true; }
} finally { fclose($socket); }
$working=new ChangeObserver();
$reader=freshObserverManager($uri);
$known=observeChanges($reader,$working,fn()=>readTicket($reader,$namespace,'O-701'));
$checks['unavailable_is_not_empty'] = $timedOut && $failed->heartbeatFailures>0;
$checks['working_route'] = $working->fleet['readable'] && $working->heartbeatOK>0;
$checks['known_document'] = count($known)===1 && $known[0]['state']==='ожидает выдачи';
$rows=[['Учебная недоступная точка',$timedOut?'Чтение не выполнено':'Чтение выполнено','Проверить подключение'],
['Рабочая точка',$known[0]['_id'],$known[0]['state']]];
return [$rows,$checks,'Первое чтение не выполнено. Рабочая база вернула O-701 — ожидает выдачи.',
['Неудачные проверки первой точки'=>$failed->heartbeatFailures,'Успешные проверки рабочей точки'=>$working->heartbeatOK]];
}
);
Ожидаемый результат
Первое чтение не выполнено. Рабочая база вернула O-701 — ожидает выдачи.
CLI-программа подключается к учебной базе, сопоставляет живые наблюдения с данными заявки и записывает result.html и result.json.
Попробуйте изменить
Сравните этот результат с отсутствующей O-509 выше. Как различаются сообщения в двух случаях, когда оператор пока не увидел карточку?
Сообщение о проблеме мониторинга не даёт готового ответа, доступна ли прикладная операция. Проверить недоступный учебный endpoint, а затем отдельно получить состояние конкретной заявки по рабочему соединению. Постановка задачи на Stack Overflow.
Классы, интерфейсы и обработчик журнала
| Что искать | Для какой задачи |
|---|---|
| MongoDB\Driver\Monitoring\LogSubscriber | Собрать диагностические сообщения PHP-расширения в счётчики известного размера. |
| MongoDB\Driver\Monitoring\LogSubscriber::log | Обработать один поступивший от драйвера диагностический сигнал, сохранив нужную категорию. |
| MongoDB\Driver\Monitoring\SDAMSubscriber | Следить за тем, какие узлы видит драйвер и как меняется его представление о соединении. |
| MongoDB\Driver\Monitoring\ServerChangedEvent | Увидеть, как драйвер пересмотрел сведения об одном узле. |
| MongoDB\Driver\Monitoring\TopologyChangedEvent | Сравнить предыдущий и новый набор известных маршрутов клиента. |
| MongoDB\Driver\ServerDescription | Узнать роль конкретного узла, время последнего наблюдения и сетевое измерение. |
| MongoDB\Driver\TopologyDescription | Решить, знал ли клиент подходящий для операции маршрут в момент проверки. |
Как продолжить разбор инцидента
Для конкретного запроса нужны события команды. Для подробного разбора отказа heartbeat пригодится исключение проверки связи. Если маршрут есть, а запись отклонена, перейдите к разбору отказов записи.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
MongoDB\Driver\Monitoring\LogSubscriberСобрать диагностические сообщения PHP-расширения в счётчики известного размера.
Интерфейс и шесть констант появились в ext-mongodb 1.17.0. На стенде установлен 2.5.5. · mongodb
interface MongoDB\Driver\Monitoring\LogSubscriber extends MongoDB\Driver\Monitoring\SubscriberСобрать диагностические сообщения PHP-расширения в счётчики известного размера.
Как получить и какие данные передаются
Объявите класс с implements LogSubscriber и методом log(int $level, string $domain, string $message): void. Создайте экземпляр, передайте его глобальной функции addSubscriber() до интересующего действия, в finally снимите через removeSubscriber().
Результат для приложения
Драйвер доставляет уровень, компонент и свободный текст. ERROR (0) — самая серьёзная ошибка, о которой расширение не смогло сообщить обычным API. CRITICAL (1) соответствует уровню libmongoc и встречается редко. WARNING (2) предупреждает о нежелательном поведении. MESSAGE (3) отмечает необычное, но допустимое событие. INFO (4) описывает обычную работу, DEBUG (5) даёт подробности.
Границы и возможные ошибки
У объекта нет потока TRACE и привязки к конкретному Manager. Попытка передать LogSubscriber в Manager::addSubscriber() вызывает InvalidArgumentException. В коде формы не следует определять наличие заявки или факт успешной записи по наличию журналирования.
Интерфейс и шесть констант появились в ext-mongodb 1.17.0. На стенде установлен 2.5.5.
Увидеть вызовы расширения при открытии первого клиента
Mon\addSubscriber($logger);После первого чтения таблица содержит состояние O-101, а отдельный блок — фактические количества по шести уровням.
Перейти к коду, исходным данным и результату
Выбрать адресный наблюдатель для одного клиента
$left->addSubscriber($sdam);Состояния двух заявок известны из документов. SDAM подключён только к левому клиенту. Глобальный logger видит большее окно.
MongoDB\Driver\Monitoring\LogSubscriber::logОбработать один поступивший от драйвера диагностический сигнал, сохранив нужную категорию.
Метод ext-mongodb с 1.17.0. Сигнатуру сверили с 2.5.5. · mongodb
public function log(int $level, string $domain, string $message): voidОбработать один поступивший от драйвера диагностический сигнал, сохранив нужную категорию.
Как получить и какие данные передаются
PHP вызывает ваш метод для поступившего сообщения. $level — число от 0 до 5. $domain — имя компонента, $message — строка описания; состав и формулировка зависят от источника. Имена параметров здесь обозначают входы одного обратного вызова.
Результат для приложения
void означает, что методу нечего возвращать вызывающей стороне. Результатом для приложения становится, например, увеличенное целое значение в массиве объекта. Сравнение со значением LEVEL_WARNING помогает отдельно подсчитать сообщения для разбора.
Границы и возможные ошибки
Количество строк не означает количество команд. Вызов может возникнуть на этапе инициализации. Свободный текст способен содержать технические и прикладные данные. В SafeTally значения domain и message никогда не попадают в HTML, консоль и JSON.
Метод ext-mongodb с 1.17.0. Сигнатуру сверили с 2.5.5.
Разделить наблюдения на два интервала
$urgent = array_sum(array_slice($logger->levels, 0, 3));Первые три позиции означают уровни 0, 1, 2. Нулевая сумма — за это окно соответствующих сообщений не было.
Перейти к коду, исходным данным и результату
Посчитать только нужный интервал
$delta = $window->total() - $before;Два чтения и контрольный сборщик показывают, когда новые вызовы перестали поступать в конкретный экземпляр.
MongoDB\Driver\Monitoring\SDAMSubscriberСледить за тем, какие узлы видит драйвер и как меняется его представление о соединении.
SDAMSubscriber существует с ext-mongodb 1.13.0. С версии 1.15.0 для PHP 8 введены tentative return types. Начиная с 2.0.0 типы возврата обязательны. · mongodb
interface MongoDB\Driver\Monitoring\SDAMSubscriber extends MongoDB\Driver\Monitoring\SubscriberСледить за тем, какие узлы видит драйвер и как меняется его представление о соединении.
Как получить и какие данные передаются
Каждый из девяти public-методов принимает один объект одноимённого события и возвращает void. У узла есть serverOpening, serverChanged, serverClosed; у проверки соединения — serverHeartbeatStarted, serverHeartbeatSucceeded, serverHeartbeatFailed; у топологии — topologyOpening, topologyChanged, topologyClosed. При реализации интерфейса требуются все девять методов. Ненужные конкретной программе могут иметь пустое тело.
Результат для приложения
В обработчике можно сохранить предыдущее/новое описание, увеличить счётчик или пометить неудачную проверку. Подпишитесь до первого запроса: создание Manager может отложить фактическое соединение до выбора узла.
Границы и возможные ошибки
Глобальная регистрация охватит другие клиенты. Manager::addSubscriber() привязывает совместимый объект к конкретному Manager. Событие о проверке связи не даёт бизнес-ответ о состоянии заказа. Полный документ команды рассматривается в отдельном семействе APM.
SDAMSubscriber существует с ext-mongodb 1.13.0. С версии 1.15.0 для PHP 8 введены tentative return types. Начиная с 2.0.0 типы возврата обязательны.
Собрать изменения только выбранного клиента
$reader->addSubscriber($observer);Наблюдения левого клиента остаются прежними, когда второй Manager обслуживает свою заявку.
Перейти к коду, исходным данным и результату
Понять, был ли контакт с адресом
public function serverHeartbeatFailed(Mon\ServerHeartbeatFailedEvent $event): void {
++$this->heartbeatFailures;
}Неудачные проверки первой точки учитываются отдельно от успешной операции рабочей базы.
MongoDB\Driver\Monitoring\ServerChangedEventУвидеть, как драйвер пересмотрел сведения об одном узле.
Событие существует с ext-mongodb 1.13.0. В версии 2.3.0 появились public readonly свойства. Методы get* позволяют читать те же значения на более ранних версиях. · mongodb
final class MongoDB\Driver\Monitoring\ServerChangedEventУвидеть, как драйвер пересмотрел сведения об одном узле.
Как получить и какие данные передаются
Объект поступает в serverChanged(). getPreviousDescription() и getNewDescription() без аргументов возвращают ServerDescription. getHost() даёт имя узла, getPort() — целый номер порта, getTopologyId() — BSON ObjectId топологии. Публичные свойства host, port, topologyId, previousDescription, newDescription доступны только для чтения.
Результат для приложения
Сопоставьте getType() двух описаний. Когда драйвер распознал самостоятельный сервер, новое значение равно ServerDescription::TYPE_STANDALONE. Приёмник также может проверить, о том ли учебном endpoint пришло событие, и сохранить в отчёте только да/нет.
Границы и возможные ошибки
Один и тот же тип в двух описаниях не означает, что событие ошибочно: в описании могли измениться и другие сведения. Смена роли secondary/primary относится к replica set, в этом опыте работает самостоятельный сервер.
Событие существует с ext-mongodb 1.13.0. В версии 2.3.0 появились public readonly свойства. Методы get* позволяют читать те же значения на более ранних версиях.
Вывести обнаруженную роль узла
$role = $event->getNewDescription()->getType();Слово Standalone в строке «Узел» получено из описания сервера.
Перейти к коду, исходным данным и результату
Учитывать изменения только адресного клиента
++$this->nodeChanges;Счётчик увеличивается при вызове serverChanged() подписанного клиента. Он не содержит запросов этого клиента.
MongoDB\Driver\Monitoring\TopologyChangedEventСравнить предыдущий и новый набор известных маршрутов клиента.
Класс ext-mongodb с 1.13.0. Свойства для чтения появились в 2.3.0. Для кода, который должен работать до 2.3.0, используйте методы. · mongodb
final class MongoDB\Driver\Monitoring\TopologyChangedEventСравнить предыдущий и новый набор известных маршрутов клиента.
Как получить и какие данные передаются
Обработчик topologyChanged() получает один объект события. У getPreviousDescription(), getNewDescription() и getTopologyId() нет аргументов. Первые два метода возвращают TopologyDescription, третий — BSON ObjectId. В актуальном расширении те же три значения доступны через previousDescription, newDescription, topologyId.
Результат для приложения
Код может сравнить доступность маршрута по обоим снимкам и посчитать узлы в новом. ID позволяет связать события узла и топологии в памяти наблюдателя. Для учебного экрана достаточно подтверждения, что ID совпали.
Границы и возможные ошибки
Снимок относится к моменту уведомления. После следующей смены состояния его вывод может устареть. Разрешена ли запись пользователю и существует ли заказ, выясняют отдельно.
Класс ext-mongodb с 1.13.0. Свойства для чтения появились в 2.3.0. Для кода, который должен работать до 2.3.0, используйте методы.
Разделить наличие маршрута и документа
$canRead = $event->getNewDescription()->hasReadableServer();Маршрут для чтения существует, запрос O-509 вернул ноль документов.
Перейти к коду, исходным данным и результату
Сосчитать узлы в принятом снимке
$known = count($event->newDescription->getServers());В изучаемой прямой топологии найден один узел. Свойство для новой версии и getNewDescription() дают согласованные сведения.
MongoDB\Driver\ServerDescriptionУзнать роль конкретного узла, время последнего наблюдения и сетевое измерение.
Описание сервера доступно с ext-mongodb 1.13.0. Примеры запускаются с 2.5.5. · mongodb
final class MongoDB\Driver\ServerDescriptionУзнать роль конкретного узла, время последнего наблюдения и сетевое измерение.
Как получить и какие данные передаются
Получите описание из Server::getServerDescription() или ServerChangedEvent. Методы без аргументов: getHost() возвращает строку, getPort() целое, getType() строку, getLastUpdateTime() целое в микросекундах, getRoundTripTime() целое в миллисекундах либо null, getHelloResponse() массив последнего hello.
Результат для приложения
Группа констант TYPE_* различает UNKNOWN (роли пока нет), STANDALONE (самостоятельный mongod), MONGOS (маршрутизатор), LOAD_BALANCER (балансировщик), POSSIBLE_PRIMARY (другой участник указал возможный primary). В replica set роли представлены RS_PRIMARY, RS_SECONDARY, RS_ARBITER, RS_OTHER и RS_GHOST. RS_OTHER может означать запуск/восстановление; RS_GHOST встречается у ещё не готового или исключённого участника.
Границы и возможные ошибки
Сравнивайте результат getType() со строковой константой класса ServerDescription. В классе Server есть отдельные числовые константы. Полный hello содержит сведения об инфраструктуре; для открытого экрана в примере выбираются роль и обработанное числовое измерение.
Описание сервера доступно с ext-mongodb 1.13.0. Примеры запускаются с 2.5.5.
Показать роль и измеренный RTT
$type = $after->getType();
$rttMs = $after->getRoundTripTime();Первое значение входит в таблицу. Второе может отсутствовать и помещено в блок технических чисел.
Перейти к коду, исходным данным и результату
Сопоставить описание с клиентом
$type = $event->getNewDescription()->getType();У каждого адресно подключённого наблюдателя своё окно. Одинаковый адрес серверов сам по себе не связывает прикладные заявки.
MongoDB\Driver\TopologyDescriptionРешить, знал ли клиент подходящий для операции маршрут в момент проверки.
Класс ext-mongodb с 1.13.0. Пример проверяет прямую топологию локального стенда с 2.5.5. · mongodb
final class MongoDB\Driver\TopologyDescriptionРешить, знал ли клиент подходящий для операции маршрут в момент проверки.
Как получить и какие данные передаются
Получите объект через TopologyChangedEvent. getServers(): array возвращает описания узлов. getType(): string описывает схему подключения. hasReadableServer(?ReadPreference $readPreference = null): bool принимает необязательную политику чтения. hasWritableServer(): bool показывает наличие узла для записи. Некорректная передача аргументов методам может привести к InvalidArgumentException.
Результат для приложения
Типы TYPE_UNKNOWN, TYPE_SINGLE, TYPE_SHARDED, TYPE_REPLICA_SET_NO_PRIMARY, TYPE_REPLICA_SET_WITH_PRIMARY, TYPE_LOAD_BALANCED различают неизвестную схему, прямое подключение, шардированный кластер, replica set без и с primary, работу через балансировщик. Например, у снимка Single с одним доступным Standalone может быть пригодный путь и для чтения, и для записи.
Границы и возможные ошибки
Проверка выполняется по описанию, которое уже получил драйвер. Она не отправляет контрольный заказ, не проверяет его _id, ограничения уникальности или права пользователя, не гарантирует успех следующей команды.
Класс ext-mongodb с 1.13.0. Пример проверяет прямую топологию локального стенда с 2.5.5.
Расшифровать «узел доступен, вставка отклонена»
$available = $snapshot->hasWritableServer();У маршрута значение true. Ошибка 11000 и сохранённая O-601 отдельно объясняют исход второй вставки.
Перейти к коду, исходным данным и результату
Показать пустую выдачу без аварии
$available = $snapshot->hasReadableServer();Даже при true запрос может вернуть ноль документов по введённому номеру.
Источники и обсуждения
- mongodb.monitoring
- ref.monitoring.functions
- class.mongodb-driver-monitoring-logsubscriber
- mongodb-driver-monitoring-logsubscriber.log
- class.mongodb-driver-monitoring-sdamsubscriber
- class.mongodb-driver-monitoring-serverchangedevent
- class.mongodb-driver-monitoring-topologychangedevent
- class.mongodb-driver-serverdescription
- class.mongodb-driver-topologydescription
- function.mongodb.driver.monitoring.addsubscriber
- function.mongodb.driver.monitoring.removesubscriber
- Manager::addSubscriber
- Stub Monitoring/LogSubscriber · ext-mongodb 2.5.5
- Stub Monitoring/SDAMSubscriber · ext-mongodb 2.5.5
- Stub Monitoring/ServerChangedEvent · ext-mongodb 2.5.5
- Stub Monitoring/TopologyChangedEvent · ext-mongodb 2.5.5
- Stub ServerDescription · ext-mongodb 2.5.5
- Stub TopologyDescription · ext-mongodb 2.5.5
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


