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

Дерево руководства
Глава 184 · После первой выборки MongoDB и знакомства с массивами
Приёмка поставок. Как проверить форму BSON-накладной и сверить остаток
Получаем пять накладных, находим лишние две единицы после $unwind, проверяем позиции и выясняем, почему физический остаток ламп отличается от ожидаемого.
Сколько строк в накладной и сколько единиц на полке
Вы приняли две лампы LED и один кабель CABLE. В приходной накладной две строки, на полке три единицы, а отчёт вдруг насчитал пять. На одной и той же партии разберём, как такое получается. Сначала прочитаем саму накладную, затем найдём конкретный лишний документ, исправим выборку и сверим результат с полкой.
$shipment = MongoDB\BSON\Document::fromPHP([
'_id' => 'A',
'items' => [
['sku'=>'LED', 'qty'=>2],
['sku'=>'CABLE', 'qty'=>1],
],
]);
echo $shipment->get('items')->get(0)->get('sku'); // LED- Документ A содержит именованное поле items
- Значение items — список двух позиций
- Позиция 0 — документ с полями sku и qty
- sku содержит LED, qty содержит 2
- Позиция 1 даёт CABLE и 1
BSON — формат документов, которым пользуется MongoDB. В этом примере Document хранит именованные поля, PackedArray хранит упорядоченные значения, а Iterator обходит один контейнер. Цепочка get сначала выбирает поле по имени, потом позицию по номеру, потом поле выбранной строки. Если вы только осваиваете эти конструкции, откройте обычные массивы PHP и первую выборку для экрана.
В восьми программах исходные данные записываются в отдельную учебную коллекцию и считываются обратно. Для первой достаточно знакомой правильной накладной. В следующих программах исходные формы намеренно различаются. Каждую можно скачать и запустить отдельно; в терминале сразу выводится полученная ведомость.
Прочитать первую поставку и напечатать приёмную ведомость
На столе у кладовщика приходная накладная A. По ней на полку поступают две лампы LED и один кабель. Нужно распечатать перечень позиций и количество физических единиц. У одного документа могут быть сразу оба числа: две строки, три единицы.
- Прочитать из базы фактическую накладную A
- Взять поле items, в нём список позиций
- У каждой строки прочитать sku и qty
- Вывести номер позиции и сложить количества
С чего начать
$items = $shipment->get('items');
$first = $items->get(0);
echo $first->get('sku'), ' × ', $first->get('qty'); // LED × 2Что покажет пример
| Что проверили | Результат |
|---|---|
| Позиция 1 | LED × 2 |
| Позиция 2 | CABLE × 1 |
| На склад, единиц | 3 |
Как прочитать результат
Document обращается к именованному полю, PackedArray — к индексу списка. В складской ведомости позиции удобнее нумеровать с 1, поэтому к индексу прибавляется единица. Перебор foreach отдаёт ключ позиции и вложенный документ. Количество складываем из qty, а не из номеров.
Это минимальный случай известной правильной накладной. Получение первой строки через get(0) требует, чтобы список действительно содержал нулевую позицию. Как отличить остальные формы, показывает следующий пример.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\{Document,PackedArray};
use MongoDB\Driver\{Manager,BulkWrite};
require __DIR__.'/s136-helper.php';
[$rows,$checks,$runtime,$counts]=s136Lab(static function(Manager $manager,string $db,string $ns): array {
$written=s136Write($manager,$ns,[s136Manifests()[0]]);
$shipment=s136Read($manager,$ns,['_id'=>'A'])[0];
$items=$shipment->get('items');
$iterator=$items->getIterator();
$rows=[];
$total=0;
foreach ($iterator as $index=>$line) {
$qty=$line->get('qty');
$rows[]=['Позиция '.($index+1),$line->get('sku').' × '.$qty];
$total+=$qty;
}
$rows[]=['На склад, единиц',$total];
$checks=['stored_one'=>$written===1,'raw_document'=>$shipment instanceof Document,'list_container'=>$items instanceof PackedArray,
'bson_iterator'=>$iterator instanceof MongoDB\BSON\Iterator,'two_positions'=>count($rows)-1===2,
'second_sku'=>$items->get(1)->get('sku')==='CABLE','unit_total'=>$total===3];
return [$rows,$checks];
});
s136Report('mongodb136-shape-first-receipt', 'Прочитать первую поставку и напечатать приёмную ведомость', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
Позиция 1 — LED × 2
Позиция 2 — CABLE × 1
На склад, единиц — 3
Самостоятельный запуск с реальной MongoDB. Таблица строится по сохранённым и заново прочитанным данным.
Открыть ведомость запуска · Скачать JSON ведомости
Попробуйте самостоятельно. Поменяйте qty для CABLE с 1 на 4. Сколько позиций и сколько физических единиц появятся в ведомости?
Разнести пять входящих накладных по решению приёмщика
Пять поставщиков прислали поле items по-разному. Отдел закупок должен отправить точный запрос: вернуть неверную структуру, попросить прислать отсутствующие строки или уточнить явно пустое значение. Общая надпись «нет товаров» скроет причину.
- Сохранить A–E и прочитать их по порядку
- Сначала установить наличие ключа items
- Для присутствующего значения различить null, объект и список
- У списка проверить наличие первой позиции
С чего начать
if (!$shipment->has('items')) {
return 'Запросить состав';
}
$items = $shipment->get('items');
if ($items instanceof PackedArray && !$items->has(0)) {
return 'Поставщик прислал пустой список';
}Что покажет пример
| Что проверили | Результат |
|---|---|
| A | список → проверить строки |
| B | пустой список → нет позиций |
| C | объект → вернуть формат |
| D | поле отсутствует → запросить состав |
| E | явный null → уточнить у поставщика |
Как прочитать результат
Проверка has отвечает на вопрос о поле. Проверка instanceof различает контейнеры, а has(0) выявляет пустой правильный список. Решение «проверить строки» ещё не меняет складской остаток.
Политика приёмки задаётся договором с поставщиком. В другом интерфейсе пустой список может означать подтверждённый нулевой приход. В этой задаче он отображается отдельно, и оператор видит, почему начислено ноль.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\{Document,PackedArray};
use MongoDB\Driver\{Manager,BulkWrite};
require __DIR__.'/s136-helper.php';
[$rows,$checks,$runtime,$counts]=s136Lab(static function(Manager $manager,string $db,string $ns): array {
$written=s136Write($manager,$ns,s136Manifests());
$orders=s136Read($manager,$ns,[]);
$decisions=['список'=>'проверить строки','пустой список'=>'нет позиций','объект'=>'вернуть формат',
'поле отсутствует'=>'запросить состав','явный null'=>'уточнить у поставщика'];
$rows=[];$shapes=[];
foreach ($orders as $order) {
$id=$order->get('_id');
$shape=s136Shape($order);
$shapes[$id]=$shape;
$rows[]=[$id,$shape.' → '.$decisions[$shape]];
}
$checks=['written'=>$written===5,'read_all'=>count($orders)===5,'correct_list'=>$shapes['A']==='список',
'empty_list'=>$shapes['B']==='пустой список','object_not_list'=>$shapes['C']==='объект',
'missing_key'=>$shapes['D']==='поле отсутствует','explicit_null'=>$shapes['E']==='явный null'];
return [$rows,$checks];
});
s136Report('mongodb136-shape-intake-board', 'Разнести пять входящих накладных по решению приёмщика', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
A — список → проверить строки
B — пустой список → нет позиций
C — объект → вернуть формат
D — поле отсутствует → запросить состав
E — явный null → уточнить у поставщика
Самостоятельный запуск с реальной MongoDB. Таблица строится по сохранённым и заново прочитанным данным.
Открыть ведомость запуска · Скачать JSON ведомости
Попробуйте самостоятельно. Откройте JSON исходных накладных в s136-helper.php. Сначала угадайте решения для D и E, потом сопоставьте с результатом программы.
Найти источник лишних двух единиц после разворачивания списка
По первой накладной на полку поступило три единицы. Отчёт поставщика показывает пять. Выведем перед суммированием каждую строку, исходный номер накладной и индекс элемента.
- Развернуть items через $unwind
- Присоединить исходный индекс позиции к результату
- Найти лишнюю строку C без индексного номера
- Сверить тип items в C с исходной записью
С чего начать
['$unwind' => ['path'=>'$items', 'includeArrayIndex'=>'pos']]Что покажет пример
| Что проверили | Результат |
|---|---|
| A · индекс 0 | LED × 2 |
| A · индекс 1 | CABLE × 1 |
| C · без индекса | LED × 2 |
| Ошибочный приход | 5 |
Как прочитать результат
$unwind создаёт по документу на элемент массива. Если путь содержит существующий объект вместо массива, данный объект проходит как одно значение. В примере у него pos = null; исходный Document подтверждает ошибку формы. Сумма 2 + 1 + 2 сама по себе арифметически верна.
Значение pos = null пригодно как след для расследования. Для расчёта явно проверяйте тип контейнера до разворачивания. При включении preserveNullAndEmptyArrays дополнительные сохранённые строки тоже могут иметь пустой индекс.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\{Document,PackedArray};
use MongoDB\Driver\{Manager,BulkWrite};
require __DIR__.'/s136-helper.php';
[$rows,$checks,$runtime,$counts]=s136Lab(static function(Manager $manager,string $db,string $ns): array {
$written=s136Write($manager,$ns,s136Manifests());
$lines=s136Aggregate($manager,$db,$ns,[
['$unwind'=>['path'=>'$items','includeArrayIndex'=>'pos']],
['$sort'=>['_id'=>1,'pos'=>1]],['$project'=>['sku'=>'$items.sku','qty'=>'$items.qty','pos'=>['$toInt'=>'$pos']]],
]);
$rows=[];$total=0;
foreach ($lines as $line) {
$position=$line['pos']===null ? 'без индекса' : 'индекс '.$line['pos'];
$rows[]=[$line['_id'].' · '.$position,$line['sku'].' × '.$line['qty']];
$total+=$line['qty'];
}
$rows[]=['Ошибочный приход',$total];
$source=s136Read($manager,$ns,['_id'=>'C'])[0];
$checks=['five_sources'=>$written===5,'unwound_three'=>count($lines)===3,'ids'=>array_column($lines,'_id')===['A','A','C'],
'positions'=>array_column($lines,'pos')===[0,1,null],'extra_quantity'=>$lines[2]['qty']===2,'sum_five'=>$total===5,
'source_object'=>$source->get('items') instanceof Document];
return [$rows,$checks];
});
s136Report('mongodb136-shape-unwind-surplus', 'Найти источник лишних двух единиц после разворачивания списка', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
A · индекс 0 — LED × 2
A · индекс 1 — CABLE × 1
C · без индекса — LED × 2
Ошибочный приход — 5
Самостоятельный запуск с реальной MongoDB. Таблица строится по сохранённым и заново прочитанным данным.
Открыть ведомость запуска · Скачать JSON ведомости
Попробуйте самостоятельно. Найдите, у какой накладной изменилась бы сумма, если бы ошибочный объект C имел qty равное 10. Какую строку можно было бы показать поставщику?
Разработчик ожидал изменения от $unwind и увидел прежнюю структуру. Отдельная выгрузка исходного типа и индекса позволяет увидеть, какой именно уровень был передан оператору. Unable to unwind existing list of Documents within a Document на Stack Overflow.
Получить на сервере 3 единицы и не считать ошибочный контейнер
Те же пять документов нужны для краткого отчёта о принятых единицах. В интерфейс достаточно отправить результат по накладной. Пусть MongoDB отберёт массивы до разворачивания и посчитает сумму.
- Оставить A и B, у которых items имеет тип массива
- Развернуть позиции, у пустой B не появится строк
- Сгруппировать по исходному _id
- Передать в PHP только A, число строк и сумму
С чего начать
[
['$match'=>['$expr'=>['$isArray'=>'$items']]],
['$unwind'=>'$items'],
['$group'=>['_id'=>'$_id', 'units'=>['$sum'=>'$items.qty']]],
]Что покажет пример
| Что проверили | Результат |
|---|---|
| Список присутствует | A, B |
| Вклад в сумму | A |
| Строк накладной | 2 |
| Единиц к приёмке | 3 |
Как прочитать результат
$match фильтрует поступающие документы. Внутри $expr выражение $isArray проверяет тип самого items. $group считает исходные позиции и общее количество отдельно. Два контейнера подошли под условие; только один дал реальные строки.
На этих пяти накладных все позиции внутри настоящих массивов корректны. $isArray проверяет только контейнер; сумма не заменит проверку артикула, знака количества или содержимого каждой строки.
Если дополнительно нужен безопасный подсчёт числа позиций при любом типе поля, перейдите к реестру всех накладных.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\{Document,PackedArray};
use MongoDB\Driver\{Manager,BulkWrite};
require __DIR__.'/s136-helper.php';
[$rows,$checks,$runtime,$counts]=s136Lab(static function(Manager $manager,string $db,string $ns): array {
$written=s136Write($manager,$ns,s136Manifests());
$pipeline=[
['$match'=>['$expr'=>['$isArray'=>'$items']]],
['$unwind'=>'$items'],
['$group'=>['_id'=>'$_id','lines'=>['$sum'=>1],'units'=>['$sum'=>'$items.qty']]],
['$sort'=>['_id'=>1]],
];
$report=s136Aggregate($manager,$db,$ns,$pipeline);
$accepted=$report[0];
$qualified=s136Aggregate($manager,$db,$ns,[['$match'=>['$expr'=>['$isArray'=>'$items']]],['$sort'=>['_id'=>1]],['$project'=>['_id'=>1]]]);
$shapeMatch=implode(', ',array_column($qualified,'_id'));
$rows=[['Список присутствует',$shapeMatch],['Вклад в сумму',$accepted['_id']],['Строк накладной',$accepted['lines']],['Единиц к приёмке',$accepted['units']]];
$checks=['written'=>$written===5,'two_arrays'=>count($qualified)===2,'right_shapes'=>$shapeMatch==='A, B',
'one_contributor'=>count($report)===1 && $accepted['_id']==='A','two_lines'=>$accepted['lines']===2,
'three_units'=>$accepted['units']===3,'no_wrong_object'=>!in_array('C',array_column($report,'_id'),true)];
return [$rows,$checks];
});
s136Report('mongodb136-shape-guarded-total', 'Получить на сервере 3 единицы и не считать ошибочный контейнер', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
Список присутствует — A, B
Вклад в сумму — A
Строк накладной — 2
Единиц к приёмке — 3
Самостоятельный запуск с реальной MongoDB. Таблица строится по сохранённым и заново прочитанным данным.
Открыть ведомость запуска · Скачать JSON ведомости
Попробуйте самостоятельно. Добавьте ещё одну правильную накладную с одной позицией LED × 4. Проверьте две отдельные строки по _id, прежде чем вычислять общий приход.
Наличие ключа не гарантирует, что $size получает массив. В реестре ниже вычисление длины находится только в массивной ветви условия. MongoDB query for array size > N fails, even with $exists check на Stack Overflow.
Показать отделу закупок накладные, пропавшие из отчёта
Кладовщик увидел одну накладную в отчёте по действительным позициям. Отдел закупок ждёт журнал всех пяти поступлений, включая пустое и незаполненное. Два экрана требуют разных запросов.
- Запомнить исходный тип items и длину только настоящего списка
- Сравнить номера после обычного и сохраняющего $unwind
- Свернуть строки обратно по номеру накладной
- Вывести исходную причину для B, D и E
С чего начать
['$set'=>[
'sourceType'=>['$type'=>'$items'],
'sourceCount'=>['$cond'=>[['$isArray'=>'$items'],['$size'=>'$items'],null]]
]]Что покажет пример
| Что проверили | Результат |
|---|---|
| Номера после обычного unwind | A, C |
| В контрольном журнале | A, B, C, D, E |
| B до разворачивания | array · 0 позиций |
| D до разворачивания | missing |
| E до разворачивания | null |
Как прочитать результат
preserveNullAndEmptyArrays сохраняет исходный документ при отсутствии строк. Присутствие такого документа ещё не означает, что товар поступил. $type вызывается до разворачивания и возвращает missing для отсутствующего поля. $size размещён только в подходящей ветви $cond, поэтому к объекту или null он не применяется.
Простой подсчёт строк после сохраняющего $unwind ошибочно начислит хотя бы по одной позиции каждой пустой поставке. Для реестра используйте исходный ID и состояние поля, для количественного отчёта — только проверенные записи.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\{Document,PackedArray};
use MongoDB\Driver\{Manager,BulkWrite};
require __DIR__.'/s136-helper.php';
[$rows,$checks,$runtime,$counts]=s136Lab(static function(Manager $manager,string $db,string $ns): array {
$written=s136Write($manager,$ns,s136Manifests());
$classify=['$set'=>['sourceType'=>['$type'=>'$items'],'sourceCount'=>[
'$cond'=>[['$isArray'=>'$items'],['$size'=>'$items'],null],
]]];
$before=s136Aggregate($manager,$db,$ns,[$classify,['$unwind'=>'$items'],['$group'=>['_id'=>'$_id']],['$sort'=>['_id'=>1]]]);
$after=s136Aggregate($manager,$db,$ns,[$classify,['$unwind'=>['path'=>'$items','preserveNullAndEmptyArrays'=>true]],
['$group'=>['_id'=>'$_id','inputType'=>['$first'=>'$sourceType'],'itemCount'=>['$first'=>'$sourceCount']]],
['$sort'=>['_id'=>1]]]);
$shown=implode(', ',array_column($before,'_id'));
$accounted=implode(', ',array_column($after,'_id'));
$by=array_column($after,null,'_id');
$rows=[['Номера после обычного unwind',$shown],['В контрольном журнале',$accounted],['B до разворачивания',$by['B']['inputType'].' · '.$by['B']['itemCount'].' позиций'],
['D до разворачивания',$by['D']['inputType']],['E до разворачивания',$by['E']['inputType']]];
$checks=['written'=>$written===5,'normal_ids'=>$shown==='A, C','all_accounted'=>$accounted==='A, B, C, D, E',
'empty_identified'=>$by['B']['inputType']==='array' && $by['B']['itemCount']===0,'missing_identified'=>$by['D']['inputType']==='missing',
'null_identified'=>$by['E']['inputType']==='null','object_identified'=>$by['C']['inputType']==='object'];
return [$rows,$checks];
});
s136Report('mongodb136-shape-preserve-shipments', 'Показать отделу закупок накладные, пропавшие из отчёта', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
Номера после обычного unwind — A, C
В контрольном журнале — A, B, C, D, E
B до разворачивания — array · 0 позиций
D до разворачивания — missing
E до разворачивания — null
Самостоятельный запуск с реальной MongoDB. Таблица строится по сохранённым и заново прочитанным данным.
Открыть ведомость запуска · Скачать JSON ведомости
Попробуйте самостоятельно. Временно уберите preserveNullAndEmptyArrays из конвейера журнала. Какие три номера исчезнут и почему длина показанной строки сама по себе не доказывает поставку?
После разворачивания пустые элементы исчезали из отчёта. Сохраняем классификацию до $unwind и проверяем число исходных накладных после группировки. MongoDB aggregation with unwind on empty array на Stack Overflow.
Отклонить накладную целиком и показать номера испорченных строк
У накладной F правильный список items. Однако второй элемент — строка вместо документа, третий содержит отрицательное количество. Отдел закупок принимает поставку только целиком. Надо сообщить об обеих проблемах за одну проверку.
- Заново получить F из MongoDB
- Для каждого индекса проверить тип строки
- Проверить разрешённый артикул и целое количество от 1 до 1000
- При любой ошибке вернуть причины и не менять остатки
С чего начать
foreach ($items as $index => $line) {
if (!$line instanceof Document) {
$errors[] = 'Строка '.($index + 1).' — требуется документ';
continue;
}
// У документа проверяем артикул и положительное целое количество.
}Что покажет пример
| Что проверили | Результат |
|---|---|
| Проверено строк | 3 |
| Правило приёмки | вся накладная целиком |
| Замечание 1 | Строка 2 — требуется документ |
| Замечание 2 | Строка 3 — нужно целое от 1 до 1000 |
| Начислено единиц | 0 |
| Остаток LED | 10 |
Как прочитать результат
foreach выдаёт индексы от 0. Для сообщения человеку прибавляем 1. continue означает переход к следующему элементу: у строки «повреждено» нельзя запрашивать поле sku. Валидные строки временно считаются, но при наличии замечаний amounts возвращается пустым.
Ограничение количества, два известных артикула и решение принимать накладную целиком — условия этой задачи. Частичная приёмка потребовала бы учёта состояния каждой позиции и отдельной договорённости о повторном импорте.
Для полного контракта обхода откройте BSON Iterator. Для обработки отказа внутри модуля — границы обработки исключений.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\{Document,PackedArray};
use MongoDB\Driver\{Manager,BulkWrite};
require __DIR__.'/s136-helper.php';
[$rows,$checks,$runtime,$counts]=s136Lab(static function(Manager $manager,string $db,string $ns): array {
$bad=Document::fromJSON('{"_id":"F","items":[{"sku":"LED","qty":2},"повреждено",{"sku":"CABLE","qty":-1}]}');
$stock=Document::fromPHP(['_id'=>'stock','balance'=>['LED'=>10,'CABLE'=>20]]);
$written=s136Write($manager,$ns,[$bad,$stock]);
$shipment=s136Read($manager,$ns,['_id'=>'F'])[0];
$review=s136Validate($shipment);
if ($review['ok']) {throw new RuntimeException('Damaged shipment was accepted');}
$balance=s136Stock(s136Read($manager,$ns,['_id'=>'stock'])[0]);
$rows=[['Проверено строк',iterator_count($shipment->get('items')->getIterator())],['Правило приёмки','вся накладная целиком'],
['Замечание 1',$review['errors'][0]],['Замечание 2',$review['errors'][1]],['Начислено единиц',array_sum($review['amounts'])],['Остаток LED',$balance['LED']]];
$checks=['two_stored'=>$written===2,'real_array'=>s136Shape($shipment)==='список','not_postable'=>!$review['ok'],
'two_issues'=>count($review['errors'])===2,'first_bad_position'=>str_starts_with($review['errors'][0],'Строка 2'),
'second_bad_position'=>str_starts_with($review['errors'][1],'Строка 3'),'nothing_posted'=>$review['amounts']===[],
'stock_untouched'=>$balance===['LED'=>10,'CABLE'=>20]];
return [$rows,$checks];
});
s136Report('mongodb136-shape-line-rejection', 'Отклонить накладную целиком и показать номера испорченных строк', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
Проверено строк — 3
Правило приёмки — вся накладная целиком
Замечание 1 — Строка 2 — требуется документ
Замечание 2 — Строка 3 — нужно целое от 1 до 1000
Начислено единиц — 0
Остаток LED — 10
Самостоятельный запуск с реальной MongoDB. Таблица строится по сохранённым и заново прочитанным данным.
Открыть ведомость запуска · Скачать JSON ведомости
Попробуйте самостоятельно. Исправьте вторую строку на CABLE × 1 и третью на CABLE × 2. Прежде чем начислять товары, отдельно проверьте, что errors пуст и суммы отражают все три позиции.
Сопоставить накладную с фактически пересчитанной полкой
До разгрузки было 10 ламп и 20 кабелей. Накладная A обещает ещё 2 лампы и 1 кабель. После разгрузки сотрудник пересчитал 11 и 21. Нужно указать конкретный артикул недостачи и её знак.
- Прочитать сохранённый остаток до поставки
- Получить и проверить строки накладной
- Отдельно загрузить фактический пересчёт
- Сопоставить ожидаемое и наблюдаемое количество по артикулу
С чего начать
$expected = $previous + $received;
$difference = $counted - $expected;Что покажет пример
| Что проверили | Результат |
|---|---|
| LED | ожидаем 12 · на полке 11 · разница -1 |
| CABLE | ожидаем 21 · на полке 21 · разница 0 |
Как прочитать результат
Расчётный и наблюдаемый остатки имеют разные источники. Минус один означает, что по LED на полке на одну единицу меньше, чем следует из поставки. По CABLE разница нулевая. Программа не изменяет ни один из снимков остатков.
На живом складе продажи или перемещения между снимками также создают расхождение. Для такого сверочного отчёта нужны согласованные границы времени, идентификатор места хранения и учёт событий движения. Здесь три записи созданы в изолированном примере.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\{Document,PackedArray};
use MongoDB\Driver\{Manager,BulkWrite};
require __DIR__.'/s136-helper.php';
[$rows,$checks,$runtime,$counts]=s136Lab(static function(Manager $manager,string $db,string $ns): array {
$receipt=s136Manifests()[0];
$stock=Document::fromPHP(['_id'=>'before','balance'=>['LED'=>10,'CABLE'=>20]]);
$physical=Document::fromPHP(['_id'=>'counted','balance'=>['LED'=>11,'CABLE'=>21]]);
$written=s136Write($manager,$ns,[$receipt,$stock,$physical]);
$review=s136Validate(s136Read($manager,$ns,['_id'=>'A'])[0]);
if (!$review['ok']) {throw new RuntimeException('Expected complete receipt');}
$before=s136Stock(s136Read($manager,$ns,['_id'=>'before'])[0]);
$counted=s136Stock(s136Read($manager,$ns,['_id'=>'counted'])[0]);
$rows=[];$diff=[];
foreach ($before as $sku=>$old) {
$expected=$old+($review['amounts'][$sku] ?? 0);
$diff[$sku]=$counted[$sku]-$expected;
$rows[]=[$sku,'ожидаем '.$expected.' · на полке '.$counted[$sku].' · разница '.$diff[$sku]];
}
$checks=['three_inputs'=>$written===3,'accepted'=>$review['ok'],'led_before'=>$before['LED']===10,'cable_before'=>$before['CABLE']===20,
'receipt_units'=>array_sum($review['amounts'])===3,'short_one'=>$diff['LED']===-1,'cable_balanced'=>$diff['CABLE']===0];
return [$rows,$checks];
});
s136Report('mongodb136-shape-stock-count', 'Сопоставить накладную с фактически пересчитанной полкой', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
LED — ожидаем 12 · на полке 11 · разница -1
CABLE — ожидаем 21 · на полке 21 · разница 0
Самостоятельный запуск с реальной MongoDB. Таблица строится по сохранённым и заново прочитанным данным.
Открыть ведомость запуска · Скачать JSON ведомости
Попробуйте самостоятельно. Замените фактический пересчёт LED на 13. Проверьте, чему равен знак разницы и какое действие попросите выполнить кладовщика.
Начислить поставку один раз и проверить остаток после повтора
Интеграция прислала одну и ту же одобренную накладную A два раза. В учебном хранилище оба остатка и номера учтённых накладных находятся в одном документе. Требуется начислить приход один раз и проверить фактическое состояние.
- Проверить форму и все строки накладной
- Из двух известных артикулов собрать прибавки
- Изменить единый складской документ при условии, что A ещё не учтена
- Повторить точно тот же вызов и заново прочитать остаток
С чего начать
$bulk->update(
['_id'=>'stock', 'applied'=>['$ne'=>'A']],
['$inc'=>['balance.LED'=>2, 'balance.CABLE'=>1], '$addToSet'=>['applied'=>'A']]
);Что покажет пример
| Что проверили | Результат |
|---|---|
| Изменений при первой приёмке | 1 |
| Изменений при повторе | 0 |
| LED на складе | 12 |
| CABLE на складе | 21 |
| Учтённая накладная | A |
Как прочитать результат
$inc увеличивает значения balance, $addToSet включает A в список учтённых поставок. Оба оператора относятся к тому же документу и к тому же условию записи. Повтор не находит подходящего документа. В учебном журнале первого запуска modified=1, второго — 0. Отдельное чтение подтверждает 12 ламп и 21 кабель.
Этот способ демонстрирует атомарность одной операции с одним документом. Он не охватывает списание с нескольких складов, неограниченный журнал номеров, сопоставление изменившегося текста накладной с тем же номером или доставку сообщений внешнему сервису. Не используйте произвольный входной артикул в составном имени поля: в примере есть явный список разрешённых артикулов.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\{Document,PackedArray};
use MongoDB\Driver\{Manager,BulkWrite};
require __DIR__.'/s136-helper.php';
[$rows,$checks,$runtime,$counts]=s136Lab(static function(Manager $manager,string $db,string $ns): array {
$stock=Document::fromPHP(['_id'=>'stock','balance'=>['LED'=>10,'CABLE'=>20],'applied'=>[]]);
$written=s136Write($manager,$ns,[s136Manifests()[0],$stock]);
$manifest=s136Read($manager,$ns,['_id'=>'A'])[0];
$review=s136Validate($manifest);
if (!$review['ok']) {throw new RuntimeException('Invalid shipment');}
$id=$manifest->get('_id');
$increments=[];
foreach ($review['amounts'] as $sku=>$qty) {$increments['balance.'.$sku]=$qty;}
$post=static function() use ($manager,$ns,$id,$increments): int {
$bulk=new BulkWrite();
$bulk->update(['_id'=>'stock','applied'=>['$ne'=>$id]],['$inc'=>$increments,'$addToSet'=>['applied'=>$id]],['upsert'=>false]);
return $manager->executeBulkWrite($ns,$bulk)->getModifiedCount();
};
$first=$post();
$afterFirst=s136Stock(s136Read($manager,$ns,['_id'=>'stock'])[0]);
$second=$post();
$finalStock=s136Read($manager,$ns,['_id'=>'stock'])[0];
$balance=s136Stock($finalStock);
$applied=$finalStock->get('applied');
$rows=[['Изменений при первой приёмке',$first],['Изменений при повторе',$second],['LED на складе',$balance['LED']],
['CABLE на складе',$balance['CABLE']],['Учтённая накладная',$applied->get(0)]];
$checks=['two_sources'=>$written===2,'valid_manifest'=>$review['ok'],'first_changed'=>$first===1,'second_skipped'=>$second===0,
'balance_after_first'=>$afterFirst===['LED'=>12,'CABLE'=>21],'balance_after_repeat'=>$balance===$afterFirst,
'one_receipt'=>$applied->has(0) && !$applied->has(1) && $applied->get(0)==='A'];
return [$rows,$checks];
});
s136Report('mongodb136-shape-post-receipt', 'Начислить поставку один раз и проверить остаток после повтора', ['Что проверили', 'Результат'], $rows,$checks,$runtime,$counts);
foreach ($rows as [$label,$value]) {echo $label, ' — ', $value, PHP_EOL;}
Ожидаемый результат
Изменений при первой приёмке — 1
Изменений при повторе — 0
LED на складе — 12
CABLE на складе — 21
Учтённая накладная — A
Самостоятельный запуск с реальной MongoDB. Таблица строится по сохранённым и заново прочитанным данным.
Открыть ведомость запуска · Скачать JSON ведомости
Попробуйте самостоятельно. Почему отдельный find «не видели A» перед безусловным $inc оставил бы промежуток для повторного начисления? Укажите часть фильтра, которая защищает запись непосредственно в этом примере.
От приёмки к сверке с поставщиком
Если форма накладной верна, но артикулы меняются местами или встречаются дважды, перейдите к сверке накладных и границе обмена. Чтобы найти остальные типы BSON, откройте карту типов и представлений. Для изменений нескольких документов полезно разобрать транзакции MongoDB.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
MongoDB\BSON\DocumentРаботать с полями BSON-документа и отличать объект поставщика от списка позиций.
ext-mongodb 1.16.0 и новее · mongodb
final class MongoDB\BSON\Document implements MongoDB\BSON\Type, ArrayAccess, IteratorAggregateКогда пригодится и как получить объект
Получите документ из курсора с картой root=bson либо создайте через fromPHP, fromJSON или fromBSON. У него есть именованные поля. Если поставщик прислал объект {"sku":"LED","qty":2}, вы можете прочитать sku. Один такой объект ещё не является списком строк поставки.
$shipment = MongoDB\BSON\Document::fromPHP(['_id'=>'A', 'items'=>[['sku'=>'LED','qty'=>2]]]);
echo $shipment->get('_id'); // A
echo $shipment->get('items')->get(0)->get('qty'); // 2Что возвращает и что проверить
has("items") проверяет наличие ключа, в том числе при явном null. get() возвращает конкретное BSON-значение и выбрасывает исключение при отсутствующем ключе. Вложенные документ и массив при чтении через get доступны как Document и PackedArray. При обходе документа ключ каждого поля является строкой. Фабрика fromPHP с массивом в корне всё равно создаёт документ; числовые ключи в нём становятся именами полей.
Экземпляр неизменяем. ArrayAccess позволяет читать через квадратные скобки, а присваивание и unset не меняют его. Для правки постройте новый документ или отправьте управляемое обновление в БД. С 1.17.0 контейнер реализует BSON Type; с 2.0 больше не реализует старый PHP-интерфейс Serializable. Приведение к string выдаёт бинарные байты BSON, которые не предназначены для HTML.
Подробный разбор каждого метода: __construct(), fromPHP(), fromJSON(), fromBSON(), has(), get(), getIterator(), offsetExists(), offsetGet(), offsetSet(), offsetUnset(), toPHP(), toCanonicalExtendedJSON(), toRelaxedExtendedJSON(), __toString().
Запустить на реальной поставке: Выявить объект вместо списка, Связать лишнюю строку с её источником.
MongoDB\BSON\PackedArrayЧитать упорядоченные позиции BSON-массива, в том числе пустого.
ext-mongodb 1.16.0 и новее · mongodb
final class MongoDB\BSON\PackedArray implements MongoDB\BSON\Type, ArrayAccess, IteratorAggregateКогда пригодится и как получить объект
В приходной накладной items — список, а не справочник артикулов с произвольными ключами. Первый элемент находится по индексу 0, второй по индексу 1. Пустой список сохраняет тот же BSON-тип и не содержит позиции 0.
$items = MongoDB\BSON\PackedArray::fromPHP([['sku'=>'LED','qty'=>2], ['sku'=>'CABLE','qty'=>1]]);
echo $items->get(1)->get('sku'); // CABLEЧто возвращает и что проверить
fromPHP принимает PHP-список с последовательными числовыми ключами от нуля. Для другого набора ключей возникает InvalidArgumentException. fromJSON ожидает представление массива. has(index) сообщает, существует ли позиция, get(index) возвращает значение и может передать вложенный Document; при отсутствующем индексе get выбрасывает исключение. Итерирование даёт целочисленные ключи.
ArrayAccess доступен для чтения. Методы записи через скобки не делают контейнер изменяемым. С 1.17 контейнер реализует BSON Type и не сериализуется на месте, где требуется корневой документ. До 1.17 там мог появиться документ; с 2.0 старый PHP-интерфейс Serializable не реализуется. При необходимости передайте список как поле документа поставки.
Подробный разбор каждого метода: __construct(), fromPHP(), fromJSON(), has(), get(), getIterator(), offsetExists(), offsetGet(), offsetSet(), offsetUnset(), toPHP(), toCanonicalExtendedJSON(), toRelaxedExtendedJSON(), __toString().
Запустить на реальной поставке: Прочитать позиции 0 и 1, Проверить строки внутри правильного контейнера.
MongoDB\BSON\IteratorУказать точную позицию проблемной строки при обходе сырого BSON.
ext-mongodb 1.16.0 и новее · mongodb
final class MongoDB\BSON\Iterator implements \IteratorКогда пригодится и как получить объект
getIterator() у BSON Document или PackedArray выдаёт итератор. Его возвращает уже существующий контейнер. С помощью foreach PHP сам перемещает итератор и выдаёт ключ и текущее значение. Код пользователя обычно не вызывает конструктор.
$it = $items->getIterator();
$it->rewind();
while ($it->valid()) {
echo 'Позиция ', $it->key() + 1, PHP_EOL;
$it->next();
}Что возвращает и что проверить
rewind ставит на первый элемент, valid проверяет наличие текущего элемента, key возвращает строковое имя у документа и целочисленный индекс у списка, current возвращает значение. next перемещает на следующую позицию. Чтение key/current за концом контейнера недопустимо. Для пустого PackedArray тело правильно построенного цикла не выполнится.
Это итератор одного уже полученного BSON-значения. Курсор MongoDB, который привозит из базы много документов и следующие порции, — другой объект. Складывать 1 с результатом key уместно при обходе списка, потому что это числовой индекс. К имени поля документа прибавлять 1 нельзя.
Подробный разбор каждого метода: __construct(), rewind(), valid(), key(), current(), next().
Запустить на реальной поставке: Показать ошибку именно во второй и третьей позициях, Напечатать нумерованную ведомость.
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


