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

Дерево руководства
Глава 187 · После первой выборки MongoDB и очереди BulkWrite
Отгрузка и журнал. Как разобрать результат пакета в двух коллекциях
Создаём отгрузку и событие в разных коллекциях, разбираем частичный отказ журнала, исправляем только разрешённое расхождение и строим ведомость четырёх заданий.
Что получит диспетчер после отправки пакета
Машина уехала со склада. В системе нужна карточка отгрузки D-101 и запись EV-101 для журнала. Перед закрытием смены сотрудник должен увидеть, какие пары готовы, где не хватает события, а где связь требует разбирательства. В этой главе построим такую ведомость и выясним, почему сообщение «ошибка пакета» ещё не означает, что отгрузка не сохранена.
- shipments: D-101 → состояние «отгружена»
- events: EV-101 → shipment D-101, kind dispatched
- BulkWriteCommand собирает адрес каждой записи
- Manager отправляет действия, BulkWriteCommandResult сообщает итог
- Диспетчер читает обе стороны и принимает решение по заданию
Представьте два журнала на рабочем столе: карточки отгрузок и записи событий. EV-101 ссылается на номер D-101. Такая модель объясняет назначение полей, но не даёт автоматического внешнего ключа или общей атомарности. MongoDB допускает событие с отсутствующей отгрузкой, пока приложение отдельно не обеспечивает согласованность.
Здесь уже нужны массивы и именованные ключи, вызовы функций, try/catch и исключения и первая выборка документов для экрана. Сам новый инструмент начинаем с двух вставок и наблюдаемого результата. Для отправки в одну коллекцию сначала можно пройти очередь импорта и обычный BulkWrite.
Как запустить отдельную учебную отгрузку
Откройте архив любого примера. В нём example.php с рабочим сценарием, dispatch-lab.php с подключением, чтением квитанций и выводом ведомости, а также README.txt с командами запуска. example.php подключает helper через require, затем runDispatch создаёт две пустые собственные коллекции и передаёт их в функцию сценария как $ship и $events.
Команда доступна с MongoDB Server 8.0 и ext-mongodb 2.1.0. Подготовленное окружение этого набора: PHP CLI 8.5.11 x64, ext-mongodb 2.5.5, mongodb/mongodb 2.5.0, standalone MongoDB 8.0.34. В README указано, как запустить отдельный сервер на 127.0.0.1:27075, установить библиотеку через Composer и задать PHPMAN_MONGODB_URI и PHPMAN_MONGODB_DATABASE. Строка подключения включает directConnection=true и retryWrites=false; имя учебной базы — php_manual_lab. Другой адрес или тип сервера helper отклоняет.
- example.php подключает dispatch-lab.php
- runDispatch получает два случайных namespace: shipments и events
- Функция сценария получает Manager и обе Collection
- submitDispatch задаёт w=1 и вызывает executeBulkWriteCommand
- Функция сценария возвращает строки ведомости и проверки
- finally удаляет только две собственные коллекции; после проверки записываются result.html, result.json и metrics.json
Валидатор только учебной коллекции events требует поля _id, shipment и kind; shipment должен быть строкой, kind — dispatched. validationAction=error отклоняет неправильный документ. Чужие коллекции и общие настройки сервера не изменяются. Для каждого запуска используются новые случайные имена; счётчики ролей до создания, после создания и после очистки должны быть 0 → 1 → 0 для каждой коллекции.
После команды php example.php ожидается указанная под примером строка. Откройте result.html как готовую ведомость; те же колонки и строки находятся в result.json. metrics.json содержит версии, условия проверки и журнал двух пространств имён. Ошибка подключения, неожиданная ошибка записи или несовпадение проверок завершает программу с ненулевым кодом; успешный результат при этом не подставляется. Автоматических повторов записи и циклов восстановления здесь нет.
Создать отгрузку и запись журнала одним пакетом
После погрузки сотруднику нужно открыть карточку отгрузки, а диспетчеру — увидеть соответствующее событие.
Входные данные
Изначально коллекции пусты. D-101 — номер отгрузки, EV-101 — собственный идентификатор записи журнала. Поле shipment связывает две записи.
Главное действие
Ниже сокращённый фрагмент полной программы этого раздела. Подключение, создание собственных коллекций и проверка результата находятся в раскрываемом примере.
$batch = new BulkWriteCommand();
$batch->insertOne($ordersNamespace, ['_id' => 'D-101', 'state' => 'отгружена']);
$batch->insertOne($eventsNamespace, ['_id' => 'EV-101', 'shipment' => 'D-101', 'kind' => 'dispatched']);
$result = $manager->executeBulkWriteCommand($batch);new создаёт отдельный объект пакета. Стрелка -> вызывает его метод. Первый аргумент insertOne — адрес коллекции, второй — массив полей документа; запись ключ => значение задаёт одно поле. Первая операция относится к shipments, вторая — к events. $manager отправляет накопленные действия, а присваивание $result = сохраняет возвращённую квитанцию. В полном примере shipmentDoc и dispatchEvent только составляют массивы; их объявления находятся в dispatch-lab.php и сами в базу не пишут.
Что получим
| Коллекция | Ключ | Что сохранилось |
|---|---|---|
| Отгрузки | D-101 | отгружена |
| Журнал | EV-101 | ссылка на D-101 |
Namespace — строка из имени базы и имени коллекции, разделённых точкой. В приложении getNamespace() берёт её из объекта коллекции. Одна передача пакета приложением может включать несколько серверных запросов при разделении большой партии драйвером.
Два успешных результата в этом опыте не устанавливают атомарность. Позже мы специально получим состояние, когда сохранилась только отгрузка.
Полный контракт объекта: BulkWriteCommand. Общие и подробные ответы разобраны в карточке BulkWriteCommandResult.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/dispatch-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager,BulkWriteCommand,BulkWriteCommandResult};
runDispatch('mongodb137-dispatch-plan-and-send', 'Создать отгрузку и запись журнала одним пакетом', ['Коллекция', 'Ключ', 'Что сохранилось'],
function (Manager $manager,Collection $ship,Collection $events): array {
$bulk=new BulkWriteCommand();
$bulk->insertOne($ship->getNamespace(),shipmentDoc('D-101'));
$bulk->insertOne($events->getNamespace(),dispatchEvent('EV-101','D-101'));
$staged=count($bulk);
$before=[$ship->countDocuments([]),$events->countDocuments([])];
$result=submitDispatch($manager,$bulk);
$s=$ship->findOne(['_id'=>'D-101']);
$e=$events->findOne(['_id'=>'EV-101']);
$checks['queue_only_before_submit'] = $staged===2 && $before===[0,0];
$checks['two_acknowledged_inserts'] = $result->isAcknowledged() && $result->getInsertedCount()===2;
$checks['linked_records'] = $e['shipment']===$s['_id'] && $s['state']==='отгружена';
$rows=[['Отгрузки',$s['_id'],$s['state']],['Журнал',$e['_id'],'ссылка на '.$e['shipment']]];
return [$rows,$checks,'D-101 отгружена. В журнале создана EV-101.'];
}
);
Ожидаемый результат
D-101 отгружена. В журнале создана EV-101.
Программа выполняет сценарий на отдельной MongoDB и строит ведомость по подтверждённым результатам и заново прочитанным документам.
Открыть ведомость запуска · Скачать строки ведомости в JSON
Попробуйте самостоятельно. Переместите контрольное чтение выше отправки и проверьте, когда каждая коллекция начинает содержать данные.
Предлагаемое решение упражнения
До submitDispatch() обе коллекции пусты, хотя count($bulk) уже равен 2. После успешной отправки оба чтения по ключу находят свои документы. Перенос чтения не переносит саму запись.
Прочитать краткую квитанцию смены
Руководителю нужен итог смены. Число добавленных документов и число отправленных машин отвечают на разные вопросы.
Входные данные
Две отгрузки D-201 и D-202, у каждой ровно одно событие. Запрошено подтверждение записи w=1 без verboseResults.
- Собираем четыре действия для двух пар документов.
- Из квитанции читаем общее число вставок.
- Отдельными запросами считаем отгрузки и события в ограниченной учебной партии.
Главное действие
Ниже сокращённый фрагмент полной программы этого раздела. Подключение, создание собственных коллекций и проверка результата находятся в раскрываемом примере.
$allInserted = $result->getInsertedCount();
$sent = $ship->countDocuments(['state' => 'отгружена']);getInsertedCount() не принимает аргументов и возвращает PHP int. Он складывает вставки во всех коллекциях этой команды. countDocuments() выполняет отдельный запрос к выбранной коллекции по условию. Строка фильтра state => отгружена относится к карточкам, kind => dispatched — к событиям. В двух маленьких собственных коллекциях посторонних смен нет.
Что получим
| Как считаем | Количество |
|---|---|
| Добавлено документов по квитанции | 4 |
| Карточки отгрузки | 2 |
| События журнала | 2 |
Четыре вставки означают четыре документа. В отчёте о количестве отгрузок показываем два. При verboseResults=false подробные карты операций равны null; это штатный режим.
В реальной коллекции укажите номер смены в поле, индекс и фильтр по нему. В опыте изоляцию обеспечивают две пустые до запуска коллекции.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/dispatch-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager,BulkWriteCommand,BulkWriteCommandResult};
runDispatch('mongodb137-dispatch-shift-receipt', 'Прочитать краткую квитанцию смены', ['Как считаем', 'Количество'],
function (Manager $manager,Collection $ship,Collection $events): array {
$bulk=new BulkWriteCommand();
foreach (['201','202'] as $serial) {
$bulk->insertOne($ship->getNamespace(),shipmentDoc('D-'.$serial));
$bulk->insertOne($events->getNamespace(),dispatchEvent('EV-'.$serial,'D-'.$serial));
}
$result=submitDispatch($manager,$bulk);
$s=$ship->countDocuments(['state'=>'отгружена']);
$e=$events->countDocuments(['kind'=>'dispatched']);
$checks['summary_ack'] = $result->isAcknowledged() && $result->getInsertedCount()===4;
$checks['no_verbose_maps'] = $result->getInsertResults()===null && $result->getUpdateResults()===null && $result->getDeleteResults()===null;
$checks['both_collections_read'] = $s===2 && $e===2;
$rows=[['Добавлено документов по квитанции',$result->getInsertedCount()],['Карточки отгрузки',$s],['События журнала',$e]];
return [$rows,$checks,'Смена оформила 2 отгрузки и 2 события, всего 4 вставки.'];
}
);
Ожидаемый результат
Смена оформила 2 отгрузки и 2 события, всего 4 вставки.
Программа выполняет сценарий на отдельной MongoDB и строит ведомость по подтверждённым результатам и заново прочитанным документам.
Открыть ведомость запуска · Скачать строки ведомости в JSON
Попробуйте самостоятельно. Уберите из входа D-202 с парным событием. Пересчитайте обе прикладные метрики и общий счётчик.
Предлагаемое решение упражнения
Оставшаяся пара D-201/EV-201 даёт 2 вставки, 1 отгрузку и 1 событие. Значение общего счётчика нельзя подписывать «2 отгрузки».
Сопоставить позицию квитанции с коллекцией
Ведомость должна подписать каждую операцию именем коллекции и бизнес-номером. Для этого нужна подробная квитанция.
Входные данные
В D-301 уже есть состояние «на рампе». Четыре действия в общем пакете: обновить D-301, добавить EV-301, добавить D-302, добавить EV-302.
- Включаем verboseResults на объекте команды до её выполнения.
- Разделяем ответы по виду операции, сохраняя индекс в общем исходном списке.
- Каждую запись сопоставляем с её исходным назначением и результатами контрольного чтения.
Главное действие
Ниже сокращённый фрагмент полной программы этого раздела. Подключение, создание собственных коллекций и проверка результата находятся в раскрываемом примере.
$inserts = $result->getInsertResults();
$id = $inserts->get('2')->get('insertedId'); // D-302true включает подробный ответ до отправки. getInsertResults() возвращает BSON Document, а get("2") читает поле с именем исходной позиции. Следующий get("insertedId") извлекает ключ D-302. В полной программе resultEntries() переводит документ в PHP-массив с сохранением ключей, затем foreach сопоставляет их с заранее известным планом. Числовые поля внутри BSON могут быть Int64: smallCount() переводит только маленькие учебные значения в int. Для произвольных 64-битных величин нужен контроль диапазона.
Что получим
| Индекс | Куда писали | Ключ | Итог |
|---|---|---|---|
| 0 | Отгрузки | D-301 | изменено 1 |
| 1 | Журнал | EV-301 | вставлено |
| 2 | Отгрузки | D-302 | вставлено |
| 3 | Журнал | EV-302 | вставлено |
Позиция 2 в карте вставок означает третье действие всего пакета. Отсчёт идёт с нуля. Преобразование в список с array_values() потеряло бы эту связь.
Индекс и _id имеют разные назначения. После формирования другого пакета индексы начнутся заново, ключи документов сохранят свой смысл.
Методы чтения имеют отдельные карточки: BulkWriteCommandResult::getInsertResults() и BulkWriteCommandResult::getUpdateResults().
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/dispatch-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager,BulkWriteCommand,BulkWriteCommandResult};
runDispatch('mongodb137-dispatch-operation-map', 'Сопоставить позицию квитанции с коллекцией', ['Индекс', 'Куда писали', 'Ключ', 'Итог'],
function (Manager $manager,Collection $ship,Collection $events): array {
$ship->insertOne(shipmentDoc('D-301','на рампе'));
$s=$ship->getNamespace(); $e=$events->getNamespace();
$bulk=new BulkWriteCommand(['verboseResults'=>true]);
$bulk->updateOne($s,['_id'=>'D-301'],['$set'=>['state'=>'отгружена']]);
$bulk->insertOne($e,dispatchEvent('EV-301','D-301'));
$bulk->insertOne($s,shipmentDoc('D-302'));
$bulk->insertOne($e,dispatchEvent('EV-302','D-302'));
$result=submitDispatch($manager,$bulk);
$updates=resultEntries($result->getUpdateResults());
$inserts=resultEntries($result->getInsertResults());
$updated=$ship->findOne(['_id'=>'D-301']);
$rows=[[0,'Отгрузки',$updated['_id'],'изменено '.smallCount($updates[0]['modifiedCount'])]];
foreach ([1=>'Журнал',2=>'Отгрузки',3=>'Журнал'] as $position=>$role) {
$rows[]=[$position,$role,$inserts[$position]['insertedId'],'вставлено'];
}
$checks['original_offsets_retained'] = array_keys($updates)===[0] && array_keys($inserts)===[1,2,3];
$checks['actual_target_keys'] = $updated['state']==='отгружена' && $events->findOne(['_id'=>'EV-301'])['shipment']==='D-301' && $ship->findOne(['_id'=>'D-302'])['state']==='отгружена' && $events->findOne(['_id'=>'EV-302'])['shipment']==='D-302';
$checks['detail_matches_summary'] = smallCount($updates[0]['matchedCount'])===$result->getMatchedCount() && $result->getMatchedCount()===1 && smallCount($updates[0]['modifiedCount'])===$result->getModifiedCount() && $result->getInsertedCount()===count($inserts);
return [$rows,$checks,'Позиция 0 изменила отгрузку. Вставки сохранили номера 1, 2 и 3.'];
}
);
Ожидаемый результат
Позиция 0 изменила отгрузку. Вставки сохранили номера 1, 2 и 3.
Программа выполняет сценарий на отдельной MongoDB и строит ведомость по подтверждённым результатам и заново прочитанным документам.
Открыть ведомость запуска · Скачать строки ведомости в JSON
Попробуйте самостоятельно. Поместите обновление между вставками. Найдите «дырку» в числовых ключах карты вставок и назовите отвечающую за неё операцию.
Предлагаемое решение упражнения
Если обновление становится операцией 1, в карте вставок не будет ключа 1. Позиции относятся ко всему пакету, поэтому ключи карты нельзя заново пронумеровать.
Обнаружить событие, для которого переход состояния не состоялся
Инженер объединил перевод заявки в «отгружена» и создание события. На практике заказ может быть отменён к моменту обработки.
Входные данные
В базе D-401 уже отменена. Фильтр обновления разрешает переход лишь со статуса «на рампе». Вторая операция безусловно добавляет строку журнала.
- В первой операции сервер находит ноль совпадений. Это завершившееся обновление, без исключения.
- Упорядоченный пакет доходит до второй записи.
- Подробная квитанция и чтение обеих коллекций выявляют несогласованное событие.
Главное действие
Ниже сокращённый фрагмент полной программы этого раздела. Подключение, создание собственных коллекций и проверка результата находятся в раскрываемом примере.
$bulk->updateOne($shipNs, ['_id'=>'D-401', 'state'=>'на рампе'],
['$set'=>['state'=>'отгружена']]);Два поля в фильтре проверяются вместе: нужен именно D-401 именно в состоянии «на рампе». $set обозначает установку указанного поля. Ноль совпадений завершает updateOne штатно, поэтому ordered=true не останавливает следующий insertOne. Здесь прямые getMatchedCount()/getModifiedCount() и подробные значения первой операции оба равны нулю. Метод не сообщает, почему фильтр не совпал: это выясняет контрольное чтение карточки.
Что получим
| Что проверяем | Результат | Как прочитать |
|---|---|---|
| Условное обновление | 0 | совпадений |
| D-401 | отменена | состояние в базе |
| EV-401 | dispatched | сверить событие |
ordered задаёт порядок, но не превращает вторую запись в условное продолжение первой. Если событие разрешено лишь после фактического перехода, проверьте matchedCount и проектируйте сохранение с нужной атомарностью.
Пример намеренно воспроизводит ошибочную схему в отдельной лаборатории. Надёжная доставка событий и транзакции требуют другой организации записи и подходящего развёртывания MongoDB.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/dispatch-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager,BulkWriteCommand,BulkWriteCommandResult};
runDispatch('mongodb137-dispatch-guarded-transition', 'Обнаружить событие, для которого переход состояния не состоялся', ['Что проверяем', 'Результат', 'Как прочитать'],
function (Manager $manager,Collection $ship,Collection $events): array {
$ship->insertOne(shipmentDoc('D-401','отменена'));
$bulk=new BulkWriteCommand(['ordered'=>true,'verboseResults'=>true]);
$bulk->updateOne($ship->getNamespace(),['_id'=>'D-401','state'=>'на рампе'],['$set'=>['state'=>'отгружена']]);
$bulk->insertOne($events->getNamespace(),dispatchEvent('EV-401','D-401'));
$result=submitDispatch($manager,$bulk);
$detail=resultEntries($result->getUpdateResults());
$matched=smallCount($detail[0]['matchedCount']);
$s=$ship->findOne(['_id'=>'D-401']); $e=$events->findOne(['_id'=>'EV-401']);
$checks['guard_matched_zero'] = $matched===0 && smallCount($detail[0]['modifiedCount'])===0 && $result->getMatchedCount()===0 && $result->getModifiedCount()===0;
$checks['second_write_ran'] = $result->getInsertedCount()===1 && $e['shipment']===$s['_id'];
$checks['original_state_unchanged'] = $s['state']==='отменена';
$rows=[['Условное обновление',$matched,'совпадений'],[$s['_id'],$s['state'],'состояние в базе'],[$e['_id'],$e['kind'],'сверить событие']];
return [$rows,$checks,'D-401 отменена. Условие обновления не совпало, но EV-401 создана.'];
}
);
Ожидаемый результат
D-401 отменена. Условие обновления не совпало, но EV-401 создана.
Программа выполняет сценарий на отдельной MongoDB и строит ведомость по подтверждённым результатам и заново прочитанным документам.
Открыть ведомость запуска · Скачать строки ведомости в JSON
Попробуйте самостоятельно. Задайте исходное состояние «на рампе». Сравните matchedCount, modifiedCount и запись журнала для двух запусков.
Предлагаемое решение упражнения
При исходном состоянии «на рампе» совпадение и изменение равны 1. При состоянии «отменена» оба равны 0; событие в этой намеренно ошибочной схеме вставляется в обоих случаях. Проверку состояния нужно связывать с разрешением самого события.
Отделить сохранённую отгрузку от сбоя журнала
Сервис получил исключение, а отгрузка уже появилась в базе. Пользователю нельзя предлагать повторную отгрузку, пока не установлена судьба первого действия.
Входные данные
Сервер проверяет, что в журнале поле kind равно dispatched. Вторая запись содержит опечатку dispatch_typo. Третья должна была создать следующую отгрузку.
- При включённом ordered сохраняется D-501.
- Валидатор отклоняет EV-501. Индекс 1 и код 121 взяты из ошибки отдельной операции.
- Драйвер прекращает последовательность. Проверяем сохранённое и отсутствующее отдельными чтениями.
Главное действие
Ниже сокращённый фрагмент полной программы этого раздела. Подключение, создание собственных коллекций и проверка результата находятся в раскрываемом примере.
$partial = $error->getPartialResult();
$issues = $error->getWriteErrors();
$code = $issues[1]->getCode(); // 121try передаёт управление в catch только при исключении. Учебная expectEventRejection() принимает лишь одну ошибку нужного индекса с кодом 121; неожиданный отказ выбрасывается дальше. У BulkWriteCommandException есть карта отдельных WriteError. getPartialResult() даёт сохранённую часть квитанции, а ?-> вызывает метод только при ненулевом объекте. Если объект равен null, выражение тоже вернёт null, а не число 0.
Что получим
| Индекс | Ключ | Фактическая судьба |
|---|---|---|
| 0 | D-501 | отгружена |
| 1 | EV-501 | отказ 121 |
| 2 | D-502 | после отказа не выполнялась |
Команда без транзакции допускает частичный результат даже при ordered=true. Один подтверждённый документ остаётся на месте, исключение ссылается на другой документ в другой коллекции.
При ошибке до любых подтверждённых успешных действий getPartialResult() может вернуть null. При сетевом обрыве отсутствие ответа само по себе ещё не устанавливает судьбу записи.
Проверьте границу между отказом записи и ошибкой подтверждения. Продолжение независимых строк показано в следующем примере.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/dispatch-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager,BulkWriteCommand,BulkWriteCommandResult};
runDispatch('mongodb137-dispatch-partial-log', 'Отделить сохранённую отгрузку от сбоя журнала', ['Индекс', 'Ключ', 'Фактическая судьба'],
function (Manager $manager,Collection $ship,Collection $events): array {
$bulk=new BulkWriteCommand(['ordered'=>true,'verboseResults'=>true]);
$bulk->insertOne($ship->getNamespace(),shipmentDoc('D-501'));
$bulk->insertOne($events->getNamespace(),dispatchEvent('EV-501','D-501','dispatch_typo'));
$bulk->insertOne($ship->getNamespace(),shipmentDoc('D-502'));
$error=expectEventRejection($manager,$bulk,1);
$partial=$error->getPartialResult();
$details=resultEntries($partial?->getInsertResults());
$s=$ship->findOne(['_id'=>'D-501']);
$e=$events->findOne(['_id'=>'EV-501']);
$later=$ship->findOne(['_id'=>'D-502']);
$checks['first_persisted'] = $s['state']==='отгружена' && $partial?->getInsertedCount()===1 && ($details[0]['insertedId'] ?? null)===$s['_id'];
$checks['event_refused'] = $error->getWriteErrors()[1]->getCode()===121 && $e===null;
$checks['ordered_tail_skipped'] = $later===null && array_keys($details)===[0];
$rows=[[0,$s['_id'],$s['state']],[1,'EV-501',$e===null?'отказ 121':'есть'],[2,'D-502',$later===null?'после отказа не выполнялась':'есть']];
return [$rows,$checks,'D-501 сохранилась, EV-501 отклонена, D-502 не выполнялась.'];
}
);
Ожидаемый результат
D-501 сохранилась, EV-501 отклонена, D-502 не выполнялась.
Программа выполняет сценарий на отдельной MongoDB и строит ведомость по подтверждённым результатам и заново прочитанным документам.
Открыть ведомость запуска · Скачать строки ведомости в JSON
Попробуйте самостоятельно. Исправьте kind в копии программы и проследите, доходит ли пакет до D-502. В уже обработанной поставке не отправляйте весь прежний пакет повторно.
Предлагаемое решение упражнения
С kind=dispatched все три операции этой новой лабораторной партии выполняются. В рабочем процессе это не разрешение повторять прежнюю отгрузку: уже сохранённые действия сначала устанавливают чтением.
После ошибки большой вставки автор хочет повторять документы по одному и не знает, что уже сохранилось. Здесь первый документ реально сохраняется, а валидатор отклоняет второй в другой коллекции: перед исправлением нужно установить судьбу каждого действия. Практический вопрос: is PyMongo / MongoDB insert_many transactional?. Контракт PHP API и MongoDB проверяется по официальным источникам этой главы; синтаксис старого Java/Python-драйвера сюда не переносится.
Продолжить независимую обработку после ошибочного события
Очередь содержит две независимые заявки. Ошибка формата журнала у одной не должна скрывать результат обработки другой.
Входные данные
План из четырёх операций: пара D-601/EV-601 и пара D-602/EV-602. Только EV-601 содержит недопустимый kind.
- Отключаем ordered, чтобы ошибка одной позиции не останавливала оставшиеся независимые попытки.
- Из частичной квитанции извлекаем исходные индексы успешных вставок и выясняем индекс отказа.
- Читаем каждую пару. На экране одна строка с расхождением и одна полностью оформленная.
Главное действие
Ниже сокращённый фрагмент полной программы этого раздела. Подключение, создание собственных коллекций и проверка результата находятся в раскрываемом примере.
$batch = new BulkWriteCommand(['ordered'=>false, 'verboseResults'=>true]);false отключает остановку на первой ошибке отдельной записи. Ошибки всё равно приводят к исключению после попыток обработки остальных операций; успешные результаты берём из partialResult. ksort() здесь упорядочивает ключи полученной карты лишь для проверки состава индексов, а не управляет сервером. Два findOne() на каждую пару показывают реальное состояние после команды.
Что получим
| Отгрузка | Карточка | Журнал | Действие |
|---|---|---|---|
| D-601 | отгружена | нет | Исправить журнал |
| D-602 | отгружена | EV-602 | Готово |
Операции не зависят от чтения предыдущего результата. Их номера сохраняются для разбора, но не сообщают порядок фактического выполнения.
Связь в этом опыте задана идентификаторами, внешнего ключа MongoDB здесь нет. Unordered не обеспечивает атомарность пары и не предназначен для исполнения зависимых шагов по порядку.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/dispatch-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager,BulkWriteCommand,BulkWriteCommandResult};
runDispatch('mongodb137-dispatch-independent-rows', 'Продолжить независимую обработку после ошибочного события', ['Отгрузка', 'Карточка', 'Журнал', 'Действие'],
function (Manager $manager,Collection $ship,Collection $events): array {
$bulk=new BulkWriteCommand(['ordered'=>false,'verboseResults'=>true]);
$bulk->insertOne($ship->getNamespace(),shipmentDoc('D-601'));
$bulk->insertOne($events->getNamespace(),dispatchEvent('EV-601','D-601','wrong'));
$bulk->insertOne($ship->getNamespace(),shipmentDoc('D-602'));
$bulk->insertOne($events->getNamespace(),dispatchEvent('EV-602','D-602'));
$error=expectEventRejection($manager,$bulk,1);
$partial=$error->getPartialResult();
$success=resultEntries($partial?->getInsertResults()) ?? [];
ksort($success);
$rows=[];
foreach ([['D-601','EV-601'],['D-602','EV-602']] as [$id,$eventId]) {
$s=$ship->findOne(['_id'=>$id]);
$e=$events->findOne(['_id'=>$eventId]);
$rows[]=[$id,$s['state'] ?? 'нет',$e['_id'] ?? 'нет',$e===null?'Исправить журнал':'Готово'];
}
$checks['successful_original_indexes'] = array_keys($success)===[0,2,3] && $partial?->getInsertedCount()===3;
$checks['one_error_at_one'] = array_keys($error->getWriteErrors())===[1];
$checks['independent_row_complete'] = $events->findOne(['_id'=>'EV-602'])['shipment']==='D-602' && $ship->countDocuments([])===2 && $events->countDocuments([])===1;
return [$rows,$checks,'D-602 оформлена полностью. Для D-601 нужно исправить только журнал.'];
}
);
Ожидаемый результат
D-602 оформлена полностью. Для D-601 нужно исправить только журнал.
Программа выполняет сценарий на отдельной MongoDB и строит ведомость по подтверждённым результатам и заново прочитанным документам.
Открыть ведомость запуска · Скачать строки ведомости в JSON
Попробуйте самостоятельно. Поменяйте ошибочную строку у второй пары. Проверьте, какие входные индексы придут в getWriteErrors(), а какие попадут в подробную квитанцию.
Предлагаемое решение упражнения
Если ошибка находится только в EV-602, индекс отказа равен 3, успешные индексы равны 0, 1 и 2. Unordered сохраняет номера для сопоставления, но не гарантирует порядок обработки.
Вопрос смешивает порядок обработки и возможность повторных попыток. В нашей лаборатории unordered продолжает остальные независимые действия после ошибки строки; автоматических повторов нет, retryWrites=false и standalone зафиксированы явно. Практический вопрос: MongoDB retryable writes in unordered bulk operation. Контракт PHP API и MongoDB проверяется по официальным источникам этой главы; синтаксис старого Java/Python-драйвера сюда не переносится.
Дополнить только отсутствующее событие после сверки
После сверки сотрудник разрешил исправить только журнал. Состояние отгрузки и фактический факт отправки уже подтверждены.
Входные данные
Первый пакет действительно сохраняет D-701 и получает код 121 на EV-701. До исправления программа читает статус D-701 и убеждается в отсутствии записи с нужным ключом.
- Для разрешённого исправления создаём новый пакет с одной операцией по коллекции событий.
- Фильтр учитывает _id, номер отгрузки и вид события. При отсутствии совпадения upsert вставляет документ; $setOnInsert применяется только при вставке.
- Подтверждаем идентификатор в подробном результате обновления и перечитываем обе коллекции.
Главное действие
Ниже сокращённый фрагмент полной программы этого раздела. Подключение, создание собственных коллекций и проверка результата находятся в раскрываемом примере.
$key = ['_id'=>'EV-701', 'shipment'=>'D-701', 'kind'=>'dispatched'];
$repair->updateOne($eventsNs, $key, ['$setOnInsert'=>['source'=>'manual-reconciliation']], ['upsert'=>true]);approvedRepair — заранее заданный учебный вход, который представляет отдельное решение сотрудника исправить EV-701 для D-701. Это не результат автоматического обработчика исключения. Проверки before выполняются непосредственно перед новым пакетом. В фильтре updateOne перечислены _id, shipment и kind; upsert=true создаёт документ при отсутствии совпадения. $setOnInsert добавляет source только при создании. В команду не входит ни insert, ни update коллекции отгрузок.
Что получим
| Когда | Отгрузка | Событие |
|---|---|---|
| До сверки | D-701 | нет |
| После исправления | D-701 | EV-701 |
Upsert, который создал документ, попадает в getUpsertedCount() и карту getUpdateResults(). Повтор идентичного запроса при неизменном документе нашёл бы существующий ключ и не вставил бы второй экземпляр. Конфликт с чужим содержимым под тем же _id требует разбирательства.
Этот обработчик требует явного решения оператора и сверки состояния. Одно чтение не блокирует параллельную отмену отгрузки. Для атомарной проверки разных документов нужно проектировать транзакцию на подходящем развёртывании или хранить подлежащее подтверждению событие вместе с состоянием.
Для большой рабочей системы решение сотрудника должно приходить из отдельного проверенного действия интерфейса. Фиксированный approvedRepair здесь только делает такой вход явным. При параллельной работе чтение и исправление не образуют транзакцию; подробнее — атомарное изменение нескольких документов.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/dispatch-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager,BulkWriteCommand,BulkWriteCommandResult};
runDispatch('mongodb137-dispatch-repair-event', 'Дополнить только отсутствующее событие после сверки', ['Когда', 'Отгрузка', 'Событие'],
function (Manager $manager,Collection $ship,Collection $events): array {
$first=new BulkWriteCommand(['ordered'=>true]);
$first->insertOne($ship->getNamespace(),shipmentDoc('D-701'));
$first->insertOne($events->getNamespace(),dispatchEvent('EV-701','D-701','bad-kind'));
expectEventRejection($manager,$first,1);
// A fixed teaching input represents a separately approved reconciliation, not an automatic retry.
$approvedRepair=['shipment'=>'D-701','event'=>'EV-701'];
$shipBefore=$ship->findOne(['_id'=>'D-701']);
$eventBefore=$events->findOne(['_id'=>'EV-701']);
if ($approvedRepair!==['shipment'=>'D-701','event'=>'EV-701'] || $shipBefore===null || $shipBefore['state']!=='отгружена' || $eventBefore!==null) {
throw new RuntimeException('Repair precondition failed');
}
// The journal has its own stable key. A conflicting payload with that key cannot be overwritten.
$repair=new BulkWriteCommand(['verboseResults'=>true]);
$key=dispatchEvent('EV-701','D-701');
$repair->updateOne($events->getNamespace(),$key,['$setOnInsert'=>['source'=>'manual-reconciliation']],['upsert'=>true]);
$report=submitDispatch($manager,$repair);
$detail=resultEntries($report->getUpdateResults());
$shipAfter=$ship->findOne(['_id'=>'D-701']);
$eventAfter=$events->findOne(['_id'=>'EV-701']);
$checks['repaired_one_event'] = count($repair)===1 && $report->getUpsertedCount()===1 && $report->getInsertedCount()===0 && ($detail[0]['upsertedId'] ?? null)==='EV-701';
$checks['original_untouched'] = $shipAfter===$shipBefore && $ship->countDocuments([])===1;
$checks['correct_relationship'] = $eventAfter['shipment']==='D-701' && $eventAfter['kind']==='dispatched' && $events->countDocuments([])===1;
$rows=[['До сверки',$shipBefore['_id'],$eventBefore['_id'] ?? 'нет'],['После исправления',$shipAfter['_id'],$eventAfter['_id']]];
return [$rows,$checks,'Для существующей D-701 добавлена EV-701. Отгрузка осталась в одном экземпляре.'];
}
);
Ожидаемый результат
Для существующей D-701 добавлена EV-701. Отгрузка осталась в одном экземпляре.
Программа выполняет сценарий на отдельной MongoDB и строит ведомость по подтверждённым результатам и заново прочитанным документам.
Открыть ведомость запуска · Скачать строки ведомости в JSON
Попробуйте самостоятельно. Подготовьте событие с тем же _id, но другим shipment. Предварительная проверка должна запретить автоматическое исправление.
Предлагаемое решение упражнения
Событие с ключом EV-701 и другим shipment не является отсутствующим. eventBefore!==null останавливает исправление до нового пакета. В рабочей очереди такую пару передают на ручную сверку содержания.
Автор ожидает список upsert и при изменении уже существующего документа. В нашем восстановлении журнала отдельно читаем getUpsertedCount и upsertedId: они относятся к созданию отсутствующего события, а не ко всем обновлениям. Практический вопрос: mongo BulkWriteOperation with upsert option doesn't return upserted entries for updates. Контракт PHP API и MongoDB проверяется по официальным источникам этой главы; синтаксис старого Java/Python-драйвера сюда не переносится.
Составить ведомость перед закрытием смены
Перед закрытием смены диспетчер проверяет четыре ожидаемых задания. Одна общая цифра вставок не укажет, какой именно заказ можно закрыть.
Входные данные
Исходный список содержит D-801…D-804 и ожидаемые ключи событий. В учебной базе созданы обе стороны для 801, только карточка для 802, только событие для 803. Для 804 совпадений нет.
- Два запроса по спискам ключей читают лишь поля состояния и связи.
- Для каждого исходного задания проверяем обе стороны, правильность shipment и значение kind.
- Сохраняем четыре разных решения в порядке исходной ведомости. Просмотр не дописывает недостающие документы.
Главное действие
Ниже сокращённый фрагмент полной программы этого раздела. Подключение, создание собственных коллекций и проверка результата находятся в раскрываемом примере.
$s = $sIndex[$id] ?? null;
$e = $eIndex[$eventIds[$i]] ?? null;В начале программы четыре вставки подготавливают разные исходные случаи. После комментария об окончании подготовки экран выполняет только чтения. Оператор $in ограничивает запрос списком нужных ключей; projection выбирает полезные поля, _id остаётся доступен. PHP-массивы sIndex и eIndex становятся справочниками по _id. Выражение ?? null даёт null для отсутствующей записи. match(true) берёт первое подходящее условие сверху вниз, поэтому сначала проверяется отсутствие обеих сторон, затем состояние и связь.
Что получим
| Номер задания | Отгрузка | Событие | Действие |
|---|---|---|---|
| D-801 | отгружена | EV-801 | Готово |
| D-802 | отгружена | нет | Добавить запись в журнал |
| D-803 | нет | EV-803 | Разобрать связь вручную |
| D-804 | нет | нет | Проверить исходное задание |
Действие «Добавить запись в журнал» предлагает оператору перейти к отдельной сверке. Одно событие без исходной отгрузки не даёт оснований объявлять, что товар отправлен. У отсутствующих обеих сторон сначала проверяют саму задачу отправки.
Список относится к небольшой закрываемой смене. Два чтения показаны на неизменяемом во время сверки наборе; на работающем складе параллельные изменения могут потребовать ещё одной сверки или согласованного чтения.
Ведомость использует четыре строки, а не четыре итоговых счётчика. Цвет дублируется текстом действия. На узком экране колонки превращаются в подписанные карточки. В этом экране нет кнопки, автоматически повторяющей отправку; отдельный сценарий добавления недостающего события начинается с решения и повторной проверки.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/dispatch-lab.php';
use MongoDB\Collection;
use MongoDB\Driver\{Manager,BulkWriteCommand,BulkWriteCommandResult};
runDispatch('mongodb137-dispatch-dispatch-desk', 'Составить ведомость перед закрытием смены', ['Номер задания', 'Отгрузка', 'Событие', 'Действие'],
function (Manager $manager,Collection $ship,Collection $events): array {
$bulk=new BulkWriteCommand();
$bulk->insertOne($ship->getNamespace(),shipmentDoc('D-801'));
$bulk->insertOne($events->getNamespace(),dispatchEvent('EV-801','D-801'));
$bulk->insertOne($ship->getNamespace(),shipmentDoc('D-802'));
$bulk->insertOne($events->getNamespace(),dispatchEvent('EV-803','D-803'));
$written=submitDispatch($manager,$bulk);
// Fixture preparation ends here. The following screen only reads the two collections.
$beforeScreen=[$ship->find([],['sort'=>['_id'=>1]])->toArray(),$events->find([],['sort'=>['_id'=>1]])->toArray()];
$ids=['D-801','D-802','D-803','D-804'];
$eventIds=['EV-801','EV-802','EV-803','EV-804'];
$sIndex=[]; $eIndex=[];
foreach ($ship->find(['_id'=>['$in'=>$ids]],['projection'=>['state'=>1]]) as $doc) { $sIndex[$doc['_id']]=$doc; }
foreach ($events->find(['_id'=>['$in'=>$eventIds]],['projection'=>['shipment'=>1,'kind'=>1]]) as $doc) { $eIndex[$doc['_id']]=$doc; }
$rows=[];
foreach ($ids as $i=>$id) {
$s=$sIndex[$id] ?? null; $e=$eIndex[$eventIds[$i]] ?? null;
$decision=match(true) {
$s===null && $e===null=>'Проверить исходное задание',
$s===null || $s['state']!=='отгружена'=>'Разобрать связь вручную',
$e===null=>'Добавить запись в журнал',
$e['shipment']!==$id || $e['kind']!=='dispatched'=>'Разобрать связь вручную',
default=>'Готово',
};
$rows[]=[$id,$s['state'] ?? 'нет',$e['_id'] ?? 'нет',$decision];
}
$checks['seed_receipt_matches_actual'] = $written->getInsertedCount()===4 && count($sIndex)===2 && count($eIndex)===2;
$checks['four_distinct_decisions'] = count(array_unique(array_column($rows,3)))===4;
$afterScreen=[$ship->find([],['sort'=>['_id'=>1]])->toArray(),$events->find([],['sort'=>['_id'=>1]])->toArray()];
$checks['no_writes_by_screen'] = $beforeScreen===$afterScreen && count($afterScreen[0])===2 && count($afterScreen[1])===2;
return [$rows,$checks,'Из четырёх заданий одно подтверждено обеими записями, три требуют разных действий.'];
}
);
Ожидаемый результат
Из четырёх заданий одно подтверждено обеими записями, три требуют разных действий.
Программа выполняет сценарий на отдельной MongoDB и строит ведомость по подтверждённым результатам и заново прочитанным документам.
Открыть ведомость запуска · Скачать строки ведомости в JSON
Попробуйте самостоятельно. Задайте EV-801 связь с D-802. Проверьте, что строка 801 больше не считается полностью подтверждённой, хотя оба ключа существуют.
Предлагаемое решение упражнения
Строка D-801 покажет «Разобрать связь вручную»: EV-801 найдена по ключу, но её shipment указывает на D-802. Проверка двух существующих ключей без проверки содержимого связи недостаточна.
Как выбрать размер партии и следующий способ записи
Пакет уменьшает число отдельных отправок приложением. Большая партия может быть разделена драйвером по ограничениям размера; подробные карты требуют ответа и памяти для каждой успешной операции. Для итоговой цифры смены достаточно общих счётчиков, для построчного расследования полезен verboseResults. Объём, время и память измеряют на реальных размерах документов; режим unordered сам по себе не обещает ускорения.
Успешные отдельные записи, подтверждение сервера и прикладная согласованность — три разные проверки. Ошибка валидатора говорит, какой документ отклонён. Ошибка подтверждения или потерянный ответ могут оставить судьбу действия неизвестной. Не превращайте любой catch в повтор: сначала сохраните исходный план, индексы и ключи, затем установите состояние каждой записи.
Retryable writes — отдельная возможность драйвера и подходящего replica set/sharded развёртывания; на standalone она не поддерживается. В этой лаборатории retryWrites=false задан явно. Режим ordered влияет на порядок и продолжение после ошибки строки, а решение о ручном восстановлении остаётся отдельным действием.
Если нужна одна коллекция, вернитесь к обычной очереди BulkWrite. Если нужен общий атомарный переход нескольких документов, продолжите транзакциями. Если важна доставка события во внешнюю систему, отдельное сохранённое событие ещё не доказывает, что внешний получатель его принял: нужен свой учёт доставки и согласованный ключ операции.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
MongoDB\Driver\BulkWriteCommandСобрать вставки, обновления и удаления для нескольких коллекций с явным порядком и подробными результатами.
ext-mongodb 2.1.0+; MongoDB Server 8.0+ · mongodb
final class MongoDB\Driver\BulkWriteCommand implements CountableКладовщику нужно сохранить D-101 в отгрузках и EV-101 в журнале. Этот объект хранит адрес и содержание каждого действия, пока Manager не отправит их в MongoDB. Он относится к клиентской команде нескольких коллекций; обычный BulkWrite предназначен для одного namespace.
- BulkWriteCommand: позиция 0 → shipments → D-101
- Тот же объект: позиция 1 → events → EV-101 со ссылкой на D-101
- Manager отправляет обе инструкции
- BulkWriteCommandResult даёт итоги; отдельные чтения проверяют связь
Класс final запрещает наследование. Интерфейс Countable позволяет передать объект в count($bulk). Ни добавление операции, ни подсчёт ещё не отправляют её на сервер. Требуются расширение mongodb версии 2.1.0+ и сервер MongoDB 8.0+. Composer-библиотека помогает создавать объекты коллекций и получать их namespace, но сама команда и Manager принадлежат расширению.
Как создать команду и выбрать поведение
$bulk = new MongoDB\Driver\BulkWriteCommand([
'ordered' => true,
'verboseResults' => true,
]);Это фрагмент настройки: конструктор принимает ?array $options = null. null или пропущенный аргумент выбирают значения по умолчанию. Пять доступных параметров относятся ко всему объекту команды; writeConcern и session передаются отдельно при выполнении.
| Опция и тип | Смысл для программы | По умолчанию |
|---|---|---|
| ordered: bool | true выполняет действия по порядку и останавливает оставшиеся после ошибки отдельной записи. false допускает произвольный порядок и продолжение независимых действий. | true |
| verboseResults: bool | Добавляет подробные карты успешных операций к итоговым счётчикам. Используйте, если нужно связать позицию и результат. | false |
| bypassDocumentValidation: bool | Разрешает обход проверки документов при вставке и обновлении. В учебной сверке оставляем проверку действующей. | false |
| comment: mixed | Метка для поиска команды в profiler, currentOp и журналах; избегайте конфиденциальных данных. | Не задана |
| let: array|object | Именованные постоянные значения или замкнутые выражения без ссылок на поля документа; в выражениях MongoDB доступны как $$имя. | Не задан |
Общая возможность let появилась у сервера в 5.0, collation — в 3.4; эта команда всё равно требует минимум 8.0. Выбор ordered не вводит условие «выполнить событие, только если обновление совпало» и не обеспечивает атомарность нескольких документов.
Все методы: что передать и что вернётся
| Метод и сигнатура | Вход и результат |
|---|---|
BulkWriteCommand::__construct()__construct(?array $options = null) | Создаёт пустой объект с настройками выше; возвращаемого значения метода нет. |
BulkWriteCommand::count()count(): int | Количество добавленных инструкций. Один updateMany считается одной инструкцией независимо от количества затронутых документов. |
BulkWriteCommand::insertOne()insertOne(string $namespace, array|object $document): mixed | Добавляет вставку. Возвращает заданный _id или созданный ObjectId, если _id не указан. Полученный ключ ещё не подтверждает сохранение. |
BulkWriteCommand::updateOne()updateOne(string $namespace, array|object $filter, array|object $update, ?array $options = null): void | Изменяет не больше одного совпавшего документа. update содержит операторы вроде $set либо конвейер обновления. |
BulkWriteCommand::updateMany()updateMany(string $namespace, array|object $filter, array|object $update, ?array $options = null): void | Применяет изменение ко всем совпадениям. При upsert и отсутствии совпадений создаёт один документ. |
BulkWriteCommand::replaceOne()replaceOne(string $namespace, array|object $filter, array|object $replacement, ?array $options = null): void | Заменяет один выбранный документ содержимым replacement. Это полный документ замены, а не набор операторов обновления. |
BulkWriteCommand::deleteOne()deleteOne(string $namespace, array|object $filter, ?array $options = null): void | Планирует удаление одного совпадения. Если его нет, число удалённых равно нулю. |
BulkWriteCommand::deleteMany()deleteMany(string $namespace, array|object $filter, ?array $options = null): void | Планирует удаление всех совпавших документов. Пустой filter подходит всем документам коллекции. |
Namespace — строка вида база.коллекция, например php_manual_lab.shipments. В готовых программах адрес извлекается из собственного объекта Collection через getNamespace(). filter задаёт условия выбора, document — поля новой записи, replacement — полный новый набор полей заменяемой записи. MongoDB сравнивает BSON-типы по своим правилам: строка с текстом ObjectId не заменяет экземпляр ObjectId. Подробнее — представление и чтение BSON.
Для updateOne/updateMany документ изменения должен содержать операторы обновления; обычный документ предназначен для replaceOne. Замена убирает не перечисленные в replacement поля, при этом нельзя произвольно поменять неизменяемый _id. Пустой фильтр в методе One выбирает один подходящий документ, в Many — все; когда нужен определённый заказ, передайте его ключ.
Параметры отдельных действий
| Параметр | Где применяется | Правило |
|---|---|---|
| upsert: bool | updateOne, updateMany, replaceOne | По умолчанию false. Если совпадений нет, создаёт один документ: для update — из равенств filter и изменения; для replace — из replacement. |
| arrayFilters: array | updateOne, updateMany | Документы условий для выбора элементов вложенного массива, которые изменяются оператором обновления. Это не фильтр самих отгрузок. |
| collation: array|object | updateOne, updateMany, replaceOne, deleteOne, deleteMany | Правила сравнения строк, например регистра. locale обязателен; прочие поля необязательны. Если опция отсутствует, действует collation коллекции; если и его нет — простое бинарное сравнение. |
| hint: string|array|object | updateOne, updateMany, replaceOne, deleteOne, deleteMany | Имя индекса либо документ его ключей. Ограничивает выбор плана этим индексом; неверный индекс может привести к отказу. |
| sort: array|object | updateOne, replaceOne | Порядок выбора одного документа при нескольких совпадениях. Для однозначности добавьте уникальный ключ в порядок. У Many и deleteOne такой опции здесь нет. |
Пример 1. Пара отгрузка — событие
$batch = new BulkWriteCommand();
$batch->insertOne($ordersNamespace, ['_id' => 'D-101', 'state' => 'отгружена']);
$batch->insertOne($eventsNamespace, ['_id' => 'EV-101', 'shipment' => 'D-101', 'kind' => 'dispatched']);
$result = $manager->executeBulkWriteCommand($batch);Это сокращение полной самостоятельной программы. До отправки в очереди 2 операции и по 0 документов в коллекциях; после успешной отправки найдены D-101/«отгружена» и EV-101/shipment=D-101. Два namespace передаются отдельным операциям, а не как один аргумент Manager.
Пример 2. Исправить одну сторону подтверждённой пары
$key = ['_id'=>'EV-701', 'shipment'=>'D-701', 'kind'=>'dispatched'];
$repair->updateOne($eventsNs, $key, ['$setOnInsert'=>['source'=>'manual-reconciliation']], ['upsert'=>true]);Это фрагмент отдельного исправления журнала после проверки D-701 и отсутствия EV-701. Новый пакет содержит только одно обновление events с upsert. Результат: создана EV-701, D-701 остаётся в единственном экземпляре с прежним содержимым. Непроверенная повторная отправка старого пакета не участвует в этом алгоритме.
Отправка, ошибки и границы
Manager::executeBulkWriteCommand() принимает BulkWriteCommand $bulk и ?array $options = null, выполняет записи на primary и возвращает BulkWriteCommandResult. В options допускаются session (Driver Session) и writeConcern (Driver WriteConcern). Значение writeConcern выводится из активной транзакции, затем из URI; в программе submitDispatch явно задаёт w=1. Одна передача объекта может потребовать нескольких сетевых команд из-за размеров партии.
Некорректные аргументы методов приводят к InvalidArgumentException. Отправка пустого либо уже выполненного объекта также недопустима: для нового решения нужен новый объект. Session нельзя сочетать с неподтверждённой записью. При w=0 одновременно нельзя требовать ordered=true или verboseResults=true. Сбой соединения даёт ConnectionException, отказ аутентификации — AuthenticationException, ошибки выполнения записи — BulkWriteCommandException. Сначала установите слой отказа и сохранённые результаты; исключение не отменяет ранее записанные документы.
Подтверждение w=1 означает ответ сервера, но не факт физической отправки товара и не согласованность двух коллекций. Если бизнес-правило требует «обе стороны вместе либо ни одной», изучите транзакции и требования к развёртыванию или объединение связанного состояния в одном документе. Здесь standalone и отсутствие транзакции выбраны для наблюдения частичного результата.
- PHP Manual — BulkWriteCommand
- PHP Manual — BulkWriteCommand::__construct
- PHP Manual — BulkWriteCommand::count
- PHP Manual — BulkWriteCommand::insertOne
- PHP Manual — BulkWriteCommand::updateOne
- PHP Manual — BulkWriteCommand::updateMany
- PHP Manual — BulkWriteCommand::replaceOne
- PHP Manual — BulkWriteCommand::deleteOne
- PHP Manual — BulkWriteCommand::deleteMany
MongoDB\Driver\BulkWriteCommandResultПрочитать подтверждение, общие счётчики и карты успешных операций клиентского пакета.
ext-mongodb 2.1.0+; MongoDB Server 8.0+ · mongodb
final class MongoDB\Driver\BulkWriteCommandResultПосле отправки пакета диспетчеру нужны два вида ответа: сколько документов обработано и какая запись относится к конкретному заданию. BulkWriteCommandResult возвращает Manager после выполнения команды; создавать квитанцию через new в прикладном коде не требуется. Это final-класс без публичных полей в synopsis. Его методы читают уже полученный результат, а не заново запрашивают текущие документы.
- Ответ команды → isAcknowledged()
- Общие счётчики → итог смены: 4 вставки
- Подробные карты → индекс операции → исходный D-301 или EV-301
- Чтение обеих коллекций → состояние и правильность связи
- Решение → закрыть задание или направить на отдельную сверку
Все девять методов без аргументов
| Метод и тип результата | Что сообщает и чего не означает |
|---|---|
| BulkWriteCommandResult::isAcknowledged() → bool | Есть ли подтверждённая квитанция. true не гарантирует атомарность пары и не сообщает, что событие относится к нужной отгрузке. |
| BulkWriteCommandResult::getInsertedCount() → int | Сумма документов прямых вставок по всем namespace. Создания через upsert в этот счётчик не включаются. |
| BulkWriteCommandResult::getMatchedCount() → int | Сумма совпадений при обновлениях. Уже актуальный документ тоже подходит фильтру. |
| BulkWriteCommandResult::getModifiedCount() → int | Сумма фактических изменений существующих документов. Установка уже текущего значения даёт 0 изменений. |
| BulkWriteCommandResult::getUpsertedCount() → int | Сколько документов создано операциями обновления или замены с upsert. Обновление существующего документа сюда не относится. |
| BulkWriteCommandResult::getDeletedCount() → int | Сумма реально удалённых документов. Одна deleteMany может дать много удалений. |
| BulkWriteCommandResult::getInsertResults() → ?MongoDB\BSON\Document | Карта успешных прямых вставок: исходная позиция → insertedId. Без verboseResults возвращает null. |
| BulkWriteCommandResult::getUpdateResults() → ?MongoDB\BSON\Document | Карта успешных обновлений и замен: matchedCount, modifiedCount и, когда создан документ, upsertedId. Без verboseResults возвращает null. |
| BulkWriteCommandResult::getDeleteResults() → ?MongoDB\BSON\Document | Карта успешных удалений: исходная позиция → deletedCount. Без verboseResults возвращает null. |
Для методов чтения счётчиков и карт неподтверждённая запись вызывает LogicException. isAcknowledged() проверяйте прежде, чем обращаться к этим сведениям. Неверные аргументы приводят к InvalidArgumentException. В примерах w=1 задан явно, поэтому подтверждение проверяется как часть условий запуска.
Сумма и карта отвечают на разные вопросы
Все счётчики складывают результаты отдельных действий. Они не являются числом уникальных бизнес-заданий: два обновления одной отгрузки могут учесть её дважды. matched и modified описывают пересекающиеся свойства обновлений; складывать их в число совершённых действий нельзя. Для одного updateOne без upsert полезны три случая:
| Состояние до команды | matched / modified | Вывод для человека |
|---|---|---|
| Нужного ключа или ожидаемого статуса нет | 0 / 0 | Фильтр не совпал; выяснить причину чтением. |
| Документ подходит, новое значение уже хранится | 1 / 0 | Документ найден и уже актуален. |
| Документ подходит, значение действительно меняется | 1 / 1 | Документ обновлён. |
Для разбора этих трёх исходов на товарах откройте проверку результата установки остатка. Для команды нескольких коллекций условный переход D-401 показывает ноль совпадений без исключения, после которого всё же выполняется другая операция.
Подробные карты содержат только успешные операции соответствующего вида. Их ключи сохраняют позиции во всём исходном пакете, начиная с 0. Если операция 0 была обновлением, первые ключи карты вставок могут быть 1, 2, 3. Отсутствие ключа в одной такой карте само по себе не означает ошибку: операция могла относиться к другому виду или не выполняться после остановки ordered. Сверяйте карту с планом и ошибками.
BSON Document — контейнер именованных полей. Чтобы прочитать позицию, используйте get("2"), затем поле вложенного документа. При toPHP задайте осмысленную карту типов. В текущем API числовые подробности представлены BSON Int64, тогда как общие методы счётчиков возвращают PHP int. Наш smallCount() рассчитан только на заведомо небольшую учебную партию. Подробнее — чтение BSON Document.
Пример 1. Четыре документа, две отгрузки
$allInserted = $result->getInsertedCount();
$sent = $ship->countDocuments(['state' => 'отгружена']);В полной квитанции смены allInserted равно 4, а sent равно 2: по одному событию и одной карточке на задание. verboseResults оставлен выключенным, поэтому getInsertResults(), getUpdateResults() и getDeleteResults() возвращают null. Это отсутствие запрошенной детализации, а не отсутствие сохранённых записей.
Пример 2. Выяснить ключ третьего действия
$inserts = $result->getInsertResults();
$id = $inserts->get('2')->get('insertedId'); // D-302В программе с общей картой позиций включён verboseResults. Операция 0 изменила D-301, вставки 1/2/3 создали EV-301/D-302/EV-302. get("2") находит D-302. Сохранённое состояние и ссылки каждой записи дополнительно читаются из соответствующей коллекции.
Что делать с исключением и частичной квитанцией
$partial = $error->getPartialResult();
$issues = $error->getWriteErrors();
$code = $issues[1]->getCode(); // 121В опыте с ошибкой журнала D-501 уже записана, индекс 1 отклонён с кодом 121, индекс 2 не исполнялся. BulkWriteCommandException::getPartialResult() возвращает BulkWriteCommandResult либо null, если нельзя установить хотя бы одну успешную подтверждённую операцию. null не следует превращать в утверждение «ничего не записано», особенно после сетевого обрыва. BulkWriteCommandException::getWriteErrors() возвращает карту WriteError с индексами исходного пакета; у ordered отдельная ошибка в ней не более одной.
Код отдельной WriteError нельзя заменять общим кодом исключения. В случаях ошибок команды и подтверждения смотрите также BulkWriteCommandException::getErrorReply() и BulkWriteCommandException::getWriteConcernErrors(). Выбор последующего действия основан на сохранённых ключах и содержимом, а не на автоматическом повторе всего пакета. Восстановление события демонстрирует отдельное разрешённое исправление, ведомость смены — чтение без исправлений.
- PHP Manual — BulkWriteCommandResult
- PHP Manual — BulkWriteCommandResult::isAcknowledged
- PHP Manual — BulkWriteCommandResult::getInsertedCount
- PHP Manual — BulkWriteCommandResult::getMatchedCount
- PHP Manual — BulkWriteCommandResult::getModifiedCount
- PHP Manual — BulkWriteCommandResult::getUpsertedCount
- PHP Manual — BulkWriteCommandResult::getDeletedCount
- PHP Manual — BulkWriteCommandResult::getInsertResults
- PHP Manual — BulkWriteCommandResult::getUpdateResults
- PHP Manual — BulkWriteCommandResult::getDeleteResults
Источники и обсуждения
- PHP Manual — BulkWriteCommand
- PHP Manual — BulkWriteCommand::__construct
- PHP Manual — BulkWriteCommand::count
- PHP Manual — BulkWriteCommand::insertOne
- PHP Manual — BulkWriteCommand::updateOne
- PHP Manual — BulkWriteCommand::updateMany
- PHP Manual — BulkWriteCommand::replaceOne
- PHP Manual — BulkWriteCommand::deleteOne
- PHP Manual — BulkWriteCommand::deleteMany
- PHP Manual — Manager::executeBulkWriteCommand
- PHP Manual — BulkWriteCommandResult
- PHP Manual — BulkWriteCommandResult::isAcknowledged
- PHP Manual — BulkWriteCommandResult::getInsertedCount
- PHP Manual — BulkWriteCommandResult::getMatchedCount
- PHP Manual — BulkWriteCommandResult::getModifiedCount
- PHP Manual — BulkWriteCommandResult::getUpsertedCount
- PHP Manual — BulkWriteCommandResult::getDeletedCount
- PHP Manual — BulkWriteCommandResult::getInsertResults
- PHP Manual — BulkWriteCommandResult::getUpdateResults
- PHP Manual — BulkWriteCommandResult::getDeleteResults
- PHP Manual — BulkWriteCommandException::getPartialResult
- PHP Manual — BulkWriteCommandException::getWriteErrors
- MongoDB Manual — bulkWrite command
- MongoDB Driver Specifications — Bulk Write
- MongoDB Manual — Atomicity and Transactions
- MongoDB Manual — Retryable Writes
- MongoDB Manual — Choose How to Handle Invalid Documents
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


