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

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

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

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

Глава 142 · После массивов, вызовов методов и Composer

MongoDB: первый каталог документов из PHP

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

От карточки товара к документу базы

Сделаем первый каталог: сохраним лампу, откроем её карточку, отберём товары для списка, изменим остаток и удалим только пробную запись. После каждого действия можно открыть реальный HTML-результат. Работа идёт с MongoDB Server и официальной библиотекой mongodb/mongodb 2.5.0 для PHP.

Где находится карточка товара
  1. PHP-массив: sku=LAMP-A, stock=5
  2. PHPLIB и расширение кодируют BSON
  3. MongoDB Server сохраняет документ в коллекции
  4. Запрос возвращает выбранные документы
  5. typeMap задаёт PHP-представление
  6. HTML показывает лампу и остаток 5

Документ — карточка с именованными полями; коллекция — набор карточек; база — пространство коллекций. Сходство с «строка → таблица → база» помогает сориентироваться после SQL, но не описывает полную эквивалентность. Документы могут содержать вложенные документы и массивы. Это не означает отсутствие схемы: правила типов, обязательных полей и связей всё равно проектирует разработчик и при необходимости закрепляет валидацией.

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

Что установить и как увидеть результат

Нужны три установленные части: MongoDB Server, расширение PHP mongodb и Composer-библиотека mongodb/mongodb. Расширение содержит сетевой драйвер и BSON-типы; библиотека даёт удобные Client, Database, Collection и операции insertOne/find. Отсутствие Client при загруженном расширении часто означает, что не подключён Composer autoload. При этом отсутствие расширения не исправляется одним require.

Контрольная среда использует PHP 8.5.11, расширение mongodb 2.5.5, PHPLIB 2.5.0 и MongoDB Server ветки 8.0. Точный сервер и версии записываются при реальном запуске в metrics.json. Архив содержит согласованные composer.json и composer.lock. В распакованной папке выполните composer install --no-dev, затем helper подключит её vendor/autoload.php. Команда update здесь не нужна. Базовые роли Composer объясняет глава об установке библиотек.

Подготовьте отдельный локальный сервер, слушающий только 127.0.0.1:27075, и учебную базу php_manual_lab. Задайте PHPMAN_MONGODB_URI=mongodb://127.0.0.1:27075/?directConnection=true и PHPMAN_MONGODB_DATABASE=php_manual_lab в окружении терминала. PHPMAN_MONGODB_AUTOLOAD необязателен: он нужен, если используется уже установленный доверенный абсолютный путь к vendor/autoload.php. Не берите этот путь из веб-запроса. Локальная конфигурация не является инструкцией открытия базы в интернет.

Каждый пример самостоятельный: php example.php создаёт собственную коллекцию mongodb115_catalog_<случайные 16 hex>, выполняет сценарий и в finally удаляет только её точное имя. Флаг владения появляется только после успешного createCollection. Затем проверяется отсутствие именно этой коллекции. Общая база не удаляется. Если процесс аварийно оборван, проверьте точное имя своего запуска; не удаляйте все коллекции по маске.

Откройте result.html: сначала полезная таблица, затем прочитанные документы и подтверждение действий. result.json сохраняет ту же таблицу, metrics.json — версии, проверки и хеши. Это настоящий запуск PHP CLI, а не проверка веб-формы. Сами HTML-файлы уже не обращаются к базе сайта.

Для самостоятельного стенда на Windows 11 x64 распакуйте официальный MongoDB Community 8.0 ZIP. В командах замените путь к mongod.exe на свой. Создайте отдельную папку данных и оставьте процесс сервера работающим; PHP запускайте во втором терминале:

New-Item -ItemType Directory -Force 'C:\mongo-lab\data'
& 'C:\MongoDB\bin\mongod.exe' --dbpath 'C:\mongo-lab\data' --bind_ip 127.0.0.1 --port 27075

Проверьте php -v, php --ini и php --ri mongodb. Если расширения нет, используйте официальную установку расширения. Windows DLL должна соответствовать версии PHP, архитектуре и TS/NTS. php_mongodb.dll помещается в extension_dir, в нужном php.ini включается extension=php_mongodb.dll; учитывайте DLL-зависимости пакета. CLI может использовать другой ini, чем веб-сервер. Только после успешного php --ri mongodb устанавливайте зависимости Composer. Эти пути — пример для собственного стенда, а не обязательные каталоги автора.

PHP, BSON и JSON: разные роли

Что видимГде существует и зачем
PHP-массивЗначения программы; ассоциативная карточка и последовательный список имеют разные структуры
BSON-документТипизированная запись MongoDB, может содержать ObjectId и другие специальные типы
BSON-массивУпорядоченный список внутри документа
JSON-строкаТекстовый формат обмена; не требуется для обычного insertOne PHP-массива
ObjectIdОтдельный BSON-тип идентификатора, не обычная строка
Client → Database → CollectionPHP-объекты с настройками; получение объекта не доказывает сетевой успех и не создаёт данные

Helper задаёт typeMap: root, document и array читаются как PHP-массивы. Без этой настройки библиотека обычно возвращает BSONDocument/BSONArray. Специальные значения вроде ObjectId сохраняют свой тип. Превращать всё через json_encode/json_decode ради доступа к полю не требуется. В нашем отчёте ObjectId отдельно превращён в текст для чтения; такой JSON не заявляется точной резервной копией BSON.

Записи => задают соответствие ключа значению в PHP-массиве; -> вызывает метод объекта или читает его свойство; ['stock'] обращается к ключу массива. getDatabase и getCollection — проверенные методы используемой PHPLIB 2.5.0. База и коллекция физически появляются при создании коллекции или первой записи, а не от одного выбора имени. В архиве создание сделано явно для безопасного владения. Если эти записи PHP пока незнакомы, опору дают массивы и вызов функции и возвращаемое значение.

Проверить связь с MongoDB и увидеть понятный отказ

Прежде чем сохранять каталог, нужно отличить «PHP создал объект с настройками» от «сервер базы действительно ответил». Иначе первая попытка записи неожиданно падает, хотя строка new Client уже отработала.

Вход и задача

УчастникЗадача
MongoDB ServerЛокальный процесс на 127.0.0.1:27075
MongoDB\ClientНастройки адреса и поведения соединения
База php_manual_labКонтекст команды ping
Отдельный проверочный клиентОбращается к собственному неслушающему TCP-порту
Путь данных
  1. new Client сохраняет настройки
  2. getDatabase выбирает имя базы в объекте PHP
  3. command([ping=>1]) требует реального ответа
  4. ok=1 подтверждает ответ сервера
  5. Другой клиент получает ожидаемое исключение, которое программа объясняет

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

ПроверкаРезультат
Клиент для недоступного адреса созданДа
Настоящий сервер ответил pingok = 1
Ping на собственный неслушающий портПерехвачен отказ соединения

Короткий вызов

$client = new MongoDB\Client($uri, ['serverSelectionTimeoutMS' => 3000]);
$db = $client->getDatabase('php_manual_lab');
$reply = $db->command(['ping' => 1])->toArray()[0];
echo $reply['ok']; // 1 — сервер действительно ответил

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

$uri — строка адреса. new MongoDB\Client(...) создаёт PHP-объект клиента, а ->getDatabase(...) вызывает его метод и возвращает объект базы. Эти действия сами по себе не означают успешную сетевую команду и не создают коллекцию. ['ping' => 1] — PHP-массив с именем команды и её значением. command отправляет команду, а toArray получает её маленький ответ. Здесь [0] означает первый документ ответа; ключ ok показывает результат.

Проверка ошибки использует отдельный адрес, которым владеет сам пример: системный TCP-порт занят без режима listen. На нём заведомо не работает mongod. Создание второго Client проходит, но команда приводит к ConnectionException. try/catch превращает этот ожидаемый отказ в понятную строку отчёта, а finally закрывает проверочный сокет. Рабочий локальный MongoDB всё это время остаётся запущенным.

serverSelectionTimeoutMS ограничивает ожидание подходящего сервера; connectTimeoutMS относится к установлению соединения, socketTimeoutMS — к операциям сокета. Это разные стадии, не обещание точного суммарного времени. В отчёте виден класс фактического исключения, а параметры и полный URI не выводятся пользователю. Ошибки прав, неправильного имени класса или отсутствия расширения не маскируются под успешный ping.

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

Ответ ping не доказывает права на все последующие действия и не проверяет бизнес-данные. Настройка локального сервера без внешнего доступа не является рецептом развёртывания публичной базы. В рабочем сервисе отдельно нужны аутентификация, права, TLS при необходимости, журнал ошибок и корректный ответ пользователю. Здесь изучаем связь и отказ на изолированном стенде.

Полная самостоятельная программа и файлы
Проверить связь с MongoDB и увидеть понятный отказСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mc115-helper.php';
[$client,$db,$runtime]=mc115Connect();
$name=mc115Name('mongodb115_catalog_'.bin2hex(random_bytes(8)));
$owned=false;
$details=[];
try {
    $db->createCollection($name); $owned=true;
    $collection=$db->getCollection($name);
$ping=$db->command(['ping'=>1])->toArray()[0];
    $socket=stream_socket_server('tcp://127.0.0.1:0',$socketCode,$socketMessage,STREAM_SERVER_BIND);
    if ($socket===false) {throw new RuntimeException('Could not reserve a local failure-test port');}
    $caught=false; $errorClass=''; $constructed=false;
    try {
        $address=stream_socket_get_name($socket,false);
        if (!preg_match('/\A127\.0\.0\.1:([0-9]+)\z/D',$address,$parts)) {throw new RuntimeException('Unexpected local socket address');}
        $failureClient=new MongoDB\Client('mongodb://127.0.0.1:'.$parts[1],[
            'directConnection'=>true,'serverSelectionTimeoutMS'=>500,'connectTimeoutMS'=>300,'socketTimeoutMS'=>300,
        ]);
        $constructed=true;
        try {$failureClient->getDatabase('php_manual_lab')->command(['ping'=>1])->toArray();}
        catch (MongoDB\Driver\Exception\ConnectionException $error) {$caught=true; $errorClass=get_class($error);}
    } finally {fclose($socket);}
    $rows=[['Клиент для недоступного адреса создан',$constructed?'Да':'Нет'],['Настоящий сервер ответил ping',(float)$ping['ok']===1.0?'ok = 1':'Нет'],['Ping на собственный неслушающий порт',$caught?'Перехвачен отказ соединения':'Неожиданный успех']];
    $checks=['real_ping_ok'=>(float)$ping['ok']===1.0,'construct_is_not_ping'=>$constructed,'expected_connection_failure'=>$caught,'collection_stays_empty'=>$collection->countDocuments([])===0];
    $details=['Класс ожидаемого исключения'=>$errorClass,'Таймауты проверочного клиента, мс'=>['selection'=>500,'connect'=>300,'socket'=>300],'Смысл'=>'Конструктор задаёт настройки. Реальная команда проверяет доступность. Исключение отдельного клиента не останавливает основной сервер.'];
} finally {
    if ($owned) {$db->dropCollection(mc115Name($name));}
}
$remaining=iterator_to_array($db->listCollectionNames(['filter'=>['name'=>$name]]),false);
$checks['owned_collection_removed']=$remaining===[];
mc115Report('mongodb115-catalog-ping-and-failure', ['Проверка', 'Результат'], $rows, $checks, $runtime, $details);
echo 'mongodb115-catalog-ping-and-failure: saved result.html and result.json
';
Ожидаемый результат
mongodb115-catalog-ping-and-failure: saved result.html and result.json

Запустите PHP CLI и откройте result.html. Все входные документы и helper включены; зависимости ставятся по приложенному Composer lock. Другие уроки запускать заранее не нужно.

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

Попробуйте сами. Остановите только свою учебную копию mongod перед ручным запуском. Основная команда уже не должна показывать ok=1: выполнение завершится исключением. Запустите сервер обратно. Не заменяйте catch на безусловное сообщение «сохранено»: отсутствие связи не означает, что запись выполнена.

Сохранить одну карточку и прочитать её по ObjectId

Нужна одна карточка товара, которую можно сохранить, затем открыть снова. Читатель увидит точные название, цену, остаток и признак активности, а также поймёт, почему поиск по видимой строке ID иногда ничего не находит.

Вход и задача

ПолеЗначение и тип
skuLAMP-A, строка
nameЛампа белая, строка
price_cents249000, целое число копеек
stock5, целое число
activetrue, логический тип
_idВ исходном массиве отсутствует; библиотека добавляет ObjectId
Путь данных
  1. PHP-массив описывает одну карточку
  2. insertOne кодирует и сохраняет BSON-документ
  3. InsertOneResult сообщает созданный _id
  4. findOne ищет документ с таким ObjectId
  5. PHP читает name и другие поля из полученной карточки

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

АртикулНазваниеЦена, коп.ОстатокАктивен
LAMP-AЛампа белая2490005true

Короткий вызов

$result = $collection->insertOne([
    'sku' => 'LAMP-A', 'name' => 'Лампа белая',
    'price_cents' => 249000, 'stock' => 5, 'active' => true,
]);
$id = $result->getInsertedId();
$product = $collection->findOne(['_id' => $id]);
echo $product['name']; // Лампа белая

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

Документ — набор именованных полей. Вызову insertOne передаётся целая карточка, а результатом является объект InsertOneResult, не сама карточка. getInsertedId возвращает её идентификатор. findOne принимает фильтр — какие значения должны совпасть — и возвращает один документ либо null, если совпадения нет. Одна коллекция объединяет такие документы; поле _id уникально внутри коллекции.

Когда _id не задан, в этом API создаётся ObjectId. Это отдельный BSON-тип, который PHP представляет объектом MongoDB\BSON\ObjectId. Его текстовая форма содержит 24 шестнадцатеричных символа. Реальный ID текущего запуска показан под таблицей; он закономерно меняется при новом запуске. Цена, название и остаток остаются проверяемыми постоянными значениями.

Программа намеренно ищет ту же карточку сначала по самому ObjectId, затем по его текстовой строке. Поиск строкой даёт null: похожее написание не делает строку тем же BSON-типом. Создание new MongoDB\BSON\ObjectId($text) возвращает нужный тип и снова находит товар. Неверная длина или не-шестнадцатеричные символы вызывают InvalidArgumentException; корректная форма ID ещё не доказывает существование документа или право его читать.

База может использовать и другие допустимые типы _id, если проект задаёт их осознанно. Нельзя без проверки превращать каждый идентификатор любой коллекции в ObjectId. Здесь договор один: _id именно ObjectId, sku — отдельный артикул. Гарантию уникальности sku мы пока не создавали; её нельзя выводить из названия поля. Механизм преобразования входных типов дополняют типы значений PHP и массивы.

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

Подтверждённая вставка означает результат этой операции, а не полную настройку резервного копирования или транзакционность нескольких записей. В учебной коллекции разрешена запись и включено подтверждение w=1. Для внешних форм нужно заранее проверить тип, диапазон цены и остатка, обязательные поля и права. Непосредственно передавать весь $_POST в insertOne не следует.

Полная самостоятельная программа и файлы
Сохранить одну карточку и прочитать её по ObjectIdСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mc115-helper.php';
[$client,$db,$runtime]=mc115Connect();
$name=mc115Name('mongodb115_catalog_'.bin2hex(random_bytes(8)));
$owned=false;
$details=[];
try {
    $db->createCollection($name); $owned=true;
    $collection=$db->getCollection($name);
$input=['sku'=>'LAMP-A','name'=>'Лампа белая','price_cents'=>249000,'stock'=>5,'active'=>true];
    $write=$collection->insertOne($input); $id=$write->getInsertedId();
    $found=$collection->findOne(['_id'=>$id]);
    if ($found===null) {throw new RuntimeException('Inserted product was not read back');}
    $asString=$collection->findOne(['_id'=>(string)$id]);
    $restored=$collection->findOne(['_id'=>new MongoDB\BSON\ObjectId((string)$id)]);
    $rows=[[$found['sku'],$found['name'],$found['price_cents'],$found['stock'],$found['active']]];
    $checks=['one_acknowledged_insert'=>$write->isAcknowledged() && $write->getInsertedCount()===1,
        'generated_object_id'=>$id instanceof MongoDB\BSON\ObjectId && preg_match('/\A[0-9a-f]{24}\z/D',(string)$id)===1,
        'card_read_back'=>$rows===[['LAMP-A','Лампа белая',249000,5,true]],
        'string_id_is_not_object_id'=>$asString===null,'restored_type_finds_same_product'=>$restored!==null && (string)$restored['_id']===(string)$id];
    $details=['Сохранённая карточка'=>$found,'Настоящий _id, текстовое представление'=>(string)$id,'Поиск строкой вернул'=>null,'Поиск с ObjectId вернул SKU'=>$restored['sku']??null];
} finally {
    if ($owned) {$db->dropCollection(mc115Name($name));}
}
$remaining=iterator_to_array($db->listCollectionNames(['filter'=>['name'=>$name]]),false);
$checks['owned_collection_removed']=$remaining===[];
mc115Report('mongodb115-catalog-insert-read', ['Артикул', 'Название', 'Цена, коп.', 'Остаток', 'Активен'], $rows, $checks, $runtime, $details);
echo 'mongodb115-catalog-insert-read: saved result.html and result.json
';
Ожидаемый результат
mongodb115-catalog-insert-read: saved result.html and result.json

Запустите PHP CLI и откройте result.html. Все входные документы и helper включены; зависимости ставятся по приложенному Composer lock. Другие уроки запускать заранее не нужно.

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

Попробуйте сами. Измените stock на 7 перед вставкой. В прочитанной карточке должно быть 7, остальные поля прежние. Затем запросите случайный новый ObjectId, которого не вставляли: findOne вернёт null. Перед чтением $product['name'] отдельно обработайте это отсутствие.

Практический вопрос: Почему findOne по видимой строке ObjectId возвращает null. PHP-код передавал строковый _id и не находил документ с BSON ObjectId. Наш собственный пример сохраняет карточку, сравнивает поиск по ObjectId и строке, затем восстанавливает нужный тип. Обсуждение даёт проблему; технические правила сверены с официальной документацией, программа и объяснения написаны отдельно.

Выбрать для каталога нужные товары и нужные поля

На странице каталога нужно показать две доступные активные лампы до 2500 рублей, начиная с недорогих. Не следует получать всю коллекцию и затем сортировать/обрезать большой PHP-массив.

Вход и задача

АртикулЦена, коп.ОстатокПочему входит или нет
LAMP-A2490005Подходит
LAMP-B1990000Нет остатка
LAMP-C1990003Подходит, дешевле
LAMP-D1990002active=false
CHAIR-A3490007Другая категория
LAMP-E2490004Подходит, но третья после сортировки
Путь данных
  1. filter отбирает light, active=true и stock>0
  2. price_cents ограничивает цену числом 250000
  3. sort упорядочивает цену и артикул
  4. limit оставляет две карточки
  5. projection возвращает только поля списка
  6. foreach выводит эти две полученные карточки

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

АртикулНазваниеЦена, коп.Остаток
LAMP-CЛампа компактная1990003
LAMP-AЛампа белая2490005

Короткий вызов

$cursor = $collection->find(
    ['category' => 'light', 'active' => true,
     'stock' => ['$gt' => 0], 'price_cents' => ['$lte' => 250000]],
    ['projection' => ['_id' => 0, 'sku' => 1, 'name' => 1,
                      'price_cents' => 1, 'stock' => 1],
     'sort' => ['price_cents' => 1, 'sku' => 1], 'limit' => 2]
);
foreach ($cursor as $product) {
    echo $product['sku'], ': ', $product['price_cents'], "\n";
}

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

find получает два аргумента: фильтр и параметры выдачи. Запись 'stock' => ['$gt' => 0] означает «остаток больше нуля»; $lte — «меньше или равно». Ключи операторов пишем в одинарных PHP-кавычках, чтобы $gt не воспринимался как переменная PHP. Обычные поля category и active проверяются на равенство. Несколько соседних условий применяются вместе.

sort задаёт направления: 1 — возрастание, -1 — убывание. Артикул уточняет порядок равных цен в нашем наборе, где все sku различны. В реальном каталоге обеспечьте уникальность этого дополнительного ключа или добавьте уникальный _id. limit=2 ограничивает число возвращённых документов; limit=0 означает отсутствие такого ограничения, а не пустую страницу.

projection — это набор возвращаемых полей: 1 включает, 0 исключает. Здесь перечислены нужные поля и отдельно отключён _id, который иначе обычно возвращается. Смешивать произвольное включение и исключение остальных полей нельзя; _id имеет специальное исключение. В PHP не приходят description и specs, которые списку не нужны. Ключи результата проверяются по составу, а не по случайному порядку полей документа.

Результат find — курсор, который foreach последовательно перебирает. Современный MongoDB\Driver\Cursor не повторяет старую цепочку ->limit()->sort(); используйте опции find показанной версии PHPLIB. Вся задача отбора, сортировки и ограничения поставлена серверу до PHP. Это тот же принцип запроса от нужного экрана, который разобран для MySQL, с другим API и моделью документа.

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

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

Полная самостоятельная программа и файлы
Выбрать для каталога нужные товары и нужные поляСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mc115-helper.php';
[$client,$db,$runtime]=mc115Connect();
$name=mc115Name('mongodb115_catalog_'.bin2hex(random_bytes(8)));
$owned=false;
$details=[];
try {
    $db->createCollection($name); $owned=true;
    $collection=$db->getCollection($name);
mc115Seed($collection);
    $filter=['category'=>'light','active'=>true,'stock'=>['$gt'=>0],'price_cents'=>['$lte'=>250000]];
    $options=['projection'=>['_id'=>0,'sku'=>1,'name'=>1,'price_cents'=>1,'stock'=>1],
        'sort'=>['price_cents'=>1,'sku'=>1],'limit'=>2,'maxTimeMS'=>1000];
    $cursor=$collection->find($filter,$options); $rows=[]; $documents=[];
    foreach ($cursor as $product) {$documents[]=$product; $rows[]=[$product['sku'],$product['name'],$product['price_cents'],$product['stock']];}
    $fields=array_keys($documents[0]??[]); sort($fields);
    $checks=['two_correct_products'=>array_column($rows,0)===['LAMP-C','LAMP-A'],
        'prices_and_stock_correct'=>$rows===[['LAMP-C','Лампа компактная',199000,3],['LAMP-A','Лампа белая',249000,5]],
        'only_selected_fields'=>count($documents)===2 && $fields===['name','price_cents','sku','stock'],
        'three_match_before_limit'=>$collection->countDocuments($filter)===3,'six_source_products'=>$collection->countDocuments([])===6];
    $details=['Фильтр'=>$filter,'Параметры выдачи'=>$options,'Реально полученные документы'=>$documents,'Совпало до limit'=>3];
} finally {
    if ($owned) {$db->dropCollection(mc115Name($name));}
}
$remaining=iterator_to_array($db->listCollectionNames(['filter'=>['name'=>$name]]),false);
$checks['owned_collection_removed']=$remaining===[];
mc115Report('mongodb115-catalog-screen-query', ['Артикул', 'Название', 'Цена, коп.', 'Остаток'], $rows, $checks, $runtime, $details);
echo 'mongodb115-catalog-screen-query: saved result.html and result.json
';
Ожидаемый результат
mongodb115-catalog-screen-query: saved result.html and result.json

Запустите PHP CLI и откройте result.html. Все входные документы и helper включены; зависимости ставятся по приложенному Composer lock. Другие уроки запускать заранее не нужно.

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

Попробуйте сами. Измените limit на 3: добавится LAMP-E. Затем уменьшите верхнюю цену до 200000: останется только LAMP-C. LAMP-B всё ещё исключена из-за нулевого остатка, LAMP-D — из-за active=false. Цена задаётся в копейках целым числом, а не строкой «2000 руб.».

Практический вопрос: Почему у современного MongoDB Cursor нет метода limit. В приложении использовалась старая цепочка find()->limit()->sort(), а новый курсор не имел limit. Наш запрос передаёт projection, sort и limit в опции find и показывает два конкретных товара. Обсуждение даёт проблему; технические правила сверены с официальной документацией, программа и объяснения написаны отдельно.

Найти белые настольные лампы по вложенному полю и тегу

Карточка лампы содержит характеристики и несколько тегов. Нужно выбрать белые настольные лампы и показать мощность каждой — без самостоятельного перебора всех характеристик всех товаров в PHP.

Вход и задача

Фрагмент LAMP-AЧто означает
specs = [color => white, power_w => 12]Вложенный документ с именованными свойствами
tags = [desk, led]Массив двух строк, позиции 0 и 1
specs.colorПуть к color внутри документа specs
tags = desk в фильтреСовпадение с элементом массива
Путь данных
  1. specs.color выбирает вложенное поле white
  2. tags=desk проверяет наличие этого значения в массиве
  3. active=true исключает снятые товары
  4. Курсор содержит LAMP-A и LAMP-E
  5. PHP читает specs[power_w] и выводит теги

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

Артикулspecs.colorspecs.power_wТеги
LAMP-Awhite12desk, led
LAMP-Ewhite15desk, led

Короткий вызов

// specs — вложенный документ, tags — массив значений.
$filter = ['active' => true, 'specs.color' => 'white', 'tags' => 'desk'];
$cursor = $collection->find($filter, ['sort' => ['sku' => 1], 'limit' => 10]);
foreach ($cursor as $product) {
    echo $product['sku'], ': ', $product['specs']['power_w'], " Вт\n";
}

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

В PHP внешняя карточка и specs записаны ассоциативными массивами с именами ключей. При сохранении это BSON-документы. tags — последовательный PHP-список с ключами 0,1; он становится BSON-массивом. Подключение задаёт typeMap, поэтому после чтения оба вида удобно доступны как PHP-массивы, однако в базе их смысл остаётся разным. JSON-строка для этих действий не требуется.

Точечная запись specs.color в фильтре адресует вложенное поле. При чтении PHP-массива та же связь записана иначе: $product['specs']['color']. Это два языка доступа на разных сторонах операции. Не нужно искать в PHP ключ с буквальным именем specs.color, когда документ содержит specs и вложенный color.

Сравнение tags со строкой desk находит документ, у которого такое значение присутствует в массиве. Сравнение tags с массивом [desk] требует равенства всему массиву: наши [desk,led] не совпадут. В отчёте первая точная проверка даёт 0, а [desk,led] — 2. Порядок элементов при равенстве всего массива также значим. Для нескольких условий к одному объекту массива нужен отдельный разбор $elemMatch; текущий пример не подменяет его простым условием для строк.

Вложение удобно для небольшого набора свойств, которые читаются вместе с товаром. Оно не обязывает хранить внутри товара бесконечную историю заказов. Когда объект существует самостоятельно или используется многими карточками, могут понадобиться отдельная коллекция и ссылка на идентификатор. MongoDB не превращает такую ссылку в реляционный внешний ключ автоматически. Общую идею вложенности дополняет глава о вложенных массивах, но BSON-документ не равен JSON-строке.

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

Отсутствующее поле, null и значение другого типа требуют отдельного договора. Здесь все шесть карточек имеют ожидаемые specs и tags; это не доказательство корректности любого внешнего документа. Пустой PHP-массив обычно означает массив, а пустой документ нужно задавать осознанно, например (object)[]. Для устойчивого каталога нужны правила валидации и ограничение роста вложенных списков.

Полная самостоятельная программа и файлы
Найти белые настольные лампы по вложенному полю и тегуСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mc115-helper.php';
[$client,$db,$runtime]=mc115Connect();
$name=mc115Name('mongodb115_catalog_'.bin2hex(random_bytes(8)));
$owned=false;
$details=[];
try {
    $db->createCollection($name); $owned=true;
    $collection=$db->getCollection($name);
mc115Seed($collection);
    $filter=['active'=>true,'specs.color'=>'white','tags'=>'desk'];
    $cursor=$collection->find($filter,['projection'=>['_id'=>0,'sku'=>1,'specs'=>1,'tags'=>1],'sort'=>['sku'=>1],'limit'=>10]);
    $rows=[]; $documents=[];
    foreach ($cursor as $product) {$documents[]=$product; $rows[]=[$product['sku'],$product['specs']['color'],$product['specs']['power_w'],implode(', ',$product['tags'])];}
    $exactOne=$collection->countDocuments(['active'=>true,'specs.color'=>'white','tags'=>['desk']]);
    $exactTwo=$collection->countDocuments(['active'=>true,'specs.color'=>'white','tags'=>['desk','led']]);
    $checks=['nested_color_and_tag_match'=>array_column($rows,0)===['LAMP-A','LAMP-E'],
        'nested_values_read'=>$rows===[['LAMP-A','white',12,'desk, led'],['LAMP-E','white',15,'desk, led']],
        'array_shape_preserved'=>is_array($documents[0]['specs']) && array_is_list($documents[0]['tags']),
        'membership_differs_from_whole_array'=>$exactOne===0 && $exactTwo===2];
    $details=['Два вложенных документа'=>$documents,'Фильтр'=>$filter,'Равенство tags массиву [desk]'=>$exactOne,'Равенство tags массиву [desk,led]'=>$exactTwo];
} finally {
    if ($owned) {$db->dropCollection(mc115Name($name));}
}
$remaining=iterator_to_array($db->listCollectionNames(['filter'=>['name'=>$name]]),false);
$checks['owned_collection_removed']=$remaining===[];
mc115Report('mongodb115-catalog-nested-arrays', ['Артикул', 'specs.color', 'specs.power_w', 'Теги'], $rows, $checks, $runtime, $details);
echo 'mongodb115-catalog-nested-arrays: saved result.html and result.json
';
Ожидаемый результат
mongodb115-catalog-nested-arrays: saved result.html and result.json

Запустите PHP CLI и откройте result.html. Все входные документы и helper включены; зависимости ставятся по приложенному Composer lock. Другие уроки запускать заранее не нужно.

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

Попробуйте сами. Замените в фильтре desk на shelf: останется LAMP-C с мощностью 8 Вт. Затем верните desk и измените цвет на blue: найдётся LAMP-B, поскольку этот запрос не проверяет stock. Если нужны именно доступные товары, добавьте stock>0 — результат станет пустым.

Изменить остаток, сохранив всю остальную карточку

На склад пришли три лампы: остаток LAMP-A нужно изменить с 5 на 8. Название, цена, характеристики, теги и идентификатор должны сохраниться. Покажем до и после, чтобы было видно, что именно изменила запись.

Вход и задача

Часть вызоваЗначение
ФильтрУже прочитанный точный ObjectId товара
ИзменениеОператор $set, поле stock, целое 8
upsertfalse: не создавать новую карточку при отсутствии
Сохраняемые соседиprice_cents=249000, specs.color=white, остальные поля
Путь данных
  1. findOne получает исходную карточку
  2. Фильтр _id указывает ровно её
  3. updateOne с $set присваивает stock=8
  4. Результат сообщает matched=1 и modified=1
  5. Повторное чтение показывает сохранность остальных полей

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

СостояниеАртикулОстатокЦена, коп.Цвет
ДоLAMP-A5249000white
ПослеLAMP-A8249000white

Короткий вызов

$change = $collection->updateOne(
    ['_id' => $id],             // Какой документ меняем.
    ['$set' => ['stock' => 8]], // Какое поле и новое значение.
    ['upsert' => false]         // Не создавать товар, если его нет.
);
echo $change->getMatchedCount();  // 1
echo $change->getModifiedCount(); // 1

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

У updateOne отдельно передаются «какой документ», «что изменить» и опции. ['$set' => ['stock' => 8]] — документ обновления: оператор $set присваивает значение выбранному полю. Это не обычная новая карточка для замены. Полную замену выполняет отдельный replaceOne; забытые при замене поля могут исчезнуть. Поэтому выбор операции определяется задачей.

getMatchedCount сообщает число найденных документов, getModifiedCount — фактически изменённых. В этой программе оба равны 1. Нулевой matched может означать, что ID уже отсутствует; это надо объяснить пользователю, а не объявить успешным изменением. Мы явно оставили upsert=false, чтобы опечатка в ID не создала новый товар. Подробные состояния результата продолжает глава о низкоуровневых записях MongoDB.

HTML показывает полные документы до и после и короткую таблицу. Проверка сравнивает все значения, а ObjectId — по BSON-типу и его значению, не по идентичности двух PHP-экземпляров. Независимые чтения могут создавать разные объекты ObjectId с одинаковым значением. Это важная разница между состоянием записи и ссылкой на объект в памяти PHP.

Для одного вложенного поля используется путь вроде ['$set' => ['specs.power_w' => 15]]. А присваивание целого specs = [power_w => 15] заменяет именно вложенный документ specs и убирает неуказанный color. Правило надо читать по точному пути: $set не означает автоматическое глубокое объединение любых массивов.

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

Присвоение stock=8 подходит для уже рассчитанного абсолютного значения в этом самостоятельном сценарии. В конкурентном учёте прочитать 5, прибавить 3 в PHP и записать 8 можно с потерей параллельного изменения. Нужны атомарные операторы вроде $inc и условия доступного остатка; отдельный сценарий следующей главы показывает условную запись. Не объявляем этот пример транзакцией нескольких документов.

Полная самостоятельная программа и файлы
Изменить остаток, сохранив всю остальную карточкуСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mc115-helper.php';
[$client,$db,$runtime]=mc115Connect();
$name=mc115Name('mongodb115_catalog_'.bin2hex(random_bytes(8)));
$owned=false;
$details=[];
try {
    $db->createCollection($name); $owned=true;
    $collection=$db->getCollection($name);
mc115Seed($collection);
    $before=$collection->findOne(['sku'=>'LAMP-A']);
    if ($before===null) {throw new RuntimeException('Fixture product is missing');}
    $change=$collection->updateOne(['_id'=>$before['_id']],['$set'=>['stock'=>8]],['upsert'=>false]);
    $after=$collection->findOne(['_id'=>$before['_id']]);
    if ($after===null) {throw new RuntimeException('Product disappeared after update');}
    $expected=$before; $expected['stock']=8;
    $rows=[['До',$before['sku'],$before['stock'],$before['price_cents'],$before['specs']['color']],['После',$after['sku'],$after['stock'],$after['price_cents'],$after['specs']['color']]];
    $checks=['one_document_changed'=>$change->isAcknowledged() && $change->getMatchedCount()===1 && $change->getModifiedCount()===1,
        'only_stock_changed'=>mc115Plain($after)===mc115Plain($expected),
        'same_id'=>$before['_id'] instanceof MongoDB\BSON\ObjectId && $after['_id'] instanceof MongoDB\BSON\ObjectId && (string)$before['_id']===(string)$after['_id'],
        'six_products_remain'=>$collection->countDocuments([])===6];
    $details=['До обновления'=>$before,'После обновления'=>$after,'Результат записи'=>['matched'=>$change->getMatchedCount(),'modified'=>$change->getModifiedCount(),'upserted'=>$change->getUpsertedCount()]];
} finally {
    if ($owned) {$db->dropCollection(mc115Name($name));}
}
$remaining=iterator_to_array($db->listCollectionNames(['filter'=>['name'=>$name]]),false);
$checks['owned_collection_removed']=$remaining===[];
mc115Report('mongodb115-catalog-update-one-field', ['Состояние', 'Артикул', 'Остаток', 'Цена, коп.', 'Цвет'], $rows, $checks, $runtime, $details);
echo 'mongodb115-catalog-update-one-field: saved result.html and result.json
';
Ожидаемый результат
mongodb115-catalog-update-one-field: saved result.html and result.json

Запустите PHP CLI и откройте result.html. Все входные документы и helper включены; зависимости ставятся по приложенному Composer lock. Другие уроки запускать заранее не нужно.

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

Попробуйте сами. В самостоятельной копии замените изменение на ['$set' => ['specs.power_w' => 15]]. После чтения мощность станет 15, color останется white, stock останется 5. Перестройте таблицу результата под эту задачу и сравните весь документ, а не только сообщение Modified=1.

Практический вопрос: Как разделить фильтр товара и документ изменения в updateOne. Фильтр и $set попали в неверно собранные аргументы PHP. В нашем уроке три аргумента разобраны отдельно, а реальное чтение до/после подтверждает, что изменён только остаток. Обсуждение даёт проблему; технические правила сверены с официальной документацией, программа и объяснения написаны отдельно.

Удалить один временный товар и показать, что осталось

После проверки импорта осталась одна пробная карточка DEMO-DELETE. Нужно убрать её и удостовериться, что шесть настоящих учебных товаров сохранились. Повторное выполнение удаления не должно затронуть следующий товар.

Вход и задача

НаборЧто с ним делаем
Шесть карточек каталогаСохраняем
DEMO-DELETEДополнительный документ с temporary=true
Фильтр удаленияТочный _id + temporary=true
Повторный вызовТот же фильтр, без расширения условий
Путь данных
  1. В отдельную коллекцию вставлены 6+1 документов
  2. deleteOne получает точный ID пробной карточки
  3. Первый вызов удаляет один документ
  4. Повторный вызов удаляет ноль
  5. Запрос оставшихся артикулов подтверждает сохранность шести

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

ДействиеРезультат
До удаления7
Удалено первым вызовом1
Удалено повторным вызовом0
Осталось товаров6
Оставшиеся артикулыCHAIR-A, LAMP-A, LAMP-B, LAMP-C, LAMP-D, LAMP-E

Короткий вызов

$filter = ['_id' => $temporaryId, 'temporary' => true];
$result = $collection->deleteOne($filter);
echo $result->getDeletedCount(); // 1
$again = $collection->deleteOne($filter);
echo $again->getDeletedCount();  // 0: этого документа больше нет

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

deleteOne принимает фильтр и удаляет не более одного совпавшего документа. Важно не только слово One: если фильтр слишком широкий, будет удалён один из подходящих документов, который может оказаться не тем. Здесь _id указывает конкретную запись, а temporary=true дополнительно проверяет её назначение.

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

Удаление документа, удаление коллекции и удаление базы — разные операции. deleteOne здесь решает задачу каталога. Отдельный finally затем убирает всю собственную одноразовую коллекцию упражнения, только после проверки оставшихся данных. Этот служебный шаг не нужно переносить в обработчик кнопки удаления реального товара.

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

Физическое удаление нельзя отменить простым обратным вызовом deleteOne. Для пользовательского каталога может понадобиться архивирование active=false, журнал или подтверждение действия в интерфейсе. Внешний ID необходимо проверять и сопоставлять с правами пользователя. Не заменяйте точный фильтр пустым [] и не принимайте операторы из входного HTTP-массива.

Полная самостоятельная программа и файлы
Удалить один временный товар и показать, что осталосьСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mc115-helper.php';
[$client,$db,$runtime]=mc115Connect();
$name=mc115Name('mongodb115_catalog_'.bin2hex(random_bytes(8)));
$owned=false;
$details=[];
try {
    $db->createCollection($name); $owned=true;
    $collection=$db->getCollection($name);
mc115Seed($collection);
    $insert=$collection->insertOne(['sku'=>'DEMO-DELETE','name'=>'Пробная карточка','temporary'=>true,'stock'=>0]);
    $id=$insert->getInsertedId();
    $beforeCount=$collection->countDocuments([]);
    $filter=['_id'=>$id,'temporary'=>true];
    $first=$collection->deleteOne($filter); $second=$collection->deleteOne($filter);
    $remaining=[];
    foreach ($collection->find([],['projection'=>['_id'=>0,'sku'=>1],'sort'=>['sku'=>1],'limit'=>10]) as $product) {$remaining[]=$product['sku'];}
    $rows=[['До удаления',$beforeCount],['Удалено первым вызовом',$first->getDeletedCount()],['Удалено повторным вызовом',$second->getDeletedCount()],['Осталось товаров',$collection->countDocuments([])],['Оставшиеся артикулы',implode(', ',$remaining)]];
    $checks=['seven_before'=>$beforeCount===7,'one_target_deleted'=>$first->isAcknowledged() && $first->getDeletedCount()===1,
        'repeat_finds_nothing'=>$second->getDeletedCount()===0,'target_absent'=>$collection->findOne(['_id'=>$id])===null,
        'six_original_products_preserved'=>$remaining===['CHAIR-A','LAMP-A','LAMP-B','LAMP-C','LAMP-D','LAMP-E']];
    $details=['Точный фильтр удаления'=>$filter,'Артикулы оставшихся документов'=>$remaining,'Удаление коллекции'=>'Отдельная служебная очистка в finally после проверки результата, не действие deleteOne.'];
} finally {
    if ($owned) {$db->dropCollection(mc115Name($name));}
}
$remaining=iterator_to_array($db->listCollectionNames(['filter'=>['name'=>$name]]),false);
$checks['owned_collection_removed']=$remaining===[];
mc115Report('mongodb115-catalog-delete-disposable', ['Действие', 'Результат'], $rows, $checks, $runtime, $details);
echo 'mongodb115-catalog-delete-disposable: saved result.html and result.json
';
Ожидаемый результат
mongodb115-catalog-delete-disposable: saved result.html and result.json

Запустите PHP CLI и откройте result.html. Все входные документы и helper включены; зависимости ставятся по приложенному Composer lock. Другие уроки запускать заранее не нужно.

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

Попробуйте сами. Перед удалением задайте temporary=false у пробного документа. Исходный фильтр _id + temporary=true больше не совпадёт: deleted=0, останется семь документов. Это полезная защитная предпосылка, а не повод автоматически убрать условие и повторить удаление.

Следующий шаг: что делает расширение под библиотекой

В этой главе удобные методы помогли выполнить задачу каталога. Теперь можно посмотреть слой ниже: команды, Query, BulkWrite, курсор, подтверждённый результат записи и частичный отказ. Это продолжает глава о работе через расширение PHP. Валидация всей модели, индексы, агрегации, транзакции и эксплуатация остаются самостоятельными темами; первая карточка не заменяет полный справочник MongoDB.

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

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

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

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

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