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

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

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

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

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

MongoDB: валидация, атомарный остаток, версия формы и частичный импорт

Шесть самостоятельных сценариев: $jsonSchema, условный $inc, optimistic locking, upsert с картой ID, ordered/unordered и findAndModify; девять полных карточек методов.

Условие записи, результат и границы атомарности

Сохранить карточку — только начало. Магазину нужно не допустить отрицательный остаток, не затереть чужое изменение, отличить новый товар от повторного импорта и честно показать частичный отказ. Здесь каждое правило превращается в условие записи и проверяемый результат на конкретных документах.

Сначала нужны документ, коллекция и первый каталог и BulkWrite, Manager и результат записи. Типы BSON и преобразование входных значений разобраны в соседней главе о значениях BSON. Фильтр — PHP-массив условий, который драйвер кодирует в BSON; база проверяет его на сервере. Операторы вроде $inc — имена действий MongoDB, не PHP-переменные. Поэтому в PHP они заключены в одинарные кавычки.

ПотребностьМеханизмЧто проверяем
Не сохранить неправильный тип$jsonSchema и errorКакие исходные записи отклонены
Не уйти в отрицательный остатокУсловие available ≥ quantity и $incMatched, Modified и новые значения
Не потерять чужую правкуОжидаемая version в фильтреНовая версия либо отсутствие совпадения
Синхронизировать по артикулуУникальный ключ и upsertВставка, правка или неизменный повтор
Объяснить неполный импортWriteResult и WriteErrorСохранённые, ошибочные и не начатые операции
Получить образ своей правкиfindAndModify с new = truevalue, а не только ok
Граница атомарной операции
  1. Фильтр описывает допустимое текущее состояние
  2. MongoDB проверяет его при записи
  3. Несколько полей одного документа меняются вместе
  4. Ответ описывает совпадение и результат
  5. Другие документы и внешние сервисы остаются отдельными операциями

Атомарность одного документа означает целостность одной правки, а не автоматическую транзакцию заказа, остатков и платежа. Наш отдельный standalone-сервер не используется для многодокументных транзакций. Схема валидации, уникальный индекс, условный фильтр и подтверждение записи решают разные задачи и могут применяться вместе.

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

Архив каждого сценария содержит example.php, ma117-helper.php и README.txt. Нужны PHP 8.4+ CLI, ext-mongodb 2.5+ и отдельный MongoDB 8.0. Composer-библиотека не требуется: используются классы MongoDB\Driver. README содержит установку сервера/расширения, запуск mongod на loopback и точные требования к правам. Здесь $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 после запуска локального mongod и настройки PHP. Руководства: MongoDB Community для Windows и расширение PHP. Helper намеренно допускает только отдельный 127.0.0.1:порт. Настройки локального сервера без аутентификации не предназначены для публичного развёртывания.

Каждая программа создаёт одну уникальную коллекцию mongodb117_atomic_…; только успешный create даёт право удалить её в finally. Затем точный listCollections подтверждает отсутствие. Создание правил и индексов внутри примера обеспечивает независимость запуска; в приложении это отдельный этап подготовки схемы. Все товарные данные синтетические. Метаданные ошибок в публичном отчёте экранируются, случайные имена объектов заменяются подписями.

Не сохранить строковый остаток и неизвестное поле: серверная схема

Поставщик присылает карточки: у одной остаток записан строкой «4», у другой появилось неизвестное поле discount. Если молча сохранить всё, сравнение и расчёт остатка будут опираться на разные типы. Правило коллекции помогает остановить такую запись независимо от того, какой сервис отправил документ.

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

Три независимые попытки вставки: CUP со stock = 4, LAMP со stock = "4" и PLATE с лишним discount. Строка в кавычках и целое число различаются уже в PHP и передаются драйвером как разные BSON-типы. У каждого документа здесь явный целочисленный _id; реальные ObjectId разобраны отдельно.

ДанныеЗначение
CUPstock = 4: целое число
LAMPstock = "4": строка
PLATEstock = 1 и незаявленное discount = 10
Путь данных
  1. Создаём свою коллекцию с validator
  2. PHP-массивы кодируются в BSON-документы
  3. Сервер проверяет каждый документ по $jsonSchema
  4. CUP сохраняется, две другие записи получают 121
  5. Индекс ошибки возвращает нас к нужной исходной строке импорта

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

$validator = ['$jsonSchema' => [
    'bsonType' => 'object', 'required' => ['stock'],
    'properties' => [
        'stock' => ['bsonType' => ['int', 'long'], 'minimum' => 0],
    ],
]]; // stock: 4 допустим; stock: '4' — другая BSON-типизация.
$command = new MongoDB\Driver\Command([
    'create' => $collection, 'validator' => $validator,
    'validationLevel' => 'strict', 'validationAction' => 'error',
]);
$manager->executeWriteCommand($database, $command);

Фрагмент использует документы и переменные, подготовленные в полной программе ниже. Он показывает центральное действие; для самостоятельного запуска скачайте весь архив. В коде -> вызывает метод или читает свойство объекта, а => связывает ключ со значением в PHP-массиве.

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

Позиция операцииSKUРезультатКод ошибки
0CUPсохраненоNULL
1LAMPотклонено121
2PLATEотклонено121

Как это работает

validator — правило коллекции; $jsonSchema — оператор проверки структуры BSON, а не вызов json_decode. bsonType задаёт тип, required требует присутствия поля, minimum ограничивает число. В коротком варианте проверяется только stock. Полная схема дополнительно требует непустые sku/title и запрещает неизвестные поля через additionalProperties = false. Поэтому _id тоже указан в properties: он существует у каждого документа и иначе оказался бы лишним. int и long разрешены вместе, чтобы принимать целые BSON-числа обеих разрядностей. strict проверяет все вставки и обновления; error отклоняет нарушающую запись. Эти настройки не преобразуют строку в число. getWriteErrors возвращает отдельные ошибки, getIndex указывает исходную операцию, getInfo может дать подробности нарушенного правила. Сам текст сообщения и дерево подробностей зависят от сервера.

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

Очередь unordered выбрана, чтобы увидеть обе причины отказа за один сценарий; её подробный разбор ниже. Серверная схема не заменяет понятные сообщения HTML-формы, авторизацию и ограничения разрешённых полей. Минимум 0 не устанавливает уникальность SKU и не обеспечивает резервирование остатка. additionalProperties = false — осознанно строгий контракт: новое поле сначала требует изменения схемы. Не включайте bypassDocumentValidation, чтобы скрыть ошибку данных. Номер 121 описывает валидацию документа; прочие ошибки и ошибки подтверждения полной программой не замалчиваются.

Полная схема коллекции: обязательные и допустимые поля
$createOptions = [
        'validator' => ['$jsonSchema' => [
            'bsonType' => 'object', 'required' => ['sku', 'title', 'stock'],
            'additionalProperties' => false,
            'properties' => [
                '_id' => ['bsonType' => ['int', 'long']],
                'sku' => ['bsonType' => 'string', 'minLength' => 1],
                'title' => ['bsonType' => 'string', 'minLength' => 1],
                'stock' => ['bsonType' => ['int', 'long'], 'minimum' => 0],
            ],
        ]], 'validationLevel' => 'strict', 'validationAction' => 'error',
    ];

Почему строка "4" не равна целому BSON-числу и откуда берутся типы: значения BSON и проверка ввода.

Полная самостоятельная программа, helper и архив
Не сохранить строковый остаток и неизвестное поле: серверная схемаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/ma117-helper.php';
[$manager, $database] = ma117Environment();
$server = ma117Command($manager, $database, ['buildInfo' => 1])[0]['version'];
$runtime = ['php' => PHP_VERSION, 'extension' => phpversion('mongodb'), 'server' => $server];
$collection = 'mongodb117_atomic_' . bin2hex(random_bytes(8));
$namespace = $database . '.' . $collection;
$owned = false; $details = [];
try {
    if (ma117Exists($manager, $database, $collection)) { throw new RuntimeException('Fixture already exists'); }
    $createOptions = [
        'validator' => ['$jsonSchema' => [
            'bsonType' => 'object', 'required' => ['sku', 'title', 'stock'],
            'additionalProperties' => false,
            'properties' => [
                '_id' => ['bsonType' => ['int', 'long']],
                'sku' => ['bsonType' => 'string', 'minLength' => 1],
                'title' => ['bsonType' => 'string', 'minLength' => 1],
                'stock' => ['bsonType' => ['int', 'long'], 'minimum' => 0],
            ],
        ]], 'validationLevel' => 'strict', 'validationAction' => 'error',
    ];
    ma117Command($manager, $database, ['create' => $collection] + $createOptions, true);
    $owned = true;
$input = [
        ['_id' => 101, 'sku' => 'CUP', 'title' => 'Кружка', 'stock' => 4],
        ['_id' => 102, 'sku' => 'LAMP', 'title' => 'Лампа', 'stock' => '4'],
        ['_id' => 103, 'sku' => 'PLATE', 'title' => 'Тарелка', 'stock' => 1, 'discount' => 10],
    ];
    $bulk = new MongoDB\Driver\BulkWrite(['ordered' => false]);
    foreach ($input as $document) { $bulk->insert($document); }
    $caught = false;
    try { $result = $manager->executeBulkWrite($namespace, $bulk); }
    catch (MongoDB\Driver\Exception\BulkWriteException $error) {
        $result = $error->getWriteResult(); $caught = true;
        if ($result->getWriteConcernError() !== null || $result->getWriteErrors() === []) { throw $error; }
        foreach ($result->getWriteErrors() as $writeError) {
            if ($writeError->getCode() !== 121) { throw $error; }
        }
    }
    $errorsByInput = []; $diagnostics = [];
    foreach ($result->getWriteErrors() as $writeError) {
        $position = $writeError->getIndex();
        $errorsByInput[$position] = $writeError->getCode();
        $diagnostics[] = ['position' => $position, 'sku' => $input[$position]['sku'],
            'code' => $writeError->getCode(), 'message' => $writeError->getMessage(),
            'info' => $writeError->getInfo()];
    }
    ksort($errorsByInput);
    $saved = ma117Read($manager, $namespace, [], ['sort' => ['_id' => 1], 'limit' => 10]);
    $rows = [];
    foreach ($input as $position => $document) {
        $rows[] = [$position, $document['sku'], isset($errorsByInput[$position]) ? 'отклонено' : 'сохранено', $errorsByInput[$position] ?? null];
    }
    $checks = ['expected_validation_errors' => $caught && $errorsByInput === [1 => 121, 2 => 121],
        'one_insert_confirmed' => $result->isAcknowledged() && $result->getInsertedCount() === 1,
        'only_valid_document_saved' => $saved === [$input[0]],
        'diagnostics_present' => count($diagnostics) === 2 && $diagnostics[0]['message'] !== '' && $diagnostics[1]['message'] !== ''];
    $details = ['Входные документы: число и строка различаются' => $input,
        'Серверные причины двух отказов' => $diagnostics, 'Сохранённый документ' => $saved];
} finally {
    if ($owned) { ma117Command($manager, $database, ['drop' => $collection], true); }
}
$checks['owned_collection_removed'] = !ma117Exists($manager, $database, $collection);
$details = ma117Public($details, $collection, $database);
ma117Report('mongodb117-atomic-validation', ['Позиция операции', 'SKU', 'Результат', 'Код ошибки'], $rows, $checks, $runtime, $details);
echo 'mongodb117-atomic-validation: saved result.html and result.json
';
Ожидаемый результат
mongodb117-atomic-validation: saved result.html and result.json

Запустите php example.php и откройте result.html. result.json повторяет таблицу; metrics.json хранит проверки, версии, ответы команд и диагностику. Все файлы для локального сценария и точная инструкция включены в архив.

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

Попробуйте сами. Замените у LAMP строку "4" на число 4, а у PLATE удалите discount. Все три записи будут приняты, getWriteErrors вернёт пустой массив, inserted станет 3. Для такого эксперимента измените и ожидаемые проверки: исходный архив намеренно ожидает два отказа, а не любой успешный выход.

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

Зарезервировать три товара только при достаточном остатке

Два покупателя хотят по три кружки, а свободно пять. Нельзя прочитать 5 в PHP, посчитать 5 − 3 и затем безусловно записать 2: другой запрос мог уже изменить остаток между чтением и записью. Условие доступности и изменение должны быть одной серверной операцией над документом.

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

Один товар 201: available = 5, reserved = 0. Запрос Q1 резервирует 3, затем Q2 пытается зарезервировать ещё 3. quantity проверяется как положительное целое; отрицательное значение иначе увеличило бы доступный остаток.

ДанныеЗначение
availableСвободно для новых заказов: 5
reservedУже отложено: 0
Q1 / Q2Две последовательные заявки по 3
Путь данных
  1. Проверяем тип и положительность quantity
  2. Фильтр: нужный _id И available ≥ quantity
  3. Один $inc уменьшает available и увеличивает reserved
  4. Matched = 1 означает найденный подходящий товар
  5. Второй фильтр уже сравнивает 2 с 3 и ничего не меняет

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

$qty = 3;
$bulk = new MongoDB\Driver\BulkWrite();
$bulk->update(
    ['_id' => 201, 'available' => ['$gte' => $qty]],
    ['$inc' => ['available' => -$qty, 'reserved' => $qty]],
    ['multi' => false, 'upsert' => false]
);
$result = $manager->executeBulkWrite($namespace, $bulk);
echo $result->getMatchedCount(); // 1 — резерв создан; 0 — условие не подошло.

Фрагмент использует документы и переменные, подготовленные в полной программе ниже. Он показывает центральное действие; для самостоятельного запуска скачайте весь архив. В коде -> вызывает метод или читает свойство объекта, а => связывает ключ со значением в PHP-массиве.

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

ЗапросКоличествоMatchedModifiedДоступноРезерв
Q131123
Q230023

Как это работает

В одном документе MongoDB применяет обновление атомарно: условие и две правки относятся к одной операции. $gte означает «больше либо равно», $inc прибавляет указанные величины; отрицательная величина уменьшает поле. multi = false ограничивает обновление одним документом, upsert = false запрещает создавать товар при отсутствии совпадения. Q1 получает matched = modified = 1, Q2 получает 0 и 0 без исключения. Сервер успешно обработал второй запрос, но бизнес-условие не выполнено. Read после каждой операции нужен здесь для наглядной таблицы; сам резерв не зависит от этого отдельного чтения. getUpsertedCount = 0 и пустой getUpsertedIds дополнительно подтверждают, что резерв не превратился в вставку.

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

Это два последовательных обращения, не испытание конкурентной нагрузки. Свойство атомарности документировано сервером; вывод показывает изменение условия на конкретных данных. Остаток товара, отдельный документ заказа и платёж не становятся общей транзакцией. Повтор HTTP-запроса может зарезервировать ещё раз, пока хватает available: нужен отдельный контракт идемпотентности и ключ запроса, которых этот пример не реализует. matched = 0 объединяет отсутствие товара и недостаток остатка; интерфейс различает их отдельным безопасным чтением с учётом прав доступа. w = 1 подтверждает ответ сервера, но не резервирует ресурсы на других системах.

Полная самостоятельная программа, helper и архив
Зарезервировать три товара только при достаточном остаткеСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/ma117-helper.php';
[$manager, $database] = ma117Environment();
$server = ma117Command($manager, $database, ['buildInfo' => 1])[0]['version'];
$runtime = ['php' => PHP_VERSION, 'extension' => phpversion('mongodb'), 'server' => $server];
$collection = 'mongodb117_atomic_' . bin2hex(random_bytes(8));
$namespace = $database . '.' . $collection;
$owned = false; $details = [];
try {
    if (ma117Exists($manager, $database, $collection)) { throw new RuntimeException('Fixture already exists'); }
    $createOptions = [];
    ma117Command($manager, $database, ['create' => $collection] + $createOptions, true);
    $owned = true;
ma117Insert($manager, $namespace, [['_id' => 201, 'sku' => 'CUP', 'available' => 5, 'reserved' => 0]]);
    $requests = [['name' => 'Q1', 'quantity' => 3], ['name' => 'Q2', 'quantity' => 3]];
    $rows = []; $allAcknowledged = true; $noUpserts = true; $snapshots = [];
    foreach ($requests as $request) {
        $qty = $request['quantity'];
        if (!is_int($qty) || $qty <= 0) { throw new InvalidArgumentException('Positive integer quantity required'); }
        $bulk = new MongoDB\Driver\BulkWrite();
        $bulk->update(['_id' => 201, 'available' => ['$gte' => $qty]],
            ['$inc' => ['available' => -$qty, 'reserved' => $qty]], ['multi' => false, 'upsert' => false]);
        $result = $manager->executeBulkWrite($namespace, $bulk);
        $allAcknowledged = $allAcknowledged && $result->isAcknowledged();
        $noUpserts = $noUpserts && $result->getUpsertedCount() === 0 && $result->getUpsertedIds() === [];
        $after = ma117Read($manager, $namespace, ['_id' => 201], ['limit' => 1])[0];
        $rows[] = [$request['name'], $qty, $result->getMatchedCount(), $result->getModifiedCount(), $after['available'], $after['reserved']];
        $snapshots[] = $after;
    }
    $checks = ['conditional_results' => $rows === [['Q1',3,1,1,2,3],['Q2',3,0,0,2,3]],
        'all_writes_acknowledged' => $allAcknowledged,
        'reserve_never_creates_product' => $noUpserts,
        'second_request_unchanged' => $snapshots[0] === $snapshots[1],
        'stock_conserved_nonnegative' => $snapshots[1]['available'] >= 0 && $snapshots[1]['available'] + $snapshots[1]['reserved'] === 5];
    $details = ['Начальный остаток' => ['available' => 5, 'reserved' => 0], 'После каждого запроса' => $snapshots,
        'Границы проверки' => ['requests_are_sequential' => true, 'concurrency_load_test' => false, 'http_idempotency_implemented' => false]];
} finally {
    if ($owned) { ma117Command($manager, $database, ['drop' => $collection], true); }
}
$checks['owned_collection_removed'] = !ma117Exists($manager, $database, $collection);
$details = ma117Public($details, $collection, $database);
ma117Report('mongodb117-atomic-reserve-stock', ['Запрос', 'Количество', 'Matched', 'Modified', 'Доступно', 'Резерв'], $rows, $checks, $runtime, $details);
echo 'mongodb117-atomic-reserve-stock: saved result.html and result.json
';
Ожидаемый результат
mongodb117-atomic-reserve-stock: saved result.html and result.json

Запустите php example.php и откройте result.html. result.json повторяет таблицу; metrics.json хранит проверки, версии, ответы команд и диагностику. Все файлы для локального сценария и точная инструкция включены в архив.

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

Попробуйте сами. Для Q2 запросите 2. Он будет принят, итог available = 0, reserved = 5, у обеих операций matched = modified = 1. Запрос ещё одной единицы после этого даст 0/0 и сохранит итог. Сумма двух полей остаётся 5.

Не затереть новую цену устаревшей формой редактирования

Два сотрудника открыли одну карточку версии 7. Первый уже изменил цену, а второй отправляет старую форму. Нужен явный конфликт, чтобы второй сотрудник увидел свежие данные и не затёр чужую работу. Это называется оптимистической блокировкой: длительной блокировки на время заполнения формы нет, при сохранении проверяется версия.

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

Товар 301: «Синяя кружка», цена 500, version = 7. Форма A меняет цену на 550. Форма B хочет название «Красная кружка», но тоже предъявляет старую версию 7. Для ясности отправляем формы последовательно.

ДанныеЗначение
Общий прочитанный документ301 / Синяя кружка / 500 / version 7
Форма AОжидает 7, задаёт price = 550
Форма BОжидает 7, задаёт title = Красная кружка
Путь данных
  1. Форма хранит версию прочитанной карточки
  2. Фильтр сохранения включает _id и expectedVersion
  3. Сервер одновременно применяет $set и version + 1
  4. new = true возвращает карточку версии 8
  5. Старая версия второй формы не совпадает: value = null

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

$command = new MongoDB\Driver\Command([
    'findAndModify' => $collection,
    'query' => ['_id' => 301, 'version' => 7],
    'update' => ['$set' => ['price' => 550], '$inc' => ['version' => 1]],
    'new' => true, 'upsert' => false,
]);
$cursor = $manager->executeReadWriteCommand($database, $command);
$reply = $cursor->toArray()[0];
if ($reply->value !== null) {
    echo $reply->value->version; // 8 при совпадении.
} else {
    echo 'Карточка изменилась или удалена';
}

Фрагмент использует документы и переменные, подготовленные в полной программе ниже. Он показывает центральное действие; для самостоятельного запуска скачайте весь архив. В коде -> вызывает метод или читает свойство объекта, а => связывает ключ со значением в PHP-массиве.

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

ФормаОжидаемая версияИзмененоЦена, руб.НазваниеТекущая версия
A71550Синяя кружка8
B70550Синяя кружка8

Как это работает

Поле version — прикладной счётчик, его не добавляет MongoDB автоматически. Все пути редактирования должны соблюдать этот договор. $set меняет перечисленные поля, $inc увеличивает версию. findAndModify объединяет условную правку и получение документа; executeReadWriteCommand возвращает Cursor с ответом команды. В нём value — товар после изменения при new = true, lastErrorObject.n — число совпадений. Форма B получает value = null и n = 0, при этом ok = 1: команда выполнена корректно. Цена остаётся 550, название остаётся «Синяя кружка», версия остаётся 8. Полная программа переводит документы ответа в PHP-массивы; короткий фрагмент оставляет стандартные объекты, поэтому там используется ->value.

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

Веб-форма не даёт права редактировать любой _id: проверка пользователя/магазина нужна до записи и в фильтре. Не передавайте клиентский набор полей прямо в $set; здесь допустимые изменения заданы программой. Конфликт версии не следует автоматически лечить повтором со свежей версией: это снова может затереть смысл чужой правки. Покажите актуальную карточку и предложите осознанное объединение. n = 0 может означать удалённый документ, а не только старую версию. Схема не защищает от кода, который обновляет карточку без проверки version.

Полная самостоятельная программа, helper и архив
Не затереть новую цену устаревшей формой редактированияСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/ma117-helper.php';
[$manager, $database] = ma117Environment();
$server = ma117Command($manager, $database, ['buildInfo' => 1])[0]['version'];
$runtime = ['php' => PHP_VERSION, 'extension' => phpversion('mongodb'), 'server' => $server];
$collection = 'mongodb117_atomic_' . bin2hex(random_bytes(8));
$namespace = $database . '.' . $collection;
$owned = false; $details = [];
try {
    if (ma117Exists($manager, $database, $collection)) { throw new RuntimeException('Fixture already exists'); }
    $createOptions = [];
    ma117Command($manager, $database, ['create' => $collection] + $createOptions, true);
    $owned = true;
ma117Insert($manager, $namespace, [['_id' => 301, 'title' => 'Синяя кружка', 'price' => 500, 'version' => 7]]);
    $forms = [
        ['name' => 'A', 'expectedVersion' => 7, 'changes' => ['price' => 550]],
        ['name' => 'B', 'expectedVersion' => 7, 'changes' => ['title' => 'Красная кружка']],
    ];
    $rows = []; $replies = []; $snapshots = [];
    foreach ($forms as $form) {
        $command = new MongoDB\Driver\Command([
            'findAndModify' => $collection,
            'query' => ['_id' => 301, 'version' => $form['expectedVersion']],
            'update' => ['$set' => $form['changes'], '$inc' => ['version' => 1]],
            'new' => true, 'upsert' => false, 'maxTimeMS' => 1000,
            'fields' => ['_id' => 1, 'title' => 1, 'price' => 1, 'version' => 1],
        ]);
        $cursor = $manager->executeReadWriteCommand($database, $command);
        $cursor->setTypeMap(['root' => 'array', 'document' => 'array', 'array' => 'array']);
        $reply = $cursor->toArray()[0]; $replies[] = $reply;
        $after = ma117Read($manager, $namespace, ['_id' => 301], ['limit' => 1])[0]; $snapshots[] = $after;
        $rows[] = [$form['name'], $form['expectedVersion'], (int)$reply['lastErrorObject']['n'], $after['price'], $after['title'], $after['version']];
    }
    $checks = ['first_form_saved' => $replies[0]['value']['price'] === 550 && $replies[0]['value']['version'] === 8,
        'stale_form_no_match' => $replies[1]['value'] === null && (int)$replies[1]['lastErrorObject']['n'] === 0,
        'stale_form_did_not_overwrite' => $snapshots[0] === $snapshots[1] && $snapshots[1]['title'] === 'Синяя кружка',
        'both_commands_successful' => (float)$replies[0]['ok'] === 1.0 && (float)$replies[1]['ok'] === 1.0];
    $details = ['Две формы читали версию 7' => $forms, 'Ответы findAndModify' => $replies, 'Состояние после каждого сохранения' => $snapshots];
} finally {
    if ($owned) { ma117Command($manager, $database, ['drop' => $collection], true); }
}
$checks['owned_collection_removed'] = !ma117Exists($manager, $database, $collection);
$details = ma117Public($details, $collection, $database);
ma117Report('mongodb117-atomic-optimistic-version', ['Форма', 'Ожидаемая версия', 'Изменено', 'Цена, руб.', 'Название', 'Текущая версия'], $rows, $checks, $runtime, $details);
echo 'mongodb117-atomic-optimistic-version: saved result.html and result.json
';
Ожидаемый результат
mongodb117-atomic-optimistic-version: saved result.html and result.json

Запустите php example.php и откройте result.html. result.json повторяет таблицу; metrics.json хранит проверки, версии, ответы команд и диагностику. Все файлы для локального сценария и точная инструкция включены в архив.

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

Попробуйте сами. После конфликта прочитайте версию 8 и отдельно подтвердите изменение названия. Новая форма с expectedVersion = 8 сохранится, даст version = 9, title = «Красная кружка», price = 550. Не меняйте expectedVersion незаметно для пользователя.

Практический вопрос: Parallel update issue in MongoDB. Разные процессы читают и сохраняют одно изменяемое значение, создавая риск потерянной правки. Здесь показаны фильтр ожидаемой версии и условное изменение без клиентского read-modify-write. Обсуждение задаёт проблему; технические правила сверены с официальной документацией, пример и данные написаны для этого урока.

Импорт по поставщику и SKU: отличить вставку, обновление и повтор

При ежедневной синхронизации поставщик передаёт артикул и цену. Новый товар нужно создать, существующий — обновить, повтор того же сообщения не должен создавать ещё одну карточку. Для отчёта важно узнать, что произошло и какой исходной строке принадлежит новый ID.

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

Естественный ключ — пара supplier + sku. В своей коллекции создаём уникальный индекс этой пары. Первый импорт добавляет S1/CUP с ID 401. Второй пакет обновляет CUP и добавляет LAMP с ID 402. Третий повторяет неизменную цену CUP.

ДанныеЗначение
Первый импорт, операция 0S1/CUP: цена 500, новый ID 401
Второй пакет, операция 0S1/CUP: цена 550
Второй пакет, операция 1S1/LAMP: цена 900, новый ID 402
ПовторS1/CUP: снова цена 550
Путь данных
  1. Уникальный индекс защищает пару поставщик + SKU
  2. upsert ищет именно эту пару
  3. Совпадение: работает $set; отсутствие: создаётся документ
  4. Только при создании применяется $setOnInsert
  5. Счётчики и getUpsertedIds связывают итог с исходной очередью

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

$bulk = new MongoDB\Driver\BulkWrite();
$bulk->update(['supplier' => 'S1', 'sku' => 'CUP'], [
    '$set' => ['title' => 'Кружка', 'price' => 500],
    '$setOnInsert' => ['_id' => 401, 'createdTag' => 'first'],
], ['upsert' => true, 'multi' => false]);
$result = $manager->executeBulkWrite($namespace, $bulk);
echo $result->getUpsertedCount(); // 1 при вставке, 0 при совпадении.
foreach ($result->getUpsertedIds() as $operationIndex => $id) {
    echo $operationIndex, ' → ', $id; // 0 → 401.
}

Фрагмент использует документы и переменные, подготовленные в полной программе ниже. Он показывает центральное действие; для самостоятельного запуска скачайте весь архив. В коде -> вызывает метод или читает свойство объекта, а => связывает ключ со значением в PHP-массиве.

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

ЗапускMatchedModifiedUpsertedПозиция → новый ID
Первый импорт0010 → 401
Смешанный пакет1111 → 402
Повтор без изменений100нет

Как это работает

upsert = true означает «обновить подходящий документ или вставить новый». При вставке простые равенства фильтра входят в создаваемый документ. $setOnInsert задаёт поля только новой карточки, поэтому createdTag товара CUP остаётся first после второго импорта. У вставки через upsert свой счётчик getUpsertedCount; это не getInsertedCount обычного insert. getUpsertedIds возвращает отображение позиции операции в её очереди на новый _id. Во втором пакете новый ID имеет ключ 1, хотя он единственный: array_values уничтожил бы привязку к строке LAMP. Повтор CUP даёт matched = 1, modified = 0, upserted = 0: товар найден, менять нечего. Числовые ID здесь выбраны для читаемого отчёта; в обычном приложении они могут быть ObjectId.

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

Уникальный индекс нужен для правила данных, а не только для ускорения. Одновременные upsert, разные фильтры и другие ограничения всё равно могут приводить к duplicate key — обработка таких конфликтов зависит от прикладного контракта. Не добавляйте в естественный ключ текущую цену или version: отсутствие совпадения тогда начнёт создавать лишние записи. Для резерва и устаревшей формы upsert обычно выключен. Пустой список новых ID при неподтверждённой записи не доказывает отсутствие вставок; примеры используют подтверждение w = 1 и isAcknowledged. Индекс создают при подготовке коллекции, а не перед каждой строкой импорта.

Как выбрать поля уникального индекса и почему уникальна комбинация: артикул в пределах магазина.

Полная самостоятельная программа, helper и архив
Импорт по поставщику и SKU: отличить вставку, обновление и повторСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/ma117-helper.php';
[$manager, $database] = ma117Environment();
$server = ma117Command($manager, $database, ['buildInfo' => 1])[0]['version'];
$runtime = ['php' => PHP_VERSION, 'extension' => phpversion('mongodb'), 'server' => $server];
$collection = 'mongodb117_atomic_' . bin2hex(random_bytes(8));
$namespace = $database . '.' . $collection;
$owned = false; $details = [];
try {
    if (ma117Exists($manager, $database, $collection)) { throw new RuntimeException('Fixture already exists'); }
    $createOptions = [];
    ma117Command($manager, $database, ['create' => $collection] + $createOptions, true);
    $owned = true;
ma117Command($manager, $database, ['createIndexes' => $collection, 'indexes' => [[
        'name' => 'supplier_sku_unique', 'key' => ['supplier' => 1, 'sku' => 1], 'unique' => true,
    ]]], true);
    $first = new MongoDB\Driver\BulkWrite();
    $first->update(['supplier' => 'S1', 'sku' => 'CUP'],
        ['$set' => ['title' => 'Кружка', 'price' => 500], '$setOnInsert' => ['_id' => 401, 'createdTag' => 'first']], ['upsert' => true, 'multi' => false]);
    $r1 = $manager->executeBulkWrite($namespace, $first);
    $mixed = new MongoDB\Driver\BulkWrite();
    $mixed->update(['supplier' => 'S1', 'sku' => 'CUP'],
        ['$set' => ['title' => 'Кружка', 'price' => 550], '$setOnInsert' => ['_id' => 401, 'createdTag' => 'second']], ['upsert' => true, 'multi' => false]);
    $mixed->update(['supplier' => 'S1', 'sku' => 'LAMP'],
        ['$set' => ['title' => 'Лампа', 'price' => 900], '$setOnInsert' => ['_id' => 402, 'createdTag' => 'initial']], ['upsert' => true, 'multi' => false]);
    $r2 = $manager->executeBulkWrite($namespace, $mixed);
    $repeat = new MongoDB\Driver\BulkWrite();
    $repeat->update(['supplier' => 'S1', 'sku' => 'CUP'],
        ['$set' => ['title' => 'Кружка', 'price' => 550]], ['upsert' => true, 'multi' => false]);
    $r3 = $manager->executeBulkWrite($namespace, $repeat);
    $rows = [];
    foreach ([['Первый импорт', $r1], ['Смешанный пакет', $r2], ['Повтор без изменений', $r3]] as [$label, $result]) {
        $idLabels = [];
        foreach ($result->getUpsertedIds() as $operationIndex => $id) { $idLabels[] = $operationIndex . ' → ' . $id; }
        $rows[] = [$label, $result->getMatchedCount(), $result->getModifiedCount(), $result->getUpsertedCount(), implode(', ', $idLabels) ?: 'нет'];
    }
    $saved = ma117Read($manager, $namespace, [], ['sort' => ['_id' => 1], 'limit' => 10]);
    $checks = ['first_upsert_id' => $r1->getUpsertedCount() === 1 && $r1->getUpsertedIds() === [0 => 401],
        'mixed_keeps_operation_index' => $r2->getUpsertedIds() === [1 => 402] && $r2->getMatchedCount() === 1 && $r2->getModifiedCount() === 1,
        'repeat_is_not_new_insert' => $r3->getUpsertedCount() === 0 && $r3->getUpsertedIds() === [] && $r3->getMatchedCount() === 1 && $r3->getModifiedCount() === 0,
        'creation_fields_preserved' => count($saved) === 2 && $saved[0]['_id'] === 401 && $saved[0]['price'] === 550 && $saved[0]['createdTag'] === 'first' && $saved[1]['_id'] === 402 && $saved[1]['price'] === 900,
        'all_acknowledged' => $r1->isAcknowledged() && $r2->isAcknowledged() && $r3->isAcknowledged()];
    $details = ['Соответствие операции новому ID' => ['first' => $r1->getUpsertedIds(), 'mixed' => $r2->getUpsertedIds(), 'repeat' => $r3->getUpsertedIds()],
        'Итоговые товары' => $saved, 'Различие счётчиков' => ['first_inserted_count' => $r1->getInsertedCount(), 'first_upserted_count' => $r1->getUpsertedCount()]];
} finally {
    if ($owned) { ma117Command($manager, $database, ['drop' => $collection], true); }
}
$checks['owned_collection_removed'] = !ma117Exists($manager, $database, $collection);
$details = ma117Public($details, $collection, $database);
ma117Report('mongodb117-atomic-upsert-result', ['Запуск', 'Matched', 'Modified', 'Upserted', 'Позиция → новый ID'], $rows, $checks, $runtime, $details);
echo 'mongodb117-atomic-upsert-result: saved result.html and result.json
';
Ожидаемый результат
mongodb117-atomic-upsert-result: saved result.html and result.json

Запустите php example.php и откройте result.html. result.json повторяет таблицу; metrics.json хранит проверки, версии, ответы команд и диагностику. Все файлы для локального сценария и точная инструкция включены в архив.

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

Попробуйте сами. Добавьте во второй пакет операцию 2 для S2/CUP с ID 403. Это другой поставщик, поэтому уникальная пара допустима. getUpsertedIds второго пакета будет [1 => 402, 2 => 403], upserted = 2; операция 0 по-прежнему обновит существующую карточку.

Разобрать частичный импорт: остановка и продолжение после дубликата

Импорт содержит дубликат в середине. Приложение не должно сообщать «ничего не записано», если часть товаров уже появилась, и не должно считать unordered режимом без ошибок. Нужен отчёт: какие операции выполнены, какие отклонены, а какие вообще не были предприняты.

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

Для каждого режима есть собственная группа batch и уже сохранённый SKU BUSY. Очередь содержит A, BUSY, B, BUSY на позициях 0–3. Уникален ключ batch + sku; режимы проверяются на независимых данных в одной собственной коллекции.

ДанныеЗначение
До очередиBUSY уже существует
Операция 0Новый A
Операция 1Повтор BUSY
Операция 2Новый B
Операция 3Ещё один повтор BUSY
Путь данных
  1. ordered: A сохраняется, ошибка позиции 1 останавливает очередь
  2. Позиции 2 и 3 ordered не выполняются
  3. unordered: ошибки 1 и 3 не мешают сохранить A и B
  4. BulkWriteException содержит частичный WriteResult
  5. getIndex сопоставляет каждую ошибку с исходным документом

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

$bulk = new MongoDB\Driver\BulkWrite(['ordered' => false]);
foreach ($input as $document) { $bulk->insert($document); }
try {
    $result = $manager->executeBulkWrite($namespace, $bulk);
} catch (MongoDB\Driver\Exception\BulkWriteException $error) {
    $result = $error->getWriteResult();
    foreach ($result->getWriteErrors() as $writeError) {
        $position = $writeError->getIndex(); // Позиция операции в исходной очереди.
        echo $input[$position]['sku'], ': ', $writeError->getCode(), "\n";
    }
    // Полная программа отдельно отклоняет неожиданные ошибки.
}

Фрагмент использует документы и переменные, подготовленные в полной программе ниже. Он показывает центральное действие; для самостоятельного запуска скачайте весь архив. В коде -> вызывает метод или читает свойство объекта, а => связывает ключ со значением в PHP-массиве.

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

РежимНовых документовОшибочные позицииСохранённые SKUAcknowledged
ordered11A, BUSYда
unordered21, 3A, B, BUSYда

Как это работает

ordered = true выполняет очередь последовательно и прекращает её при первой ошибке записи. Уже успешный insert A не откатывается. Режим unordered задаётся параметром ordered = false: он разрешает продолжение и не обещает порядок исполнения. Оба повтора BUSY конфликтуют с заранее существующей записью, поэтому результат не зависит от того, какую из новых операций сервер выполнит первой. Исключение остаётся исключением: успешные операции и ошибки читаются из его getWriteResult. getWriteErrors содержит WriteError; getIndex — исходная позиция операции, getCode — 11000, getMessage — диагностический текст, getInfo — дополнительные сведения или null. Для удобства отчёта позиции отсортированы после получения, это не утверждение о порядке серверного выполнения.

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

BulkWrite не является транзакцией всей очереди. getWriteErrors описывает ошибки отдельных операций, но не все возможные проблемы соединения или write concern. Полная программа принимает только ожидаемые 11000 и повторно выбрасывает остальные отказы, включая ошибку подтверждения. Не повторяйте весь пакет вслепую после исключения: часть данных уже сохранена. Текст сервера не следует разбирать регулярными выражениями вместо кода ошибки и нельзя без очистки показывать внешнему пользователю — он может содержать значения ключей. Проверка здесь не моделирует аварийное отключение сети и неопределённый результат записи.

Полная самостоятельная программа, helper и архив
Разобрать частичный импорт: остановка и продолжение после дубликатаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/ma117-helper.php';
[$manager, $database] = ma117Environment();
$server = ma117Command($manager, $database, ['buildInfo' => 1])[0]['version'];
$runtime = ['php' => PHP_VERSION, 'extension' => phpversion('mongodb'), 'server' => $server];
$collection = 'mongodb117_atomic_' . bin2hex(random_bytes(8));
$namespace = $database . '.' . $collection;
$owned = false; $details = [];
try {
    if (ma117Exists($manager, $database, $collection)) { throw new RuntimeException('Fixture already exists'); }
    $createOptions = [];
    ma117Command($manager, $database, ['create' => $collection] + $createOptions, true);
    $owned = true;
ma117Command($manager, $database, ['createIndexes' => $collection, 'indexes' => [[
        'name' => 'batch_sku_unique', 'key' => ['batch' => 1, 'sku' => 1], 'unique' => true,
    ]]], true);
    $rows = []; $details = []; $results = [];
    foreach ([['ordered', true, 500], ['unordered', false, 600]] as [$mode, $ordered, $base]) {
        ma117Insert($manager, $namespace, [['_id' => $base, 'batch' => $mode, 'sku' => 'BUSY']]);
        $input = [
            ['_id' => $base + 1, 'batch' => $mode, 'sku' => 'A'],
            ['_id' => $base + 2, 'batch' => $mode, 'sku' => 'BUSY'],
            ['_id' => $base + 3, 'batch' => $mode, 'sku' => 'B'],
            ['_id' => $base + 4, 'batch' => $mode, 'sku' => 'BUSY'],
        ];
        $bulk = new MongoDB\Driver\BulkWrite(['ordered' => $ordered]);
        foreach ($input as $document) { $bulk->insert($document); }
        $caught = false;
        try { $result = $manager->executeBulkWrite($namespace, $bulk); }
        catch (MongoDB\Driver\Exception\BulkWriteException $error) {
            $result = $error->getWriteResult(); $caught = true;
            if ($result->getWriteConcernError() !== null || $result->getWriteErrors() === []) { throw $error; }
            foreach ($result->getWriteErrors() as $writeError) { if ($writeError->getCode() !== 11000) { throw $error; } }
        }
        $positions = []; $errors = [];
        foreach ($result->getWriteErrors() as $writeError) {
            $position = $writeError->getIndex(); $positions[] = $position;
            $errors[] = ['operation_index' => $position, 'input' => $input[$position],
                'code' => $writeError->getCode(), 'message' => $writeError->getMessage(), 'info' => $writeError->getInfo()];
        }
        sort($positions);
        $saved = ma117Read($manager, $namespace, ['batch' => $mode], ['sort' => ['sku' => 1], 'limit' => 10]);
        $skus = array_column($saved, 'sku');
        $rows[] = [$mode, $result->getInsertedCount(), implode(', ', $positions), implode(', ', $skus), $result->isAcknowledged() ? 'да' : 'нет'];
        $results[$mode] = ['caught' => $caught, 'positions' => $positions, 'saved' => $skus, 'ack' => $result->isAcknowledged()];
        $details[$mode] = ['input' => $input, 'errors' => $errors, 'saved' => $saved,
            'not_attempted_indices' => $ordered ? [2,3] : []];
    }
    $checks = ['ordered_stops_after_first_error' => $results['ordered']['caught'] && $results['ordered']['positions'] === [1] && $results['ordered']['saved'] === ['A','BUSY'],
        'unordered_reports_both_errors' => $results['unordered']['caught'] && $results['unordered']['positions'] === [1,3] && $results['unordered']['saved'] === ['A','B','BUSY'],
        'confirmed_partial_success' => $results['ordered']['ack'] && $results['unordered']['ack'] && $rows[0][1] === 1 && $rows[1][1] === 2,
        'all_originals_still_exist' => count(ma117Read($manager, $namespace, ['_id' => ['$in' => [500,600]]], ['limit' => 2])) === 2];
} finally {
    if ($owned) { ma117Command($manager, $database, ['drop' => $collection], true); }
}
$checks['owned_collection_removed'] = !ma117Exists($manager, $database, $collection);
$details = ma117Public($details, $collection, $database);
ma117Report('mongodb117-atomic-partial-bulk', ['Режим', 'Новых документов', 'Ошибочные позиции', 'Сохранённые SKU', 'Acknowledged'], $rows, $checks, $runtime, $details);
echo 'mongodb117-atomic-partial-bulk: saved result.html and result.json
';
Ожидаемый результат
mongodb117-atomic-partial-bulk: saved result.html and result.json

Запустите php example.php и откройте result.html. result.json повторяет таблицу; metrics.json хранит проверки, версии, ответы команд и диагностику. Все файлы для локального сценария и точная инструкция включены в архив.

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

Попробуйте сами. Замените BUSY в операции 1 на C, оставив дубликат в позиции 3. ordered сохранит A, C и B и остановится на 3; unordered тоже сохранит три новых товара и сообщит ошибку 3. В обоих случаях итоговый набор A, B, BUSY, C, но общий порядок unordered всё ещё не гарантирован.

Практический вопрос: BulkWriteError on insert_many when ordered=False. Пользователь ожидает, что unordered уберёт исключение дубликата. Урок показывает продолжение допустимых операций вместе с исключением и частичным результатом. Обсуждение задаёт проблему; технические правила сверены с официальной документацией, пример и данные написаны для этого урока.

Вернуть новый остаток вместе с условным резервированием

После нажатия «Зарезервировать» интерфейсу нужен новый свободный остаток. Отдельная запись, затем отдельный find могут увидеть изменение, сделанное между ними другим запросом. Когда нужен результат именно своей атомарной правки, сервер может вернуть изменённый документ в ответе той же команды.

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

Товар 701 «Лампа»: available = 4, reserved = 0. Сначала запрашиваем две единицы, затем три. Во втором ответе товар отсутствует не потому, что его удалили: условию available ≥ 3 уже не соответствует остаток 2.

ДанныеЗначение
До записиID 701 / доступно 4 / резерв 0
Первый запросКоличество 2
Второй запросКоличество 3
Путь данных
  1. findAndModify получает условие и $inc
  2. Сервер ищет и изменяет один подходящий документ
  3. new = true выбирает образ после изменения
  4. value передаёт нужные поля интерфейсу
  5. Нет совпадения: value = null, а запись не происходит

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

$command = new MongoDB\Driver\Command([
    'findAndModify' => $collection,
    'query' => ['_id' => 701, 'available' => ['$gte' => 2]],
    'update' => ['$inc' => ['available' => -2, 'reserved' => 2]],
    'new' => true, 'upsert' => false,
]);
$reply = $manager->executeReadWriteCommand($database, $command)->toArray()[0];
if ($reply->value !== null) {
    echo $reply->value->available; // 2: документ после именно этой записи.
} else {
    echo 'Условие не подошло';
}

Фрагмент использует документы и переменные, подготовленные в полной программе ниже. Он показывает центральное действие; для самостоятельного запуска скачайте весь архив. В коде -> вызывает метод или читает свойство объекта, а => связывает ключ со значением в PHP-массиве.

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

ЗапрошеноРезультатДоступно в valueРезерв в value
2резерв создан22
3нет подходящего документаNULLNULL

Как это работает

Название findAndModify не означает два отдельных клиентских вызова: команда читает и меняет один документ как единую операцию. query — фильтр, update — изменения, new = true — вернуть образ после записи, fields — проекция ответа. Без new по умолчанию возвращался бы предыдущий образ; в нашем первом запросе available был бы 4 вместо 2. executeReadWriteCommand исполняет такие команды на primary и возвращает Cursor с документом ответа, а не WriteResult. После первого запроса value содержит available = 2, reserved = 2. После второго value равно null; lastErrorObject.n = 0 и ok = 1 различают отсутствие совпадения и ошибку команды. NULL в таблице означает отсутствие value, а не обнуление остатка товара.

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

Возвращённый образ относится к этой операции, но не обещает, что другой запрос никогда не изменит документ сразу после неё. Здесь _id делает выбор однозначным; при выборе среди нескольких кандидатов нужен осознанный порядок. upsert = false запрещает создавать товар из условия дефицита. Этот пример не делает одновременно платёж и изменение нескольких товарных документов. Запросы снова последовательные; таймаут maxTimeMS — ограничение серверной работы, а не полный дедлайн HTTP. Не используйте вывод ok как единственный признак успешного резервирования.

Полная самостоятельная программа, helper и архив
Вернуть новый остаток вместе с условным резервированиемСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/ma117-helper.php';
[$manager, $database] = ma117Environment();
$server = ma117Command($manager, $database, ['buildInfo' => 1])[0]['version'];
$runtime = ['php' => PHP_VERSION, 'extension' => phpversion('mongodb'), 'server' => $server];
$collection = 'mongodb117_atomic_' . bin2hex(random_bytes(8));
$namespace = $database . '.' . $collection;
$owned = false; $details = [];
try {
    if (ma117Exists($manager, $database, $collection)) { throw new RuntimeException('Fixture already exists'); }
    $createOptions = [];
    ma117Command($manager, $database, ['create' => $collection] + $createOptions, true);
    $owned = true;
ma117Insert($manager, $namespace, [['_id' => 701, 'title' => 'Лампа', 'available' => 4, 'reserved' => 0]]);
    $rows = []; $replies = [];
    foreach ([2,3] as $qty) {
        $command = new MongoDB\Driver\Command([
            'findAndModify' => $collection, 'query' => ['_id' => 701, 'available' => ['$gte' => $qty]],
            'update' => ['$inc' => ['available' => -$qty, 'reserved' => $qty]],
            'new' => true, 'upsert' => false, 'maxTimeMS' => 1000,
            'fields' => ['_id' => 1, 'title' => 1, 'available' => 1, 'reserved' => 1],
        ]);
        $cursor = $manager->executeReadWriteCommand($database, $command);
        $cursor->setTypeMap(['root' => 'array', 'document' => 'array', 'array' => 'array']);
        $reply = $cursor->toArray()[0]; $replies[] = $reply;
        $value = $reply['value'];
        $rows[] = [$qty, $value === null ? 'нет подходящего документа' : 'резерв создан', $value['available'] ?? null, $value['reserved'] ?? null];
    }
    $after = ma117Read($manager, $namespace, ['_id' => 701], ['limit' => 1])[0];
    $checks = ['new_value_returned' => $replies[0]['value']['available'] === 2 && $replies[0]['value']['reserved'] === 2 && (int)$replies[0]['lastErrorObject']['n'] === 1,
        'no_match_returns_null' => $replies[1]['value'] === null && (int)$replies[1]['lastErrorObject']['n'] === 0,
        'successful_command_not_business_success' => (float)$replies[1]['ok'] === 1.0,
        'failed_condition_did_not_change_stock' => $after['available'] === 2 && $after['reserved'] === 2];
    $details = ['Ответы команды: value, lastErrorObject, ok' => $replies, 'Итоговый документ' => $after];
} finally {
    if ($owned) { ma117Command($manager, $database, ['drop' => $collection], true); }
}
$checks['owned_collection_removed'] = !ma117Exists($manager, $database, $collection);
$details = ma117Public($details, $collection, $database);
ma117Report('mongodb117-atomic-find-and-modify', ['Запрошено', 'Результат', 'Доступно в value', 'Резерв в value'], $rows, $checks, $runtime, $details);
echo 'mongodb117-atomic-find-and-modify: saved result.html and result.json
';
Ожидаемый результат
mongodb117-atomic-find-and-modify: saved result.html and result.json

Запустите php example.php и откройте result.html. result.json повторяет таблицу; metrics.json хранит проверки, версии, ответы команд и диагностику. Все файлы для локального сценария и точная инструкция включены в архив.

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

Попробуйте сами. Поменяйте new на false только в первом запросе. Ответ покажет available = 4, reserved = 0, но повторное чтение всё равно обнаружит 2 и 2. Это предыдущий образ успешной записи, а не доказательство, что $inc не сработал.

Девять методов: подтверждение, новые ID, ошибки и ответ команды

Каждая карточка содержит полный контракт метода, параметры, результат, ошибки, версии и два применения с переходом к самостоятельной программе и её видимому результату. Обычные счётчики matched/modified и отправка BulkWrite уже объяснены в главе о драйвере.

МетодПрактический смысл
MongoDB\Driver\WriteResult::getUpsertedCountУзнать число новых документов, созданных операциями update с upsert.
MongoDB\Driver\WriteResult::getUpsertedIdsСвязать новые ID с исходными операциями очереди upsert.
MongoDB\Driver\WriteResult::isAcknowledgedПонять, подтверждена ли запись, прежде чем интерпретировать счётчики.
MongoDB\Driver\WriteResult::getWriteErrorsПолучить ошибки конкретных операций и связать их с исходными данными.
MongoDB\Driver\WriteError::getCodeВыбрать обработку известной причины отказа без разбора текста сообщения.
MongoDB\Driver\WriteError::getIndexНайти исходную операцию, отклонённую сервером.
MongoDB\Driver\WriteError::getMessageСохранить понятную разработчику диагностику конкретного отказа записи.
MongoDB\Driver\WriteError::getInfoПрочитать дополнительные структурированные сведения об ошибке, если они доступны.
MongoDB\Driver\Manager::executeReadWriteCommandВыполнить на primary команду, которая читает и изменяет данные, и получить её ответ.

Связать правило данных с интерфейсом сервиса

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

Продолжение по данным: BSON, время, деньги и внешние значения. По скорости поиска и ограничениям: индексы и уникальность. По итогам на сервере: aggregation и отчёты. Сравнение с реляционным подходом: конкурентные изменения и транзакции MySQL. Полные транзакции MongoDB и идемпотентность HTTP — самостоятельные темы, здесь они не объявляются реализованными.

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

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

MongoDB\Driver\WriteResult::getUpsertedCountУзнать число новых документов, созданных операциями update с upsert.

mongodb 1.0.0+ · mongodb

final public MongoDB\Driver\WriteResult::getUpsertedCount(): int

Узнать число новых документов, созданных операциями update с upsert.

final public MongoDB\Driver\WriteResult::getUpsertedCount(): int

Параметры

Параметров нет. Вызывайте метод у объекта соответствующего класса, полученного от драйвера.

Возвращаемое значение

int: количество документов, вставленных через upsert. Обычные insert отражаются в getInsertedCount; существующие совпадения — в getMatchedCount. Значение 0 при подтверждённом результате означает отсутствие upsert-вставок, а не обязательно отсутствие изменений.

Ошибки и ограничения

LogicException, если запись не подтверждена: доступного счётчика нет. Документирован InvalidArgumentException при ошибке разбора аргументов. Метод не принимает параметров; его вызывают у уже полученного объекта, а не через new.

Версии

Метод существует с mongodb 1.0.0. С mongodb 2.0.0 для неподтверждённой записи выбрасывается LogicException; ранее возвращался null. В этой главе ext-mongodb 2.5+ и w = 1.

Посчитать новые карточки в синхронизации

echo $r1->getUpsertedCount(); // 1: создан CUP.
echo $r2->getUpsertedCount(); // 1: создан только LAMP.
echo $r3->getUpsertedCount(); // 0: повтор CUP.

Изменённая цена CUP не увеличивает число новых карточек. Счётчик пригоден для отчёта «добавлено / изменено», если отдельно учитывать modified.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

Проверить запрет создания товара при резерве

if ($result->getUpsertedCount() !== 0) {
    throw new RuntimeException('Reserve unexpectedly inserted a product');
}

Оба условных обновления дают 0: в режиме upsert = false дефицит не создаёт новый товар. Дополнительная проверка итогового документа подтверждает бизнес-состояние.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

MongoDB\Driver\WriteResult::getUpsertedIdsСвязать новые ID с исходными операциями очереди upsert.

mongodb 1.0.0+ · mongodb

final public MongoDB\Driver\WriteResult::getUpsertedIds(): array

Связать новые ID с исходными операциями очереди upsert.

final public MongoDB\Driver\WriteResult::getUpsertedIds(): array

Параметры

Параметров нет. Вызывайте метод у объекта соответствующего класса, полученного от драйвера.

Возвращаемое значение

Массив ID документов, вставленных через upsert; ключ каждого элемента — позиция соответствующей операции в исходном BulkWrite. Значение имеет тип самого _id, например int или MongoDB\BSON\ObjectId. Это не список всех обновлённых ID и не обязательная непрерывная нумерация с нуля.

Ошибки и ограничения

Документирован InvalidArgumentException при ошибке разбора аргументов. Метод не принимает параметров; его вызывают у уже полученного объекта, а не через new.

Версии

Доступен с mongodb 1.0.0. В отличие от счётчиков, страница этого метода не документирует LogicException для неподтверждённой записи. Не делайте вывод «не было вставок» только по пустому массиву без подтверждения результата.

При первом импорте getUpsertedIds возвращает [0 => 401], а повтор без изменений возвращает []. При JSON-выводе массив с единственным ключом 1 выглядит как объект с ключом "1" — это сохранённая карта позиций, а не потерянная строка.

Привязать новый товар к строке смешанного импорта

foreach ($r2->getUpsertedIds() as $operationIndex => $id) {
    echo $operationIndex, ' → ', $id; // 1 → 402.
}

Сохраняется ключ 1: операция 0 обновила CUP, операция 1 создала LAMP. array_values перенумеровал бы этот массив и связал ID с неверной строкой.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

Убедиться, что ответ резерва не содержит созданных карточек

if ($result->getUpsertedIds() !== []) {
    throw new RuntimeException('Unexpected new product ID in reserve result');
}

При обоих подтверждённых резервированиях массив пуст. Такой ответ не нужно ошибочно считать отсутствием найденного товара: первый запрос matched = 1.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

MongoDB\Driver\WriteResult::isAcknowledgedПонять, подтверждена ли запись, прежде чем интерпретировать счётчики.

mongodb 1.0.0+ · mongodb

final public MongoDB\Driver\WriteResult::isAcknowledged(): bool

Понять, подтверждена ли запись, прежде чем интерпретировать счётчики.

final public MongoDB\Driver\WriteResult::isAcknowledged(): bool

Параметры

Параметров нет. Вызывайте метод у объекта соответствующего класса, полученного от драйвера.

Возвращаемое значение

true, если запись подтверждена; false для неподтверждённой записи. При подтверждении доступны показатели результата. true не означает, что каждая операция очереди принята или что фильтр совпал с документом.

Ошибки и ограничения

Документирован InvalidArgumentException при ошибке разбора аргументов. Метод не принимает параметров; его вызывают у уже полученного объекта, а не через new.

Версии

Доступен с mongodb 1.0.0. Здесь w = 1; поведение w = 0 справочно объясняется, но пример не переключает сервер или клиент в неподтверждённый режим.

Неизвестный результат из-за сетевого сбоя нельзя превращать в «вставлено 0». Проверка isAcknowledged применима, когда объект WriteResult уже получен; многие исключения соединения его вообще не дают.

Отделить подтверждение запроса от нехватки остатка

if (!$result->isAcknowledged()) {
    throw new RuntimeException('No acknowledged reserve result');
}
echo $result->getMatchedCount();

Q2 подтверждён, но matched = 0: сервер обработал команду, а товар с достаточным available не найден.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

Учесть подтверждённый частичный импорт после исключения

$result = $error->getWriteResult();
if ($result->isAcknowledged()) {
    echo $result->getInsertedCount();
}

В ordered подтверждена одна вставка, в unordered — две. true сосуществует с ошибкой 11000, потому что подтверждение не отменяет ошибок отдельных операций.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

MongoDB\Driver\WriteResult::getWriteErrorsПолучить ошибки конкретных операций и связать их с исходными данными.

mongodb 1.0.0+ · mongodb

final public MongoDB\Driver\WriteResult::getWriteErrors(): array

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

final public MongoDB\Driver\WriteResult::getWriteErrors(): array

Параметры

Параметров нет. Вызывайте метод у объекта соответствующего класса, полученного от драйвера.

Возвращаемое значение

Массив объектов MongoDB\Driver\WriteError; пустой массив, если ошибок записи отдельных операций нет. Положение элемента в этом массиве не следует считать позицией операции: для неё есть getIndex.

Ошибки и ограничения

Документирован InvalidArgumentException при ошибке разбора аргументов. Метод не принимает параметров; его вызывают у уже полученного объекта, а не через new.

Версии

Доступен с mongodb 1.0.0. Документация не заявляет LogicException для неподтверждённого результата. Используйте подтверждённый ответ и не подменяйте этим методом обработку других исключений.

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

Получить два отдельных отказа схемы

foreach ($result->getWriteErrors() as $writeError) {
    echo $writeError->getIndex(), ': ', $writeError->getCode(), "\n";
}

Валидация отклонила позиции 1 и 2 с кодом 121; валидная позиция 0 уже сохранена.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

Сравнить остановленный и продолжающийся импорт

$result = $error->getWriteResult();
$errors = $result->getWriteErrors();
foreach ($errors as $writeError) {
    $failedDocument = $input[$writeError->getIndex()];
}

ordered возвращает ошибку позиции 1, unordered — позиций 1 и 3. В ordered позиции 2 и 3 не выполнялись, поэтому они не присутствуют как ошибки.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

MongoDB\Driver\WriteError::getCodeВыбрать обработку известной причины отказа без разбора текста сообщения.

mongodb 1.0.0+ · mongodb

final public MongoDB\Driver\WriteError::getCode(): int

Выбрать обработку известной причины отказа без разбора текста сообщения.

final public MongoDB\Driver\WriteError::getCode(): int

Параметры

Параметров нет. Вызывайте метод у объекта соответствующего класса, полученного от драйвера.

Возвращаемое значение

Целочисленный код ошибки одной операции записи, переданный сервером.

Ошибки и ограничения

Документирован InvalidArgumentException при ошибке разбора аргументов. Метод не принимает параметров; его вызывают у уже полученного объекта, а не через new.

Версии

Доступен с mongodb 1.0.0. Набор серверных кодов и условия их появления относятся к MongoDB; расширение возвращает код, а не переводит его в прикладной статус формы.

Принять только ожидаемый отказ схемы

foreach ($result->getWriteErrors() as $writeError) {
    if ($writeError->getCode() !== 121) { throw $error; }
}

121 означает, что запись не прошла проверку документа. Другой код не выдаётся за обычную ошибку поля: исходное исключение повторно выбрасывается.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

Отделить duplicate key от остальных сбоев импорта

foreach ($result->getWriteErrors() as $writeError) {
    if ($writeError->getCode() !== 11000) { throw $error; }
}

В очереди ожидаются только дубликаты BUSY с кодом 11000. Код не говорит сам по себе, какой именно ключ конфликтует; для связи с данными нужны getIndex и знание собственного индекса.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

MongoDB\Driver\WriteError::getIndexНайти исходную операцию, отклонённую сервером.

mongodb 1.0.0+ · mongodb

final public MongoDB\Driver\WriteError::getIndex(): int

Найти исходную операцию, отклонённую сервером.

final public MongoDB\Driver\WriteError::getIndex(): int

Параметры

Параметров нет. Вызывайте метод у объекта соответствующего класса, полученного от драйвера.

Возвращаемое значение

Индекс операции в исходном BulkWrite, которая вызвала эту ошибку. Отсчёт начинается с нуля. Это не _id документа, не номер строки файла с заголовком и не позиция элемента в getWriteErrors.

Ошибки и ограничения

Документирован InvalidArgumentException при ошибке разбора аргументов. Метод не принимает параметров; его вызывают у уже полученного объекта, а не через new.

Версии

Доступен с mongodb 1.0.0. Даже если драйвер разбивает очередь на серверные пакеты, потребитель использует индекс исходной очереди, возвращённый этим методом.

Подсветить исходную карточку с неверным типом

$position = $writeError->getIndex();
$sku = $input[$position]['sku'];

Позиция 1 указывает на LAMP со строковым stock; позиция 2 — на PLATE с лишним discount. Нумерацию для отображения человеку можно увеличить на 1, сохранив исходный индекс для массива.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

Сформировать список исправляемых строк unordered-импорта

$position = $writeError->getIndex();
$failedDocument = $input[$position];

Ошибки принадлежат операциям 1 и 3. У второй ошибки позиция в массиве ошибок может быть 1, но её исходная операция — 3; смешение этих двух индексов подсветило бы не тот товар.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

MongoDB\Driver\WriteError::getMessageСохранить понятную разработчику диагностику конкретного отказа записи.

mongodb 1.0.0+ · mongodb

final public MongoDB\Driver\WriteError::getMessage(): string

Сохранить понятную разработчику диагностику конкретного отказа записи.

final public MongoDB\Driver\WriteError::getMessage(): string

Параметры

Параметров нет. Вызывайте метод у объекта соответствующего класса, полученного от драйвера.

Возвращаемое значение

Текст ошибки записи. Он предназначен для диагностики и не является стабильным машинным контрактом для ветвления бизнес-логики.

Ошибки и ограничения

Документирован InvalidArgumentException при ошибке разбора аргументов. Метод не принимает параметров; его вызывают у уже полученного объекта, а не через new.

Версии

Доступен с mongodb 1.0.0. Формулировка зависит от сервера и причины; примеры проверяют код и индекс, а не точное совпадение английской строки.

В рабочем сервисе сообщение может содержать namespace и значения ключей. Сохранение в защищённом журнале и текст для посетителя — разные задачи. Не выводите серверную строку как HTML без htmlspecialchars.

Добавить серверное пояснение к протоколу отклонённой карточки

$diagnostic = [
    'sku' => $input[$writeError->getIndex()]['sku'],
    'message' => $writeError->getMessage(),
];

Диагностика связана с конкретной исходной карточкой. Структурированные подробности дополнительно читаются через getInfo; краткий текст сам не обязан перечислить все поля.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

Объяснить разработчику конфликт уникального ключа

$diagnostic = [
    'code' => $writeError->getCode(),
    'message' => $writeError->getMessage(),
];

Отчёт сохраняет фактическое сообщение о duplicate key. Публичный HTML экранирует текст и заменяет случайное имя учебной коллекции; прикладное решение принимается по 11000.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

MongoDB\Driver\WriteError::getInfoПрочитать дополнительные структурированные сведения об ошибке, если они доступны.

mongodb 1.0.0+ · mongodb

final public MongoDB\Driver\WriteError::getInfo(): ?object

Прочитать дополнительные структурированные сведения об ошибке, если они доступны.

final public MongoDB\Driver\WriteError::getInfo(): ?object

Параметры

Параметров нет. Вызывайте метод у объекта соответствующего класса, полученного от драйвера.

Возвращаемое значение

Объект дополнительных метаданных ошибки либо null, если их нет. Форма содержимого зависит от серверной ошибки; это не обязательный фиксированный массив с одним набором ключей.

Ошибки и ограничения

Документирован InvalidArgumentException при ошибке разбора аргументов. Метод не принимает параметров; его вызывают у уже полученного объекта, а не через new.

Версии

Доступен с mongodb 1.0.0. Подробности серверной валидации могут зависеть от версии MongoDB; контракт метода допускает отсутствие сведений.

Метод возвращает объект, не уже готовую строку JSON. Для отчёта helper рекурсивно обрабатывает stdClass и массивы, затем сериализует их. Для работы с данными базы такое преобразование не требуется.

Исследовать, какое правило схемы нарушено

$info = $writeError->getInfo();
if ($info !== null) {
    $diagnostic['info'] = $info;
}

Отказы LAMP и PLATE имеют разные исходные причины: тип stock и неизвестное поле. Пример сохраняет реальный объект подробностей, но не требует неизменной структуры вложенных полей.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

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

$info = $writeError->getInfo();
$diagnostic = ['code' => $writeError->getCode(), 'info' => $info];

Для duplicate key сведения могут отсутствовать. null остаётся допустимым в JSON/HTML-диагностике; сопоставление с исходной операцией продолжается через getIndex и код 11000.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

MongoDB\Driver\Manager::executeReadWriteCommandВыполнить на primary команду, которая читает и изменяет данные, и получить её ответ.

mongodb 1.4.0+ · mongodb

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

Выполнить на primary команду, которая читает и изменяет данные, и получить её ответ.

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

Параметры

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

Возвращаемое значение

MongoDB\Driver\Cursor с ответом команды. Это не WriteResult: структура документа определяется командой. findAndModify возвращает value, lastErrorObject и ok; отсутствие подходящего документа может быть успешным ответом с value = null.

Ошибки и ограничения

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

Версии

Метод появился в mongodb 1.4.0. С 1.4.4 комбинация session с неподтверждённой записью вызывает InvalidArgumentException. Этот пакет использует ext-mongodb 2.5+ и самостоятельный MongoDB 8.0; многодокументные транзакции здесь не запускаются.

Метод выбирает primary; настройки чтения secondary не превращают его в команду только для чтения. Значения new, query, update, fields и upsert относятся к документу findAndModify, а не к $options метода. Параметры драйвера и параметры серверной команды — разные уровни.

Сохранить форму с ожидаемой версией и вернуть новую карточку

$command = new MongoDB\Driver\Command([
    'findAndModify' => $collection,
    'query' => ['_id' => 301, 'version' => 7],
    'update' => ['$set' => ['price' => 550], '$inc' => ['version' => 1]],
    'new' => true, 'upsert' => false,
]);
$reply = $manager->executeReadWriteCommand($database, $command)->toArray()[0];

Первый ответ содержит цену 550 и версию 8. Старая форма с версией 7 получает value = null, а не исключение о конфликте.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

Получить остаток после своего резервирования

$command = new MongoDB\Driver\Command([
    'findAndModify' => $collection,
    'query' => ['_id' => 701, 'available' => ['$gte' => 2]],
    'update' => ['$inc' => ['available' => -2, 'reserved' => 2]],
    'new' => true, 'upsert' => false,
]);
$reply = $manager->executeReadWriteCommand($database, $command)->toArray()[0];

value возвращает available = 2, reserved = 2 в ответе той же операции. Отдельного find для получения этого образа не требуется.

Разобрать весь сценарий и исходные данные · Посмотреть фактический результат

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

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

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

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

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