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

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

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

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

Глава 144 · После первого каталога и основ PHP-драйвера MongoDB

MongoDB: индексы каталога, уникальные артикулы и страницы без пропусков

Составной индекс под экран, unique по магазину и SKU, варианты через elemMatch, partial unique, keyset по цене и ID, точечное управление индексами.

Что индекс меняет в работе каталога

Каталог уже умеет сохранять и читать товары. Следующий шаг — получать небольшой нужный экран, не допускать конфликтующих артикулов и корректно продолжать список. Здесь шесть самостоятельных сценариев: от индекса под готовый фильтр до аккуратного удаления одного индекса. В каждом видны исходные документы и полученный результат.

Перед этой главой нужны документ, коллекция и BSON и Query, BulkWrite и Cursor. Напоминание: PHP-массив описывает запрос, драйвер кодирует его в BSON, MongoDB исполняет запрос, курсор возвращает выбранные документы. JSON применяется здесь для сохранения читаемого отчёта, а не как промежуточная ступень обращения к базе.

Указатель товаров и сами товары
  1. Документ 22: магазин A, active, цена 200
  2. Индекс хранит упорядоченный ключ A → active → 200 → 22
  3. Запрос задаёт подходящий участок ключей и порядок
  4. MongoDB читает необходимые документы и возвращает четыре строки
  5. PHP выводит ID, название и цену

Аналогия индекса с указателем в книге помогает понять поиск, но имеет границы: индекс базы обновляется вместе с записями и может содержать несколько ключей одного документа с массивом. Создание ещё одного индекса расходует ресурсы. Индекс помогает определённым запросам; он не избавляет от необходимости правильно описывать условие и выбрать нужные поля.

ТерминЗачем нужен в каталоге
Составной индексУпорядоченная комбинация полей, например магазин + статус + цена + ID.
Уникальный индексСервер не принимает повтор ключа; у пары tenant + sku уникальна именно комбинация.
MultikeyИндекс учитывает элементы массива, а не только одно скалярное значение документа.
PartialИндекс содержит только документы, удовлетворяющие условию.
Keyset-пагинацияПродолжение после последней пары значений вместо загрузки всего набора в PHP.

Подготовить самостоятельный запуск

Самостоятельные архивы запускаются в PHP 8.4+ CLI с ext-mongodb 2.5+ и локальным MongoDB 8.0. Composer не требуется. Точные команды установки сервера/расширения, настройки и запуска находятся в README.txt каждого архива. В уроках используются $manager (драйвер), $database (имя базы), $collection (имя коллекции) и $namespace = база.коллекция. Эти переменные создаются в полной программе перед коротким рабочим фрагментом.

$env:PHPMAN_MONGODB_URI='mongodb://127.0.0.1:27075/?directConnection=true&retryWrites=false'
$env:PHPMAN_MONGODB_DATABASE='php_manual_lab'
php example.php

Это команды PowerShell, а не PHP. Сервер должен быть заранее запущен на указанном локальном порту. Руководства: MongoDB для Windows, установка расширения PHP. Helper принимает только отдельный loopback-стенд; это ограничение учебной программы, не возможностей драйвера.

Каждая программа явно создаёт собственную коллекцию с уникальным именем, выполняет сценарий, удаляет только её в finally и отдельно проверяет отсутствие. Нужные права перечислены в README. Индексы в рабочем приложении создают при подготовке схемы, а не на каждом открытии страницы. При аварийном завершении PHP finally не гарантируется; порядок точечной уборки указан в архиве.

Подобрать индекс под экран: магазин, статус, цена и четыре товара

Покупателю магазина A нужны четыре самых дешёвых активных товара от 200 рублей. Экрану достаточно ID, названия и цены. Сначала формулируем именно этот запрос; затем создаём индекс, соответствующий его условиям и порядку. PHP не получает 120 документов, чтобы отсеивать и сортировать их самостоятельно.

Исходные данные

В программе 120 товаров: ID 1–80 относятся к A, остальные к B; каждый третий — draft. Цена равна 100 + остаток от деления ID на 10 × 50. Например, ID 2 и 22 активны и стоят 200, ID 12 стоит столько же, но это черновик. Цена здесь целое число рублей; в реальном каталоге зафиксируйте валюту и единицу хранения.

Данные / условиеСмысл
tenant = AНе показывать товары магазина B
status = activeНе показывать draft
price >= 200Применить нижнюю границу цены
price ASC, _id ASC; limit 4Равные цены упорядочить по ID; получить один экран
Путь конкретных данных
  1. Условие выбирает A + active + цену от 200
  2. MongoDB упорядочивает price, затем _id
  3. Возвращаются четыре документа с тремя полями
  4. PHP выводит готовые строки
  5. Два explain показывают обычный выбор и пригодность заданного индекса

Самая короткая полезная операция

$filter = ['tenant' => 'A', 'status' => 'active', 'price' => ['$gte' => 200]];
$query = new MongoDB\Driver\Query($filter, [
    'projection' => ['_id' => 1, 'title' => 1, 'price' => 1],
    'sort' => ['price' => 1, '_id' => 1], 'limit' => 4,
]);
$cursor = $manager->executeQuery($namespace, $query);

Это выделенный фрагмент самостоятельной программы ниже: подключение и начальные документы находятся в полном примере. Кавычки вокруг $gte, $or и других операторов — одинарные, чтобы PHP не подставлял переменные вместо имён операторов MongoDB.

Что получится

IDТоварЦена, руб.
2Товар 2200
22Товар 22200
32Товар 32200
52Товар 52200

Как это устроено

tenant и status проверяются на точное равенство; после них в ключе идут price и _id в порядке сортировки. Число 1 в key означает возрастание, -1 — убывание. Порядок полей составного индекса важен: это не неупорядоченный набор. Индекс можно представить как отдельный алфавитный указатель, где сначала выбирают раздел магазина, затем статуса и читают цены по порядку. Он хранит ключи и ссылки на документы, а не заменяет сами карточки. title в этом индексе отсутствует, поэтому не объявляем запрос покрытым индексом. projection выбирает передаваемые поля, limit ограничивает итог, $gte означает «больше или равно».

Создать соответствующий индекс отдельно от чтения

$command = new MongoDB\Driver\Command([
    'createIndexes' => $collection,
    'indexes' => [[
        'name' => 'catalog_screen',
        'key' => ['tenant' => 1, 'status' => 1, 'price' => 1, '_id' => 1],
    ]],
]);
$cursor = $manager->executeWriteCommand($database, $command);
$reply = $cursor->toArray();
echo $reply[0]->numIndexesAfter; // 2: _id_ и catalog_screen.

createIndexes — имя команды и коллекция; indexes — список определений; name — устойчивое имя для диагностики и последующего управления; key — поля в нужном порядке. Числа 1 здесь означают направление, а не тип поля. Наличие индекса подтверждается ответом, а пригодность для запроса исследуется отдельно.

Границы и ошибки

Создание индекса требует места, работы при вставках/обновлениях и отдельного планирования на большой коллекции. В полной программе обычный запрос выполняется без hint; отдельный hint задаёт catalog_screen для проверки пригодности. Это не доказательство, что оптимизатор всегда выберет его или что он всегда быстрее. Маленькие 120 документов служат проверке смысла, не нагрузочному тесту. executionStats выполняет чтение: nReturned — выданные документы, totalKeysExamined — просмотренные ключи, totalDocsExamined — просмотренные документы. Их нельзя считать одним и тем же. Полные планы сохранены в HTML; форма плана и миллисекунды зависят от сервера и данных.

Полная самостоятельная программа, helper и архив
Подобрать индекс под экран: магазин, статус, цена и четыре товараСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mi116-helper.php';
[$manager, $database] = mi116Environment();
$server = mi116Command($manager, $database, ['buildInfo' => 1])[0]['version'];
$runtime = ['php' => PHP_VERSION, 'extension' => phpversion('mongodb'), 'server' => $server];
$collection = 'mongodb116_index_' . bin2hex(random_bytes(8));
$namespace = $database . '.' . $collection;
$owned = false; $details = [];
try {
    if (mi116Exists($manager, $database, $collection)) { throw new RuntimeException('Fixture already exists'); }
    mi116Command($manager, $database, ['create' => $collection], true);
    $owned = true;
$documents = [];
    for ($i = 1; $i <= 120; $i++) {
        $documents[] = ['_id' => $i, 'tenant' => $i <= 80 ? 'A' : 'B',
            'status' => $i % 3 === 0 ? 'draft' : 'active',
            'title' => 'Товар ' . $i, 'price' => 100 + ($i % 10) * 50];
    }
    mi116Insert($manager, $namespace, $documents);
    $index = ['name' => 'catalog_screen', 'key' => ['tenant' => 1, 'status' => 1, 'price' => 1, '_id' => 1]];
    $createCursor = $manager->executeWriteCommand($database, new MongoDB\Driver\Command([
        'createIndexes' => $collection, 'indexes' => [$index], 'maxTimeMS' => 2000,
    ]));
    $createCursor->setTypeMap(['root' => 'array', 'document' => 'array', 'array' => 'array']);
    $created = $createCursor->toArray()[0];
    $filter = ['tenant' => 'A', 'status' => 'active', 'price' => ['$gte' => 200]];
    $options = ['projection' => ['_id' => 1, 'title' => 1, 'price' => 1],
        'sort' => ['price' => 1, '_id' => 1], 'limit' => 4];
    $products = mi116Read($manager, $namespace, $filter, $options);
    $hintedProducts = mi116Read($manager, $namespace, $filter, $options + ['hint' => 'catalog_screen']);
    $find = ['find' => $collection, 'filter' => $filter, 'projection' => $options['projection'],
        'sort' => $options['sort'], 'limit' => 4, 'maxTimeMS' => 1000];
    $naturalPlan = mi116Command($manager, $database, ['explain' => $find, 'verbosity' => 'executionStats'])[0];
    $hintedPlan = mi116Command($manager, $database, ['explain' => $find + ['hint' => 'catalog_screen'], 'verbosity' => 'executionStats'])[0];
    $rows = array_map(fn($d) => [$d['_id'], $d['title'], $d['price']], $products);
    $checks = ['screen_rows' => $rows === [[2,'Товар 2',200],[22,'Товар 22',200],[32,'Товар 32',200],[52,'Товар 52',200]],
        'same_rows_with_hint' => $products === $hintedProducts,
        'index_created' => (int)$created['numIndexesBefore'] === 1 && (int)$created['numIndexesAfter'] === 2,
        'hinted_index_applicable' => mi116PlanContains($hintedPlan['queryPlanner'], 'indexName', 'catalog_screen'),
        'both_plans_return_four' => $naturalPlan['executionStats']['nReturned'] === 4 && $hintedPlan['executionStats']['nReturned'] === 4];
    $details = ['Создание индекса' => $created, 'Обычный выбор оптимизатора' => $naturalPlan,
        'Проверка заданного индекса через hint' => $hintedPlan,
        'Что измерено' => ['fixture_documents' => 120, 'performance_claim' => false, 'screen_limit' => 4]];
} finally {
    if ($owned) { mi116Command($manager, $database, ['drop' => $collection], true); }
}
$checks['owned_collection_removed'] = !mi116Exists($manager, $database, $collection);
$details = mi116Public($details, $collection, $database);
mi116Report('mongodb116-index-compound-screen', ['ID', 'Товар', 'Цена, руб.'], $rows, $checks, $runtime, $details);
echo 'mongodb116-index-compound-screen: saved result.html and result.json
';
Ожидаемый результат
mongodb116-index-compound-screen: saved result.html and result.json

Запустите php example.php из распакованного каталога и откройте result.html. Архив содержит исходные данные, mi116-helper.php и README.txt. result.json повторяет итоговую таблицу; metrics.json хранит проверки и диагностику.

Открыть фактический HTML-результат · Скачать таблицу JSON

Попробуйте сами. Покажите вместо четырёх шесть товаров. Решение: поменять limit в Query и в исследуемом find; следующим будет ID 62 за 200 рублей, затем ID 13 за 250. Индекс не нужно пересоздавать только из-за размера страницы.

Не допустить два одинаковых артикула внутри одного магазина

Два независимых магазина вправе продавать товар с артикулом LAMP. Но внутри A второй LAMP создаст неоднозначность импорта: какую карточку обновлять? Составной уникальный индекс закрепляет правило «одна пара магазин + артикул» непосредственно в базе.

Исходные данные

В коллекцию записываются A/LAMP, B/LAMP и A/CUP. Затем отдельная вставка пытается создать ещё один A/LAMP с новым _id = 104. Правильный результат — отказ 11000 и три прежних документа, а не замена первого товара.

Данные / условиеСмысл
101 / A / LAMPЛампа A
102 / B / LAMPЛампа B: тот же артикул, другой магазин
103 / A / CUPКружка
104 / A / LAMPПопытка дубликата: должна быть отклонена
Путь конкретных данных
  1. Создаём unique по tenant + sku
  2. Три разных пары сохраняются
  3. Новая пара A + LAMP совпадает с существующей
  4. Сервер отклоняет только эту вставку кодом 11000
  5. Повторное чтение подтверждает исходные ID и название

Самая короткая полезная операция

$command = new MongoDB\Driver\Command([
    'createIndexes' => $collection,
    'indexes' => [['name' => 'tenant_sku_unique',
        'key' => ['tenant' => 1, 'sku' => 1], 'unique' => true]],
]);
$manager->executeWriteCommand($database, $command);

Это выделенный фрагмент самостоятельной программы ниже: подключение и начальные документы находятся в полном примере. Кавычки вокруг $gte, $or и других операторов — одинарные, чтобы PHP не подставлял переменные вместо имён операторов MongoDB.

Что получится

IDМагазинАртикулНазвание
101ALAMPЛампа A
102BLAMPЛампа B
103ACUPКружка

Как это устроено

unique: true относится ко всей паре полей. Два отдельных unique-индекса на tenant и sku были бы другим, слишком строгим правилом: они запретили бы даже несколько товаров одного магазина. Индекс проверяет конкурирующие записи на сервере, поэтому предварительный find «такого ещё нет» не заменяет его: между чтением и вставкой другой запрос может успеть записать ту же пару. Здесь insert отправляется через BulkWrite; ожидаемый BulkWriteException обрабатывается только при коде 11000, другие причины передаются дальше. При конфликте приложение может показать «Этот артикул уже существует» и предложить перейти к карточке.

Границы и ошибки

Существующие дубликаты помешают построить unique-индекс; сначала надо решить, какие записи объединить или исправить. tenant и sku в этом контракте — обязательные непустые строки: unique сам не делает поля обязательными и не проверяет бизнес-формат. Нормализацию регистра/пробелов и правила collation выбирают отдельно. tenant берут из подтверждённого контекста пользователя, а не доверяют ID магазина из запроса. Проверка 11000 относится к конкретной операции: в большом пакете могут существовать другие уникальные ограничения.

Полная самостоятельная программа, helper и архив
Не допустить два одинаковых артикула внутри одного магазинаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mi116-helper.php';
[$manager, $database] = mi116Environment();
$server = mi116Command($manager, $database, ['buildInfo' => 1])[0]['version'];
$runtime = ['php' => PHP_VERSION, 'extension' => phpversion('mongodb'), 'server' => $server];
$collection = 'mongodb116_index_' . bin2hex(random_bytes(8));
$namespace = $database . '.' . $collection;
$owned = false; $details = [];
try {
    if (mi116Exists($manager, $database, $collection)) { throw new RuntimeException('Fixture already exists'); }
    mi116Command($manager, $database, ['create' => $collection], true);
    $owned = true;
mi116Command($manager, $database, ['createIndexes' => $collection, 'indexes' => [[
        'name' => 'tenant_sku_unique', 'key' => ['tenant' => 1, 'sku' => 1], 'unique' => true,
    ]]], true);
    mi116Insert($manager, $namespace, [
        ['_id' => 101, 'tenant' => 'A', 'sku' => 'LAMP', 'title' => 'Лампа A'],
        ['_id' => 102, 'tenant' => 'B', 'sku' => 'LAMP', 'title' => 'Лампа B'],
        ['_id' => 103, 'tenant' => 'A', 'sku' => 'CUP', 'title' => 'Кружка'],
    ]);
    $duplicate = false; $errorCode = null;
    try {
        mi116Insert($manager, $namespace, [['_id' => 104, 'tenant' => 'A', 'sku' => 'LAMP', 'title' => 'Повтор']]);
    } catch (MongoDB\Driver\Exception\BulkWriteException $error) {
        if ($error->getCode() !== 11000) { throw $error; }
        $duplicate = true; $errorCode = $error->getCode();
    }
    $saved = mi116Read($manager, $namespace, [], ['sort' => ['_id' => 1], 'limit' => 10]);
    $rows = array_map(fn($d) => [$d['_id'], $d['tenant'], $d['sku'], $d['title']], $saved);
    $checks = ['duplicate_11000' => $duplicate && $errorCode === 11000,
        'same_sku_different_tenants' => array_column($saved, '_id') === [101,102,103],
        'original_not_replaced' => $saved[0]['title'] === 'Лампа A',
        'unique_spec_saved' => mi116PlanContains(mi116Command($manager, $database, ['listIndexes' => $collection]), 'unique', true)];
    $details = ['Отказ повторной записи' => ['attempt_id' => 104, 'tenant' => 'A', 'sku' => 'LAMP', 'code' => $errorCode, 'inserted' => false],
        'Сохранённые документы' => $saved];
} finally {
    if ($owned) { mi116Command($manager, $database, ['drop' => $collection], true); }
}
$checks['owned_collection_removed'] = !mi116Exists($manager, $database, $collection);
$details = mi116Public($details, $collection, $database);
mi116Report('mongodb116-index-tenant-unique', ['ID', 'Магазин', 'Артикул', 'Название'], $rows, $checks, $runtime, $details);
echo 'mongodb116-index-tenant-unique: saved result.html and result.json
';
Ожидаемый результат
mongodb116-index-tenant-unique: saved result.html and result.json

Запустите php example.php из распакованного каталога и откройте result.html. Архив содержит исходные данные, mi116-helper.php и README.txt. result.json повторяет итоговую таблицу; metrics.json хранит проверки и диагностику.

Открыть фактический HTML-результат · Скачать таблицу JSON

Попробуйте сами. Добавьте B/CUP с новым ID. Он сохранится: ни B, ни CUP по отдельности не обязаны быть уникальны. Повторный A/CUP будет отклонён. Для обновления товара используйте проверенную пару или _id, не удаляйте старую карточку ради обхода ограничения.

Практическая проблема из обсуждения: Уникальность комбинации двух полей. Автор различает уникальность всей пары и дополнительные правила значений внутри документа. В нашем каталоге пара tenant/SKU защищается индексом, а обязательность и формат полей проверяются отдельно. Обсуждение помогает выбрать ситуацию; технические правила проверены по официальным источникам, данные и решение написаны для этого урока.

Найти красный вариант в наличии, а не два разных варианта

Посетитель выбирает красную кружку, которая есть в наличии. У кружки A красный вариант закончился, зато остались синие. Два независимых условия на массив могут ошибочно включить A в выдачу. Нужно связать цвет и остаток с одним элементом variants.

Исходные данные

variants — BSON-массив вложенных документов. В PHP ему соответствует список массивов: каждый элемент описывает один вариант. Индекс по variants.color и variants.stock становится multikey, потому что учитывает значения элементов массива.

Данные / условиеСмысл
11 / Кружка Ared: 0; blue: 4
12 / Кружка Bred: 5; blue: 0
13 / Кружка Cblue: 7
Путь конкретных данных
  1. Раздельный dotted-фильтр находит red где-то и stock > 0 где-то
  2. Он возвращает ID 11 и 12
  3. Вкладываем оба условия в variants.$elemMatch
  4. Один элемент должен удовлетворить обоим условиям
  5. Остаётся только ID 12; hint не меняет смысл отбора

Самая короткая полезная операция

$filter = ['variants' => ['$elemMatch' => [
    'color' => 'red', 'stock' => ['$gt' => 0],
]]];
$query = new MongoDB\Driver\Query($filter, [
    'projection' => ['_id' => 1, 'title' => 1], 'limit' => 10,
]);
$cursor = $manager->executeQuery($namespace, $query);

Это выделенный фрагмент самостоятельной программы ниже: подключение и начальные документы находятся в полном примере. Кавычки вокруг $gte, $or и других операторов — одинарные, чтобы PHP не подставлял переменные вместо имён операторов MongoDB.

Что получится

УсловиеФактический результат
Раздельные условия11, 12
Один элемент через $elemMatch12
Название подходящего товараКружка B

Как это устроено

В имени variants.color точка означает путь к полю вложенного документа. Без $elemMatch два предиката должны быть истинны для документа, но не обязаны относиться к одному элементу массива. $elemMatch задаёт «существует элемент, в котором одновременно color = red и stock > 0». Это условие поиска документов, а не команда вырезать все другие варианты из массива. Если запросить variants целиком, придёт весь массив подходящего товара. Для отдельного списка элементов нужен другой шаг преобразования данных. Один общий путь variants позволяет связать границы составного multikey-индекса; полный пример показывает isMultiKey в диагностике.

Следующий шаг: развернуть массив в строки и построить отчёт. Сама фильтрация по $elemMatch не создаёт такой отчёт.

Границы и ошибки

Нельзя бездумно включать в один составной multikey-индекс два независимых массива одного документа. Здесь оба поля принадлежат одному массиву variants. Индекс не исправляет логически неправильный фильтр. В поясняющем explain hint проверяет существующий variant_stock, но итоговые IDs проверяются независимо от формы плана. Самих вариантов немного; если их число неограниченно растёт и их редактируют независимо, стоит пересмотреть модель данных, а не бесконечно расширять документ товара.

Полная самостоятельная программа, helper и архив
Найти красный вариант в наличии, а не два разных вариантаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mi116-helper.php';
[$manager, $database] = mi116Environment();
$server = mi116Command($manager, $database, ['buildInfo' => 1])[0]['version'];
$runtime = ['php' => PHP_VERSION, 'extension' => phpversion('mongodb'), 'server' => $server];
$collection = 'mongodb116_index_' . bin2hex(random_bytes(8));
$namespace = $database . '.' . $collection;
$owned = false; $details = [];
try {
    if (mi116Exists($manager, $database, $collection)) { throw new RuntimeException('Fixture already exists'); }
    mi116Command($manager, $database, ['create' => $collection], true);
    $owned = true;
mi116Insert($manager, $namespace, [
        ['_id' => 11, 'title' => 'Кружка A', 'variants' => [['color' => 'red', 'stock' => 0], ['color' => 'blue', 'stock' => 4]]],
        ['_id' => 12, 'title' => 'Кружка B', 'variants' => [['color' => 'red', 'stock' => 5], ['color' => 'blue', 'stock' => 0]]],
        ['_id' => 13, 'title' => 'Кружка C', 'variants' => [['color' => 'blue', 'stock' => 7]]],
    ]);
    mi116Command($manager, $database, ['createIndexes' => $collection, 'indexes' => [[
        'name' => 'variant_stock', 'key' => ['variants.color' => 1, 'variants.stock' => 1],
    ]]], true);
    $options = ['projection' => ['_id' => 1, 'title' => 1], 'sort' => ['_id' => 1], 'limit' => 10];
    $separate = mi116Read($manager, $namespace, ['variants.color' => 'red', 'variants.stock' => ['$gt' => 0]], $options);
    $filter = ['variants' => ['$elemMatch' => ['color' => 'red', 'stock' => ['$gt' => 0]]]];
    $same = mi116Read($manager, $namespace, $filter, $options);
    $hinted = mi116Read($manager, $namespace, $filter, $options + ['hint' => 'variant_stock']);
    $plan = mi116Command($manager, $database, ['explain' => ['find' => $collection, 'filter' => $filter,
        'hint' => 'variant_stock', 'limit' => 10, 'maxTimeMS' => 1000], 'verbosity' => 'executionStats'])[0];
    $rows = [['Раздельные условия', implode(', ', array_column($separate, '_id'))],
        ['Один элемент через $elemMatch', implode(', ', array_column($same, '_id'))],
        ['Название подходящего товара', $same[0]['title']]];
    $checks = ['separate_conditions_include_false_positive' => array_column($separate, '_id') === [11,12],
        'same_variant_only' => array_column($same, '_id') === [12], 'hint_preserves_semantics' => $same === $hinted,
        'index_is_multikey' => mi116PlanContains($plan['queryPlanner'], 'isMultiKey', true),
        'one_matched_document' => $plan['executionStats']['nReturned'] === 1];
    $details = ['Документы с вариантами' => mi116Read($manager, $namespace, [], ['sort' => ['_id' => 1], 'limit' => 10]),
        'План заданного multikey-индекса' => $plan];
} finally {
    if ($owned) { mi116Command($manager, $database, ['drop' => $collection], true); }
}
$checks['owned_collection_removed'] = !mi116Exists($manager, $database, $collection);
$details = mi116Public($details, $collection, $database);
mi116Report('mongodb116-index-same-variant', ['Условие', 'Фактический результат'], $rows, $checks, $runtime, $details);
echo 'mongodb116-index-same-variant: saved result.html and result.json
';
Ожидаемый результат
mongodb116-index-same-variant: saved result.html and result.json

Запустите php example.php из распакованного каталога и откройте result.html. Архив содержит исходные данные, mi116-helper.php и README.txt. result.json повторяет итоговую таблицу; metrics.json хранит проверки и диагностику.

Открыть фактический HTML-результат · Скачать таблицу JSON

Попробуйте сами. Замените остаток красного варианта A с 0 на 2. После этого оба фильтра вернут 11 и 12. Уберите красный вариант у B: правильный фильтр оставит только 11. Для вывода всех подходящих вариантов переходите к обработке массивов в aggregation.

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

Разрешить архивные копии, но оставить только один активный артикул

Магазин хранит архивные версии карточки, но в продаже должна быть только одна активная карточка с данным артикулом. Обычный unique запретит и архивные копии. Частичный индекс включает только документы, которые удовлетворяют заданному условию.

Исходные данные

У A есть LAMP со статусами active, archived и draft. У B — собственный active LAMP. Программа пробует активировать черновик A, затем добавляет ещё одну архивную копию. Первая операция должна завершиться отказом, вторая — успешно.

Данные / условиеСмысл
201 / A / LAMPactive — занимает уникальный ключ
202 / A / LAMParchived — вне индекса
203 / A / LAMPdraft — попытка активировать
204 / B / LAMPactive — другая пара
205 / A / LAMPНовая archived-копия разрешена
Путь конкретных данных
  1. partialFilterExpression выбирает только status = active
  2. unique запрещает повтор tenant + sku среди выбранных
  3. Изменение draft → active пытается занять уже занятый ключ
  4. Ошибка 11000 оставляет ID 203 черновиком
  5. Новая archived-копия сохраняется; активные ID по-прежнему 201 и 204

Самая короткая полезная операция

$index = [
    'name' => 'active_sku_unique',
    'key' => ['tenant' => 1, 'sku' => 1], 'unique' => true,
    'partialFilterExpression' => ['status' => 'active'],
];
$command = new MongoDB\Driver\Command([
    'createIndexes' => $collection, 'indexes' => [$index],
]);
$manager->executeWriteCommand($database, $command);

Это выделенный фрагмент самостоятельной программы ниже: подключение и начальные документы находятся в полном примере. Кавычки вокруг $gte, $or и других операторов — одинарные, чтобы PHP не подставлял переменные вместо имён операторов MongoDB.

Что получится

IDМагазинАртикулСтатус
201ALAMPactive
202ALAMParchived
203ALAMPdraft
204BLAMPactive
205ALAMParchived

Как это устроено

partialFilterExpression — область действия индекса, unique — правило внутри этой области. Проверка срабатывает не только при insert, но и когда update переводит документ внутрь области. $set меняет только status; отказ этой одиночной записи не превращает весь бизнес-процесс в транзакцию. В интерфейсе можно объяснить, какая активная карточка уже использует артикул. Документы вне индекса всё равно существуют и доступны обычным запросам. В учебном отчёте видны все пять документов, поэтому поведение нельзя спутать с удалением архивов.

Границы и ошибки

Для использования partial-индекса в чтении запрос должен ограничивать результат его областью или подходящим подмножеством. Запрос «все статусы» нельзя насильно направлять в такой индекс: он не представляет всю коллекцию. Здесь partial — не механизм авторизации и не TTL-удаление. Проверяйте допустимые статусы отдельно: опечатка activ будет вне условия. Если нужно атомарно заменить активную версию двумя изменениями разных документов, это отдельная задача транзакций и репликации; данный standalone-пример её не обещает.

Полная самостоятельная программа, helper и архив
Разрешить архивные копии, но оставить только один активный артикулСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mi116-helper.php';
[$manager, $database] = mi116Environment();
$server = mi116Command($manager, $database, ['buildInfo' => 1])[0]['version'];
$runtime = ['php' => PHP_VERSION, 'extension' => phpversion('mongodb'), 'server' => $server];
$collection = 'mongodb116_index_' . bin2hex(random_bytes(8));
$namespace = $database . '.' . $collection;
$owned = false; $details = [];
try {
    if (mi116Exists($manager, $database, $collection)) { throw new RuntimeException('Fixture already exists'); }
    mi116Command($manager, $database, ['create' => $collection], true);
    $owned = true;
mi116Command($manager, $database, ['createIndexes' => $collection, 'indexes' => [[
        'name' => 'active_sku_unique', 'key' => ['tenant' => 1, 'sku' => 1],
        'unique' => true, 'partialFilterExpression' => ['status' => 'active'],
    ]]], true);
    mi116Insert($manager, $namespace, [
        ['_id' => 201, 'tenant' => 'A', 'sku' => 'LAMP', 'status' => 'active'],
        ['_id' => 202, 'tenant' => 'A', 'sku' => 'LAMP', 'status' => 'archived'],
        ['_id' => 203, 'tenant' => 'A', 'sku' => 'LAMP', 'status' => 'draft'],
        ['_id' => 204, 'tenant' => 'B', 'sku' => 'LAMP', 'status' => 'active'],
    ]);
    $duplicate = false;
    try {
        $activation = new MongoDB\Driver\BulkWrite;
        $activation->update(['_id' => 203], ['$set' => ['status' => 'active']], ['multi' => false]);
        $manager->executeBulkWrite($namespace, $activation);
    } catch (MongoDB\Driver\Exception\BulkWriteException $error) {
        if ($error->getCode() !== 11000) { throw $error; }
        $duplicate = true;
    }
    $archived = mi116Insert($manager, $namespace, [['_id' => 205, 'tenant' => 'A', 'sku' => 'LAMP', 'status' => 'archived']]);
    $saved = mi116Read($manager, $namespace, [], ['sort' => ['_id' => 1], 'limit' => 10]);
    $active = mi116Read($manager, $namespace, ['status' => 'active'], ['projection' => ['_id' => 1], 'sort' => ['_id' => 1], 'limit' => 10]);
    $rows = array_map(fn($d) => [$d['_id'], $d['tenant'], $d['sku'], $d['status']], $saved);
    $checks = ['activation_rejected_11000' => $duplicate, 'failed_document_still_draft' => $saved[2]['status'] === 'draft',
        'archive_duplicate_accepted' => $archived->getInsertedCount() === 1 && count($saved) === 5,
        'active_ids_preserved' => array_column($active, '_id') === [201,204],
        'partial_spec_saved' => mi116PlanContains(mi116Command($manager, $database, ['listIndexes' => $collection]), 'partialFilterExpression', ['status' => ['$eq' => 'active']])
            || mi116PlanContains(mi116Command($manager, $database, ['listIndexes' => $collection]), 'partialFilterExpression', ['status' => 'active'])];
    $details = ['Попытка активации' => ['id' => 203, 'error_code' => 11000, 'status_after_error' => $saved[2]['status']],
        'Активные ID' => array_column($active, '_id'), 'Архивная вставка' => $archived->getInsertedCount()];
} finally {
    if ($owned) { mi116Command($manager, $database, ['drop' => $collection], true); }
}
$checks['owned_collection_removed'] = !mi116Exists($manager, $database, $collection);
$details = mi116Public($details, $collection, $database);
mi116Report('mongodb116-index-partial-active', ['ID', 'Магазин', 'Артикул', 'Статус'], $rows, $checks, $runtime, $details);
echo 'mongodb116-index-partial-active: saved result.html and result.json
';
Ожидаемый результат
mongodb116-index-partial-active: saved result.html and result.json

Запустите php example.php из распакованного каталога и откройте result.html. Архив содержит исходные данные, mi116-helper.php и README.txt. result.json повторяет итоговую таблицу; metrics.json хранит проверки и диагностику.

Открыть фактический HTML-результат · Скачать таблицу JSON

Попробуйте сами. Сначала архивируйте 201, затем попробуйте активировать 203. В последовательной учебной проверке ключ освободится и 203 станет active. В реальном сервисе заранее решите, допустим ли промежуток без активной карточки и что делать при конкурентной активации.

Показать следующую страницу, не потеряв товары с одинаковой ценой

Кнопка «Показать ещё» должна продолжить список товаров по возрастанию цены. Цена не уникальна: три блюдца стоят 150 рублей. Одного фильтра price > последняя_цена недостаточно — он потеряет ещё не показанные товары с той же ценой.

Исходные данные

Шесть активных товаров имеют пары (price, _id): (100,11), (100,12), (150,21), (150,22), (150,23), (220,31). Есть также дешёвый draft 99, который не относится к экрану. Размер страницы — два. В этом небольшом стенде _id — заданное целое число; это допустимый BSON-тип ID, не преобразованный ObjectId.

Данные / условиеСмысл
Первая страница100/11, 100/12
Вторая страница150/21, 150/22
Последняя показанная параprice = 150, _id = 22
Продолжение150/23, 220/31
Путь конкретных данных
  1. Первый запрос: active, sort price + _id, limit 2
  2. Запоминаем оба значения последнего документа
  3. Следующий запрос: цена больше ИЛИ равная цена и ID больше
  4. Повторяем тем же порядком и размером страницы
  5. Пустая четвёртая страница завершает обход

Самая короткая полезная операция

// Последняя показанная строка: price = 150, _id = 22.
$filter = ['status' => 'active', '$or' => [
    ['price' => ['$gt' => 150]],
    ['price' => 150, '_id' => ['$gt' => 22]],
]];
$query = new MongoDB\Driver\Query($filter, [
    'projection' => ['_id' => 1, 'title' => 1, 'price' => 1],
    'sort' => ['price' => 1, '_id' => 1], 'limit' => 2,
]); // Дальше придут ID 23 и 31.

Это выделенный фрагмент самостоятельной программы ниже: подключение и начальные документы находятся в полном примере. Кавычки вокруг $gte, $or и других операторов — одинарные, чтобы PHP не подставлял переменные вместо имён операторов MongoDB.

Что получится

СтраницаIDТоварЦена, руб.
111Чашка A100
112Чашка B100
221Блюдце A150
222Блюдце B150
323Блюдце C150
331Тарелка220

Как это устроено

Ключ продолжения — пара значений, а не номер строки. Такой курсор страницы хранит price и _id; объект MongoDB\Driver\Cursor — другой механизм: итератор ответа одного запроса. $or содержит два допустимых способа оказаться после последнего товара. _id разрешает ничью цен и делает порядок однозначным. Внутри второго условия price и _id соединены логическим И. Индекс status + price + _id соответствует этой модели, а сортировка и limit выполняются в MongoDB до передачи данных PHP. Полная программа не накапливает коллекцию для сортировки: она обходит небольшие страницы и сохраняет только учебный отчёт. Отдельный неправильный запрос price > 150 возвращает 31 и демонстрирует потерю 23.

Границы и ошибки

Проверка отсутствия пропусков относится к неизменному набору документов. Если между страницами меняется price/status, товар может переместиться относительно курсора; уникальный _id не создаёт снимок данных. Для другого порядка сортировки меняются сравнения и ключ продолжения. Не заменяйте ObjectId текстовой строкой в BSON-фильтре реальной коллекции. Курсор HTTP API должен содержать проверяемые значения нужных типов, быть связан с фильтром/магазином и иметь ограниченный размер страницы. Keyset удобен для последовательного продолжения; произвольный переход на страницу 100 требует другого интерфейсного решения.

Полная самостоятельная программа, helper и архив
Показать следующую страницу, не потеряв товары с одинаковой ценойСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mi116-helper.php';
[$manager, $database] = mi116Environment();
$server = mi116Command($manager, $database, ['buildInfo' => 1])[0]['version'];
$runtime = ['php' => PHP_VERSION, 'extension' => phpversion('mongodb'), 'server' => $server];
$collection = 'mongodb116_index_' . bin2hex(random_bytes(8));
$namespace = $database . '.' . $collection;
$owned = false; $details = [];
try {
    if (mi116Exists($manager, $database, $collection)) { throw new RuntimeException('Fixture already exists'); }
    mi116Command($manager, $database, ['create' => $collection], true);
    $owned = true;
mi116Insert($manager, $namespace, [
        ['_id'=>11,'title'=>'Чашка A','price'=>100,'status'=>'active'],
        ['_id'=>12,'title'=>'Чашка B','price'=>100,'status'=>'active'],
        ['_id'=>21,'title'=>'Блюдце A','price'=>150,'status'=>'active'],
        ['_id'=>22,'title'=>'Блюдце B','price'=>150,'status'=>'active'],
        ['_id'=>23,'title'=>'Блюдце C','price'=>150,'status'=>'active'],
        ['_id'=>31,'title'=>'Тарелка','price'=>220,'status'=>'active'],
        ['_id'=>99,'title'=>'Черновик','price'=>50,'status'=>'draft'],
    ]);
    mi116Command($manager, $database, ['createIndexes'=>$collection, 'indexes'=>[[
        'name'=>'page_price_id', 'key'=>['status'=>1,'price'=>1,'_id'=>1],
    ]]], true);
    $last = null; $rows = []; $seen = []; $pageSizes = []; $emptyEnd = false;
    for ($page = 1; $page <= 10; $page++) {
        $filter = ['status'=>'active'];
        if ($last !== null) {
            $filter['$or'] = [
                ['price'=>['$gt'=>$last['price']]],
                ['price'=>$last['price'], '_id'=>['$gt'=>$last['_id']]],
            ];
        }
        $products = mi116Read($manager, $namespace, $filter, [
            'projection'=>['_id'=>1,'title'=>1,'price'=>1], 'sort'=>['price'=>1,'_id'=>1], 'limit'=>2,
        ]);
        if ($products === []) { $emptyEnd = true; break; }
        $pageSizes[] = count($products);
        foreach ($products as $product) {
            $rows[] = [$page,$product['_id'],$product['title'],$product['price']];
            $seen[] = $product['_id'];
        }
        $last = $products[array_key_last($products)];
    }
    $priceOnly = mi116Read($manager, $namespace, ['status'=>'active','price'=>['$gt'=>150]], [
        'projection'=>['_id'=>1], 'sort'=>['price'=>1,'_id'=>1], 'limit'=>2,
    ]);
    $checks = ['no_missing_ids' => $seen === [11,12,21,22,23,31],
        'no_duplicate_ids' => count($seen) === count(array_unique($seen)),
        'bounded_pages' => $pageSizes === [2,2,2] && $emptyEnd,
        'tie_continues_on_page_three' => $rows[4] === [3,23,'Блюдце C',150],
        'price_only_loses_tied_item' => array_column($priceOnly, '_id') === [31]];
    $details = ['Курсор после второй страницы' => ['price'=>150,'_id'=>22],
        'Следующий правильный экран' => array_slice($rows,4),
        'Неполный фильтр price > 150' => ['returned_ids'=>array_column($priceOnly,'_id'),'missing_id'=>23],
        'Конец списка' => ['empty_page'=>$page,'page_sizes'=>$pageSizes,'dataset_changed_during_run'=>false]];
} finally {
    if ($owned) { mi116Command($manager, $database, ['drop' => $collection], true); }
}
$checks['owned_collection_removed'] = !mi116Exists($manager, $database, $collection);
$details = mi116Public($details, $collection, $database);
mi116Report('mongodb116-index-keyset-pages', ['Страница', 'ID', 'Товар', 'Цена, руб.'], $rows, $checks, $runtime, $details);
echo 'mongodb116-index-keyset-pages: saved result.html and result.json
';
Ожидаемый результат
mongodb116-index-keyset-pages: saved result.html and result.json

Запустите php example.php из распакованного каталога и откройте result.html. Архив содержит исходные данные, mi116-helper.php и README.txt. result.json повторяет итоговую таблицу; metrics.json хранит проверки и диагностику.

Открыть фактический HTML-результат · Скачать таблицу JSON

Попробуйте сами. Поставьте limit = 3. Первая страница закончится на (150,21), вторая начнётся с (150,22) и покажет 22,23,31. Убедитесь, что сохраняются обе части курсора; вариант только с price снова потеряет товары той же цены.

Практическая проблема из обсуждения: Повторы при сортировке и пагинации MongoDB. У автора страницы содержат повторы после сортировки по неуникальным полям. Урок добавляет уникальный tie-breaker, затем показывает диапазонное продолжение по всей паре и отдельно оговаривает изменение данных между запросами. Обсуждение помогает выбрать ситуацию; технические правила проверены по официальным источникам, данные и решение написаны для этого урока.

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

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

Исходные данные

Есть три товара и три индекса: обязательный _id_, catalog_price и catalog_title. Команда listIndexes возвращает курсор описаний индексов. При batchSize = 1 первый пакет мал; полный обход всё равно должен получить все описания.

Данные / условиеСмысл
_id_Системный уникальный индекс ID
catalog_priceСортировка/поиск по price
catalog_titleИндекс по title, выбранный для точечного удаления
Путь конкретных данных
  1. Читаем документы до операции
  2. listIndexes: rewind → valid → current/key → next
  3. Проверяем точное имя catalog_title
  4. executeWriteCommand выполняет dropIndexes для одного имени
  5. Новый listIndexes и повторное чтение доказывают результат

Самая короткая полезная операция

$command = new MongoDB\Driver\Command([
    'dropIndexes' => $collection,
    'index' => 'catalog_title', // Точное имя одного учебного индекса.
]);
$cursor = $manager->executeWriteCommand($database, $command);
$reply = $cursor->toArray();
echo (int) $reply[0]->ok; // 1: команда подтверждена.

Это выделенный фрагмент самостоятельной программы ниже: подключение и начальные документы находятся в полном примере. Кавычки вокруг $gte, $or и других операторов — одинарные, чтобы PHP не подставлял переменные вместо имён операторов MongoDB.

Что получится

ЭтапИмя индексаКлюч
До удаления_id_{"_id":1}
До удаленияcatalog_price{"price":1}
До удаленияcatalog_title{"title":1}
После удаления_id_{"_id":1}
После удаленияcatalog_price{"price":1}

Как это устроено

rewind ставит новый курсор в начало; valid отвечает, есть ли текущий элемент; current даёт описание индекса; key — позицию 0,1,2 в этом ответе, а не имя индекса и не ID товара; next продвигает курсор. batchSize управляет порциями сетевого ответа, не ограничивает общее число индексов. Описание содержит name и key: catalog_title и {title: 1} — разные части. dropIndexes удаляет выбранную структуру доступа. В таблице после команды остаются _id_ и catalog_price, а в диагностике — все три неизменённых товара.

Границы и ошибки

Имена индексов берутся из проверенного описания, не из параметра URL. Для демонстрации выбран один точный индекс; строка * имела бы более широкий смысл. _id_-индекс нельзя удалить обычным dropIndexes. На рабочей коллекции удаление индекса влияет на запросы, блокировки и ограничения; отсутствие использования одним экраном ещё не доказывает, что индекс лишний для всей системы. Операция в примере относится к заранее созданной структуре, происхождение которой известно. После продвижения курсора не рассчитывайте произвольно перечитывать его с начала — выполняйте новый запрос.

Полная самостоятельная программа, helper и архив
Прочитать список индексов и убрать один без изменения товаровСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mi116-helper.php';
[$manager, $database] = mi116Environment();
$server = mi116Command($manager, $database, ['buildInfo' => 1])[0]['version'];
$runtime = ['php' => PHP_VERSION, 'extension' => phpversion('mongodb'), 'server' => $server];
$collection = 'mongodb116_index_' . bin2hex(random_bytes(8));
$namespace = $database . '.' . $collection;
$owned = false; $details = [];
try {
    if (mi116Exists($manager, $database, $collection)) { throw new RuntimeException('Fixture already exists'); }
    mi116Command($manager, $database, ['create' => $collection], true);
    $owned = true;
mi116Insert($manager, $namespace, [['_id'=>301,'title'=>'Кружка','price'=>500],
        ['_id'=>302,'title'=>'Лампа','price'=>900],['_id'=>303,'title'=>'Блюдце','price'=>250]]);
    mi116Command($manager, $database, ['createIndexes'=>$collection, 'indexes'=>[
        ['name'=>'catalog_price','key'=>['price'=>1]], ['name'=>'catalog_title','key'=>['title'=>1]],
    ]], true);
    $readIndexes = function() use ($manager, $database, $collection): array {
        $cursor = $manager->executeReadCommand($database, new MongoDB\Driver\Command([
            'listIndexes'=>$collection, 'cursor'=>['batchSize'=>1],
        ]));
        $cursor->setTypeMap(['root'=>'array','document'=>'array','array'=>'array']);
        $indexes = []; $positions = [];
        $cursor->rewind();
        while ($cursor->valid()) {
            $positions[] = $cursor->key();
            $index = $cursor->current();
            $indexes[$index['name']] = $index['key'];
            $cursor->next();
        }
        ksort($indexes);
        return [$indexes, $positions];
    };
    $beforeDocuments = mi116Read($manager, $namespace, [], ['sort'=>['_id'=>1],'limit'=>10]);
    [$beforeIndexes, $beforePositions] = $readIndexes();
    $dropCursor = $manager->executeWriteCommand($database, new MongoDB\Driver\Command([
        'dropIndexes'=>$collection, 'index'=>'catalog_title',
    ]));
    $dropCursor->setTypeMap(['root'=>'array','document'=>'array','array'=>'array']);
    $dropped = $dropCursor->toArray()[0];
    [$afterIndexes, $afterPositions] = $readIndexes();
    $afterDocuments = mi116Read($manager, $namespace, [], ['sort'=>['_id'=>1],'limit'=>10]);
    $rows = [];
    foreach (['До удаления'=>$beforeIndexes,'После удаления'=>$afterIndexes] as $stage=>$indexes) {
        foreach ($indexes as $name=>$key) {
            $rows[] = [$stage,$name,json_encode($key, JSON_THROW_ON_ERROR)];
        }
    }
    $checks = ['three_indexes_before' => array_keys($beforeIndexes) === ['_id_','catalog_price','catalog_title'],
        'only_named_index_removed' => array_keys($afterIndexes) === ['_id_','catalog_price'],
        'documents_unchanged' => $beforeDocuments === $afterDocuments && count($afterDocuments) === 3,
        'cursor_positions' => $beforePositions === [0,1,2] && $afterPositions === [0,1],
        'drop_acknowledged' => (float)$dropped['ok'] === 1.0];
    $details = ['Ответ dropIndexes' => $dropped, 'Документы после удаления индекса' => $afterDocuments,
        'Позиции курсора до и после' => [$beforePositions,$afterPositions], 'batchSize' => 1];
} finally {
    if ($owned) { mi116Command($manager, $database, ['drop' => $collection], true); }
}
$checks['owned_collection_removed'] = !mi116Exists($manager, $database, $collection);
$details = mi116Public($details, $collection, $database);
mi116Report('mongodb116-index-index-lifecycle', ['Этап', 'Имя индекса', 'Ключ'], $rows, $checks, $runtime, $details);
echo 'mongodb116-index-index-lifecycle: saved result.html and result.json
';
Ожидаемый результат
mongodb116-index-index-lifecycle: saved result.html and result.json

Запустите php example.php из распакованного каталога и откройте result.html. Архив содержит исходные данные, mi116-helper.php и README.txt. result.json повторяет итоговую таблицу; metrics.json хранит проверки и диагностику.

Открыть фактический HTML-результат · Скачать таблицу JSON

Попробуйте сами. Удалите в самостоятельном стенде catalog_price вместо catalog_title. В новом списке останутся _id_ и catalog_title, документы сохранятся. Затем создайте нужный индекс заново отдельной командой createIndexes и сравните список, не подменяя проверку наличия документов проверкой ok.

Справочник: команда записи и подтверждённый ответ

MongoDB\Driver\Manager::executeWriteCommand

Карточка содержит все параметры метода, возвращаемое значение, версии, ошибки и два разных применения: создание индекса для экрана и точечное удаление индекса. Это часть официального справочника драйвера, не отдельная программа с пропущенным подключением.

Перейти от выдачи товаров к отчётам

Выбирайте индекс после формулировки экрана и правил данных. Уникальность защищает запись, $elemMatch связывает условия одного варианта, курсор из двух значений сохраняет ничьи при пагинации. Для подсчёта итогов и преобразования документов на сервере продолжайте в aggregation и отчётах. Сопоставить этот подход с реляционной базой можно в проектировании индексов MySQL.

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

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

MongoDB\Driver\Manager::executeWriteCommandВыполнить серверную команду записи: создать индекс или удалить один выбранный индекс.

mongodb 1.4.0+; примеры mongodb 2.5+ · mongodb

final public MongoDB\Driver\Manager::executeWriteCommand(string $db, MongoDB\Driver\Command $command, ?array $options = null): MongoDB\Driver\Cursor

Выполняет команду записи на primary; в этой главе создаёт и удаляет индексы. Это метод расширения mongodb, а не Composer-библиотеки.

final public MongoDB\Driver\Manager::executeWriteCommand(string $db, MongoDB\Driver\Command $command, ?array $options = null): MongoDB\Driver\Cursor

Параметры

ПараметрЗначение
$db: stringИмя базы; не namespace база.коллекция и не URI.
$command: MongoDB\Driver\CommandДокумент команды. В createIndexes/dropIndexes имя коллекции находится внутри команды.
$options: ?array = nullНеобязательные настройки операции; null использует настройки по умолчанию.
options[session]: MongoDB\Driver\SessionСвязать операцию с сессией.
options[writeConcern]: MongoDB\Driver\WriteConcernТребования к подтверждению записи. По умолчанию учитывается активная транзакция сессии, затем URI.

Результат и ошибки

Успех возвращает Cursor с ответом команды; это не WriteResult и не число изменённых товаров. Поля ответа зависят от команды. InvalidArgumentException: неверные аргументы; session вместе с неподтверждённой записью; активная транзакция вместе с явно заданным readConcern/writeConcern. Последние настраивают при startTransaction. ConnectionException — отказ соединения, AuthenticationException — отказ аутентификации, RuntimeException — другие ошибки, включая неверную команду. readConcern не входит в список поддерживаемых options этого метода.

Для insert/update/delete используйте Manager::executeBulkWrite. Факт создания Command ещё не означает выполнения. w = 1 в стенде подтверждает ответ одного сервера; он не доказывает устойчивость к потере узла.

Версии

Доступен с mongodb 1.4.0; с 1.4.4 сочетание session с неподтверждённой записью вызывает InvalidArgumentException. Примеры используют расширение 2.5.5 и MongoDB 8.0.34; серверная поддержка определяется конкретной командой.

Применение 1: создать индекс для экрана каталога

$command = new MongoDB\Driver\Command([
    'createIndexes' => $collection,
    'indexes' => [[
        'name' => 'catalog_screen',
        'key' => ['tenant' => 1, 'status' => 1, 'price' => 1, '_id' => 1],
    ]],
]);
$cursor = $manager->executeWriteCommand($database, $command);
$reply = $cursor->toArray();
echo $reply[0]->numIndexesAfter; // 2: _id_ и catalog_screen.

В новой коллекции количество индексов меняется с 1 до 2. Обычная выборка показывает ID 2,22,32,52, отдельный explain проверяет пригодность catalog_screen. Полный запуск и данные.

Применение 2: удалить один индекс после проверки его имени

$command = new MongoDB\Driver\Command([
    'dropIndexes' => $collection,
    'index' => 'catalog_title', // Точное имя одного учебного индекса.
]);
$cursor = $manager->executeWriteCommand($database, $command);
$reply = $cursor->toArray();
echo (int) $reply[0]->ok; // 1: команда подтверждена.

Удаляется только catalog_title; catalog_price, _id_ и три товара остаются. Полный запуск listIndexes → dropIndexes → повторное чтение. Это другая операция, а не повтор создания с новым названием.

Фрагменты используют переменные самостоятельных архивов: $manager — подключённый драйвер, $database — база, $collection — собственная учебная коллекция. Правила команд: createIndexes и dropIndexes. Для чтения списков и отчётов перейдите к Manager::executeReadCommand.

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

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

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

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

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