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

Дерево руководства
Глава 185 · После работы с массивами и чтения документа
MongoDB: сверка накладной и скана по артикулам
Принять совпавший товар, проследить повторы до номера строки, показать недобор и излишек и передать цену с датой без утраты BSON-типов.
Почему две одинаковые поставки выглядят по-разному
В накладной поставщик указал три светильника LED в двух строках. В скане кладовщика те же три штуки находятся в одной строке и на другом месте. Приёмщику нужен ответ по каждой коробке. Начнём с одной строки, затем соберём ведомость для пяти артикулов.
- Поставщик прислал LED × 2 и ещё одну LED × 1
- Сканер записал LED × 3
- Из базы читаем обе стороны одной накладной и находим их по артикулу LED
- В строке LED видно: ожидали 3, приняли 3, можно принять
Артикул (SKU) — код товара. Позиция — порядковый номер строки в конкретном источнике. Количество — число штук в ней. Если светильник записан в первой и третьей строках, у него один артикул и две позиции. Записи двух разных поставок этой программой не объединяются.
$line = ['sku' => 'LED', 'qty' => 2];
echo $line['sku'], ' × ', $line['qty']; // LED × 2Квадратные скобки задают PHP-массив, знак => связывает имя поля с его значением. Верхний уровень invoice — последовательность строк. Каждая его строка — набор полей. Как устроены ключи и значения в PHP · Как MongoDB отличает список строк от документа с полями.
BSON в приёмке и карта классов
BSON — формат, в котором MongoDB представляет документы вместе с типами их значений. В документе поставки _id называет конкретную накладную, invoice и scan содержат вложенные списки. Один список отвечает на вопрос «что обещано», второй — «что видели». На уровне BSON документ с полями sku и qty и массив из двух таких документов — разные контейнеры.
| С чем работает программа | Куда перейти | Что это даёт |
|---|---|---|
| Документ, список и обход | Document, PackedArray, Iterator | Читать именованные поля, элементы списка по позиции и последовательно обходить исходные BSON-значения. У контейнеров нет операции изменения на месте. |
| Точные числа | Decimal128, Int64 | Цена не проходит через двоичный float. В raw BSON значение int64 представлено Int64 даже на 64-разрядном PHP. |
| Идентификаторы и время | ObjectId, UTCDateTime, Timestamp | ObjectId служит идентификатором, UTCDateTime хранит момент в миллисекундах. Timestamp — специальное значение MongoDB для времени и порядкового счётчика, у него другое назначение. |
| Бинарные данные и векторы | Binary, VectorType | У Binary есть подтип. Перечисление VectorType (с 2.2.0) указывает формат данных вектора при подтипе TYPE_VECTOR. |
| Выражения и специальные значения | Regex, Javascript, MinKey, MaxKey | Сохранённое регулярное выражение, значение BSON JavaScript и границы BSON-порядка сравнения имеют разные типы. Данные поставщика не исполняются как код. |
| Контракт собственных PHP-классов | Type, Serializable, Unserializable, Persistable | Type — внутренний базовый интерфейс. Для явного представления и восстановления полей своего объекта служат интерфейсы сериализации и материализации. |
| Контракты значений | BinaryInterface, Decimal128Interface, JavascriptInterface, MinKeyInterface, MaxKeyInterface, ObjectIdInterface, TimestampInterface, UTCDateTimeInterface, RegexInterface | Именованные интерфейсы значений помогают типизировать параметры и возвращаемые данные в прикладных классах; конкретные члены перечислены в соответствующих карточках. |
| Данные старых коллекций | DBPointer, Symbol, Undefined | Устаревшие BSON-типы ещё могут встретиться при чтении. Соответствующие объекты создаёт драйвер при декодировании; для новой накладной используйте подходящие современные значения. |
Если ваш счётчик должен показать, где потерялся товар, сначала проверьте форму поля, затем связывайте строки по артикулу. Для даты/цены через текстовый транспорт полезна сверка типов у исходника и у импортированной копии. Полное дерево BSON в официальном PHP Manual.
Шесть исторических BSON-функций и их замена
При переносе старого импортёра можно встретить функции в пространстве MongoDB\BSON. Они обрабатывали строку бинарного BSON. У класса Document своя форма результата: например, из fromPHP() приходит объект, а строковые BSON-байты при необходимости получают приведением (string). Имена ниже ведут к уже существующим карточкам миграции.
| Историческая функция | Результат и прежний контракт | Действие в современном коде |
|---|---|---|
| fromPHP | PHP-массив/объект → бинарная строка BSON. | Document::fromPHP() возвращает документ. Возьмите (string)$doc, если адресату нужны байты. |
| fromJSON | Текст Extended JSON → бинарная строка BSON. | Document::fromJSON() разбирает вход в объект Document. |
| toPHP | Бинарный BSON → значение PHP, возможна карта типов. | Document::fromBSON(), затем Document::toPHP() с нужной картой типов. |
| toCanonicalExtendedJSON | Бинарный BSON → текст с явными метками типов. | Document::toCanonicalExtendedJSON() для типизированного обмена. |
| toRelaxedExtendedJSON | Бинарный BSON → более читаемый Extended JSON. | Document::toRelaxedExtendedJSON() для удобства чтения. Некоторые числовые типы выглядят одинаково. |
| toJSON | Бинарный BSON → исторический Legacy Extended JSON. | Document::toCanonicalExtendedJSON() или relaxed-вариант по контракту получателя. Совпадение формата со старой строкой требует отдельной проверки. |
fromJSON, fromPHP, toPHP и toJSON были доступны с ext-mongodb 1.0.0. Процедурные canonical/relaxed варианты добавлены в 1.3.0. Все шесть функций помечены устаревшими в 1.20.0 и удалены в 2.0.0. В этих новых упражнениях на 2.5.5 вызываются методы Document. В диагностическом выводе старого проекта проверяйте отдельно версию ext-mongodb в php --ri mongodb и версию Composer-библиотеки. Список шести исторических функций.
На чём запустить и что считается одной поставкой
Все исходные товарные данные показаны в тексте и лежат в example.php. Для запуска нужен отдельный учебный standalone на 127.0.0.1:27075, база php_manual_lab, PHP CLI с ext-mongodb и библиотека mongodb/mongodb. Версии стенда, команды PowerShell и окружение приведены в README.txt каждого архива. Программа записывает данные в свою новую коллекцию, читает их обратно, затем удаляет коллекцию и проверяет это отдельным запросом.
Для этих небольших накладных выбран предел 1000 строк на источник, 1–9999 штук на строку и артикул из 1–32 заглавных латинских букв, цифр и дефисов. Регистр и ведущие нули существенны. Пример с несколькими источниками сначала читает одну поставку по _id. Полная коллекция под сортировку в PHP не выгружается. Для самостоятельного упражнения работайте в копии распакованной программы. При изменении чисел пересчитайте ожидаемые проверки в $checks и текст итоговой строки.
Проверить одну строку накладной
Кладовщик открыл коробку с двумя светильниками. Прежде чем подписать накладную, он сравнивает маркировку и количество.
Что пришло от поставщика и сканера
В учебной П-42 два списка. В invoice поставщик заявил LED × 2. В scan сотрудник отдельно записал LED × 2.
- Сохраняем обе записи в одном документе поставки и читаем его по номеру П-42.
- Индекс 0 обозначает первую позицию. Ключ sku содержит артикул, qty — количество.
- Два строгих сравнения дают «Совпало» только при одинаковом артикуле и числе штук.
Короткий фрагмент
$expected = $saved['invoice'][0];
$seen = $saved['scan'][0];
$sameSku = $expected['sku'] === $seen['sku'];
$sameQty = $expected['qty'] === $seen['qty'];Что получит приёмщик
П-42: по накладной две LED, принято две.
Открыть фактический результат в HTML · Строки результата в JSON.
| Поставка | Артикул | По накладной | На приёмке | Решение |
|---|---|---|---|---|
| П-42 | LED | 2 | 2 | Совпало |
Результат «Принято две» относится к исходному скану. Кнопка проведения складского остатка в этой задаче не участвует.
Границы решения
Прямое обращение к [0] имеет смысл, пока в каждом списке ровно одна строка. Следующая программа покажет опасность позиционного сравнения.
Самостоятельная программа и ZIP
<?php
declare(strict_types=1);
require __DIR__.'/receipt-lab.php';
use MongoDB\Collection;
use MongoDB\BSON\{Document,PackedArray,Decimal128,UTCDateTime};
runReceipt('mongodb136-reconcile-first-line', 'Проверить одну строку накладной', ['Поставка', 'Артикул', 'По накладной', 'На приёмке', 'Решение'],
function (Collection $receipts): array {
$invoice=[['sku'=>'LED','qty'=>2]];
$scan=[['sku'=>'LED','qty'=>2]];
$saved=storeAndRead($receipts,$invoice,$scan);
$expected=$saved['invoice'][0];
$seen=$saved['scan'][0];
$match=$expected['sku']===$seen['sku'] && $expected['qty']===$seen['qty'];
$rows=[[$saved['_id'],$expected['sku'],$expected['qty'],$seen['qty'],$match?'Совпало':'Проверить']];
$checks['stored_sources_read'] = $saved['invoice']===$invoice && $saved['scan']===$scan;
$checks['one_item_agrees'] = count($saved['invoice'])===1 && count($saved['scan'])===1 && $match;
return [$rows,$checks,'П-42: по накладной две LED, принято две.'];
}
);
Ожидаемый результат
П-42: по накладной две LED, принято две.
Программа записывает учебные данные, читает их обратно из MongoDB и формирует result.html и result.json. Эти файлы содержат итог конкретного запуска.
Попробуйте изменить
Поставьте у сканирования qty, равное 1. Посмотрите, почему перед подписанием потребуется проверка.
Найти строку скана при другом порядке коробок
Коробки оказались у сканера в обратном порядке. Обе правильные позиции при этом должны пройти приёмку.
Что пришло от поставщика и сканера
Накладная идёт в порядке LED, CABLE. Скан идёт в порядке CABLE, LED. Номер для человека считается с единицы.
- Один проход по скану строит словарь артикул → количество и номера строк.
- Для каждой строки накладной получаем запись из словаря по её артикулу.
- Вывод остаётся в привычном документном порядке, с номером строки самого скана.
Короткий фрагмент
$scanIndex = tallyPositions($saved['scan']);
$key = 'sku:' . $line['sku'];
$actual = $scanIndex[$key] ?? null;Что получит приёмщик
LED найдена во второй строке скана, CABLE — в первой.
Открыть фактический результат в HTML · Строки результата в JSON.
| Артикул | Строка накладной | Строка скана | Принято, шт. |
|---|---|---|---|
| LED | 1 | 2 | 2 |
| CABLE | 2 | 1 | 3 |
Сортировать два массива не требуется. Поиск по артикулу переживает перестановку даже тогда, когда принято одинаковое количество разных товаров.
Границы решения
Здесь у каждого SKU по одной позиции скана. Повторы требуют учёта всех количеств. Префикс sku: сохраняет строковый ключ PHP даже для артикула из цифр.
Самостоятельная программа и ZIP
<?php
declare(strict_types=1);
require __DIR__.'/receipt-lab.php';
use MongoDB\Collection;
use MongoDB\BSON\{Document,PackedArray,Decimal128,UTCDateTime};
runReceipt('mongodb136-reconcile-sku-order', 'Найти строку скана при другом порядке коробок', ['Артикул', 'Строка накладной', 'Строка скана', 'Принято, шт.'],
function (Collection $receipts): array {
$saved=storeAndRead($receipts,[['sku'=>'LED','qty'=>2],['sku'=>'CABLE','qty'=>3]],
[['sku'=>'CABLE','qty'=>3],['sku'=>'LED','qty'=>2]]);
$scanIndex=tallyPositions($saved['scan']);
$rows=[];
foreach ($saved['invoice'] as $n=>$line) {
$key='sku:'.$line['sku'];
$actual=$scanIndex[$key] ?? null;
if ($actual===null || count($actual['positions'])!==1) { throw new RuntimeException('Expected one matching scan'); }
$rows[]=[$line['sku'],$n+1,$actual['positions'][0],$actual['quantity']];
}
$checks['order_changed'] = $saved['invoice'][0]['sku']!==$saved['scan'][0]['sku'];
$checks['sku_pairs'] = $rows===[['LED',1,2,2],['CABLE',2,1,3]];
return [$rows,$checks,'LED найдена во второй строке скана, CABLE — в первой.'];
}
);
Ожидаемый результат
LED найдена во второй строке скана, CABLE — в первой.
Программа записывает учебные данные, читает их обратно из MongoDB и формирует result.html и result.json. Эти файлы содержат итог конкретного запуска.
Попробуйте изменить
Сделайте количества LED и CABLE одинаковыми. Перепутать товары по номеру строки всё ещё возможно; словарь по SKU сохранит соответствие.
Как обратиться к нужному товару, когда искомое значение может стоять в любой позиции списка? Индекс сканирования по SKU с сохранением номера строки позволяет найти товар в другом порядке. Постановка задачи на Stack Overflow.
Объединить повторный SKU и показать исходные позиции
В накладной одно изделие указано на двух строках. Кладовщик хочет видеть итог и обе позиции для сверки с бумагой.
Что пришло от поставщика и сканера
Три записи из MongoDB: LED × 2, CABLE × 1, LED × 1. Оба LED принадлежат одной проверяемой поставке.
- Для первого появления SKU начинаем с quantity = 0 и пустого списка positions.
- Оператор += прибавляет количество. Пустые квадратные скобки помещают очередной номер в конец списка.
- Контрольная сумма всей накладной остаётся равна четырём.
Короткий фрагмент
$index[$key]['quantity'] += $qty;
$index[$key]['positions'][] = $offset + 1;Что получит приёмщик
LED: 3 штуки из строк 1 и 3. CABLE: 1 штука из строки 2.
Открыть фактический результат в HTML · Строки результата в JSON.
| Артикул | Строки накладной | Всего, шт. |
|---|---|---|
| LED | 1, 3 | 3 |
| CABLE | 2 | 1 |
Присваивание новой строки по ключу потеряло бы первый LED. Сумма и происхождение каждой части сохраняются независимо от того, рядом ли одинаковые SKU.
Границы решения
Объединять можно только сопоставимые единицы одной поставки. Если в хозяйственном учёте различаются партии, серии или единицы измерения, они входят в ключ группировки.
Самостоятельная программа и ZIP
<?php
declare(strict_types=1);
require __DIR__.'/receipt-lab.php';
use MongoDB\Collection;
use MongoDB\BSON\{Document,PackedArray,Decimal128,UTCDateTime};
runReceipt('mongodb136-reconcile-repeat-lines', 'Объединить повторный SKU и показать исходные позиции', ['Артикул', 'Строки накладной', 'Всего, шт.'],
function (Collection $receipts): array {
$saved=storeAndRead($receipts,[['sku'=>'LED','qty'=>2],['sku'=>'CABLE','qty'=>1],['sku'=>'LED','qty'=>1]],[]);
$index=tallyPositions($saved['invoice']);
$rows=[];
foreach ($index as $item) {
$rows[]=[$item['sku'],implode(', ',$item['positions']),$item['quantity']];
}
$checks['one_total_per_sku'] = count($index)===2 && $index['sku:LED']['quantity']===3;
$checks['all_source_positions'] = $index['sku:LED']['positions']===[1,3] && $index['sku:CABLE']['positions']===[2];
$checks['sum_preserved'] = array_sum(array_column($index,'quantity'))===array_sum(array_column($saved['invoice'],'qty'));
return [$rows,$checks,'LED: 3 штуки из строк 1 и 3. CABLE: 1 штука из строки 2.'];
}
);
Ожидаемый результат
LED: 3 штуки из строк 1 и 3. CABLE: 1 штука из строки 2.
Программа записывает учебные данные, читает их обратно из MongoDB и формирует result.html и result.json. Эти файлы содержат итог конкретного запуска.
Попробуйте изменить
Разместите LED в пяти разных строках, затем посчитайте итог вручную и сверьте его с колонкой «Всего».
Как просуммировать количества для каждого заранее неизвестного идентификатора? Используем группировку по артикулу и сохраняем список исходных номеров для проверяющего. Постановка задачи на Stack Overflow.
Показать недобор и излишек в штуках
Общее число единиц в коробках отличается от накладной на одну штуку. Этой цифры недостаточно, чтобы понять, что пересчитать.
Что пришло от поставщика и сканера
Для LED ожидают 3 и находят 2; для CABLE ожидают 2 и находят 4; для TAPE ожидают и находят 1.
- Связываем записи по артикулу, объединяя повторы внутри каждого источника.
- Вычитаем объявленное количество из фактически принятого.
- У каждой разницы остаются свой знак и свой артикул.
Короткий фрагмент
$delta = $fact - $plan;Что получит приёмщик
LED: недобор 1. CABLE: излишек 2. TAPE: совпало.
Открыть фактический результат в HTML · Строки результата в JSON.
| Артикул | План | Факт | Факт − план | Итог |
|---|---|---|---|---|
| LED | 3 | 2 | -1 | Недобор |
| CABLE | 2 | 4 | 2 | Излишек |
| TAPE | 1 | 1 | 0 | Совпало |
Знак указывает направление разбора. Если сотрудник сравнит только суммы по всей поставке, излишек кабеля частично погасит недобор светильников.
Границы решения
Математическая разница обнаруживает расхождение, но не определяет его причину. Ошибочную строку накладной и двойное сканирование разбирают отдельно.
Самостоятельная программа и ZIP
<?php
declare(strict_types=1);
require __DIR__.'/receipt-lab.php';
use MongoDB\Collection;
use MongoDB\BSON\{Document,PackedArray,Decimal128,UTCDateTime};
runReceipt('mongodb136-reconcile-quantity-gap', 'Показать недобор и излишек в штуках', ['Артикул', 'План', 'Факт', 'Факт − план', 'Итог'],
function (Collection $receipts): array {
$saved=storeAndRead($receipts,[['sku'=>'LED','qty'=>3],['sku'=>'CABLE','qty'=>2],['sku'=>'TAPE','qty'=>1]],
[['sku'=>'TAPE','qty'=>1],['sku'=>'CABLE','qty'=>4],['sku'=>'LED','qty'=>2]]);
$rows=[];
foreach (reconcileDelivery($saved['invoice'],$saved['scan']) as [$sku,$plan,$fact,$delta,$action]) {
$detail=match(true) { $delta<0=>'Недобор', $delta>0=>'Излишек', default=>'Совпало' };
$rows[]=[$sku,$plan,$fact,$delta,$detail];
}
$checks['signed_deltas'] = array_column($rows,3)===[-1,2,0];
$checks['balances'] = array_sum(array_column($rows,3))===array_sum(array_column($saved['scan'],'qty'))-array_sum(array_column($saved['invoice'],'qty'));
return [$rows,$checks,'LED: недобор 1. CABLE: излишек 2. TAPE: совпало.'];
}
);
Ожидаемый результат
LED: недобор 1. CABLE: излишек 2. TAPE: совпало.
Программа записывает учебные данные, читает их обратно из MongoDB и формирует result.html и result.json. Эти файлы содержат итог конкретного запуска.
Попробуйте изменить
Измените план CABLE на 3. Теперь общие суммы совпадут. Найдите два артикула, по которым всё ещё нужно пересчитать коробки.
Развести недостающий и лишний артикулы
Водитель ждёт отметку приёмщика. Ему нужен короткий список спорных позиций с двумя различными действиями.
Что пришло от поставщика и сканера
FUSE и TAPE указаны в накладной. Сканер видел TAPE и CLIP. Нулевое число в ведомости означает, что с этой стороны строк нет.
- Рассматриваем все ключи обеих сторон, включая артикулы только из скана.
- Если отсутствует строка скана, просим найти товар по накладной. Для неизвестного накладной товара уточняем поставку.
- Совпавший TAPE скрывается из списка расхождений.
Короткий фрагмент
$invoiceHasSku = isset($expected[$key]);
$scanHasSku = isset($seen[$key]);
$plan = $expected[$key]['quantity'] ?? 0;
$fact = $seen[$key]['quantity'] ?? 0;Что получит приёмщик
FUSE не найден в скане. CLIP отсутствует в накладной.
Открыть фактический результат в HTML · Строки результата в JSON.
| Артикул | В накладной | В скане | Действие |
|---|---|---|---|
| FUSE | 2 | 0 | Найти по накладной |
| CLIP | 0 | 1 | Уточнить поставку |
Простой перебор одних строк накладной никогда не увидел бы CLIP. Присутствие ключа и количество — отдельные свойства.
Границы решения
В учебном договоре строка означает положительное число штук. Для отдельного процесса возврата или допустимых нулей нужны явные правила учёта.
Самостоятельная программа и ZIP
<?php
declare(strict_types=1);
require __DIR__.'/receipt-lab.php';
use MongoDB\Collection;
use MongoDB\BSON\{Document,PackedArray,Decimal128,UTCDateTime};
runReceipt('mongodb136-reconcile-missing-extra', 'Развести недостающий и лишний артикулы', ['Артикул', 'В накладной', 'В скане', 'Действие'],
function (Collection $receipts): array {
$saved=storeAndRead($receipts,[['sku'=>'FUSE','qty'=>2],['sku'=>'TAPE','qty'=>1]],
[['sku'=>'TAPE','qty'=>1],['sku'=>'CLIP','qty'=>1]]);
$rows=[];
foreach (reconcileDelivery($saved['invoice'],$saved['scan']) as [$sku,$plan,$fact,$delta,$action]) {
if ($action!=='Принять') { $rows[]=[$sku,$plan,$fact,$action]; }
}
$checks['missing_row'] = $rows[0]===['FUSE',2,0,'Найти по накладной'];
$checks['extra_row'] = $rows[1]===['CLIP',0,1,'Уточнить поставку'];
$checks['matching_rows_hidden'] = count($rows)===2 && !in_array('TAPE',array_column($rows,0),true);
return [$rows,$checks,'FUSE не найден в скане. CLIP отсутствует в накладной.'];
}
);
Ожидаемый результат
FUSE не найден в скане. CLIP отсутствует в накладной.
Программа записывает учебные данные, читает их обратно из MongoDB и формирует result.html и result.json. Эти файлы содержат итог конкретного запуска.
Попробуйте изменить
Добавьте только в скан ещё один новый артикул: отчёт должен показать второе обращение для уточнения.
Сохранить разницу между пустым списком и пустой карточкой
Сервис проверяет, можно ли принять пустую ведомость. Поставщик вправе прислать пустой список, но тот же ключ с пустым объектом нарушает договор.
Что пришло от поставщика и сканера
Первый пакет содержит {"lines":[]}. Второй содержит {"lines":{}}. Их обрабатываем тремя способами, а BSON-представление каждого сохраняем в MongoDB и читаем обратно.
- json_decode с true превращает вложенный объект и массив в одинаковый пустой PHP-массив.
- С false поле, исходно бывшее объектом JSON, остаётся экземпляром stdClass.
- Document::fromJSON понимает Extended JSON; в сохранённом BSON вложенные пустые контейнеры различаются по классу.
Короткий фрагмент
$packet = Document::fromJSON($text);
$lines = $packet->get('lines');
$isList = $lines instanceof PackedArray;Что получит приёмщик
Пустой список принят. Для объекта вместо списка нужна исправленная ведомость.
Открыть фактический результат в HTML · Строки результата в JSON.
| Пакет | PHP в режиме array, затем JSON | PHP в режиме object | BSON из базы | Действие |
|---|---|---|---|---|
| S-LIST | [] | array | PackedArray | Пустая ведомость |
| S-OBJECT | [] | stdClass | Document | Запросить список |
На полке кладовщика ноль коробок может быть полным ответом. Объект вместо массива сообщает о неверной структуре, даже если в нём тоже ноль элементов. Проверку нужно сделать до сведения двух форм к одному представлению.
Границы решения
Из обычного пустого PHP-массива после json_decode(..., true) восстановить исходную разновидность контейнера невозможно. Пример относится к вложенному полю lines внутри документа.
Самостоятельная программа и ZIP
<?php
declare(strict_types=1);
require __DIR__.'/receipt-lab.php';
use MongoDB\Collection;
use MongoDB\BSON\{Document,PackedArray,Decimal128,UTCDateTime};
runReceipt('mongodb136-reconcile-empty-boundary', 'Сохранить разницу между пустым списком и пустой карточкой', ['Пакет', 'PHP в режиме array, затем JSON', 'PHP в режиме object', 'BSON из базы', 'Действие'],
function (Collection $receipts): array {
$incoming=['S-LIST'=>'{"lines":[]}','S-OBJECT'=>'{"lines":{}}'];
$rows=[];
foreach ($incoming as $id=>$text) {
$asArray=json_decode($text,true,512,JSON_THROW_ON_ERROR);
$asObject=json_decode($text,false,512,JSON_THROW_ON_ERROR);
$receipts->insertOne(['_id'=>$id,'packet'=>Document::fromJSON($text)]);
$fromDatabase=rawReceipt($receipts,$id)->get('packet')->get('lines');
$ordinary=is_array($asObject->lines)?'array':'stdClass';
$raw=match(true) { $fromDatabase instanceof PackedArray=>'PackedArray', $fromDatabase instanceof Document=>'Document', default=>'другой тип' };
$rows[]=[$id,json_encode($asArray['lines'],JSON_THROW_ON_ERROR),$ordinary,$raw,
$raw==='PackedArray'?'Пустая ведомость':'Запросить список'];
}
$checks['assoc_collapses_shapes'] = array_column($rows,1)===['[]','[]'];
$checks['json_object_mode'] = array_column($rows,2)===['array','stdClass'];
$checks['database_shape_retained'] = array_column($rows,3)===['PackedArray','Document'];
return [$rows,$checks,'Пустой список принят. Для объекта вместо списка нужна исправленная ведомость.'];
}
);
Ожидаемый результат
Пустой список принят. Для объекта вместо списка нужна исправленная ведомость.
Программа записывает учебные данные, читает их обратно из MongoDB и формирует result.html и result.json. Эти файлы содержат итог конкретного запуска.
Попробуйте изменить
Передайте третий пакет с "lines":null и выберите отдельную причину отказа. Значение null не реализует ни один класс контейнеров.
Как отличить пустую запись от пустого списка до преобразования в массив PHP? Сравниваем оба режима json_decode с двумя объектами BSON из действительно сохранённых документов. Постановка задачи на Stack Overflow.
Перенести цену и дату по Extended JSON и сверить их в MongoDB
Один отдел передаёт другому стоимость единицы и момент приёмки. После обмена цену должно быть можно обрабатывать как точное число, а дату — сравнивать как дату.
Что пришло от поставщика и сканера
Учебная цена 1499.90 хранится в Decimal128. Момент 1 июня 2026 в 08:30 UTC хранится как UTCDateTime. Исходная запись и копия имеют разные _id в одной коллекции.
- Получаем документ, действительно считанный из MongoDB. Канонический Extended JSON записывает метки $numberDecimal и $date со значением миллисекунд внутри $numberLong.
- Восстанавливаем BSON из строки, сохраняем вложенный документ под новым _id и читаем его отдельным запросом.
- Агрегация с $type проверяет типы на сервере у обоих документов, PHP отдельно сверяет классы и значения копии.
Короткий фрагмент
$transport = $stored->toCanonicalExtendedJSON();
$decoded = Document::fromJSON($transport);
$receipts->insertOne(['_id' => 'copy', 'line' => $decoded]);Что получит приёмщик
Копия из базы: 1499.90 и 2026-06-01 08:30:00 UTC. Типы decimal и date сохранены.
Открыть фактический результат в HTML · Строки результата в JSON.
| Поле | Прочитано в копии | Тип на сервере |
|---|---|---|
| Цена | 1499.90 | decimal |
| Дата приёмки, UTC | 2026-06-01 08:30:00 | date |
Строка «1499.90» в отчёте служит для отображения; в MongoDB это Decimal128. Формат времени для человека также применяется после чтения.
Границы решения
Договор касается полей одного документа. Каноническая строка не является резервной копией коллекции, индексов и прав. Стоимость указана в одной заранее согласованной валюте.
Самостоятельная программа и ZIP
<?php
declare(strict_types=1);
require __DIR__.'/receipt-lab.php';
use MongoDB\Collection;
use MongoDB\BSON\{Document,PackedArray,Decimal128,UTCDateTime};
runReceipt('mongodb136-reconcile-typed-transfer', 'Перенести цену и дату по Extended JSON и сверить их в MongoDB', ['Поле', 'Прочитано в копии', 'Тип на сервере'],
function (Collection $receipts): array {
$original=Document::fromPHP([
'sku'=>'LED','price'=>new Decimal128('1499.90'),
'receivedAt'=>new UTCDateTime(new DateTimeImmutable('2026-06-01T08:30:00Z')),
]);
$receipts->insertOne(['_id'=>'source','line'=>$original]);
$stored=rawReceipt($receipts,'source')->get('line');
$transport=$stored->toCanonicalExtendedJSON();
$decoded=Document::fromJSON($transport);
$receipts->insertOne(['_id'=>'copy','line'=>$decoded]);
$copy=rawReceipt($receipts,'copy')->get('line');
$serverTypes=$receipts->aggregate([
['$match'=>['_id'=>['$in'=>['source','copy']]]],
['$project'=>['_id'=>1,'priceType'=>['$type'=>'$line.price'],'dateType'=>['$type'=>'$line.receivedAt']]],
['$sort'=>['_id'=>1]],
])->toArray();
$checks['server_types_both_copies'] = count($serverTypes)===2 && array_column($serverTypes,'priceType')===['decimal','decimal'] && array_column($serverTypes,'dateType')===['date','date'];
$checks['round_trip_classes'] = $copy->get('price') instanceof Decimal128 && $copy->get('receivedAt') instanceof UTCDateTime;
$checks['round_trip_values'] = (string)$copy->get('price')===(string)$original->get('price') && (string)$copy->get('receivedAt')===(string)$original->get('receivedAt');
$rows=[['Цена',(string)$copy->get('price'),$serverTypes[0]['priceType']],
['Дата приёмки, UTC',$copy->get('receivedAt')->toDateTime()->format('Y-m-d H:i:s'),$serverTypes[0]['dateType']]];
return [$rows,$checks,'Копия из базы: 1499.90 и 2026-06-01 08:30:00 UTC. Типы decimal и date сохранены.'];
}
);
Ожидаемый результат
Копия из базы: 1499.90 и 2026-06-01 08:30:00 UTC. Типы decimal и date сохранены.
Программа записывает учебные данные, читает их обратно из MongoDB и формирует result.html и result.json. Эти файлы содержат итог конкретного запуска.
Попробуйте изменить
Сохраните цену отдельной строкой "1499.90" в дополнительной учебной записи и запросите $type: на экране значения будут похожи, но база различит string и decimal.
Собрать итоговую ведомость приёмщика
Перед закрытием поставки кладовщику нужен один экран. По нему можно сразу принять совпавшие артикулы, вернуться к спорной коробке и задать поставщику точный вопрос.
Что пришло от поставщика и сканера
В накладной идут LED × 2, CABLE × 3, LED × 1, FUSE × 2, TAPE × 1. Скан содержит TAPE × 1, LED × 3, CLIP × 1, CABLE × 1.
- Сначала сверяем документный номер и читаем оба источника в одной записи поставки.
- Суммируем повторный LED, сравниваем количества и выделяем артикулы, которые есть только у одной стороны.
- Первые строки оформляем в порядке накладной. После них идут дополнительные артикулы из скана.
Короткий фрагмент
$rows = reconcileDelivery($saved['invoice'], $saved['scan']);
$decisions = array_count_values(array_column($rows, 4));
$accepted = $decisions['Принять'] ?? 0;Что получит приёмщик
П-42: принять 2 артикула, проверить ещё 3.
Открыть фактический результат в HTML · Строки результата в JSON.
| Артикул | По накладной, шт. | По скану, шт. | Факт − план | Действие |
|---|---|---|---|---|
| LED | 3 | 3 | 0 | Принять |
| CABLE | 3 | 1 | -2 | Пересчитать |
| FUSE | 2 | 0 | -2 | Найти по накладной |
| TAPE | 1 | 1 | 0 | Принять |
| CLIP | 0 | 1 | 1 | Уточнить поставку |
На экране из двух полос видно, что для LED три запланированных штуки действительно найдены. У FUSE нет отсканированных единиц, у CLIP отсутствует план. Счётчик «2 артикула» означает две совпавшие группы, а не две штуки товара.
Границы решения
Ведомость отражает момент чтения и реальные в рамках примера исходные данные. Для нескольких накладных сначала ограничьте запрос нужной поставкой. Привязку чужого скана к документу проверяют на этапе приёма данных.
Самостоятельная программа и ZIP
<?php
declare(strict_types=1);
require __DIR__.'/receipt-lab.php';
use MongoDB\Collection;
use MongoDB\BSON\{Document,PackedArray,Decimal128,UTCDateTime};
require __DIR__.'/receiving-view.php';
runReceipt('mongodb136-reconcile-receiving-sheet', 'Собрать итоговую ведомость приёмщика', ['Артикул', 'По накладной, шт.', 'По скану, шт.', 'Факт − план', 'Действие'],
function (Collection $receipts): array {
$invoice=[['sku'=>'LED','qty'=>2],['sku'=>'CABLE','qty'=>3],['sku'=>'LED','qty'=>1],['sku'=>'FUSE','qty'=>2],['sku'=>'TAPE','qty'=>1]];
$scan=[['sku'=>'TAPE','qty'=>1],['sku'=>'LED','qty'=>3],['sku'=>'CLIP','qty'=>1],['sku'=>'CABLE','qty'=>1]];
$saved=storeAndRead($receipts,$invoice,$scan);
$rows=reconcileDelivery($saved['invoice'],$saved['scan']);
$decisions=array_count_values(array_column($rows,4));
$accepted=$decisions['Принять'] ?? 0;
$issues=count($rows)-$accepted;
$checks['receipt_reference'] = $saved['_id']==='П-42';
$checks['all_distinct_skus'] = array_column($rows,0)===['LED','CABLE','FUSE','TAPE','CLIP'];
$checks['business_decisions'] = $accepted===2 && $issues===3 && ($decisions['Пересчитать'] ?? 0)===1 && ($decisions['Найти по накладной'] ?? 0)===1 && ($decisions['Уточнить поставку'] ?? 0)===1;
$checks['quantity_audit'] = array_sum(array_column($rows,1))===9 && array_sum(array_column($rows,2))===6;
return [$rows,$checks,$saved['_id'].': принять '.$accepted.' артикула, проверить ещё '.$issues.'.'];
}, 'receivingSheetView'
);
Ожидаемый результат
П-42: принять 2 артикула, проверить ещё 3.
Программа записывает учебные данные, читает их обратно из MongoDB и формирует result.html и result.json. Эти файлы содержат итог конкретного запуска.
Попробуйте изменить
Добавьте скан FUSE × 2 и снова сформируйте ведомость. Убедитесь, что для FUSE действие сменится на «Принять», а расхождение CABLE сохранится.
К какому источнику вернуться после расхождения
У отсутствующего по скану FUSE проверьте маркировку коробки и исходные строки. Для лишнего CLIP сверьте документ поставки. Если сами строки не читаются как список, сначала вернитесь к формам документов поставщика. Для обмена между отделами сравните Document::toCanonicalExtendedJSON() и Document::toRelaxedExtendedJSON().
Источники и обсуждения
- MongoDB BSON Classes and Functions
- BSON Functions
- MongoDB BSON fromjson
- MongoDB BSON fromphp
- MongoDB BSON tophp
- MongoDB BSON tojson
- MongoDB BSON tocanonicalextendedjson
- MongoDB BSON torelaxedextendedjson
- Document
- Document::fromPHP
- Document::fromJSON
- Document::toCanonicalExtendedJSON
- MongoDB Extended JSON v2
- $type expression
- json_decode
- MongoDB BSON Int64
- MongoDB BSON VectorType
- MongoDB BSON Serializable
- MongoDB BSON dbpointer
- MongoDB BSON symbol
- MongoDB BSON undefined
- MongoDB BSON binaryinterface
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


