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

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

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

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

Глава 145 · После первого каталога MongoDB и низкоуровневого драйвера

MongoDB: aggregation — расчёты, группы, связи и отчёт каталога

От первой стоимости остатка до group, unwind, lookup, facet и понятного последовательного чтения результата.

Что такое pipeline и как его записать в PHP

Для страницы каталога или отчёта по складу часто нужны не все сохранённые документы, а готовые ответы: три самых дорогих остатка, сумма по категориям, число тегов и название связанной категории. Aggregation pipeline задаёт последовательность преобразований на сервере. PHP отправляет описание и оформляет уже подготовленный результат.

Предварительный маршрут: документ и первый каталог → команда драйвера и курсор. Если синтаксис PHP ещё незнаком, рядом есть массивы, циклы и исключения. Здесь каждый новый этап MongoDB объясняется перед использованием.

Путь именно нашего первого расчёта
  1. PHP: Лампа, price_kopecks=1500, stock=2
  2. BSON-документ хранится в коллекции
  3. PHP описывает $multiply двух полей
  4. Сервер вычисляет 3000 копеек
  5. Cursor возвращает документ ответа
  6. Таблица показывает Лампу и 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Кружкаkitchenactive5004gift, home
102Лампаlightingactive15002home, desk
103Тарелкаkitchenactive3005home
104Черновикkitchendraft100100gift
105Блокнотstationeryactive2000пустой массив
106Кабельelectronicsactive7003desk

У всех документов tenant_id=1. В каждом ZIP эти данные создаются заново. Деньги представлены малыми целыми копейками, а количество — штуками. Такой набор позволяет проверить каждый итог вручную; он не выдаётся за тест производительности или модель полного финансового учёта.

Первые три товара по стоимости доступного остатка

Менеджеру склада нужны три доступных товара с наибольшей стоимостью остатка. В MongoDB хранятся цена и количество, а готового поля «стоимость остатка» нет. Pipeline вычислит его на сервере и вернёт только нужную тройку: Лампа — 3000, Кабель — 2100, Кружка — 2000 копеек.

Входные данные

В общем учебном каталоге шесть товаров. Черновик исключается по status, Блокнот — по stock=0. У Лампы price_kopecks=1500 и stock=2. Все значения синтетические, цены заданы в копейках, а не строками «15 рублей».

Преобразование данных
  1. 6 документов коллекции
  2. match: active и stock > 0 → 4 товара
  3. project: Лампа 1500 × 2 → 3000 коп.
  4. sort: 3000, 2100, 2000, 1500
  5. 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Лампа150023000
106Кабель70032100
101Кружка50042000

Разбор

Первый короткий фрагмент делает только отбор и расчёт. Каждый элемент внешнего списка $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. В отличие от предыдущего экрана, здесь сохраняем карточку Блокнота с нулевым остатком: она всё ещё существует в каталоге. Черновик с сотней единиц в отчёт не входит.

Преобразование данных
  1. 5 активных карточек
  2. group key = category
  3. kitchen: Кружка + Тарелка
  4. products 1+1; units 4+5; value 2000+1500
  5. 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.

Фрагмент выделяет механизм на уже подготовленной коллекции. Полная программа ниже добавляет соединение, точный фильтр, обработку результата, проверки и очистку. Изменения входных данных не требуют запуска соседних примеров.

Что получится в полном примере

КатегорияТоваровШтукСтоимость, коп.
electronics132100
kitchen293500
lighting123000
stationery100

Разбор

В $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 пуст. Внутри каждого учебного массива теги уникальны.

Преобразование данных
  1. Кружка: tags=[gift, home], stock=4
  2. unwind → gift/4 и home/4
  3. group по тегу → desk 2, gift 1, home 3
  4. Ошибочная сумма stock после unwind → 20
  5. Сумма исходных пяти товаров → 14

Короткая операция

$pipeline = [
    ['$match' => ['status' => 'active']],
    ['$unwind' => '$tags'],
    ['$group' => ['_id' => '$tags', 'occurrences' => ['$sum' => 1]]],
    ['$sort' => ['_id' => 1]],
];
// Кружка с tags=['gift','home'] даёт две строки, каждая со stock=4.
// Суммировать stock этих двух строк как остаток товара нельзя.

Фрагмент выделяет механизм на уже подготовленной коллекции. Полная программа ниже добавляет соединение, точный фильтр, обработку результата, проверки и очистку. Изменения входных данных не требуют запуска соседних примеров.

Что получится в полном примере

Часть отчётаЧто считаемКоличество
Тегdesk2
Тегgift1
Тегhome3
ДиагностикаТоваров до unwind5
ДиагностикаСтрок после unwind6
ОшибкаОстаток после 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 разрешает такую запись, поскольку строковая ссылка автоматически не создаёт внешний ключ.

Преобразование данных
  1. Товар 101: category=kitchen
  2. lookup в своих categories по _id → [Посуда]
  3. unwind → одна строка с Посудой
  4. Товар 107: unknown → []
  5. 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. Нулевой остаток здесь не исключается: Блокнот остаётся активной карточкой и первым попадает в дешёвую страницу. Это другой смысл фильтра, чем «доступно на складе» в первом примере.

Преобразование данных
  1. match → 5 активных товаров
  2. Ветка page: sort по цене/ID → limit 2
  3. Ветка total: count без limit → 5
  4. Ветка categories: group → 4 категории
  5. Один документ ответа с тремя массивами

Короткая операция

$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товаров
Категорияelectronics1товаров
Категорияkitchen2товаров
Категорияlighting1товаров
Категорияstationery1товаров
Пустая выборкаКоличество при отсутствии совпадений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 не является универсально самым быстрым способом; сравнивайте с отдельными индексируемыми запросами по задаче.

Полная самостоятельная программа и архив
Получить страницу, общее количество и категории одним pipelineСамостоятельный пример
<?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-массив.

Преобразование данных
  1. Создан Cursor: current=null, обход ещё не начат
  2. rewind → первая позиция; valid=true
  3. current → Кружка; key=0; next → Лампа
  4. Пять строк прочитаны → valid=false, current=null
  5. 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РезультатСтоимость, коп.
Документ0101Кружка2000
Документ1102Лампа3000
Документ2103Тарелка1500
Документ3105Блокнот0
Документ4106Кабель2100
ИсчерпаниеNULLNULLvalid=false; current=nullNULL
Повторный rewindNULLNULLLogicExceptionNULL
Новый курсорNULLNULL101, 102, 103, 105, 106NULL
Пустой курсорNULLNULL0 документов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.readConcernMongoDB\Driver\ReadConcern; уровень подтверждённых данных, сервер MongoDB 3.2+. Старый сервер отклонит неподдерживаемую настройку.
options.readPreferenceMongoDB\Driver\ReadPreference; выбор подходящего сервера для чтения.
options.sessionMongoDB\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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.