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

Дерево руководства
Глава 145 · После первого каталога MongoDB и низкоуровневого драйвера
MongoDB: aggregation — расчёты, группы, связи и отчёт каталога
От первой стоимости остатка до group, unwind, lookup, facet и понятного последовательного чтения результата.
Что такое pipeline и как его записать в PHP
Для страницы каталога или отчёта по складу часто нужны не все сохранённые документы, а готовые ответы: три самых дорогих остатка, сумма по категориям, число тегов и название связанной категории. Aggregation pipeline задаёт последовательность преобразований на сервере. PHP отправляет описание и оформляет уже подготовленный результат.
Предварительный маршрут: документ и первый каталог → команда драйвера и курсор. Если синтаксис PHP ещё незнаком, рядом есть массивы, циклы и исключения. Здесь каждый новый этап MongoDB объясняется перед использованием.
- PHP: Лампа, price_kopecks=1500, stock=2
- BSON-документ хранится в коллекции
- PHP описывает $multiply двух полей
- Сервер вычисляет 3000 копеек
- Cursor возвращает документ ответа
- Таблица показывает Лампу и 3000
Полезная модель — несколько последовательных рабочих столов: первый отбирает карточки, второй дописывает расчёт, третий сортирует. Название этапа задаёт действие, его параметры — правила. Это модель результата, а не обещание физического исполнения: оптимизатор может менять допустимый порядок операций, а group/sort иногда ждут весь входной набор.
| Запись PHP | Что означает |
|---|---|
| $pipeline = [ ... ]; | Переменная со списком этапов, в нужном порядке. |
| ['$match' => ['status' => 'active']] | Один этап: имя оператора и документ его настроек. |
| ['$multiply' => ['$price_kopecks', '$stock']] | Выражение: взять два поля текущего документа и умножить. |
| 'status' => 'active' | Имя поля и буквальное значение в условии, не ссылка на поле. |
| '$stock' | Строковый путь к полю MongoDB; это не PHP-переменная $stock. |
| 'cursor' => ['batchSize' => 2] | Непустой документ параметров курсора. |
| 'cursor' => (object) [] | Пустой BSON-документ, если параметры курсора не нужны; [] само по себе — BSON-массив. |
Одинарные кавычки в PHP сохраняют знак $ в именах операторов и путях: '$match', '$stock'. В двойных кавычках PHP попытался бы подставить переменную. Внешний список pipeline содержит этапы, а ассоциативный массив каждого этапа превращается в BSON-документ. JSON-строка для этого не нужна.
Обычный match использует синтаксис фильтра: ['stock' => ['$gt' => 0]]. Для выражения, сравнивающего два поля, нужен $expr; нельзя механически перенести запись выражения в фильтр и получить тот же смысл. Литеральные массивы и пути полей также различаются. В наших этапах константа active — значение, а $category — значение поля текущего документа.
Самостоятельный запуск и границы учебной базы
Нужны PHP 8.4+ CLI x64, расширение mongodb 2.5+ и отдельный локальный MongoDB 8.0. Контрольная среда пакета: PHP 8.5.11, mongodb 2.5.5, сервер 8.0.34. Используется низкоуровневое расширение MongoDB\Driver, Composer не требуется. Установку сервера и расширения показывает первая глава MongoDB.
Скачайте архив нужной программы, распакуйте и задайте переменные PHPMAN_MONGODB_URI и PHPMAN_MONGODB_DATABASE по README, затем выполните php example.php. Helper ma116-helper.php разрешает один явный 127.0.0.1:порт без учётных данных; это ограничение локального упражнения. Не отключайте TLS или авторизацию рабочего сервера. Для интерфейса сайта серверные права и проверка пользовательских параметров проектируются отдельно.
Каждый пример явно создаёт коллекцию mongodb116_aggregation_… с новым случайным суффиксом; lookup дополнительно создаёт свою коллекцию категорий. Владение фиксируется только после успешного create. finally удаляет точные собственные имена, затем listCollections проверяет отсутствие всех своих коллекций. Нужны соответствующие права в учебной базе, listCollections и buildInfo. При принудительном завершении процесса finally может не выполниться: порядок ручной точечной уборки есть в README.
Каталог, на котором виден каждый расчёт
| ID | Товар | Категория | Статус | Цена, коп. | Штук | Теги |
|---|---|---|---|---|---|---|
| 101 | Кружка | kitchen | active | 500 | 4 | gift, home |
| 102 | Лампа | lighting | active | 1500 | 2 | home, desk |
| 103 | Тарелка | kitchen | active | 300 | 5 | home |
| 104 | Черновик | kitchen | draft | 100 | 100 | gift |
| 105 | Блокнот | stationery | active | 200 | 0 | пустой массив |
| 106 | Кабель | electronics | active | 700 | 3 | desk |
У всех документов tenant_id=1. В каждом ZIP эти данные создаются заново. Деньги представлены малыми целыми копейками, а количество — штуками. Такой набор позволяет проверить каждый итог вручную; он не выдаётся за тест производительности или модель полного финансового учёта.
Первые три товара по стоимости доступного остатка
Менеджеру склада нужны три доступных товара с наибольшей стоимостью остатка. В MongoDB хранятся цена и количество, а готового поля «стоимость остатка» нет. Pipeline вычислит его на сервере и вернёт только нужную тройку: Лампа — 3000, Кабель — 2100, Кружка — 2000 копеек.
Входные данные
В общем учебном каталоге шесть товаров. Черновик исключается по status, Блокнот — по stock=0. У Лампы price_kopecks=1500 и stock=2. Все значения синтетические, цены заданы в копейках, а не строками «15 рублей».
- 6 документов коллекции
- match: active и stock > 0 → 4 товара
- project: Лампа 1500 × 2 → 3000 коп.
- sort: 3000, 2100, 2000, 1500
- limit 3 → три строки экрана
Короткая операция
$pipeline = [
['$match' => ['status' => 'active', 'stock' => ['$gt' => 0]]],
['$project' => ['title' => 1,
'value' => ['$multiply' => ['$price_kopecks', '$stock']]]],
];
$command = new MongoDB\Driver\Command([
'aggregate' => $collection, 'pipeline' => $pipeline,
'cursor' => ['batchSize' => 2],
]);
$cursor = $manager->executeReadCommand($database, $command);
// Для Лампы вычислено value = 1500 * 2 = 3000 копеек.Фрагмент выделяет механизм на уже подготовленной коллекции. Полная программа ниже добавляет соединение, точный фильтр, обработку результата, проверки и очистку. Изменения входных данных не требуют запуска соседних примеров.
Что получится в полном примере
| ID | Товар | Цена, коп. | Остаток, шт. | Стоимость остатка, коп. |
|---|---|---|---|---|
| 102 | Лампа | 1500 | 2 | 3000 |
| 106 | Кабель | 700 | 3 | 2100 |
| 101 | Кружка | 500 | 4 | 2000 |
Разбор
Первый короткий фрагмент делает только отбор и расчёт. Каждый элемент внешнего списка $pipeline — один этап; у этапа одно имя оператора. $match принимает обычное условие поиска. $project задаёт поля ответа: 1 оставляет поле, а выражение $multiply создаёт вычисляемое поле. Строки '$price_kopecks' и '$stock' обозначают значения полей текущего документа, которые сервер перемножает.
Полная программа развивает этот механизм: добавляет tenant_id=1, явные поля для таблицы, сортировку по стоимости и ограничение трёх строк. tenant_id отделяет данные магазина, но сам по себе не заменяет проверку прав пользователя. Убывание задаётся -1, возрастание — 1. Второй ключ _id разрешает равенство стоимости и делает порядок однозначным.
Вычисление меняет документ в потоке ответа. Исходная запись Лампы остаётся прежней: дополнительная проверка $type подтверждает, что stock_value_kopecks в сохранённом документе отсутствует. Для записи результата потребовалась бы отдельная операция; здесь $out и $merge не используются.
Границы и типичные ошибки
Сначала определите смысл «доступный»: здесь нужны active и stock>0. Если поставить limit раньше sort, получится тройка из случайно отобранного поднабора. Сортировка вычисленного значения требует работы сервера и не становится бесплатной от наличия любого индекса. Цена/остаток обязаны иметь ожидаемые числовые типы; $multiply не является проверкой внешних данных. Малые целые копейки выбраны явно: это не универсальная модель денежных расчётов и переполнения.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/ma116-helper.php';
[$manager,$database]=ma116Environment();
$server=ma116Command($manager,$database,['buildInfo'=>1])[0]['version'];
$runtime=['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),'server'=>$server];
$collection='mongodb116_aggregation_'.bin2hex(random_bytes(8));
$namespace=$database.'.'.$collection;
$owned=[]; $details=[];
try {
ma116Create($manager,$database,$collection,$owned);
$inserted=ma116Insert($manager,$namespace,ma116Products());
if($inserted!==6){throw new RuntimeException('Fixture insertion incomplete');}
$pipeline=[
['$match'=>['tenant_id'=>1,'status'=>'active','stock'=>['$gt'=>0]]],
['$project'=>['_id'=>1,'title'=>1,'price_kopecks'=>1,'stock'=>1,
'stock_value_kopecks'=>['$multiply'=>['$price_kopecks','$stock']]]],
['$sort'=>['stock_value_kopecks'=>-1,'_id'=>1]],
['$limit'=>3],
];
$documents=ma116Rows($manager,$database,$collection,$pipeline);
$rows=[];
foreach($documents as $document){
$rows[]=[$document['_id'],$document['title'],$document['price_kopecks'],$document['stock'],$document['stock_value_kopecks']];
}
$unchanged=ma116Rows($manager,$database,$collection,[
['$match'=>['_id'=>102]],['$project'=>['_id'=>1,'stored'=>['$type'=>'$stock_value_kopecks']]]]);
$checks=['top_three_ids'=>array_column($documents,'_id')===[102,106,101],
'integer_values'=>array_column($documents,'stock_value_kopecks')===[3000,2100,2000],
'computed_field_not_written'=>$unchanged[0]['stored']==='missing',
'projection_excludes_status'=>!array_key_exists('status',$documents[0])];
} finally {
ma116Cleanup($manager,$database,$owned);
}
$removed=true;
foreach($owned as $name){$removed=!ma116Exists($manager,$database,$name) && $removed;}
$checks['owned_collections_removed']=$removed;
ma116Report('mongo116-aggregation-computed-stock', ['ID', 'Товар', 'Цена, коп.', 'Остаток, шт.', 'Стоимость остатка, коп.'], $rows, $checks, $runtime, $details);
echo 'mongo116-aggregation-computed-stock: saved result.html and result.json
';
Ожидаемый результат
mongo116-aggregation-computed-stock: saved result.html and result.json
После php example.php откройте result.html. Архив содержит все собственные данные, ma116-helper.php и README. Проверки новых программ выполняются на отдельном локальном MongoDB.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте и проверьте смысл. В полной программе измените limit на 4: четвёртой будет Тарелка со стоимостью 1500 копеек. Затем сортируйте стоимость по возрастанию, сохранив _id: первой станет Тарелка. Фрагменты упражнения не обозначают дополнительный выполненный запуск.
Посчитать товары, штуки и стоимость по категориям
Для сводки склада нужны количество карточек, количество физических единиц и стоимость по категориям. Это три разных показателя. Например, в kitchen две карточки, девять предметов и 3500 копеек стоимости. MongoDB должна вернуть уже сгруппированные строки, чтобы PHP оформил таблицу.
Входные данные
Берём active-товары магазина 1. В отличие от предыдущего экрана, здесь сохраняем карточку Блокнота с нулевым остатком: она всё ещё существует в каталоге. Черновик с сотней единиц в отчёт не входит.
- 5 активных карточек
- group key = category
- kitchen: Кружка + Тарелка
- products 1+1; units 4+5; value 2000+1500
- sort category → четыре строки отчёта
Короткая операция
$pipeline = [
['$match' => ['status' => 'active']],
['$group' => [
'_id' => '$category',
'products' => ['$sum' => 1],
'units' => ['$sum' => '$stock'],
]],
['$sort' => ['_id' => 1]],
];
// kitchen: products = 2, units = 4 + 5 = 9.Фрагмент выделяет механизм на уже подготовленной коллекции. Полная программа ниже добавляет соединение, точный фильтр, обработку результата, проверки и очистку. Изменения входных данных не требуют запуска соседних примеров.
Что получится в полном примере
| Категория | Товаров | Штук | Стоимость, коп. |
|---|---|---|---|
| electronics | 1 | 3 | 2100 |
| kitchen | 2 | 9 | 3500 |
| lighting | 1 | 2 | 3000 |
| stationery | 1 | 0 | 0 |
Разбор
В $group поле _id — ключ группы. Запись '_id' => '$category' объединяет документы по значению category. Это _id результирующей группы, а не сохранённый идентификатор товара. Для общего итога по всем входным документам ключом можно сделать null.
Аккумулятор $sum накапливает значение по документам группы. Константа 1 считает документы; путь $stock складывает количество штук. В value_kopecks вложенное выражение сначала умножает цену на остаток отдельного товара, затем внешняя $sum складывает эти стоимости. Общие контрольные итоги: 5 карточек, 14 штук, 8600 копеек.
$group не задаёт порядок результата, поэтому после него стоит $sort по ключу категории. Полная программа отдельно запускает тот же отчёт для отсутствующего tenant_id=999: результат — пустой список, а не искусственно созданная группа с нулями. Если интерфейс обязан показать категории без товаров, требуется отдельный справочник категорий и другая форма запроса, а не добавление случайных нулевых строк.
Границы и типичные ошибки
Это стоимость текущих остатков, не выручка по заказам. Названия категорий в fixture заполнены и имеют один тип; отсутствующие значения потребовали бы отдельной политики. $sum пропускает нечисловые значения, поэтому неверный тип не всегда проявляется аварией — проверяйте модель данных. Группировка обрабатывает подходящий набор и может потреблять память; ранний осмысленный match уменьшает вход, но не доказывает высокую скорость любого отчёта.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/ma116-helper.php';
[$manager,$database]=ma116Environment();
$server=ma116Command($manager,$database,['buildInfo'=>1])[0]['version'];
$runtime=['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),'server'=>$server];
$collection='mongodb116_aggregation_'.bin2hex(random_bytes(8));
$namespace=$database.'.'.$collection;
$owned=[]; $details=[];
try {
ma116Create($manager,$database,$collection,$owned);
$inserted=ma116Insert($manager,$namespace,ma116Products());
if($inserted!==6){throw new RuntimeException('Fixture insertion incomplete');}
$pipeline=[
['$match'=>['tenant_id'=>1,'status'=>'active']],
['$group'=>['_id'=>'$category','products'=>['$sum'=>1],
'units'=>['$sum'=>'$stock'],
'value_kopecks'=>['$sum'=>['$multiply'=>['$price_kopecks','$stock']]]]],
['$sort'=>['_id'=>1]],
];
$documents=ma116Rows($manager,$database,$collection,$pipeline);
$rows=[];
foreach($documents as $document){$rows[]=[$document['_id'],$document['products'],$document['units'],$document['value_kopecks']];}
$emptyPipeline=$pipeline;
$emptyPipeline[0]=['$match'=>['tenant_id'=>999,'status'=>'active']];
$empty=ma116Rows($manager,$database,$collection,$emptyPipeline);
$details=['empty_filter_result'=>$empty];
$checks=['category_order'=>array_column($documents,'_id')===['electronics','kitchen','lighting','stationery'],
'product_count'=>array_sum(array_column($documents,'products'))===5,
'unit_count'=>array_sum(array_column($documents,'units'))===14,
'value_total'=>array_sum(array_column($documents,'value_kopecks'))===8600,
'zero_stock_category_preserved'=>$documents[3]['units']===0,
'empty_input_no_invented_group'=>$empty===[]];
} finally {
ma116Cleanup($manager,$database,$owned);
}
$removed=true;
foreach($owned as $name){$removed=!ma116Exists($manager,$database,$name) && $removed;}
$checks['owned_collections_removed']=$removed;
ma116Report('mongo116-aggregation-category-totals', ['Категория', 'Товаров', 'Штук', 'Стоимость, коп.'], $rows, $checks, $runtime, $details);
echo 'mongo116-aggregation-category-totals: saved result.html and result.json
';
Ожидаемый результат
mongo116-aggregation-category-totals: saved result.html and result.json
После php example.php откройте result.html. Архив содержит все собственные данные, ma116-helper.php и README. Проверки новых программ выполняются на отдельном локальном MongoDB.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте и проверьте смысл. Добавьте к match stock>0: категория stationery исчезнет, число карточек станет 4, а общие 14 штук и 8600 копеек останутся прежними. Так проверяется различие «карточка существует» и «товар доступен».
Посчитать теги и увидеть ошибку повторного суммирования
В боковом фильтре каталога нужно показать, сколько раз встречаются теги desk, gift и home. Теги лежат внутри массивов каждого товара. Их удобно развернуть в поток отдельных строк, но после такого действия привычная сумма stock даст ложный остаток.
Входные данные
Кружка имеет два тега gift/home и stock=4; Лампа — home/desk и stock=2; Тарелка — home и stock=5; Кабель — desk и stock=3. У активного Блокнота массив tags пуст. Внутри каждого учебного массива теги уникальны.
- Кружка: tags=[gift, home], stock=4
- unwind → gift/4 и home/4
- group по тегу → desk 2, gift 1, home 3
- Ошибочная сумма stock после unwind → 20
- Сумма исходных пяти товаров → 14
Короткая операция
$pipeline = [
['$match' => ['status' => 'active']],
['$unwind' => '$tags'],
['$group' => ['_id' => '$tags', 'occurrences' => ['$sum' => 1]]],
['$sort' => ['_id' => 1]],
];
// Кружка с tags=['gift','home'] даёт две строки, каждая со stock=4.
// Суммировать stock этих двух строк как остаток товара нельзя.Фрагмент выделяет механизм на уже подготовленной коллекции. Полная программа ниже добавляет соединение, точный фильтр, обработку результата, проверки и очистку. Изменения входных данных не требуют запуска соседних примеров.
Что получится в полном примере
| Часть отчёта | Что считаем | Количество |
|---|---|---|
| Тег | desk | 2 |
| Тег | gift | 1 |
| Тег | home | 3 |
| Диагностика | Товаров до unwind | 5 |
| Диагностика | Строк после unwind | 6 |
| Ошибка | Остаток после unwind: повторный счёт | 20 |
| Верный итог | Остаток по исходным товарам | 14 |
Разбор
$unwind берёт поле массива и выдаёт документ на каждый элемент, подставляя текущий элемент вместо массива. Остальные поля копируются в каждую полученную строку. Поэтому две строки Кружки содержат один и тот же stock=4: это всё тот же физический остаток, а не восемь кружек.
Текущий отчёт считает вхождения тегов. Поскольку у каждого товара учебного каталога нет повторяющегося тега, этот счётчик одновременно равен числу товаров с тегом. Для данных с tags=[home,home] такое равенство уже неверно: сначала нужен договор об уникальности элементов или устранение повторов на уровне «товар + тег». Нельзя молча назвать число строк числом товаров.
По умолчанию unwind не выдаёт строку для пустого массива, null или отсутствующего поля. Поэтому Блокнот не попадает в таблицу тегов. Если значение непустое, но вообще не массив, сервер обрабатывает его как одиночный элемент; это ещё одна причина проверять схему заранее. Опция includeArrayIndex добавляет позицию элемента в новое поле, preserveNullAndEmptyArrays сохраняет документы без элементов — её рабочее применение идёт в следующем примере.
Полная программа специально вычисляет и подписывает неверный итог: 4×2 + 2×2 + 5 + 3 = 20. Правильный остаток считается по исходным active-документам до разворачивания: 4+2+5+0+3=14. Это отдельные метрики с разной единицей наблюдения. Если нужны обе одновременно, можно разветвить поток через facet; не пытайтесь исправить двойной счёт случайным делением.
Границы и типичные ошибки
Разворачивание нескольких независимых массивов может многократно увеличить число строк. До каждого sum ответьте: одна текущая строка обозначает товар, тег или комбинацию? includeArrayIndex — позиция элемента массива, а не ID товара. В примере нет бесконечных массивов и нагрузочного измерения; большой вход требует ограничений схемы и анализа запроса.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/ma116-helper.php';
[$manager,$database]=ma116Environment();
$server=ma116Command($manager,$database,['buildInfo'=>1])[0]['version'];
$runtime=['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),'server'=>$server];
$collection='mongodb116_aggregation_'.bin2hex(random_bytes(8));
$namespace=$database.'.'.$collection;
$owned=[]; $details=[];
try {
ma116Create($manager,$database,$collection,$owned);
$inserted=ma116Insert($manager,$namespace,ma116Products());
if($inserted!==6){throw new RuntimeException('Fixture insertion incomplete');}
$match=['$match'=>['tenant_id'=>1,'status'=>'active']];
$tags=ma116Rows($manager,$database,$collection,[
$match,['$unwind'=>'$tags'],
['$group'=>['_id'=>'$tags','occurrences'=>['$sum'=>1]]],
['$sort'=>['_id'=>1]],
]);
$before=ma116Rows($manager,$database,$collection,[
$match,['$group'=>['_id'=>null,'documents'=>['$sum'=>1],'units'=>['$sum'=>'$stock']]]])[0];
$after=ma116Rows($manager,$database,$collection,[
$match,['$unwind'=>'$tags'],
['$group'=>['_id'=>null,'rows'=>['$sum'=>1],'wrong_units'=>['$sum'=>'$stock']]]])[0];
$rows=[];
foreach($tags as $tag){$rows[]=['Тег',$tag['_id'],$tag['occurrences']];}
$rows[]=['Диагностика','Товаров до unwind',$before['documents']];
$rows[]=['Диагностика','Строк после unwind',$after['rows']];
$rows[]=['Ошибка','Остаток после unwind: повторный счёт',$after['wrong_units']];
$rows[]=['Верный итог','Остаток по исходным товарам',$before['units']];
$details=['tags_per_product'=>['101'=>2,'102'=>2,'103'=>1,'105'=>0,'106'=>1]];
$checks=['tag_counts'=>array_column($tags,'occurrences','_id')===['desk'=>2,'gift'=>1,'home'=>3],
'original_five_products'=>$before['documents']===5,
'six_unwound_rows'=>$after['rows']===6,
'wrong_sum_observed'=>$after['wrong_units']===20,
'correct_stock_before_unwind'=>$before['units']===14];
} finally {
ma116Cleanup($manager,$database,$owned);
}
$removed=true;
foreach($owned as $name){$removed=!ma116Exists($manager,$database,$name) && $removed;}
$checks['owned_collections_removed']=$removed;
ma116Report('mongo116-aggregation-unwind-tags', ['Часть отчёта', 'Что считаем', 'Количество'], $rows, $checks, $runtime, $details);
echo 'mongo116-aggregation-unwind-tags: saved result.html and result.json
';
Ожидаемый результат
mongo116-aggregation-unwind-tags: saved result.html and result.json
После php example.php откройте result.html. Архив содержит все собственные данные, ma116-helper.php и README. Проверки новых программ выполняются на отдельном локальном MongoDB.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте и проверьте смысл. Добавьте Кружке третий уникальный тег kitchen. В отчёте тегов появится kitchen=1, число строк станет 7, ошибочная сумма — 24. Верный складской остаток останется 14. Это ожидаемый разбор изменения, а не утверждение, что лишние четыре штуки реально появились.
Реальная ситуация: Aggregate $sum values from two arrays. Разработчик хочет совместить несколько итогов после разворачивания массивов. В уроке отдельно показана смена смысла строки и реальная ошибочная сумма повторённого остатка. Обсуждение — источник вопроса; правила сверены по первичной документации, программа и данные урока оригинальные.
Добавить название категории и сохранить товар без связи
Экран товара хранит короткую ссылку category, а пользователю нужно название категории. Названия находятся в отдельной коллекции и меняются независимо. Кроме обычных товаров, нужно показать новый товар с ещё не заведённой категорией, чтобы редактор заметил проблему.
Входные данные
Категории kitchen/lighting/stationery/electronics имеют уникальные строковые _id и названия. В собственную коллекцию товаров добавлен ID 107 с category=unknown. Связанной категории нет; MongoDB разрешает такую запись, поскольку строковая ссылка автоматически не создаёт внешний ключ.
- Товар 101: category=kitchen
- lookup в своих categories по _id → [Посуда]
- unwind → одна строка с Посудой
- Товар 107: unknown → []
- preserve + ifNull → «Категория не найдена»
Короткая операция
$pipeline = [
['$lookup' => [
'from' => $categories, 'localField' => 'category',
'foreignField' => '_id', 'as' => 'category_rows',
]],
['$unwind' => [
'path' => '$category_rows', 'preserveNullAndEmptyArrays' => true,
]],
];
// unknown -> category_rows=[]; preserveNullAndEmptyArrays сохраняет товар.Фрагмент выделяет механизм на уже подготовленной коллекции. Полная программа ниже добавляет соединение, точный фильтр, обработку результата, проверки и очистку. Изменения входных данных не требуют запуска соседних примеров.
Что получится в полном примере
| ID | Товар | Ссылка category | Название категории |
|---|---|---|---|
| 101 | Кружка | kitchen | Посуда |
| 102 | Лампа | lighting | Свет |
| 103 | Тарелка | kitchen | Посуда |
| 105 | Блокнот | stationery | Канцелярия |
| 106 | Кабель | electronics | Электроника |
| 107 | Новый товар | unknown | Категория не найдена |
Разбор
$lookup в этом варианте ищет равенство localField текущего документа и foreignField другой коллекции той же базы. from — точное имя коллекции категорий, as — имя нового массива найденных документов. Даже одно совпадение сначала представлено массивом. Индекс _id категорий уникален, поэтому каждому существующему ключу соответствует максимум одна категория.
$unwind превращает найденный элемент в обычное вложенное поле. Без preserveNullAndEmptyArrays=true пустой результат unknown исчезнет; полная программа действительно выполняет оба варианта и проверяет пять строк в ошибочном и шесть в правильном. $ifNull подставляет пояснение вместо отсутствующего category_rows.title. Он считает отсутствующее поле и undefined как null, но не заменяет любую пустую строку.
Все связи здесь сопоставляются по строковым значениям. ObjectId и его строковое представление — разные типы; такую разницу нельзя исправить изменением имени поля. Аналогия с реляционной связью помогает понять направление поиска, но lookup не обеспечивает внешние ключи, каскадное удаление и обязательное существование категории.
Вложить название категории прямо в товар бывает удобнее, если оно должно храниться как исторический снимок и читаться вместе с товаром. Отдельный справочник подходит для общей редактируемой сущности. Выбор модели зависит от требований; запрос lookup сам по себе не доказывает, что любое разделение на коллекции удачно.
Границы и типичные ошибки
При связи с неуникальным foreignField возможно несколько совпадений и размножение строк после unwind. Это следует учесть до суммы стоимости. Большие соединения нуждаются в подходящем индексе и ограничении исходного набора. В учебном запуске обе коллекции принадлежат только сценарию и удаляются по точным именам; рабочие данные не используются.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/ma116-helper.php';
[$manager,$database]=ma116Environment();
$server=ma116Command($manager,$database,['buildInfo'=>1])[0]['version'];
$runtime=['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),'server'=>$server];
$collection='mongodb116_aggregation_'.bin2hex(random_bytes(8));
$namespace=$database.'.'.$collection;
$owned=[]; $details=[];
try {
ma116Create($manager,$database,$collection,$owned);
$inserted=ma116Insert($manager,$namespace,ma116Products());
if($inserted!==6){throw new RuntimeException('Fixture insertion incomplete');}
$categories=$collection.'_categories';
ma116Create($manager,$database,$categories,$owned);
$categoryInserted=ma116Insert($manager,$database.'.'.$categories,[
['_id'=>'kitchen','title'=>'Посуда'],['_id'=>'lighting','title'=>'Свет'],
['_id'=>'stationery','title'=>'Канцелярия'],['_id'=>'electronics','title'=>'Электроника'],
]);
ma116Insert($manager,$namespace,[[
'_id'=>107,'tenant_id'=>1,'title'=>'Новый товар','category'=>'unknown',
'status'=>'active','price_kopecks'=>900,'stock'=>1,'tags'=>[],
]]);
$base=[
['$match'=>['tenant_id'=>1,'status'=>'active']],
['$lookup'=>['from'=>$categories,'localField'=>'category','foreignField'=>'_id','as'=>'category_rows']],
];
$pipeline=array_merge($base,[
['$unwind'=>['path'=>'$category_rows','preserveNullAndEmptyArrays'=>true]],
['$project'=>['_id'=>1,'title'=>1,'category'=>1,
'category_title'=>['$ifNull'=>['$category_rows.title','Категория не найдена']]]],
['$sort'=>['_id'=>1]],
]);
$documents=ma116Rows($manager,$database,$collection,$pipeline);
$bad=ma116Rows($manager,$database,$collection,array_merge($base,[
['$unwind'=>'$category_rows'],['$sort'=>['_id'=>1]],['$project'=>['_id'=>1]],
]));
$rows=[];
foreach($documents as $document){$rows[]=[$document['_id'],$document['title'],$document['category'],$document['category_title']];}
$details=['default_unwind_ids'=>array_column($bad,'_id'),'default_unwind_count'=>count($bad)];
$checks=['four_categories_created'=>$categoryInserted===4,
'six_products_preserved'=>array_column($documents,'_id')===[101,102,103,105,106,107],
'missing_reference_is_visible'=>$documents[5]['category_title']==='Категория не найдена',
'default_unwind_drops_unmatched'=>array_column($bad,'_id')===[101,102,103,105,106],
'unique_foreign_key_no_multiplication'=>count(array_unique(array_column($documents,'_id')))===6];
} finally {
ma116Cleanup($manager,$database,$owned);
}
$removed=true;
foreach($owned as $name){$removed=!ma116Exists($manager,$database,$name) && $removed;}
$checks['owned_collections_removed']=$removed;
ma116Report('mongo116-aggregation-category-lookup', ['ID', 'Товар', 'Ссылка category', 'Название категории'], $rows, $checks, $runtime, $details);
echo 'mongo116-aggregation-category-lookup: saved result.html and result.json
';
Ожидаемый результат
mongo116-aggregation-category-lookup: saved result.html and result.json
После php example.php откройте result.html. Архив содержит все собственные данные, ma116-helper.php и README. Проверки новых программ выполняются на отдельном локальном MongoDB.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте и проверьте смысл. Добавьте в массив категорий ['_id'=>'unknown', 'title'=>'Новинки'] перед запуском. ID 107 сохранится, но вместо предупреждения появится «Новинки», а вариант с обычным unwind тоже вернёт шесть товаров.
Реальная ситуация: Почему после $lookup исчезают документы без совпадения. При соединении пропадает исходный документ с пустым массивом связи. Урок сравнивает обычный unwind и preserveNullAndEmptyArrays на товаре107. Обсуждение — источник вопроса; правила сверены по первичной документации, программа и данные урока оригинальные.
Получить страницу, общее количество и категории одним pipeline
Экрану каталога одновременно нужны первые две карточки, общее количество подходящих товаров и числа возле категорий. Если считать total после limit, посетитель увидит «всего 2», хотя подходящих карточек пять. Facet позволяет явно разделить эти задачи.
Входные данные
Общий фильтр — tenant_id=1 и status=active. Нулевой остаток здесь не исключается: Блокнот остаётся активной карточкой и первым попадает в дешёвую страницу. Это другой смысл фильтра, чем «доступно на складе» в первом примере.
- match → 5 активных товаров
- Ветка page: sort по цене/ID → limit 2
- Ветка total: count без limit → 5
- Ветка categories: group → 4 категории
- Один документ ответа с тремя массивами
Короткая операция
$pipeline = [
['$match' => ['status' => 'active']],
['$facet' => [
'page' => [
['$sort' => ['price_kopecks' => 1, '_id' => 1]],
['$limit' => 2],
],
'total' => [['$count' => 'value']],
]],
];
// page содержит 2 товара; total[0].value = 5.
// При пустом входе total=[]: в PHP используем $reply['total'][0]['value'] ?? 0.Фрагмент выделяет механизм на уже подготовленной коллекции. Полная программа ниже добавляет соединение, точный фильтр, обработку результата, проверки и очистку. Изменения входных данных не требуют запуска соседних примеров.
Что получится в полном примере
| Часть ответа | Элемент | Значение | Единица |
|---|---|---|---|
| Страница | 105 · Блокнот | 200 | коп. |
| Страница | 103 · Тарелка | 300 | коп. |
| Всего по фильтру | Все активные товары | 5 | товаров |
| Категория | electronics | 1 | товаров |
| Категория | kitchen | 2 | товаров |
| Категория | lighting | 1 | товаров |
| Категория | stationery | 1 | товаров |
| Пустая выборка | Количество при отсутствии совпадений | 0 | товаров |
Разбор
$facet принимает документ: ключи page, total и categories задают имена частей ответа, а значением каждого ключа является самостоятельный список этапов. Ветки получают один входной поток после общего match. Limit находится только в page и не обрезает данные для двух остальных веток.
Страница сортируется по price_kopecks и _id. Она содержит Блокнот за 200 копеек и Тарелку за 300; поле total сообщает о пяти карточках, а kitchen=2 означает две карточки категории, не две штуки остатка. PHP только раскладывает уже готовые части по элементам интерфейса.
$count — этап, который выдаёт документ с указанным именем поля; при пустом входе он не выдаёт документа. Поэтому total является массивом вида [[value=>5]], а при отсутствии совпадений — пустым массивом. Полная программа запускает пустой фильтр и использует $reply['total'][0]['value'] ?? 0; page и categories тоже пусты. Это ожидаемое отсутствие данных, а не ошибка соединения.
Общий tenant/status-фильтр вынесен перед facet намеренно. Смысл «число во всём каталоге» и «число после пользовательского фильтра» различается — размещайте условия согласно подписи интерфейса. Само наличие одной команды не превращает чтение в транзакционный снимок при параллельных изменениях; уровень согласованности требует отдельного решения.
Границы и типичные ошибки
Каждый формируемый в facet документ ограничен 100 MB, и этот этап не выгружает данные на диск: allowDiskUse не отменяет ограничение. Итоговый BSON-документ должен укладываться в 16 MiB. Page ограничена двумя элементами, категории в fixture заведомо малы; для неограниченного числа категорий нужна отдельная стратегия, а не огромный массив ответа. Facet не является универсально самым быстрым способом; сравнивайте с отдельными индексируемыми запросами по задаче.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/ma116-helper.php';
[$manager,$database]=ma116Environment();
$server=ma116Command($manager,$database,['buildInfo'=>1])[0]['version'];
$runtime=['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),'server'=>$server];
$collection='mongodb116_aggregation_'.bin2hex(random_bytes(8));
$namespace=$database.'.'.$collection;
$owned=[]; $details=[];
try {
ma116Create($manager,$database,$collection,$owned);
$inserted=ma116Insert($manager,$namespace,ma116Products());
if($inserted!==6){throw new RuntimeException('Fixture insertion incomplete');}
$facets=[
'page'=>[
['$sort'=>['price_kopecks'=>1,'_id'=>1]],['$limit'=>2],
['$project'=>['_id'=>1,'title'=>1,'price_kopecks'=>1]],
],
'total'=>[['$count'=>'value']],
'categories'=>[
['$group'=>['_id'=>'$category','products'=>['$sum'=>1]]],
['$sort'=>['_id'=>1]],
],
];
$pipeline=[['$match'=>['tenant_id'=>1,'status'=>'active']],['$facet'=>$facets]];
$reply=ma116Rows($manager,$database,$collection,$pipeline)[0];
$total=$reply['total'][0]['value']??0;
$rows=[];
foreach($reply['page'] as $product){$rows[]=['Страница',$product['_id'].' · '.$product['title'],$product['price_kopecks'],'коп.'];}
$rows[]=['Всего по фильтру','Все активные товары',$total,'товаров'];
foreach($reply['categories'] as $category){$rows[]=['Категория',$category['_id'],$category['products'],'товаров'];}
$empty=ma116Rows($manager,$database,$collection,[
['$match'=>['tenant_id'=>999,'status'=>'active']],['$facet'=>$facets],
])[0];
$rows[]=['Пустая выборка','Количество при отсутствии совпадений',$empty['total'][0]['value']??0,'товаров'];
$details=['page_size'=>count($reply['page']),'empty_facets'=>$empty];
$checks=['first_page'=>array_column($reply['page'],'_id')===[105,103],
'total_not_limited_to_page'=>$total===5,
'category_counts'=>array_column($reply['categories'],'products','_id')===['electronics'=>1,'kitchen'=>2,'lighting'=>1,'stationery'=>1],
'empty_count_array'=>$empty['total']===[],
'empty_page_and_categories'=>$empty['page']===[] && $empty['categories']===[]];
} finally {
ma116Cleanup($manager,$database,$owned);
}
$removed=true;
foreach($owned as $name){$removed=!ma116Exists($manager,$database,$name) && $removed;}
$checks['owned_collections_removed']=$removed;
ma116Report('mongo116-aggregation-facet-catalog', ['Часть ответа', 'Элемент', 'Значение', 'Единица'], $rows, $checks, $runtime, $details);
echo 'mongo116-aggregation-facet-catalog: saved result.html and result.json
';
Ожидаемый результат
mongo116-aggregation-facet-catalog: saved result.html and result.json
После php example.php откройте result.html. Архив содержит все собственные данные, ma116-helper.php и README. Проверки новых программ выполняются на отдельном локальном MongoDB.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте и проверьте смысл. Добавьте stock>0 к общему match: total станет 4, категория stationery исчезнет. Первая страница будет Тарелка (300) и Кружка (500). Если добавить это условие только в page, total останется 5 — тогда интерфейс должен честно объяснять разные критерии.
Реальная ситуация: Count total and matched documents in MongoDB aggregation pipeline. Счётчик после limit показывает число элементов страницы вместо количества всех совпадений. Ветка total в уроке отделена от page, а область общего фильтра подписана. Обсуждение — источник вопроса; правила сверены по первичной документации, программа и данные урока оригинальные.
Прочитать отчёт по порциям и корректно получить его повторно
Нужно последовательно прочитать строки складского отчёта и понимать, когда закончились данные. Повторный обход того же Cursor часто пытаются использовать для второго виджета; после продвижения такой «возврат в начало» недоступен. Покажем состояния объекта явно.
Входные данные
Pipeline выбирает пять активных товаров и вычисляет стоимость остатка. Начальная порция ответа запрошена размером 2, а строк всего 5. Выборка отсортирована по _id. Type map настроен до чтения, поэтому current возвращает PHP-массив.
- Создан Cursor: current=null, обход ещё не начат
- rewind → первая позиция; valid=true
- current → Кружка; key=0; next → Лампа
- Пять строк прочитаны → valid=false, current=null
- rewind → LogicException; новый запрос → новый Cursor
Короткая операция
$cursor->rewind();
while ($cursor->valid()) {
$document = $cursor->current();
echo $cursor->key(), ': ', $document['title'], "\n";
$cursor->next();
}
// 0: Кружка ... 4: Кабель. Ключи 0..4 — позиции, а ID товаров 101..106.
// После продвижения rewind не начинает запрос заново: создайте новый Cursor.Фрагмент выделяет механизм на уже подготовленной коллекции. Полная программа ниже добавляет соединение, точный фильтр, обработку результата, проверки и очистку. Изменения входных данных не требуют запуска соседних примеров.
Что получится в полном примере
| Шаг | Ключ курсора | ID | Результат | Стоимость, коп. |
|---|---|---|---|---|
| Документ | 0 | 101 | Кружка | 2000 |
| Документ | 1 | 102 | Лампа | 3000 |
| Документ | 2 | 103 | Тарелка | 1500 |
| Документ | 3 | 105 | Блокнот | 0 |
| Документ | 4 | 106 | Кабель | 2100 |
| Исчерпание | NULL | NULL | valid=false; current=null | NULL |
| Повторный rewind | NULL | NULL | LogicException | NULL |
| Новый курсор | NULL | NULL | 101, 102, 103, 105, 106 | NULL |
| Пустой курсор | NULL | NULL | 0 документов | NULL |
Разбор
Cursor — объект последовательного чтения, а не автоматически готовый массив всех документов. При явном обходе rewind устанавливает первую позицию, valid отвечает, существует ли текущая строка, current читает её, key сообщает номер позиции, next продвигает дальше. У первой строки key=0, хотя ID товара равен 101. _id берут из документа, а не из ключа итератора.
Выполнение next не возвращает следующий документ: его результат void. После продвижения снова проверяют valid и только затем читают current. Драйвер может получать очередные данные из сервера; сетевой сбой не равен корректному окончанию выборки. Проверки valid/current не являются проверкой здоровья соединения.
batchSize=2 относится к порции обмена, а limit — к числу документов результата. Программа запрашивает пять документов и действительно читает все пять. Она не проверяет размеры сетевых пакетов и не обещает ровно два документа в каждом серверном ответе. Для маленькой таблицы результаты сохраняются в PHP-массив; это не пример неограниченного экспорта без потребления памяти. В большом экспорте строку обрабатывают или записывают по мере чтения, не накапливая все документы; для экрана прежде ограничивают серверный ответ.
После продвижения за первую позицию rewind вызывает LogicException. Полная программа ловит именно этот ожидаемый класс и затем выполняет команду заново. Полученные ID совпадают на неподвижных учебных данных. При параллельных изменениях новый запрос может увидеть другое состояние: это не повтор сохранённого снимка. Для повторного чтения маленького неизменного ответа можно один раз материализовать его в массив через Cursor::toArray и карты типов.
Последняя проверка получает действительно пустой курсор, вызывает rewind, наблюдает valid=false и current=null. Отдельная нулевая строка товара не выдумывается. В обычном прикладном коде foreach удобнее, если не нужно управлять позицией вручную; здесь явные методы нужны, чтобы понять механизм и составить корректный цикл.
Границы и типичные ошибки
Курсор однонаправленный: не смешивайте несколько независимых обходов и не называйте rewind повторным выполнением запроса. Агрегация с group/sort может сначала обрабатывать набор на сервере; последовательное чтение клиента не делает все серверные этапы потоковыми. Транзакции, tailable/awaitData и живые потоки изменений в этом уроке не рассматриваются. Таймаут чтения, завершение выборки и серверная ошибка — разные ситуации.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/ma116-helper.php';
[$manager,$database]=ma116Environment();
$server=ma116Command($manager,$database,['buildInfo'=>1])[0]['version'];
$runtime=['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),'server'=>$server];
$collection='mongodb116_aggregation_'.bin2hex(random_bytes(8));
$namespace=$database.'.'.$collection;
$owned=[]; $details=[];
try {
ma116Create($manager,$database,$collection,$owned);
$inserted=ma116Insert($manager,$namespace,ma116Products());
if($inserted!==6){throw new RuntimeException('Fixture insertion incomplete');}
$pipeline=[
['$match'=>['tenant_id'=>1,'status'=>'active']],
['$sort'=>['_id'=>1]],
['$project'=>['_id'=>1,'title'=>1,
'value_kopecks'=>['$multiply'=>['$price_kopecks','$stock']]]],
];
$cursor=ma116Aggregate($manager,$database,$collection,$pipeline,2);
$beforeStart=$cursor->current();
$rows=[]; $ids=[]; $keys=[];
$cursor->rewind();
while($cursor->valid()){
$document=$cursor->current();
$key=$cursor->key();
$ids[]=$document['_id']; $keys[]=$key;
$rows[]=['Документ',$key,$document['_id'],$document['title'],$document['value_kopecks']];
$cursor->next();
}
$exhausted=!$cursor->valid() && $cursor->current()===null;
$rewindCaught=false;
try{$cursor->rewind();}
catch(MongoDB\Driver\Exception\LogicException $failure){$rewindCaught=true;}
$fresh=ma116Aggregate($manager,$database,$collection,$pipeline,2);
$again=[];
$fresh->rewind();
while($fresh->valid()){
$again[]=$fresh->current()['_id'];
$fresh->next();
}
$empty=ma116Aggregate($manager,$database,$collection,[['$match'=>['tenant_id'=>999]]],2);
$empty->rewind();
$emptyValid=$empty->valid(); $emptyCurrent=$empty->current();
$rows[]=['Исчерпание',null,null,$exhausted?'valid=false; current=null':'ошибка',null];
$rows[]=['Повторный rewind',null,null,$rewindCaught?'LogicException':'ошибка',null];
$rows[]=['Новый курсор',null,null,implode(', ',$again),null];
$rows[]=['Пустой курсор',null,null,(!$emptyValid && $emptyCurrent===null)?'0 документов':'ошибка',null];
$details=['requested_initial_batch_size'=>2,'documents_consumed'=>count($ids),
'current_before_start_was_null'=>$beforeStart===null];
$checks=['cursor_five_rows'=>$ids===[101,102,103,105,106],
'numeric_keys_not_ids'=>$keys===[0,1,2,3,4],
'current_before_start_null'=>$beforeStart===null,
'cursor_exhausted'=>$exhausted,'rewind_after_progress_caught'=>$rewindCaught,
'fresh_cursor_repeat'=>$again===$ids,'empty_cursor_valid_false'=>!$emptyValid && $emptyCurrent===null];
} finally {
ma116Cleanup($manager,$database,$owned);
}
$removed=true;
foreach($owned as $name){$removed=!ma116Exists($manager,$database,$name) && $removed;}
$checks['owned_collections_removed']=$removed;
ma116Report('mongo116-aggregation-cursor-export', ['Шаг', 'Ключ курсора', 'ID', 'Результат', 'Стоимость, коп.'], $rows, $checks, $runtime, $details);
echo 'mongo116-aggregation-cursor-export: saved result.html and result.json
';
Ожидаемый результат
mongo116-aggregation-cursor-export: saved result.html and result.json
После php example.php откройте result.html. Архив содержит все собственные данные, ma116-helper.php и README. Проверки новых программ выполняются на отдельном локальном MongoDB.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте и проверьте смысл. Измените batchSize с 2 на 1: содержание и ключи пяти строк останутся теми же. Затем добавьте в pipeline limit=2: строк будет две. Это разные настройки, и именно limit меняет смысл ответа. Для такого упражнения согласуйте ожидаемые проверки полной программы с новым требованием.
Реальная ситуация: Trying to iterate over a MongoDB cursor twice. Один результат пытаются дважды обойти для разных элементов интерфейса. Современная программа демонстрирует однонаправленный Iterator API и новый курсор после ожидаемой LogicException. Обсуждение — источник вопроса; правила сверены по первичной документации, программа и данные урока оригинальные.
Команда чтения и пять методов обхода курсора
У каждой карточки есть полный контракт соответствующей страницы PHP.net, версия, возврат и ограничения, затем два прикладных использования: строки отчёта и реальный перечень индексов. Это шесть новых карточек, а не повторный подсчёт уже описанных методов.
| Метод | Задача |
|---|---|
| MongoDB\Driver\Manager::executeReadCommand | Выполнить команду чтения с правилами выбора сервера и получить курсор её ответа. |
| MongoDB\Driver\Cursor::rewind | Начать явный обход курсора с первой позиции; учитывать однонаправленность после продвижения. |
| MongoDB\Driver\Cursor::valid | Определить, можно ли читать документ в текущей позиции. |
| MongoDB\Driver\Cursor::current | Прочитать текущий BSON-документ как PHP-массив или объект, не двигая курсор. |
| MongoDB\Driver\Cursor::key | Получить числовой номер текущего результата для нумерации отчёта. |
| MongoDB\Driver\Cursor::next | Продвинуться к следующему документу и при необходимости получить очередную порцию ответа. |
Собрать свой отчёт и выбрать следующий шаг
Сначала сформулируйте строку результата: товар, категория или тег. Затем выберите фильтр, преобразования и предел ответа. Когда форма строки меняется, заново проверьте смысл суммы. PHP должен оформлять нужный отчёт, а не получать всю базу ради последующей сортировки.
Связанный материал: индексы под реальные запросы каталога, карты типов и запись через драйвер, реляционные связи и выборка веток. Полный набор aggregation-операторов, Decimal128, UTCDateTime, валидация схемы, транзакции и изменение потоков остаются отдельными темами справочника; эта глава не объявляет весь MongoDB завершённым.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
MongoDB\Driver\Manager::executeReadCommandВыполнить команду чтения с правилами выбора сервера и получить курсор её ответа.
Метод существует с mongodb 1.4.0; программы пакета рассчитаны на mongodb 2.5+ и MongoDB 8.0. Транзакционная топология здесь не создаётся. · mongodb
final public MongoDB\Driver\Manager::executeReadCommand(string $db, MongoDB\Driver\Command $command, ?array $options = null): MongoDB\Driver\CursorВыполнить команду чтения с правилами выбора сервера и получить курсор её ответа.
final public MongoDB\Driver\Manager::executeReadCommand(string $db, MongoDB\Driver\Command $command, ?array $options = null): MongoDB\Driver\CursorПараметры
| Параметр | Назначение |
|---|---|
| db | Имя базы, не namespace «база.коллекция». |
| command | Объект MongoDB\Driver\Command с документом команды. |
| options.readConcern | MongoDB\Driver\ReadConcern; уровень подтверждённых данных, сервер MongoDB 3.2+. Старый сервер отклонит неподдерживаемую настройку. |
| options.readPreference | MongoDB\Driver\ReadPreference; выбор подходящего сервера для чтения. |
| options.session | MongoDB\Driver\Session; сессия этой операции. Сам параметр не начинает транзакцию. |
Результат
При успехе MongoDB\Driver\Cursor. Если команда создаёт серверный курсор, читаются его документы; это не обещание массива всех данных в памяти.
Ошибки и границы
InvalidArgumentException: неверные аргументы или readConcern/writeConcern у операции внутри уже активной транзакции session; такие уровни задают при startTransaction. ConnectionException: подключение, AuthenticationException: аутентификация, RuntimeException: прочие ошибки, например неверная команда. Значения readPreference/readConcern по умолчанию берутся из активной транзакции, затем URI. Это не универсальный исполнитель записи: read-only aggregate без $out/$merge соответствует данной главе.
Версии
Метод существует с mongodb 1.4.0; программы пакета рассчитаны на mongodb 2.5+ и MongoDB 8.0. Транзакционная топология здесь не создаётся.
Применение 1: строки складского отчёта
$cursor = $manager->executeReadCommand($database, new MongoDB\Driver\Command([
'aggregate' => $collection, 'pipeline' => $pipeline,
'cursor' => ['batchSize' => 2],
]));Складской отчёт выдаёт пять документов с вычисленной стоимостью; каждый документ проходит через явный цикл чтения.
Полный сценарий чтения отчёта · Фактический отчёт
Применение 2: служебный перечень индексов
$cursor = $manager->executeReadCommand($database, new MongoDB\Driver\Command([
'listIndexes' => $collection, 'cursor' => ['batchSize' => 1],
]));Администратор видит документы описания индексов своей коллекции до и после удаления выбранного учебного индекса. Та же команда чтения обслуживает другую прикладную задачу.
Полный сценарий жизненного цикла индекса · Фактический перечень индексов
Фрагменты выделяют действие внутри двух самостоятельных программ. $manager — настроенный драйвер, $database — имя учебной базы, $collection — собственная коллекция, $cursor — результат текущей команды. Подготовка, данные, обработка и очистка находятся в связанных архивах; это не дополнительные неполные программы.
MongoDB\Driver\Cursor::rewindНачать явный обход курсора с первой позиции; учитывать однонаправленность после продвижения.
Метод явного Iterator API доступен с mongodb 1.9.0; до этого примеры с новым набором методов нельзя переносить буквально. · mongodb
public MongoDB\Driver\Cursor::rewind(): voidНачать явный обход курсора с первой позиции; учитывать однонаправленность после продвижения.
public MongoDB\Driver\Cursor::rewind(): voidПараметры
Параметров нет; метод вызывается у имеющегося Cursor.
Результат
Полезного возвращаемого значения нет: метод меняет позицию. Наличие первого документа затем проверяют valid().
Ошибки и границы
Когда курсор продвинулся дальше первой позиции, повторный rewind вызывает MongoDB\Driver\Exception\LogicException. Также документация перечисляет InvalidArgumentException при разборе аргументов, ConnectionException и AuthenticationException. Это не повтор команды на сервере. foreach начинает обход автоматически; для нового запроса создайте новый Cursor.
Версии
Метод явного Iterator API доступен с mongodb 1.9.0; до этого примеры с новым набором методов нельзя переносить буквально.
Применение 1: строки складского отчёта
$cursor->rewind();
while ($cursor->valid()) {
$document = $cursor->current();
$cursor->next();
}Первый обход читает отчёт; намеренный rewind после исчерпания перехвачен как LogicException, а новая команда снова выдаёт пять строк.
Полный сценарий чтения отчёта · Фактический отчёт
Применение 2: служебный перечень индексов
$cursor->rewind();
// Теперь можно читать первое описание индекса.
while ($cursor->valid()) {
$index = $cursor->current();
$cursor->next();
}Начинается просмотр индексов коллекции. Для перечня после изменения схемы создаётся новый курсор, чтобы получить актуальное состояние.
Полный сценарий жизненного цикла индекса · Фактический перечень индексов
Фрагменты выделяют действие внутри двух самостоятельных программ. $manager — настроенный драйвер, $database — имя учебной базы, $collection — собственная коллекция, $cursor — результат текущей команды. Подготовка, данные, обработка и очистка находятся в связанных архивах; это не дополнительные неполные программы.
MongoDB\Driver\Cursor::validОпределить, можно ли читать документ в текущей позиции.
С mongodb 1.9.0; явное состояние проверяется в программах на mongodb 2.5+. · mongodb
public MongoDB\Driver\Cursor::valid(): boolОпределить, можно ли читать документ в текущей позиции.
public MongoDB\Driver\Cursor::valid(): boolПараметры
Параметров нет; метод вызывается у имеющегося Cursor.
Результат
true — текущая позиция допустима; false — в этой позиции документа нет. Это состояние итератора, не число строк и не ping сервера.
Ошибки и границы
На официальной странице отдельный список исключений для valid не заявлен. Сетевые ошибки операции и продвижения не заменяйте значением false. В пустом результате после rewind цикл не выполняется ни разу; current не следует трактовать как документ, когда valid=false.
Версии
С mongodb 1.9.0; явное состояние проверяется в программах на mongodb 2.5+.
Применение 1: строки складского отчёта
$cursor->rewind();
while ($cursor->valid()) {
$document = $cursor->current();
$cursor->next();
}Пять строк отчёта прочитаны; для второго запроса с отсутствующим магазином valid сразу false, в таблице честно показано 0 документов.
Полный сценарий чтения отчёта · Фактический отчёт
Применение 2: служебный перечень индексов
$cursor->rewind();
while ($cursor->valid()) {
$index = $cursor->current();
$cursor->next();
}Перечень индексов заканчивается по состоянию курсора, а не по заранее угаданному числу индексов. После удаления индекс исключён из нового перечня.
Полный сценарий жизненного цикла индекса · Фактический перечень индексов
Фрагменты выделяют действие внутри двух самостоятельных программ. $manager — настроенный драйвер, $database — имя учебной базы, $collection — собственная коллекция, $cursor — результат текущей команды. Подготовка, данные, обработка и очистка находятся в связанных архивах; это не дополнительные неполные программы.
MongoDB\Driver\Cursor::currentПрочитать текущий BSON-документ как PHP-массив или объект, не двигая курсор.
С mongodb 1.9.0. В примерах карта типов root/document/array настроена в array, поэтому используются квадратные скобки. · mongodb
public MongoDB\Driver\Cursor::current(): array|object|nullПрочитать текущий BSON-документ как PHP-массив или объект, не двигая курсор.
public MongoDB\Driver\Cursor::current(): array|object|nullПараметры
Параметров нет; метод вызывается у имеющегося Cursor.
Результат
Массив или объект определяется type map курсора. До начала обхода либо при недопустимой текущей позиции возвращается null.
Ошибки и границы
Официальная страница не выделяет собственного перечня исключений. Перед чтением полей проверьте valid; null — отсутствие текущего документа, а не BSON-документ с пустыми полями. current не продвигает курсор: без next цикл будет повторять одну строку. setTypeMap настраивают до декодирования нужных документов.
Версии
С mongodb 1.9.0. В примерах карта типов root/document/array настроена в array, поэтому используются квадратные скобки.
Применение 1: строки складского отчёта
$document = $cursor->current();
$title = $document['title'];В первой позиции извлекается название Кружка, затем Лампа и остальные товары; до старта и после конца отдельно проверяется null.
Полный сценарий чтения отчёта · Фактический отчёт
Применение 2: служебный перечень индексов
$index = $cursor->current();
$name = $index['name'];Из текущего описания индекса извлекается его имя. Это другой документ с собственной схемой; поля title у описания индекса нет.
Полный сценарий жизненного цикла индекса · Фактический перечень индексов
Фрагменты выделяют действие внутри двух самостоятельных программ. $manager — настроенный драйвер, $database — имя учебной базы, $collection — собственная коллекция, $cursor — результат текущей команды. Подготовка, данные, обработка и очистка находятся в связанных архивах; это не дополнительные неполные программы.
MongoDB\Driver\Cursor::keyПолучить числовой номер текущего результата для нумерации отчёта.
С mongodb 1.9.0. В контрольном обходе позиции 0..4 относятся к ID 101,102,103,105,106. · mongodb
public MongoDB\Driver\Cursor::key(): intПолучить числовой номер текущего результата для нумерации отчёта.
public MongoDB\Driver\Cursor::key(): intПараметры
Параметров нет; метод вызывается у имеющегося Cursor.
Результат
Целое число — индекс текущего результата внутри курсора. Он не совпадает по смыслу с полем _id документа.
Ошибки и границы
Официальная страница не задаёт специального списка исключений и не обещает прикладной идентификатор при недопустимой позиции. Используйте key только после valid=true. Повторный запрос создаёт свою последовательность позиций; для связи с товаром храните его _id, а не номер строки.
Версии
С mongodb 1.9.0. В контрольном обходе позиции 0..4 относятся к ID 101,102,103,105,106.
Применение 1: строки складского отчёта
$rowNumber = $cursor->key();
$productId = $cursor->current()['_id'];Можно пронумеровать строки складского отчёта, одновременно сохранив настоящий ID товара. Проверка доказывает, что позиция 0 соответствует товару 101.
Полный сценарий чтения отчёта · Фактический отчёт
Применение 2: служебный перечень индексов
$position = $cursor->key();
$indexName = $cursor->current()['name'];Служебный экран нумерует описания индексов отдельно от их имён. Для команды удаления выбирается разрешённое имя своего индекса, а не позиция курсора.
Полный сценарий жизненного цикла индекса · Фактический перечень индексов
Фрагменты выделяют действие внутри двух самостоятельных программ. $manager — настроенный драйвер, $database — имя учебной базы, $collection — собственная коллекция, $cursor — результат текущей команды. Подготовка, данные, обработка и очистка находятся в связанных архивах; это не дополнительные неполные программы.
MongoDB\Driver\Cursor::nextПродвинуться к следующему документу и при необходимости получить очередную порцию ответа.
С mongodb 1.9.0. Batch size управляет порцией, а не общим количеством результатов. · mongodb
public MongoDB\Driver\Cursor::next(): voidПродвинуться к следующему документу и при необходимости получить очередную порцию ответа.
public MongoDB\Driver\Cursor::next(): voidПараметры
Параметров нет; метод вызывается у имеющегося Cursor.
Результат
Полезного возвращаемого значения нет. Метод меняет позицию; следующий документ получают отдельным current после проверки valid.
Ошибки и границы
Документация перечисляет InvalidArgumentException при разборе аргументов, ConnectionException и AuthenticationException. Не присваивайте $document=$cursor->next(): это не документ. Переставив next перед обработкой текущей строки, легко пропустить первую. Сбой получения следующей порции не означает нормального конца отчёта.
Версии
С mongodb 1.9.0. Batch size управляет порцией, а не общим количеством результатов.
Применение 1: строки складского отчёта
$document = $cursor->current();
// Здесь обработать текущую строку.
$cursor->next();После обработки Кружки курсор переходит к Лампе; затем читаются все пять результатов несмотря на начальный batchSize=2.
Полный сценарий чтения отчёта · Фактический отчёт
Применение 2: служебный перечень индексов
$index = $cursor->current();
// Здесь сохранить/показать разрешённые поля описания.
$cursor->next();Служебный перечень индексов обходится целиком при batchSize=1. На следующем просмотре используется отдельный курсор после изменения набора индексов.
Полный сценарий жизненного цикла индекса · Фактический перечень индексов
Фрагменты выделяют действие внутри двух самостоятельных программ. $manager — настроенный драйвер, $database — имя учебной базы, $collection — собственная коллекция, $cursor — результат текущей команды. Подготовка, данные, обработка и очистка находятся в связанных архивах; это не дополнительные неполные программы.
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


