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

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

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

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

Глава 185 · После работы с массивами и чтения документа

MongoDB: сверка накладной и скана по артикулам

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

Почему две одинаковые поставки выглядят по-разному

В накладной поставщик указал три светильника LED в двух строках. В скане кладовщика те же три штуки находятся в одной строке и на другом месте. Приёмщику нужен ответ по каждой коробке. Начнём с одной строки, затем соберём ведомость для пяти артикулов.

Что происходит со строками П-42
  1. Поставщик прислал LED × 2 и ещё одну LED × 1
  2. Сканер записал LED × 3
  3. Из базы читаем обе стороны одной накладной и находим их по артикулу LED
  4. В строке 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, TimestampObjectId служит идентификатором, UTCDateTime хранит момент в миллисекундах. Timestamp — специальное значение MongoDB для времени и порядкового счётчика, у него другое назначение.
Бинарные данные и векторыBinary, VectorTypeУ Binary есть подтип. Перечисление VectorType (с 2.2.0) указывает формат данных вектора при подтипе TYPE_VECTOR.
Выражения и специальные значенияRegex, Javascript, MinKey, MaxKeyСохранённое регулярное выражение, значение BSON JavaScript и границы BSON-порядка сравнения имеют разные типы. Данные поставщика не исполняются как код.
Контракт собственных PHP-классовType, Serializable, Unserializable, PersistableType — внутренний базовый интерфейс. Для явного представления и восстановления полей своего объекта служат интерфейсы сериализации и материализации.
Контракты значенийBinaryInterface, Decimal128Interface, JavascriptInterface, MinKeyInterface, MaxKeyInterface, ObjectIdInterface, TimestampInterface, UTCDateTimeInterface, RegexInterfaceИменованные интерфейсы значений помогают типизировать параметры и возвращаемые данные в прикладных классах; конкретные члены перечислены в соответствующих карточках.
Данные старых коллекцийDBPointer, Symbol, UndefinedУстаревшие BSON-типы ещё могут встретиться при чтении. Соответствующие объекты создаёт драйвер при декодировании; для новой накладной используйте подходящие современные значения.

Если ваш счётчик должен показать, где потерялся товар, сначала проверьте форму поля, затем связывайте строки по артикулу. Для даты/цены через текстовый транспорт полезна сверка типов у исходника и у импортированной копии. Полное дерево BSON в официальном PHP Manual.

Шесть исторических BSON-функций и их замена

При переносе старого импортёра можно встретить функции в пространстве MongoDB\BSON. Они обрабатывали строку бинарного BSON. У класса Document своя форма результата: например, из fromPHP() приходит объект, а строковые BSON-байты при необходимости получают приведением (string). Имена ниже ведут к уже существующим карточкам миграции.

Историческая функцияРезультат и прежний контрактДействие в современном коде
fromPHPPHP-массив/объект → бинарная строка 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.

От входа к ведомости
  1. Сохраняем обе записи в одном документе поставки и читаем его по номеру П-42.
  2. Индекс 0 обозначает первую позицию. Ключ sku содержит артикул, qty — количество.
  3. Два строгих сравнения дают «Совпало» только при одинаковом артикуле и числе штук.

Короткий фрагмент

$expected = $saved['invoice'][0];
$seen = $saved['scan'][0];
$sameSku = $expected['sku'] === $seen['sku'];
$sameQty = $expected['qty'] === $seen['qty'];

Что получит приёмщик

П-42: по накладной две LED, принято две.

Открыть фактический результат в HTML · Строки результата в JSON.

ПоставкаАртикулПо накладнойНа приёмкеРешение
П-42LED22Совпало

Результат «Принято две» относится к исходному скану. Кнопка проведения складского остатка в этой задаче не участвует.

Границы решения

Прямое обращение к [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. Номер для человека считается с единицы.

От входа к ведомости
  1. Один проход по скану строит словарь артикул → количество и номера строк.
  2. Для каждой строки накладной получаем запись из словаря по её артикулу.
  3. Вывод остаётся в привычном документном порядке, с номером строки самого скана.

Короткий фрагмент

$scanIndex = tallyPositions($saved['scan']);
$key = 'sku:' . $line['sku'];
$actual = $scanIndex[$key] ?? null;

Что получит приёмщик

LED найдена во второй строке скана, CABLE — в первой.

Открыть фактический результат в HTML · Строки результата в JSON.

АртикулСтрока накладнойСтрока сканаПринято, шт.
LED122
CABLE213

Сортировать два массива не требуется. Поиск по артикулу переживает перестановку даже тогда, когда принято одинаковое количество разных товаров.

Границы решения

Здесь у каждого 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 принадлежат одной проверяемой поставке.

От входа к ведомости
  1. Для первого появления SKU начинаем с quantity = 0 и пустого списка positions.
  2. Оператор += прибавляет количество. Пустые квадратные скобки помещают очередной номер в конец списка.
  3. Контрольная сумма всей накладной остаётся равна четырём.

Короткий фрагмент

$index[$key]['quantity'] += $qty;
$index[$key]['positions'][] = $offset + 1;

Что получит приёмщик

LED: 3 штуки из строк 1 и 3. CABLE: 1 штука из строки 2.

Открыть фактический результат в HTML · Строки результата в JSON.

АртикулСтроки накладнойВсего, шт.
LED1, 33
CABLE21

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

Границы решения

Объединять можно только сопоставимые единицы одной поставки. Если в хозяйственном учёте различаются партии, серии или единицы измерения, они входят в ключ группировки.

Самостоятельная программа и ZIP
Объединить повторный SKU и показать исходные позицииСамостоятельный пример
<?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.

От входа к ведомости
  1. Связываем записи по артикулу, объединяя повторы внутри каждого источника.
  2. Вычитаем объявленное количество из фактически принятого.
  3. У каждой разницы остаются свой знак и свой артикул.

Короткий фрагмент

$delta = $fact - $plan;

Что получит приёмщик

LED: недобор 1. CABLE: излишек 2. TAPE: совпало.

Открыть фактический результат в HTML · Строки результата в JSON.

АртикулПланФактФакт − планИтог
LED32-1Недобор
CABLE242Излишек
TAPE110Совпало

Знак указывает направление разбора. Если сотрудник сравнит только суммы по всей поставке, излишек кабеля частично погасит недобор светильников.

Границы решения

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

Самостоятельная программа и 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. Нулевое число в ведомости означает, что с этой стороны строк нет.

От входа к ведомости
  1. Рассматриваем все ключи обеих сторон, включая артикулы только из скана.
  2. Если отсутствует строка скана, просим найти товар по накладной. Для неизвестного накладной товара уточняем поставку.
  3. Совпавший TAPE скрывается из списка расхождений.

Короткий фрагмент

$invoiceHasSku = isset($expected[$key]);
$scanHasSku = isset($seen[$key]);
$plan = $expected[$key]['quantity'] ?? 0;
$fact = $seen[$key]['quantity'] ?? 0;

Что получит приёмщик

FUSE не найден в скане. CLIP отсутствует в накладной.

Открыть фактический результат в HTML · Строки результата в JSON.

АртикулВ накладнойВ сканеДействие
FUSE20Найти по накладной
CLIP01Уточнить поставку

Простой перебор одних строк накладной никогда не увидел бы 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 и читаем обратно.

От входа к ведомости
  1. json_decode с true превращает вложенный объект и массив в одинаковый пустой PHP-массив.
  2. С false поле, исходно бывшее объектом JSON, остаётся экземпляром stdClass.
  3. Document::fromJSON понимает Extended JSON; в сохранённом BSON вложенные пустые контейнеры различаются по классу.

Короткий фрагмент

$packet = Document::fromJSON($text);
$lines = $packet->get('lines');
$isList = $lines instanceof PackedArray;

Что получит приёмщик

Пустой список принят. Для объекта вместо списка нужна исправленная ведомость.

Открыть фактический результат в HTML · Строки результата в JSON.

ПакетPHP в режиме array, затем JSONPHP в режиме objectBSON из базыДействие
S-LIST[]arrayPackedArrayПустая ведомость
S-OBJECT[]stdClassDocumentЗапросить список

На полке кладовщика ноль коробок может быть полным ответом. Объект вместо массива сообщает о неверной структуре, даже если в нём тоже ноль элементов. Проверку нужно сделать до сведения двух форм к одному представлению.

Границы решения

Из обычного пустого 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 в одной коллекции.

От входа к ведомости
  1. Получаем документ, действительно считанный из MongoDB. Канонический Extended JSON записывает метки $numberDecimal и $date со значением миллисекунд внутри $numberLong.
  2. Восстанавливаем BSON из строки, сохраняем вложенный документ под новым _id и читаем его отдельным запросом.
  3. Агрегация с $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.90decimal
Дата приёмки, UTC2026-06-01 08:30:00date

Строка «1499.90» в отчёте служит для отображения; в MongoDB это Decimal128. Формат времени для человека также применяется после чтения.

Границы решения

Договор касается полей одного документа. Каноническая строка не является резервной копией коллекции, индексов и прав. Стоимость указана в одной заранее согласованной валюте.

Самостоятельная программа и ZIP
Перенести цену и дату по Extended JSON и сверить их в MongoDBСамостоятельный пример
<?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.

От входа к ведомости
  1. Сначала сверяем документный номер и читаем оба источника в одной записи поставки.
  2. Суммируем повторный LED, сравниваем количества и выделяем артикулы, которые есть только у одной стороны.
  3. Первые строки оформляем в порядке накладной. После них идут дополнительные артикулы из скана.

Короткий фрагмент

$rows = reconcileDelivery($saved['invoice'], $saved['scan']);
$decisions = array_count_values(array_column($rows, 4));
$accepted = $decisions['Принять'] ?? 0;

Что получит приёмщик

П-42: принять 2 артикула, проверить ещё 3.

Открыть фактический результат в HTML · Строки результата в JSON.

АртикулПо накладной, шт.По скану, шт.Факт − планДействие
LED330Принять
CABLE31-2Пересчитать
FUSE20-2Найти по накладной
TAPE110Принять
CLIP011Уточнить поставку

На экране из двух полос видно, что для 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().

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

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

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

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

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