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

Дерево руководства
Глава 175 · После переменных, массивов и знакомства с MongoDB
MongoDB: от запроса к экрану заказов на PHP
Карточка и очередь сборщика, первая вставка через PHPLIB, безопасный фильтр из HTML-формы, проекция, продолжение списка и один проход по курсору.
От номера заказа к экрану сборщика
На складе есть три готовых к сборке заказа магазина A. Сотрудник хочет открыть номер, прочитать название товара и сразу увидеть сумму. Начнём с одного заказа, затем добавим очередь, вторую страницу и обработку формы. В каждом шаге ссылка «Открыть экран» показывает HTML, который создал PHP из реально прочитанных документов.
- Сборщик знает номер A-101
- MongoDB проверяет shop=A и orderNo=A-101
- Возвращаются товар «Лампа <мини>» и сумма 1250 ₽
- PHP кодирует название для HTML → карточка сотрудника
$number = 'A-101';
echo $number; // A-101В первой строке $number — имя переменной. = сохраняет в ней текст между кавычками. echo во второй строке выводит этот текст. Пока это просто номер. В следующем фрагменте он станет одним из значений фильтра.
$filter = ['shop' => 'A', 'orderNo' => $number];
$query = new MongoDB\Driver\Query($filter);В массиве слева от => стоит имя поля, справа — искомое значение. new создаёт объект Query, который хранит задание для базы. Выполнить его и получить данные поможет Manager. Полученный курсор похож на движущуюся ленту: PHP забирает текущий заказ и переходит дальше. Драйвер может доставлять сразу целую порцию, поэтому одной итерации PHP не обязательно соответствует один сетевой обмен.
В официальном разделе учебных маршрутов две ветви. Для первого экрана пригодится путь через PHP Library. Когда возникнет вопрос, какие команды ушли на сервер и где тратится время, откройте наблюдение за началом и окончанием команд (APM) и разбор отказов команд. Там показано, как связаны наблюдаемая команда, её результат и ошибка. Официальное оглавление Tutorials.
Первая вставка и чтение с PHP Library (PHPLIB)
Расширение mongodb предоставляет PHP классы Manager, Query, Cursor и типы BSON. Пакет Composer mongodb/mongodb добавляет Client, Database, Collection и методы уровня коллекции. Эти два слоя работают вместе. Первому примеру с Manager достаточно расширения. Для ветви PHPLIB установите также библиотеку.
composer require mongodb/mongodb:2.5.0Команда для отдельной учебной папки устанавливает версию, на которой проверялась эта глава. composer.json описывает зависимости проекта, composer.lock хранит выбранные версии, а vendor/autoload.php подключает их классы. При переносе готового проекта вместе с lock-файлом используют composer install. Установите расширение в том PHP CLI, который запускает Composer.
require __DIR__.'/vendor/autoload.php';
$client = new MongoDB\Client($uri);
$db = $client->getDatabase('php_manual_lab');
$collection = $db->getCollection($collectionName);
$receipt = $collection->insertOne(['orderNo' => 'A-201', 'title' => 'Маркер']);
$id = $receipt->getInsertedId();
$order = $collection->findOne(['_id' => $id]);
echo $order['title']; // МаркерЗдесь $uri — адрес локального учебного сервера, а $collectionName — имя созданной для опыта коллекции. Цепочка Client → Database → Collection задаёт, где выполнять операцию. Первая команда insertOne создаёт документ. При отсутствии _id расширение само создаст BSON ObjectId, который вернётся в квитанции. findOne возвращает найденный документ или null, find возвращает курсор для foreach.
При стандартной карте типов PHPLIB документ представлен BSONDocument, а вложенный список — BSONArray. Они допускают обращение через квадратные скобки и позволяют при обратном сохранении сохранить различие BSON-документа и BSON-массива. В примере с двумя ячейками хранения показаны оба типа и явная карта для PHP-массива. В сравнении точек входа одна и та же задача разобрана через PHPLIB и через драйвер. Официальный урок по PHPLIB.
Запуск и результат в браузере
Распакуйте ZIP интересующего примера в отдельную папку. Для учебного стенда использованы PHP 8.5.11 CLI x64, ext-mongodb 2.5.5, библиотека 2.5.0 и отдельный MongoDB Server 8.0.34. Настройка локального сервера описана в README.txt. Команды ниже предназначены для PowerShell и стенда на 127.0.0.1:27075.
$env:PHPMAN_MONGODB_URI='mongodb://127.0.0.1:27075/?directConnection=true&retryWrites=false'
$env:PHPMAN_MONGODB_DATABASE='php_manual_lab'
php example.phpОткройте result.html из той же папки. На первом экране будут карточки сборщика, ниже — таблица значений, которые их объясняют. Эти же строки есть в result.json. Программа создаёт свою временную коллекцию, удаляет её в finally и проверяет очистку. Для двух примеров с библиотекой в README описана автозагрузка. В архиве обработки статуса есть также веб-пример для локального сервера и отдельная инструкция web-README.md.
Открыть один заказ по номеру
Покупатель называет номер A-101. Сборщику нужны товар и сумма этого заказа. После выполнения программа напечатает «Заказ A-101 · Лампа <мини> · 1250 ₽» и создаст карточку для браузера.
Исходные данные
Один заказ — один документ. shop обозначает магазин, orderNo — номер, title — название, status — состояние, totalRub — целая сумма в рублях. Имя переменной начинается с $. Квадратные скобки создают массив, а знак => связывает имя поля со значением.
- Номер A-101 + магазин A → условие поиска
- MongoDB отдаёт одну запись → PHP берёт title и totalRub
- Для HTML угловые скобки кодируются → покупатель видит название целиком
Короткий фрагмент
$number = 'A-101';
$filter = ['shop' => 'A', 'orderNo' => $number];
$query = new Query($filter, ['limit' => 1, 'projection' => ['_id' => 0, 'title' => 1]]);Что увидит сотрудник
Заказ A-101 · Лампа <мини> · 1250 ₽
Открыть готовый экран из фактического запуска · Строки результата в JSON
| Номер | Товар | Состояние | Сумма, ₽ |
|---|---|---|---|
| A-101 | Лампа <мини> | К сборке | 1250 |
Почему получился такой результат
Query хранит описание поиска. В базу обращается executeQuery(), который возвращает курсор для чтения. Полученное название содержит угловые скобки: htmlspecialchars() сохраняет их как видимый текст при выводе в HTML.
Границы примера
В этом наборе номер уникален. limit=1 ограничивает ответ, но сам по себе не вводит уникальность в живой базе. Для номера внутри магазина можно создать уникальный составной индекс по shop и orderNo. Готовность результата при отсутствующем заказе отдельно разобрана ниже.
Полная программа и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/order-screen.php';
use MongoDB\Driver\{Manager, Query, Cursor, CursorInterface};
runScreen('mongodb131-screen-order-card', 'Карточка заказа', ['Номер', 'Товар', 'Состояние', 'Сумма, ₽'],
function (Manager $manager, string $namespace, string $uri, string $db, string $name): array {
$inserted = sampleOrders($manager, $namespace);
$query = new Query(['shop' => 'A', 'orderNo' => 'A-101'], ['limit' => 1, 'projection' => SCREEN_FIELDS]);
$cursor = $manager->executeQuery($namespace, $query);
$cursor->setTypeMap(ARRAY_MAP);
$orders = $cursor->toArray();
$order = $orders[0];
$message = 'Заказ '.$order['orderNo'].' · '.$order['title'].' · '.$order['totalRub'].' ₽';
$rows = [[$order['orderNo'], $order['title'], statusLabel($order['status']), $order['totalRub']]];
$checks['fixture_contains_other_orders'] = $inserted === 5;
$checks['one_order'] = count($orders) === 1 && $order['orderNo'] === 'A-101';
$checks['private_note_excluded'] = !array_key_exists('internalNote', $order);
$checks['real_price'] = $order['totalRub'] === 1250;
$checks['literal_title_safe'] = htmlValue($order['title']) === 'Лампа <мини>';
$checks['query_abi'] = (new ReflectionClass($query))->isFinal() && (new ReflectionClass($query))->getConstructor()->isPublic();
return [$orders, $message, $rows, $checks, ['filter' => ['shop' => 'A', 'orderNo' => 'A-101'], 'limit' => 1]];
}
);
Ожидаемый результат
Заказ A-101 · Лампа <мини> · 1250 ₽
Скрипт читает документы у локального MongoDB, печатает итог в терминале и строит HTML-карточки. Файл order-screen.php и исходные данные входят в ZIP.
Самостоятельное изменение
Поставьте A-102 вместо A-101. Вы увидите «Штатив», «Упакован» и 3490 ₽. Измените соответствующие ожидаемые значения в учебном файле.
Найти созданный заказ по идентификатору из квитанции
После регистрации сборщику нужна карточка именно только что созданного заказа. Если строка из адреса внешне совпадает с идентификатором, запрос всё равно может ничего не вернуть.
Исходные данные
Клиент передаёт в insertOne() новый документ без _id. Расширение формирует ObjectId. Получаем это значение из InsertOneResult и читаем сохранённый заказ. Две ячейки хранения показывают, как библиотека возвращает вложенный список.
- insertOne без _id → в квитанции появился ObjectId
- Текст из символов этого ID → совпадений нет
- Тот же BSON ObjectId → заказ A-201, ячейки P-1 и P-2
Короткий фрагмент
$receipt = $collection->insertOne(['orderNo' => 'A-201', 'title' => 'Маркер']);
$id = $receipt->getInsertedId();
$order = $collection->findOne(['_id' => $id]);Что увидит сотрудник
Создан заказ A-201 · ячейки P-1, P-2
Открыть готовый экран из фактического запуска · Строки результата в JSON
| Номер | Товар | Ячейка 1 | Ячейка 2 |
|---|---|---|---|
| A-201 | Маркер | P-1 | P-2 |
Почему получился такой результат
Тип входит в значение BSON. Объект ObjectId и строка из тех же 24 шестнадцатеричных символов различаются при таком сравнении. Библиотека обычно возвращает BSONDocument для документа и BSONArray для списка. Доступ $doc["bins"][0] работает благодаря их интерфейсу. Отдельный запрос с typeMap возвращает массив для шаблона.
Границы примера
Если ключ изначально сохраняли строкой, его и нужно искать строкой. При получении ID через HTTP сначала проверяйте вход, затем создавайте ObjectId в предусмотренной контрактом ветви. Текст случайного ObjectId меняется в каждом запуске.
Полная программа и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/order-screen.php';
use MongoDB\Driver\{Manager, Query, Cursor, CursorInterface};
runScreen('mongodb131-screen-object-receipt', 'Заказ зарегистрирован', ['Номер', 'Товар', 'Ячейка 1', 'Ячейка 2'],
function (Manager $manager, string $namespace, string $uri, string $db, string $name): array {
$collection = libraryCollection($uri, $db, $name);
$receipt = $collection->insertOne(['shop' => 'A', 'orderNo' => 'A-201', 'title' => 'Маркер',
'status' => 'ready', 'totalRub' => 240, 'bins' => ['P-1', 'P-2']]);
$id = $receipt->getInsertedId();
$wrongType = $collection->findOne(['_id' => (string)$id]);
$doc = $collection->findOne(['_id' => $id, 'shop' => 'A']);
$backAsArray = $collection->findOne(['_id' => $id], ['typeMap' => ARRAY_MAP, 'projection' => SCREEN_FIELDS]);
$message = 'Создан заказ '.$doc['orderNo'].' · ячейки '.implode(', ', $doc['bins']->getArrayCopy());
$checks['identifier_generated'] = $id instanceof MongoDB\BSON\ObjectId;
$checks['text_id_not_matched'] = $wrongType === null;
$checks['document_model'] = $doc instanceof MongoDB\Model\BSONDocument;
$checks['array_model'] = $doc['bins'] instanceof MongoDB\Model\BSONArray;
$checks['same_id'] = (string)$doc['_id'] === (string)$id;
$checks['mapped_for_screen'] = is_array($backAsArray) && $backAsArray['totalRub'] === 240 && !isset($backAsArray['_id']);
$rows = [[$doc['orderNo'], $doc['title'], $doc['bins'][0], $doc['bins'][1]]];
return [[$backAsArray], $message, $rows, $checks, ['id_type' => get_class($id), 'wrong_search_found' => $wrongType !== null,
'document_class' => get_class($doc), 'list_class' => get_class($doc['bins'])]];
}
);
Ожидаемый результат
Создан заказ A-201 · ячейки P-1, P-2
Скрипт читает документы у локального MongoDB, печатает итог в терминале и строит HTML-карточки. Файл order-screen.php и исходные данные входят в ZIP.
Самостоятельное изменение
Получите вторую ячейку через $doc["bins"][1], затем проследите тип корня и вложенного поля при явном typeMap. В обоих вариантах вторая ячейка P-2.
Поиск по тексту идентификатора вернул пустой результат при наличии записи. Сравнить текст с BSON ObjectId в двух адресных чтениях и использовать идентификатор из InsertOneResult. Вопрос разработчика на Stack Overflow.
Показать очередь своего магазина без внутренних заметок
Сборщик не должен видеть заказ другого магазина и служебные заметки. Из пяти документов на экране останутся три карточки, в каждой — только данные для сборки.
Исходные данные
Условие shop ограничивает магазин, status — этап работы. Оба условия находятся в первом аргументе. Второй аргумент содержит projection со списком возвращаемых полей. 1 включает поле, 0 для _id явно убирает служебный ключ.
- 5 заказов → shop=A и status=ready → 3 записи
- projection → номер, товар, состояние, сумма
- Курсор → три карточки; internalNote отсутствует даже в строке PHP
Короткий фрагмент
$filter = ['shop' => 'A', 'status' => 'ready'];
$options = ['projection' => ['_id' => 0, 'orderNo' => 1, 'title' => 1, 'status' => 1, 'totalRub' => 1],
'sort' => ['orderNo' => 1], 'limit' => 10];
$query = new Query($filter, $options);Что увидит сотрудник
К сборке · магазин A · заказов 3
Открыть готовый экран из фактического запуска · Строки результата в JSON
| Номер | Товар | Сумма, ₽ |
|---|---|---|
| A-101 | Лампа <мини> | 1250 |
| A-103 | Кабель | 890 |
| A-104 | Чехол | 990 |
Почему получился такой результат
Фильтр решает, какие документы попадут в выдачу, проекция — какие поля приедут. Если спрятать поле только в шаблоне, оно всё равно придёт с сервера. Одинаковый порядок задаётся в sort, а максимум строк запрошен через limit.
Границы примера
Для обычной проекции включения и исключения поля смешивать нельзя, кроме особого случая _id. Проекция не заменяет проверку права на магазин. В рабочем приложении shop берут из проверенного контекста сотрудника.
Полная программа и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/order-screen.php';
use MongoDB\Driver\{Manager, Query, Cursor, CursorInterface};
runScreen('mongodb131-screen-public-fields', 'Очередь на сборку', ['Номер', 'Товар', 'Сумма, ₽'],
function (Manager $manager, string $namespace, string $uri, string $db, string $name): array {
sampleOrders($manager, $namespace);
$filter = ['shop' => 'A', 'status' => 'ready'];
$cursor = orderRows($manager, $namespace, $filter, [
'projection' => SCREEN_FIELDS, 'sort' => ['orderNo' => 1], 'limit' => 10,
]);
$orders = $cursor->toArray();
$rows = [];
foreach ($orders as $order) {
$keys = array_keys($order);
sort($keys);
$checks['only_requested_fields'] = ($checks['only_requested_fields'] ?? true) && $keys === ['orderNo','status','title','totalRub'];
$rows[] = [$order['orderNo'], $order['title'], $order['totalRub']];
}
$checks['visible_order_numbers'] = array_column($orders, 'orderNo') === ['A-101','A-103','A-104'];
$checks['packed_excluded'] = !in_array('A-102', array_column($orders, 'orderNo'), true);
$checks['other_shop_excluded'] = !in_array('B-105', array_column($orders, 'orderNo'), true);
$message = 'К сборке · магазин A · заказов '.count($orders);
return [$orders, $message, $rows, $checks, ['filter' => $filter, 'projection' => SCREEN_FIELDS]];
}
);
Ожидаемый результат
К сборке · магазин A · заказов 3
Скрипт читает документы у локального MongoDB, печатает итог в терминале и строит HTML-карточки. Файл order-screen.php и исходные данные входят в ZIP.
Самостоятельное изменение
Уберите только status из фильтра. В списке появится A-102, но чужой B-105 всё ещё не должен вернуться. Исправьте ожидания, сравните набор ключей.
Смена старого API на find() оставила в выдаче поля, которые разработчик считал исключёнными. Вложить список полей в projection и проверить ключи каждого реально прочитанного документа. Вопрос разработчика на Stack Overflow.
Продолжить очередь после последней строки первой страницы
На узком экране помещаются две карточки. Сначала нужны срочные заказы; два сотрудника, открывшие одну и ту же очередь без изменений, ожидают одинаковый порядок.
Исходные данные
Значение -1 упорядочивает по убыванию, 1 — по возрастанию. У A-103 и A-104 одинаковый приоритет 3. Второй ключ orderNo различает их; в учебном наборе он уникален внутри магазина.
- sort priority↓, orderNo↑ → A-103, A-104, A-101
- limit=2 → запоминаем пару (3, A-104)
- Следующая страница допускает меньший приоритет либо тот же с большим номером → A-101
Короткий фрагмент
$sort = ['priority' => -1, 'orderNo' => 1];
$firstPage = new Query(['shop' => 'A', 'status' => 'ready'], ['sort' => $sort, 'limit' => 2]);Что увидит сотрудник
Страница 1 · A-103, A-104. Страница 2 · A-101
Открыть готовый экран из фактического запуска · Строки результата в JSON
| Страница | Номер | Приоритет |
|---|---|---|
| 1 | A-103 | 3 |
| 1 | A-104 | 3 |
| 2 | A-101 | 2 |
Почему получился такой результат
Для следующей страницы важны обе части позиции. Условие только по приоритету пропустило бы оставшиеся заказы того же приоритета, если бы граница пришлась на середину группы. При подборе индекса для такой очереди учитывают равенства shop и status, затем порядок priority и orderNo.
Границы примера
Два отдельных чтения не фиксируют общий снимок. Если заказ меняет приоритет между страницами, он может переместиться. Ограничение limit относится к количеству возвращённых документов, а batchSize — к размерам порций передачи. Индекс и план проверяют на рабочих объёмах.
Полная программа и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/order-screen.php';
use MongoDB\Driver\{Manager, Query, Cursor, CursorInterface};
runScreen('mongodb131-screen-next-page', 'Очередь по приоритету', ['Страница', 'Номер', 'Приоритет'],
function (Manager $manager, string $namespace, string $uri, string $db, string $name): array {
sampleOrders($manager, $namespace);
$filter = ['shop' => 'A', 'status' => 'ready'];
$options = ['projection' => SCREEN_FIELDS + ['priority' => 1], 'sort' => ['priority' => -1, 'orderNo' => 1], 'limit' => 2];
$first = orderRows($manager, $namespace, $filter, $options)->toArray();
$last = $first[array_key_last($first)];
$after = ['$or' => [
['priority' => ['$lt' => $last['priority']]],
['priority' => $last['priority'], 'orderNo' => ['$gt' => $last['orderNo']]],
]];
$second = orderRows($manager, $namespace, $filter + $after, $options)->toArray();
$orders = array_merge($first, $second);
$rows = [];
foreach ([1 => $first, 2 => $second] as $page => $items) {
foreach ($items as $order) {
$rows[] = [$page, $order['orderNo'], $order['priority']];
}
}
$checks['first_page'] = array_column($first,'orderNo') === ['A-103','A-104'];
$checks['second_page'] = array_column($second,'orderNo') === ['A-101'];
$checks['tie_breaker'] = $first[0]['priority'] === $first[1]['priority'] && $last['orderNo'] === 'A-104';
$checks['no_duplicates'] = count(array_unique(array_column($orders, 'orderNo'))) === count($orders);
$checks['page_limit'] = count($first) <= 2 && count($second) <= 2;
$message = 'Страница 1 · '.implode(', ', array_column($first,'orderNo')).'. Страница 2 · '.implode(', ',array_column($second,'orderNo'));
return [$orders, $message, $rows, $checks, ['sort' => $options['sort'], 'limit' => 2, 'next_filter' => $after]];
}
);
Ожидаемый результат
Страница 1 · A-103, A-104. Страница 2 · A-101
Скрипт читает документы у локального MongoDB, печатает итог в терминале и строит HTML-карточки. Файл order-screen.php и исходные данные входят в ZIP.
Самостоятельное изменение
Поставьте limit=1 и продолжите после A-103. Благодаря второй ветви условия следующим станет A-104, хотя их приоритеты совпадают.
Показать пустую очередь без ошибки приложения
Сотрудник открыл раздел «Отправленные», когда все заказы магазина ещё проходят сборку. Ему нужен понятный пустой экран, а не пустая карточка или предупреждение PHP.
Исходные данные
По shop=A есть четыре записи. Ни у одной нет status=shipped. Успешный поиск всё равно возвращает объект Cursor, но цикл не выполнит своё тело ни разу.
- Запрос к доступной базе → обычный курсор
- 0 совпадений → 0 элементов foreach
- Экран выводит «Отправленных заказов пока нет»
Короткий фрагмент
$orders = [];
foreach ($cursor as $order) {
$orders[] = $order;
}
if ($orders === []) {
echo 'Отправленных заказов пока нет';
}Что увидит сотрудник
Отправленных заказов пока нет
Открыть готовый экран из фактического запуска · Строки результата в JSON
| Раздел | Число заказов |
|---|---|
| Отправленные | 0 |
| Все состояния магазина | 4 |
Почему получился такой результат
Если не было исключения и перебор завершился, пустой массив означает отсутствие подходящих документов. Обращаться к $orders[0] без проверки здесь нельзя. Отдельное ограниченное чтение подтверждает, что в этом учебном магазине вообще есть заказы.
Границы примера
Объект курсора сам по себе считается истинным в условии if ($cursor). Его наличие не доказывает, что сервер нашёл хоть одну строку. Ошибку подключения нельзя превращать в сообщение об отсутствии заказов.
Полная программа и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/order-screen.php';
use MongoDB\Driver\{Manager, Query, Cursor, CursorInterface};
runScreen('mongodb131-screen-empty-list', 'Отправленные заказы', ['Раздел', 'Число заказов'],
function (Manager $manager, string $namespace, string $uri, string $db, string $name): array {
sampleOrders($manager, $namespace);
$filter = ['shop' => 'A', 'status' => 'shipped'];
$orders = [];
$cursor = orderRows($manager, $namespace, $filter, ['projection' => SCREEN_FIELDS, 'limit' => 10]);
foreach ($cursor as $order) {
$orders[] = $order;
}
$message = $orders === [] ? 'Отправленных заказов пока нет' : 'Отправлено заказов: '.count($orders);
$all = orderRows($manager, $namespace, ['shop' => 'A'], ['projection' => ['_id' => 1], 'limit' => 10])->toArray();
$checks['empty_success'] = $orders === [];
$checks['other_statuses_exist'] = count($all) === 4;
$checks['cursor_interface'] = $cursor instanceof CursorInterface;
$checks['cursor_exhausted'] = !$cursor->valid();
$checks['no_phantom_card'] = !str_contains(renderOrders('Отправленные', $message, $orders), '<article>');
return [$orders, $message, [['Отправленные', count($orders)], ['Все состояния магазина', count($all)]], $checks,
['filter' => $filter, 'successful_empty_response' => true]];
}
);
Ожидаемый результат
Отправленных заказов пока нет
Скрипт читает документы у локального MongoDB, печатает итог в терминале и строит HTML-карточки. Файл order-screen.php и исходные данные входят в ZIP.
Самостоятельное изменение
Смените shipped на packed. Теперь в выдаче будет один заказ A-102. Поменяйте подпись раздела и сравните обе ветви.
Посчитать сумму и показать заказы одним проходом
На листе сборки должны быть три карточки и итоговая сумма. В шаблоне легко провести курсор один раз для списка и ошибочно попытаться прочитать его повторно для суммы.
Исходные данные
Значения totalRub равны 1250, 890 и 990. += прибавляет сумму текущего заказа к накопленной. Функция принимает CursorInterface, то есть от входа требуется поведение курсора.
- Курсор читает A-101 → сумма 1250
- Следующие A-103 и A-104 → 2140, затем 3130
- Карточки и сумма уже получены; проверка rewind после полного обхода даёт LogicException
Короткий фрагмент
$total = 0;
$orders = [];
foreach ($cursor as $order) {
$orders[] = $order;
$total += $order['totalRub'];
}Что увидит сотрудник
В сборке 3 заказа · сумма 3130 ₽
Открыть готовый экран из фактического запуска · Строки результата в JSON
| Показатель | Результат |
|---|---|
| Строк в листе | 3 |
| Сумма, ₽ | 3130 |
Почему получился такой результат
Курсор похож на чтение очереди по одной карточке. У драйвера есть начальная позиция, но после продвижения за первый документ произвольно вернуться назад нельзя. Реальный объект класса Cursor сюда вернул Manager. Даже при batchSize=1 цикл дошёл до конца выдачи, получая очередные порции по мере необходимости.
Границы примера
Здесь массив накапливает максимум 10 записей из запроса. Для большой выгрузки строки обычно выводят по одной, а для суммы всей коллекции можно использовать серверный $group. Повторный запрос создаёт новый курсор и может увидеть изменившиеся данные.
Полная программа и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/order-screen.php';
use MongoDB\Driver\{Manager, Query, Cursor, CursorInterface};
runScreen('mongodb131-screen-one-pass', 'Лист сборки', ['Показатель', 'Результат'],
function (Manager $manager, string $namespace, string $uri, string $db, string $name): array {
sampleOrders($manager, $namespace);
$cursor = orderRows($manager, $namespace, ['shop' => 'A', 'status' => 'ready'],
['projection' => SCREEN_FIELDS, 'sort' => ['orderNo' => 1], 'limit' => 10, 'batchSize' => 1]);
$makeSheet = static function (CursorInterface $source): array {
$orders = [];
$total = 0;
foreach ($source as $order) {
$orders[] = $order;
$total += $order['totalRub'];
}
return [$orders, $total];
};
[$orders, $total] = $makeSheet($cursor);
$rewindFailed = false;
try {
$cursor->rewind();
} catch (MongoDB\Driver\Exception\LogicException $error) {
$rewindFailed = true;
}
$checks['implements_iterator'] = $cursor instanceof Iterator && $cursor instanceof CursorInterface;
$checks['real_cursor'] = $cursor instanceof Cursor && (new ReflectionClass($cursor))->getConstructor()->isPrivate();
$checks['modern_id_type'] = $cursor->getId() instanceof MongoDB\BSON\Int64;
$checks['one_list_three_orders'] = array_column($orders, 'orderNo') === ['A-101', 'A-103', 'A-104'];
$checks['sum_calculated'] = $total === 3130;
$checks['second_pass_rejected'] = $rewindFailed;
$message = 'В сборке '.count($orders).' заказа · сумма '.$total.' ₽';
return [$orders, $message, [['Строк в листе', count($orders)], ['Сумма, ₽', $total]], $checks,
['batchSize' => 1, 'limit' => 10, 'rewind_after_full_iteration' => $rewindFailed ? 'LogicException' : 'rewound']];
}
);
Ожидаемый результат
В сборке 3 заказа · сумма 3130 ₽
Скрипт читает документы у локального MongoDB, печатает итог в терминале и строит HTML-карточки. Файл order-screen.php и исходные данные входят в ZIP.
Самостоятельное изменение
Уменьшите limit до 2 и считайте итог только двух видимых строк. Для сортировки по номеру это будут A-101 и A-103, всего 2140 ₽.
Шаблон второй раз обходит уже продвинутый курсор. Посчитать итог и собрать строки за один проход, а для повторного показа использовать полученные строки. Вопрос разработчика на Stack Overflow.
Превратить одно поле формы в безопасный фильтр
Сборщик выбрал «Упакован» в форме. Хотим показать именно упакованные заказы его магазина и обработать ссылку, в которой вместо одного статуса передали неизвестную строку или массив.
Исходные данные
У элемента select есть name="status". Браузер отправляет значение выбранной опции, например packed, в query string GET-запроса. PHP собирает параметры в $_GET. Форма передаёт статус, а магазин остаётся в контексте приложения.
- select name=status → ?status=packed → $_GET["status"]
- Проверка строки и списка допустимых значений → shop=A, status=packed
- MongoDB → карточка A-102 «Штатив», 3490 ₽ → HTML браузера
Поле, которое передаёт браузер
<form method="get">
<label>Состояние
<select name="status">
<option value="ready">К сборке</option>
<option value="packed">Упакован</option>
<option value="shipped">Отправлен</option>
</select>
</label>
<button>Показать заказы</button>
</form>Короткий фрагмент
$value = $_GET['status'] ?? 'ready';
if (!is_string($value) || !in_array($value, ['ready', 'packed', 'shipped'], true)) {
http_response_code(400);
exit('Выберите одно из предложенных состояний');
}
$filter = ['shop' => 'A', 'status' => $value];Что увидит сотрудник
Упакован · A-102 · Штатив · 3490 ₽
Открыть готовый экран из фактического запуска · Строки результата в JSON
| Поле формы | Проверка | Найдено |
|---|---|---|
| packed | Принят | 1 |
| Массив | Некорректный статус | 0 |
| unknown | Некорректный статус | 0 |
Почему получился такой результат
CLI-программа воспроизводит формы входных параметров через parse_str. Массив status[$ne] и неизвестное значение отбрасываются до поиска заказов. Указанные для них нули в таблице означают, что строки не извлекались. В том же ZIP есть настоящая HTML-форма с GET и PHP-обработчиком. Команды в web-README.md запускают её локально. Проверьте ready (A-103, A-104, A-101), packed (A-102) и shipped (пустая очередь).
Границы примера
При неверном поле веб-версия отвечает HTTP 400. Когда учебные данные ещё не подготовлены, она отвечает 503. В рабочем кабинете значение shop надо получать после аутентификации пользователя. Проверка только на строковый тип недостаточна: перечень поддерживаемых состояний также ограничивает контракт.
Полная программа и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/order-screen.php';
use MongoDB\Driver\{Manager, Query, Cursor, CursorInterface};
runScreen('mongodb131-screen-status-input', 'Очередь по состоянию', ['Поле формы', 'Проверка', 'Найдено'],
function (Manager $manager, string $namespace, string $uri, string $db, string $name): array {
sampleOrders($manager, $namespace);
$rows = [];
$orders = [];
$searches = 0;
foreach (['status=packed', 'status%5B%24ne%5D=packed', 'status=unknown'] as $rawQuery) {
parse_str($rawQuery, $form);
$status = $form['status'] ?? null;
$valid = is_string($status) && in_array($status, ['ready', 'packed', 'shipped'], true);
if (!$valid) {
$rows[] = [is_array($status) ? 'Массив' : (string)$status, 'Некорректный статус', 0];
continue;
}
++$searches;
$orders = orderRows($manager, $namespace, ['shop' => 'A', 'status' => $status], [
'projection' => SCREEN_FIELDS, 'sort' => ['priority' => -1, 'orderNo' => 1], 'limit' => 10,
])->toArray();
$rows[] = [$status, 'Принят', count($orders)];
}
$checks['only_one_search'] = $searches === 1;
$checks['packed_order'] = array_column($orders, 'orderNo') === ['A-102'];
$checks['operator_array_rejected'] = $rows[1] === ['Массив', 'Некорректный статус', 0];
$checks['unknown_rejected'] = $rows[2] === ['unknown', 'Некорректный статус', 0];
$checks['amount_from_db'] = $orders[0]['totalRub'] === 3490;
$message = 'Упакован · '.$orders[0]['orderNo'].' · '.$orders[0]['title'].' · '.$orders[0]['totalRub'].' ₽';
return [$orders, $message, $rows, $checks, ['valid_searches' => $searches, 'allowed_statuses' => ['ready','packed','shipped']]];
}
);
Ожидаемый результат
Упакован · A-102 · Штатив · 3490 ₽
Скрипт читает документы у локального MongoDB, печатает итог в терминале и строит HTML-карточки. Файл order-screen.php и исходные данные входят в ZIP.
В том же ZIP есть web-README.md с запуском формы на локальном PHP-сервере. Готовая страница выше отражает результат CLI-примера, форма работает при запуске веб-версии из архива.
Самостоятельное изменение
Откройте локальную форму, смените статус и сравните адрес с названием опции. Затем пришлите массив status[$ne]=packed: сервер должен ответить 400 до поиска, а не открыть заказы с любым другим состоянием.
Перенести экран между PHPLIB и низкоуровневым драйвером
В одном модуле уже используется PHPLIB, в другом только Manager. Команде нужно убедиться, что замена API сохраняет видимый порядок заказов, названия и цены первой страницы.
Исходные данные
Обе операции выполняются в одной учебной коллекции с одним фильтром, проекцией, порядком приоритетов и limit=2. Карты типов явно приводят корневые документы к массивам в обоих случаях.
- Collection::find → курсор → A-103, A-104
- Manager + Query → курсор → A-103, A-104
- Общий PHP-обработчик принимает CursorInterface → сравниваются все значения и порядок
Короткий фрагмент
$fromLibrary = $collection->find($filter, $options + ['typeMap' => ARRAY_MAP]);
$fromDriver = $manager->executeQuery($namespace, new Query($filter, $options));
$fromDriver->setTypeMap(ARRAY_MAP);Что увидит сотрудник
PHPLIB и драйвер · A-103, A-104
Открыть готовый экран из фактического запуска · Строки результата в JSON
| Позиция | PHPLIB | Драйвер | Сумма, ₽ |
|---|---|---|---|
| 1 | A-103 | A-103 | 890 |
| 2 | A-104 | A-104 | 990 |
Почему получился такой результат
Равенство проверено на двух настоящих чтениях. Функция consume требует только интерфейс курсора и переносит документы в массив видимой страницы за один проход. При отдельном запросе каждая реализация получает свой курсор. В таблице показан результат обеих точек входа.
Границы примера
Два чтения на рабочем сервере могут разделять изменения данных. Здесь их никто не меняет между запросами. Из равенства результата нельзя вывести равенство скорости: сетевое время, индексы и размер выдачи потребуют отдельного измерения.
Полная программа и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/order-screen.php';
use MongoDB\Driver\{Manager, Query, Cursor, CursorInterface};
runScreen('mongodb131-screen-driver-parity', 'Одинаковая выдача двух API', ['Позиция', 'PHPLIB', 'Драйвер', 'Сумма, ₽'],
function (Manager $manager, string $namespace, string $uri, string $db, string $name): array {
sampleOrders($manager, $namespace);
$collection = libraryCollection($uri, $db, $name);
$filter = ['shop' => 'A', 'status' => 'ready'];
$options = ['projection' => SCREEN_FIELDS, 'sort' => ['priority' => -1, 'orderNo' => 1], 'limit' => 2, 'maxTimeMS' => 1500];
$libraryCursor = $collection->find($filter, $options + ['typeMap' => ARRAY_MAP]);
$driverCursor = $manager->executeQuery($namespace, new Query($filter, $options));
$driverCursor->setTypeMap(ARRAY_MAP);
$consume = static function (CursorInterface $source): array {
$items = [];
foreach ($source as $document) {
$items[] = $document;
}
return $items;
};
$fromLibrary = $consume($libraryCursor);
$fromDriver = $consume($driverCursor);
$checks['identical_documents'] = $fromLibrary === $fromDriver;
$checks['ordered_page'] = array_column($fromLibrary, 'orderNo') === ['A-103','A-104'];
$checks['right_prices'] = array_column($fromDriver, 'totalRub') === [890,990];
$checks['common_contract'] = $libraryCursor instanceof CursorInterface && $driverCursor instanceof CursorInterface;
$checks['bounded'] = count($fromLibrary) === 2 && count($fromDriver) === 2;
$message = 'PHPLIB и драйвер · '.implode(', ',array_column($fromDriver,'orderNo'));
$rows = [];
foreach ($fromLibrary as $index => $order) {
$rows[] = [$index + 1, $order['orderNo'], $fromDriver[$index]['orderNo'], $order['totalRub']];
}
return [$fromDriver, $message, $rows, $checks, ['filter' => $filter, 'sort' => $options['sort'],
'limit' => $options['limit'], 'typeMap' => ARRAY_MAP]];
}
);
Ожидаемый результат
PHPLIB и драйвер · A-103, A-104
Скрипт читает документы у локального MongoDB, печатает итог в терминале и строит HTML-карточки. Файл order-screen.php и исходные данные входят в ZIP.
Самостоятельное изменение
Снимите второй ключ сортировки у одного варианта. Даже если строки в этом запуске совпадут, при равном приоритете бизнес-порядок больше не задан обоими запросами.
Из чего состоит путь запроса
| Класс или интерфейс | Назначение |
|---|---|
| MongoDB\Driver\Query | Составить запрос к коллекции из условия поиска и опций экрана. |
| MongoDB\Driver\Cursor | Прочитать результаты настоящего запроса по порядку и отдать их шаблону. |
| MongoDB\Driver\CursorInterface | Передать курсор в общий обработчик, которому важен способ чтения результата. |
Следующий шаг для растущей очереди
Когда очередь вырастет, важным станет индекс, поддерживающий условия и порядок экрана. Следующий материал — подбор индекса для запроса. Отдельно можно изучить порции и состояние серверного курсора, основные операции драйвера или передачу полей из формы в PHP.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
MongoDB\Driver\QueryСоставить запрос к коллекции из условия поиска и опций экрана.
Расширение mongodb с 1.0.0. В 2.5.5 Query — final, у него публичный конструктор и нет метода find(). Полный перечень опций закреплён за карточкой конструктора. · mongodb
final class MongoDB\Driver\Query { final public function __construct(array|object $filter, ?array $queryOptions = null); }Вызов и параметры
Первый аргумент — PHP-массив или объект с критериями. Пустой массив допускает все документы. Поле shop рядом с полем status задаёт совместное выполнение обоих условий. BSON учитывает типы значений. Сравнивайте ObjectId с ObjectId; скаляр из $_GET сначала разбирают по правилам вашего приложения.
Результат
Конструктор возвращает готовый объект Query. Конкретную коллекцию «база.коллекция» передают позднее в Manager::executeQuery(). Возвращённый им Cursor позволяет читать документы. Query не хранит найденные строки.
Ошибки и ограничения
Второй аргумент принимает массив опций или null. projection задаёт поля, sort — порядок, limit — верхнюю границу, skip — смещение, batchSize — число документов в порции. Отрицательное значение limit с драйвера 2.0 не включает singleBatch автоматически. maxTimeMS задаётся в миллисекундах. Более специальные hint, collation и readConcern нужны для отдельных требований. Ошибки типов и некорректных опций могут проявиться при конструировании, серверные ошибки запроса или соединения — при выполнении и чтении.
Версии
Расширение mongodb с 1.0.0. В 2.5.5 Query — final, у него публичный конструктор и нет метода find(). Полный перечень опций закреплён за карточкой конструктора.
Экран очереди
$query = new Query(['shop' => 'A', 'status' => 'ready'], ['projection' => SCREEN_FIELDS]);На экране три заказа, а поле internalNote не доходит до PHP.
Открыть самостоятельный пример и увидеть экран
Граница страниц
$query = new Query($filter, ['sort' => ['priority' => -1, 'orderNo' => 1], 'limit' => 2]);При равном приоритете используется номер заказа. На первой странице будут A-103 и A-104.
Открыть самостоятельный пример и увидеть экран
Связанные темы · Все параметры Query::__construct · Запросы каталога и индексы.
MongoDB\Driver\CursorПрочитать результаты настоящего запроса по порядку и отдать их шаблону.
Cursor с 1.0.0, реализует CursorInterface с 1.6.0 и Iterator с 1.9.0. В 2.x чтение ID даёт BSON Int64. Этот выпуск проверяет Cursor из расширения 2.5.5. · mongodb
final class MongoDB\Driver\Cursor implements MongoDB\Driver\CursorInterfaceВызов и параметры
У Cursor закрытый конструктор. Получите объект из executeQuery() или операции, которая возвращает курсор. До обхода при необходимости задайте setTypeMap(array $typemap). Карта root/document/array определяет форму корневого документа, вложенных документов и списков. foreach использует методы итератора rewind(), valid(), current(), key() и next().
Результат
current() даёт array|object|null согласно карте типов и позиции. key() в драйвере 2.5.5 даёт ?int. valid() и isDead() отвечают на разные вопросы: первый относится к текущей позиции, второй — к тому, может ли быть продолжение с сервера. В локальном буфере могут остаться результаты даже после закрытия серверного курсора. getId() возвращает MongoDB\BSON\Int64, getServer() — сервер конкретной операции. toArray() материализует результаты в PHP-памяти.
Ошибки и ограничения
После продвижения за первую позицию rewind() больше не возвращает курсор на начало и выбрасывает LogicException. Переход к новой порции результатов сам может потребовать сетевого запроса и завершиться исключением. Чтобы дважды показать короткую страницу, соберите её один раз с конечным limit. Для больших списков выводите элементы по мере обхода. Пустой курсор даёт ноль итераций и сам по себе не означает сбой.
Версии
Cursor с 1.0.0, реализует CursorInterface с 1.6.0 и Iterator с 1.9.0. В 2.x чтение ID даёт BSON Int64. Этот выпуск проверяет Cursor из расширения 2.5.5.
Лист и сумма за один цикл
foreach ($cursor as $order) {
$orders[] = $order;
$total += $order['totalRub'];
}Три строки дали 3130 ₽. Для повторного вывода на странице можно читать уже собранный $orders.
Открыть самостоятельный пример и увидеть экран
Пустое состояние
$orders = $cursor->toArray();
if ($orders === []) {
echo 'Отправленных заказов пока нет';
}Для страницы с ограничением в десять строк получаем пустой массив. Выводим сообщение вместо несуществующей карточки.
Открыть самостоятельный пример и увидеть экран
Связанные темы · Cursor::getServer · Cursor::isDead · Cursor::toArray · Cursor::rewind.
MongoDB\Driver\CursorInterfaceПередать курсор в общий обработчик, которому важен способ чтения результата.
Интерфейс с ext-mongodb 1.6.0. С 1.15.0 на PHP 8 появились предварительные типы возвращаемых значений. С 2.0 интерфейс расширяет Iterator и требует совместимых объявлений типов. · mongodb
interface MongoDB\Driver\CursorInterface extends IteratorВызов и параметры
Интерфейс используют в типе параметра, свойства или результата. Например, function makeSheet(CursorInterface $orders) получает уже созданный курсор. Интерфейс не создаёт соединение и не предоставляет собственного экземпляра, который можно было бы открыть с new.
Результат
На ext-mongodb 2.5.5 интерфейс расширяет Iterator. Доступны current(): array|object|null, key(): ?int и методы обхода. Контракт включает getId(): MongoDB\BSON\Int64, getServer(): MongoDB\Driver\Server, isDead(): bool, setTypeMap(array $typemap): void и toArray(): array. Класс Cursor предоставляет реализацию. Для функции, которой нужен только foreach, подходящим более общим типом иногда будет iterable.
Ошибки и ограничения
Если передать обычный PHP-массив в параметр CursorInterface, проверка типа завершится TypeError. Контракт даёт операции над курсором, но не обещает возможность повторного обхода или определённый класс каждого документа. Когда шаблон ожидает массивы, согласуйте typeMap или функцию извлечения полей заранее.
Версии
Интерфейс с ext-mongodb 1.6.0. С 1.15.0 на PHP 8 появились предварительные типы возвращаемых значений. С 2.0 интерфейс расширяет Iterator и требует совместимых объявлений типов.
Один обработчик для двух методов чтения
function readVisiblePage(CursorInterface $source): array {
return $source->toArray(); // запрос ограничен двумя документами
}Оба курсора содержат первые два заказа в одном порядке. Функции не нужно знать, как вызывающий код сформировал свой запрос.
Открыть самостоятельный пример и увидеть экран
Передать лист сборщику
$makeSheet = static function (CursorInterface $source): array {
$sum = 0;
foreach ($source as $order) {
$sum += $order['totalRub'];
}
return ['totalRub' => $sum];
};Итог 3130 ₽ получен без повторного похода к базе. Полная программа в том же проходе собирает отображаемые строки.
Открыть самостоятельный пример и увидеть экран
Связанные темы · CursorInterface::getId · CursorInterface::setTypeMap · Как PHP выполняет foreach и Iterator.
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


