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

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

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

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

Глава 153 · После BSON-документов, списков и классов PHP

MongoDB: объекты PHP, карты типов и контролируемое восстановление

Serializable, Unserializable и Persistable на товаре, остатке, адресах и корзине; полный разбор BSON persistence, __pclass, проекций, инвариантов и версий схемы.

Зачем превращать данные документа в объект

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

Начальные понятия: BSON-документ и именованные поля, список и вложенные BSON-массивы, серверные insert и find. Здесь класс — описание PHP-объекта, интерфейс — набор обязательных методов, а DTO — небольшой объект с данными для конкретного действия/экрана. BSON-документ не хранит PHP-код методов. Мы не добавляем полноценную ODM-библиотеку: используем контракты самого расширения.

Сначала создадим одну карточку товара. class объявляет класс, new создаёт отдельный объект этого класса, public разрешает читать свойство снаружи. Свойство — значение, принадлежащее конкретному объекту; запись ->sku обращается к его артикулу. Подробные основы: класс, объект и первая карточка товара; public, protected и private; интерфейс как обязательный набор действий.

class ProductCard { public $sku = 'CUP'; }
$product = new ProductCard();
echo $product->sku; // CUP
$document = MongoDB\BSON\Document::fromPHP($product);
echo $document->get('sku'); // CUP

Здесь один public-артикул становится одним BSON-полем sku. Объект и документ — разные значения в памяти; база пока не используется. Если у объекта есть отладочное public-поле, по обычным правилам оно тоже попадёт в BSON. В первом полном сценарии Serializable задаёт явный список сохраняемых полей. implements означает, что класс обязуется предоставить метод интерфейса; метод — функция, принадлежащая объекту или классу.

Два направления преобразования
  1. PHP-объект товара
  2. bsonSerialize выбирает sku и price
  3. BSON-документ хранит только выбранные данные
  4. Карта типов либо __pclass выбирает класс чтения
  5. bsonUnserialize проверяет поля и назначает свойства
  6. Готовый PHP-объект используется экраном
$document = MongoDB\BSON\Document::fromPHP(['sku' => 'CUP', 'price' => 500]);
$data = $document->toPHP(['root' => 'array']);
echo $data['sku']; // CUP

Это простейший вариант без собственного класса: fromPHP получает поля, toPHP возвращает массив. Собственный класс нужен, когда появляется полезное правило — выбор сохраняемых полей, проверка количества, форматирование адреса или совместимость схемы. :: вызывает фабрику класса, -> вызывает метод объекта, => связывает имя поля/настройки со значением. Только после этой операции рассматриваем реализацию интерфейса.

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

Требуются PHP 8.4+ CLI и ext-mongodb 2.5+. Шесть сценариев codec-only не используют сервер. projected-object действительно записывает и читает MongoDB 8.0 на отдельном локальном порту 27075; его README содержит установку, запуск mongod, переменные окружения и cleanup. Composer не нужен. В текущем драйвере методы интерфейсов должны иметь совместимые возвращаемые типы.

php --ri mongodb
php example.php

Для программы с базой после запуска отдельного локального mongod:

$env:PHPMAN_MONGODB_URI='mongodb://127.0.0.1:27075/?directConnection=true&retryWrites=false'
$env:PHPMAN_MONGODB_DATABASE='php_manual_lab'
php example.php

Выбрать поля хранения, не выгружая весь объект

У объекта товара есть цена для покупателя, служебная отладочная пометка и внутренние данные. В BSON нужно отправить ровно sku/price. Сравним обычное правило публичных свойств с явным списком сохраняемых полей.

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

PlainProduct120 содержит public sku/debug, protected supplierCost и private apiToken. StoredProduct120 хранит sku/price внутри объекта и сам выбирает их в bsonSerialize. Токен в примере синтетический.

Путь данных и вызовов
  1. Обычный объект → только public sku/debug
  2. Serializable → вызов bsonSerialize()
  3. Метод возвращает sku/price и исключает debug/apiToken
  4. Document::fromPHP кодирует выбранный результат
  5. Короткая таблица показывает реально вошедшие поля

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

public function bsonSerialize(): array {
    return ['sku' => $this->sku, 'price' => $this->price];
} // Фрагмент класса: debug и apiToken в результат не включены.

Видимый результат

СпособСохранённые поляАртикул
Обычный объектsku, debugCUP
Serializablesku, priceCUP

Почему получается именно так

implements объявляет контракт класса: драйвер может вызвать его публичный bsonSerialize. Метод не получает аргументов, возвращает данные для хранения и не выполняет insert. Он имеет доступ к private-свойствам своего объекта; поэтому можно сохранить нужное закрытое поле, не открывая всё наружу. Отсутствующие в возвращённом массиве поля не попадают в BSON. Дополнительный Tags120 показывает правило формы: возвращённый список во вложенном поле становится BSON-массивом; тот же список на корневом уровне документа превращается в поля "0", "1".

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

public — видимость PHP, а не разрешение публиковать поле в API. Явный список сохранения и отдельная проекция экрана решают разные задачи. Не используйте (array)$object для вытаскивания приватных данных: PHP добавляет к именам служебные нулевые байты. BSON-сериализация не вызывает PHP __serialize или JsonSerializable автоматически. В этом сценарии сервер не используется.

Полная программа, helper и архив примера
Выбрать поля хранения, не выгружая весь объектСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mo120-helper.php';
$runtime=mo120Runtime();
final class PlainProduct120 {
    public string $sku='CUP'; public string $debug='temporary';
    protected int $supplierCost=300; private string $apiToken='synthetic-token';
}
final class StoredProduct120 implements MongoDB\BSON\Serializable {
    public string $debug='temporary';
    public function __construct(private string $sku,private int $price,private string $apiToken) {}
    public function bsonSerialize(): array { return ['sku'=>$this->sku,'price'=>$this->price]; }
}
final class Tags120 implements MongoDB\BSON\Serializable {
    public function bsonSerialize(): array { return ['red','gift']; }
}
$plain=MongoDB\BSON\Document::fromPHP(new PlainProduct120());
$product=new StoredProduct120('CUP',500,'synthetic-token');
$selected=MongoDB\BSON\Document::fromPHP($product);
$rootList=MongoDB\BSON\Document::fromPHP(new Tags120());
$embedded=MongoDB\BSON\Document::fromPHP(['tags'=>new Tags120()]);
$rows=[['Обычный объект',implode(', ',array_keys($plain->toPHP(['root'=>'array']))),$plain->get('sku')],
    ['Serializable',implode(', ',array_keys($selected->toPHP(['root'=>'array']))),$selected->get('sku')]];
$checks=['public_only'=>$plain->has('debug') && !$plain->has('supplierCost') && !$plain->has('apiToken'),
    'allowlisted_fields'=>$selected->get('price')===500 && !$selected->has('debug') && !$selected->has('apiToken'),
    'serializable_is_type'=>$product instanceof MongoDB\BSON\Type,
    'root_is_document'=>$rootList->has('0') && $rootList->get('0')==='red',
    'embedded_list'=>$embedded->get('tags') instanceof MongoDB\BSON\PackedArray];
$details=['Обычный объект в BSON'=>$plain->toRelaxedExtendedJSON(),'Выбранные поля'=>$selected->toRelaxedExtendedJSON(),
    'Последовательность как корень'=>$rootList->toRelaxedExtendedJSON(),'Последовательность во вложенном поле'=>$embedded->toRelaxedExtendedJSON()];
mo120Report('mongodb120-objects-stored-fields',['Способ', 'Сохранённые поля', 'Артикул'],$rows,$checks,$runtime,$details);
echo 'mongodb120-objects-stored-fields: saved result.html and result.json
';
Ожидаемый результат
mongodb120-objects-stored-fields: saved result.html and result.json

Запустите php example.php; откройте result.html. Все классы находятся в программе, helper и README входят в ZIP. Режим codec-only не проверяет запись на сервер; projected-object выполняет реальный insert/find и удаляет свою коллекцию.

Открыть результат запуска · Таблица JSON

Упражнение с направлением решения. Добавьте поле displayLabel в объект, но не сохраняйте его. Решение: вычислять его для экрана и не включать в возвращаемый массив. Проверьте has("displayLabel") у Document: false.

Практический вопрос: Error saving object with private member to MongoDb with PHP Driver. Автору нужно хранить часть данных объекта, у которого есть private-поля и служебная ссылка. Наш пример отделяет сохраняемые поля от внутреннего состояния через современный Serializable; старый mongo/1.2.6 и обходы преобразования объекта в массив не переносятся в решение. Технические правила проверены по текущим первичным источникам; исторический ответ не является готовым кодом для ext-mongodb 2.x.

Восстановить объект с проверкой, не надеясь на конструктор

Из BSON пришёл остаток CUP=2 без валюты. Нужен пригодный к работе объект: количество неотрицательное, валюта RUB по умолчанию. Неправильный остаток LAMP=-1 должен быть отклонён.

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

Документ с полями sku и quantity. Класс StockRow120 реализует Unserializable, а root в карте типов указывает его имя. Счётчик в конструкторе помогает увидеть, запускался ли он при восстановлении.

Путь данных и вызовов
  1. toPHP читает BSON и карту root=StockRow120
  2. Драйвер выделяет объект без обычного конструктора
  3. bsonUnserialize получает ассоциативный массив полей
  4. Проверки разрешают quantity=2 и назначают currency=RUB
  5. Неверное количество прерывает восстановление исключением

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

$document = MongoDB\BSON\Document::fromPHP(['sku' => 'CUP', 'quantity' => 2]);
$row = $document->toPHP(['root' => StockRow120::class]);
echo $row->quantity; // 2; проверка выполнена в bsonUnserialize, не в __construct.

Видимый результат

Артикул/ситуацияКоличествоВалюта/результатВызовов конструктора
CUP2RUB0
LAMP: отказотрицательное количествонет объекта0

Почему получается именно так

bsonUnserialize(array $data): void играет роль инициализатора восстановленного объекта. __construct не вызывается; значит, проверки и умолчания из одного лишь конструктора здесь не помогут. Полная программа сначала проверяет все входные значения и только потом назначает свойства. Это не приведение строки "2" к числу: контракт требует int. Для новой сущности обычный конструктор может выполнять ту же доменную проверку через общий метод; здесь механизмы разведены, чтобы разница была видна.

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

Нельзя просто foreach присвоить все входные поля свойствам: так теряется контроль над допустимыми именами, типами и состоянием. Типизированные свойства без назначения остаются неинициализированными. Для _id из реальной базы также нужен явный контракт; в этом небольшом остатке _id не требуется. Исключение проверки данных не нужно маскировать как ошибку соединения.

Полная программа, helper и архив примера
Восстановить объект с проверкой, не надеясь на конструкторСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mo120-helper.php';
$runtime=mo120Runtime();
final class StockRow120 implements MongoDB\BSON\Unserializable {
    public static int $constructorCalls=0;
    public string $sku; public int $quantity; public string $currency;
    public function __construct() { self::$constructorCalls++; $this->currency='USD'; }
    public function bsonUnserialize(array $data): void {
        if (!isset($data['sku'],$data['quantity']) || !is_string($data['sku']) || $data['sku']===''
            || !is_int($data['quantity']) || $data['quantity']<0) { throw new UnexpectedValueException('Invalid stock row'); }
        $currency=$data['currency']??'RUB';
        if (!is_string($currency) || !in_array($currency,['RUB','USD'],true)) { throw new UnexpectedValueException('Invalid currency'); }
        $this->sku=$data['sku']; $this->quantity=$data['quantity']; $this->currency=$currency;
    }
}
$source=MongoDB\BSON\Document::fromPHP(['sku'=>'CUP','quantity'=>2]);
$stock=$source->toPHP(['root'=>StockRow120::class]);
$rejected=false;
try { MongoDB\BSON\Document::fromPHP(['sku'=>'LAMP','quantity'=>-1])->toPHP(['root'=>StockRow120::class]); }
catch (UnexpectedValueException $e) { $rejected=true; }
$rows=[[$stock->sku,$stock->quantity,$stock->currency,StockRow120::$constructorCalls],['LAMP: отказ','отрицательное количество','нет объекта',StockRow120::$constructorCalls]];
$checks=['mapped_class'=>$stock instanceof StockRow120,'default_applied'=>$stock->currency==='RUB',
    'constructor_not_called'=>StockRow120::$constructorCalls===0,'invalid_rejected'=>$rejected];
$details=['Вход'=>['sku'=>'CUP','quantity'=>2],'Почему RUB'=>'Умолчание назначено в bsonUnserialize; __construct не вызывался'];
mo120Report('mongodb120-objects-restore-invariants',['Артикул/ситуация', 'Количество', 'Валюта/результат', 'Вызовов конструктора'],$rows,$checks,$runtime,$details);
echo 'mongodb120-objects-restore-invariants: saved result.html and result.json
';
Ожидаемый результат
mongodb120-objects-restore-invariants: saved result.html and result.json

Запустите php example.php; откройте result.html. Все классы находятся в программе, helper и README входят в ZIP. Режим codec-only не проверяет запись на сервер; projected-object выполняет реальный insert/find и удаляет свою коллекцию.

Открыть результат запуска · Таблица JSON

Упражнение с направлением решения. Передайте currency=USD — она сохранится. Затем передайте currency=123. Решение: второй документ должен быть отклонён до выдачи объекта; конструктор и в этих случаях не является проверкой.

Практический вопрос: Fetch MongoDB Driver Cursor objects directly into specified class. Читатель хочет получить свой класс вместо stdClass и ожидает вызова конструктора с аргументами. Мы показываем явную карту, реальный bsonUnserialize без конструктора и обязательную проверку полей до выдачи объекта. Технические правила проверены по текущим первичным источникам; исторический ответ не является готовым кодом для ext-mongodb 2.x.

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

Заказ содержит домашний и рабочий адрес, а также служебный документ audit. Требуется получить Address120 только в адресах доставки, сохранив audit обычным массивом.

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

В destinations находится список записей label/address. В каждом address есть city/postcode. Карта fieldPaths задаёт путь destinations.$.address; знак $ означает любой элемент этого списка.

Путь данных и вызовов
  1. Корень и обычные документы → PHP array
  2. По пути destinations.$.address выбран Address120
  3. Каждый адрес проходит bsonUnserialize
  4. Метод label() формирует строку для доставки
  5. Соседний audit не превращается в адрес

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

$map = ['root' => 'array', 'document' => 'array',
    'fieldPaths' => ['destinations.$.address' => Address120::class]];
$delivery = $document->toPHP($map); // Address120 только в адресах назначений.

Видимый результат

НазначениеPHP-класс адресаМетка для доставки
homeAddress120300000, Тула
officeAddress120101000, Москва

Почему получается именно так

root касается только верхнего документа, document — вложенных документов, array — BSON-массивов. Если задать document=Address120 глобально, в адрес попытается превратиться и audit. fieldPaths точнее: он выбирает только нужное место структуры. Восстановленные адреса имеют метод label и проверенные строковые поля; внешний список остаётся обычным PHP-массивом, его обходит foreach. Назначение карты не меняет данные на сервере.

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

Путь — часть схемы приложения, не произвольный параметр пользователя. Если данные перестали соответствовать этому пути, нужно обновить контракт и проверки. bson разрешён в root/document/array, но не в fieldPaths. Пользовательский класс должен существовать, быть конкретным и реализовывать Unserializable. Возможный __pclass у доверенных Persistable-данных имеет отдельный приоритет, описанный ниже.

Полная программа, helper и архив примера
Восстановить адреса только по выбранному вложенному путиСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mo120-helper.php';
$runtime=mo120Runtime();
final class Address120 implements MongoDB\BSON\Unserializable {
    public string $city; public string $postcode;
    public function bsonUnserialize(array $data): void {
        if (!isset($data['city'],$data['postcode']) || !is_string($data['city']) || !is_string($data['postcode'])) { throw new UnexpectedValueException('Invalid address'); }
        $this->city=$data['city']; $this->postcode=$data['postcode'];
    }
    public function label(): string { return $this->postcode.', '.$this->city; }
}
$document=MongoDB\BSON\Document::fromPHP(['destinations'=>[
    ['label'=>'home','address'=>['city'=>'Тула','postcode'=>'300000']],
    ['label'=>'office','address'=>['city'=>'Москва','postcode'=>'101000']],
],'audit'=>['city'=>'служебная подпись']]);
$mapped=$document->toPHP(['root'=>'array','document'=>'array','array'=>'array',
    'fieldPaths'=>['destinations.$.address'=>Address120::class]]);
$rows=[];
foreach ($mapped['destinations'] as $place) { $rows[]=[$place['label'],get_debug_type($place['address']),$place['address']->label()]; }
$checks=['two_addresses'=>count($rows)===2 && $mapped['destinations'][0]['address'] instanceof Address120 && $mapped['destinations'][1]['address'] instanceof Address120,
    'unrelated_document_untouched'=>is_array($mapped['audit']) && $mapped['audit']['city']==='служебная подпись',
    'labels_correct'=>$rows===[['home','Address120','300000, Тула'],['office','Address120','101000, Москва']]];
$details=['Путь назначения'=>'destinations.$.address','Остальное поле audit'=>$mapped['audit'],
    'Правило'=>'$ означает каждый элемент массива; не все документы проекта становятся Address120'];
mo120Report('mongodb120-objects-nested-paths',['Назначение', 'PHP-класс адреса', 'Метка для доставки'],$rows,$checks,$runtime,$details);
echo 'mongodb120-objects-nested-paths: saved result.html and result.json
';
Ожидаемый результат
mongodb120-objects-nested-paths: saved result.html and result.json

Запустите php example.php; откройте result.html. Все классы находятся в программе, helper и README входят в ZIP. Режим codec-only не проверяет запись на сервер; projected-object выполняет реальный insert/find и удаляет свою коллекцию.

Открыть результат запуска · Таблица JSON

Упражнение с направлением решения. Добавьте третье назначение pickup с адресом пункта выдачи. Решение: тот же путь с $ восстановит третий Address120; добавление другого вложенного документа само по себе не делает его адресом.

Практический вопрос: MongoDB PHP: return document property as an array instead of multiple properties. После чтения списка на клиент приходит объект с числовыми полями. Сценарий выбирает формы списка, вложенных документов и отдельных адресов осознанной картой типов; повторное преобразование через JSON или правка vendor-классов не требуются. Технические правила проверены по текущим первичным источникам; исторический ответ не является готовым кодом для ext-mongodb 2.x.

Сохранить тип объекта и разобраться в приоритете __pclass

Внутри одного PHP-приложения нужно сохранить товар и затем восстановить именно его класс без повторного указания root. Посмотрим, откуда берётся эта идентичность и когда можно отключить автоматический выбор класса.

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

Product120 реализует Persistable, возвращает _id/sku и намеренно неверную строку __pclass. Идентификатор — заранее заданный ObjectId. Дополнительный Fallback120 нужен для наблюдения приоритета карты.

Путь данных и вызовов
  1. bsonSerialize выбирает данные товара
  2. Драйвер добавляет Binary __pclass с именем Product120 и subtype 0x80
  3. toPHP по умолчанию восстанавливает Product120
  4. Валидный маркер перекрывает custom root=Fallback120
  5. Явные array/object/bson отключают этот выбор на соответствующем уровне

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

$bson = MongoDB\BSON\Document::fromPHP($product); // Product120 implements Persistable
$restored = $bson->toPHP(); // Product120 по __pclass
$plain = $bson->toPHP(['root' => 'array']); // Массив без вызова Product120::bsonUnserialize

Видимый результат

Карта восстановленияПолученный PHP-типАртикул
По умолчаниюProduct120CUP
root = Fallback120Product120CUP
root = arrayarrayCUP
root = objectstdClassCUP
root = bsonMongoDB\BSON\DocumentCUP

Почему получается именно так

Persistable объединяет Serializable и Unserializable. Служебный __pclass добавляется самим драйвером и перезаписывает одноимённое значение результата bsonSerialize. Это не строка JSON с именем класса, а BSON Binary subtype 0x80. При восстановлении корректный маркер с доступным Persistable-классом может иметь приоритет над явно указанным пользовательским классом карты. Но специальные карты array/object/bson не запускают этот механизм на своём уровне. Результат — новый PHP-объект с тем же сохранённым ID, а не та же ссылка на объект в памяти.

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

Маркер связывает хранилище с PHP-именем класса: переименование/удаление класса требует миграции или совместимости. Не считайте его безопасным механизмом для любого внешнего BSON: восстановление может задействовать автозагрузку и пользовательский метод. root=array не нейтрализует автоматически все вложенные документы; для нейтрального дерева явно задают document/array тоже. BSON Type — базовый интерфейс без методов; его напрямую не реализуют вместо Serializable.

Полная программа, helper и архив примера
Сохранить тип объекта и разобраться в приоритете __pclassСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mo120-helper.php';
$runtime=mo120Runtime();
final class Product120 implements MongoDB\BSON\Persistable {
    public static int $constructorCalls=0;
    public MongoDB\BSON\ObjectId $id; public string $sku;
    public function __construct(MongoDB\BSON\ObjectId $id,string $sku) { self::$constructorCalls++; $this->id=$id; $this->sku=$sku; }
    public function bsonSerialize(): array { return ['_id'=>$this->id,'sku'=>$this->sku,'__pclass'=>'will be replaced']; }
    public function bsonUnserialize(array $data): void {
        if (!($data['_id']??null) instanceof MongoDB\BSON\ObjectId || !is_string($data['sku']??null)) { throw new UnexpectedValueException('Invalid product'); }
        $this->id=$data['_id']; $this->sku=$data['sku'];
    }
}
final class Fallback120 implements MongoDB\BSON\Unserializable {
    public array $data;
    public function bsonUnserialize(array $data): void { $this->data=$data; }
}
$product=new Product120(new MongoDB\BSON\ObjectId('000000000000000000000120'),'CUP');
$document=MongoDB\BSON\Document::fromPHP($product); $marker=$document->get('__pclass');
$automatic=$document->toPHP(); $custom=$document->toPHP(['root'=>Fallback120::class]);
$asArray=$document->toPHP(['root'=>'array','document'=>'array','array'=>'array']);
$asObject=$document->toPHP(['root'=>'object','document'=>'object','array'=>'array']);
$asBson=$document->toPHP(['root'=>'bson']);
$rows=[['По умолчанию',get_debug_type($automatic),'CUP'],['root = Fallback120',get_debug_type($custom),'CUP'],
    ['root = array',get_debug_type($asArray),$asArray['sku']],['root = object',get_debug_type($asObject),$asObject->sku],['root = bson',get_debug_type($asBson),$asBson->get('sku')]];
$checks=['marker_injected'=>$marker instanceof MongoDB\BSON\Binary && $marker->getType()===0x80 && $marker->getData()===Product120::class,
    'automatic_identity'=>$automatic instanceof Product120 && (string)$automatic->id==='000000000000000000000120',
    'marker_overrides_custom_class'=>$custom instanceof Product120,
    'explicit_neutral_types'=>is_array($asArray) && $asObject instanceof stdClass && $asBson instanceof MongoDB\BSON\Document,
    'constructor_only_original'=>Product120::$constructorCalls===1,
    'builtins_are_type'=>$product instanceof MongoDB\BSON\Type && $product->id instanceof MongoDB\BSON\Type];
$details=['Маркер'=>['class'=>$marker->getData(),'binarySubtype'=>$marker->getType()],
    'ID исходного и восстановленного'=>[(string)$product->id,(string)$automatic->id],
    'Граница доверия'=>'Все классы и данные этого примера принадлежат программе; маркер не принимается из внешнего запроса'];
mo120Report('mongodb120-objects-persistable-identity',['Карта восстановления', 'Полученный PHP-тип', 'Артикул'],$rows,$checks,$runtime,$details);
echo 'mongodb120-objects-persistable-identity: saved result.html and result.json
';
Ожидаемый результат
mongodb120-objects-persistable-identity: saved result.html and result.json

Запустите php example.php; откройте result.html. Все классы находятся в программе, helper и README входят в ZIP. Режим codec-only не проверяет запись на сервер; projected-object выполняет реальный insert/find и удаляет свою коллекцию.

Открыть результат запуска · Таблица JSON

Упражнение с направлением решения. Замените карту root=Fallback120 на root=array и сравните тип. Решение: получится массив с обычным элементом __pclass; сохранённый маркер никуда не исчез, просто не используется для выбора класса.

Проекция товара: когда класс восстанавливается, а когда нет

Экран товара показывает артикул и цену. Не нужно скачивать внутреннее примечание. При этом иногда экран ожидает готовый объект класса. Покажем четыре варианта и отдельный отказ неполной проекции.

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

Один реально записанный товар 701/CUP/500 копеек. Persistable добавляет __pclass. Пять ограниченных запросов одного товара сравнивают сохранение маркера, явную карту и отсутствие обязательной price.

Путь данных и вызовов
  1. insert сохраняет товар и служебный маркер
  2. Фильтр _id=701 и limit=1 ограничивают выборку
  3. Проекция без __pclass → обычный stdClass
  4. Проекция с маркером либо явной картой → ScreenProduct120
  5. Отсутствующая price нарушает контракт восстановления и отклоняется

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

$query = new MongoDB\Driver\Query(['_id' => 701], [
    'projection' => ['_id' => 0, 'sku' => 1, 'price' => 1, '__pclass' => 1], 'limit' => 1,
]); // __pclass сохраняет автоматический выбор класса; price нужен его проверке.

Видимый результат

Проекция/картаPHP-типАртикулЦена, копейки
Без __pclassstdClassCUP500
С __pclassScreenProduct120CUP500
Без маркера, явный rootScreenProduct120CUP500
Маркер + root=arrayarrayCUP500

Почему получается именно так

Автоматическое восстановление работает только по данным, которые действительно вернул запрос. Если projection исключила __pclass, драйвер не знает исходного класса: по умолчанию получится stdClass. Можно либо включить маркер, либо назначить доверенный root-класс явно. При нейтральной карте array маркер остаётся данными. Служебный internal не нужен ни одному экрану и не выбирается. _id в нашем DTO допускается отсутствующим и получает 0; это явное учебное правило отображения, а не идентификатор для последующей записи.

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

Проекция должна соответствовать минимальному набору полей конкретного объекта. Не подставляйте нули/пустые строки за любую отсутствующую бизнес-информацию ради подавления ошибки. Для кратких карточек часто лучше отдельный DTO чтения, чем неполная сущность заказа. Проекция не заменяет авторизацию. Запись и чтение происходят в собственной коллекции; finally удаляет только её.

Полная программа, helper и архив примера
Проекция товара: когда класс восстанавливается, а когда нетСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mo120-helper.php';
$runtime=mo120Runtime();
final class ScreenProduct120 implements MongoDB\BSON\Persistable {
    public int $id; public string $sku; public int $price;
    public function __construct(int $id,string $sku,int $price) { $this->id=$id; $this->sku=$sku; $this->price=$price; }
    public function bsonSerialize(): array { return ['_id'=>$this->id,'sku'=>$this->sku,'price'=>$this->price,'internal'=>'not for screen']; }
    public function bsonUnserialize(array $data): void {
        if (!is_string($data['sku']??null) || !is_int($data['price']??null) || $data['price']<0) { throw new UnexpectedValueException('Screen product requires sku and price'); }
        $this->id=is_int($data['_id']??null)?$data['_id']:0; $this->sku=$data['sku']; $this->price=$data['price'];
    }
}
[$manager,$database]=mo120Database(); $runtime['server']=mo120Command($manager,$database,['buildInfo'=>1])[0]['version']; $runtime['execution']='database';
$collection='mongodb120_objects_'.bin2hex(random_bytes(8)); $namespace=$database.'.'.$collection; $owned=false;
try {
    if (mo120Exists($manager,$database,$collection)) { throw new RuntimeException('Fixture exists'); }
    mo120Command($manager,$database,['create'=>$collection],true); $owned=true;
    $bulk=new MongoDB\Driver\BulkWrite(); $bulk->insert(new ScreenProduct120(701,'CUP',500));
    $write=$manager->executeBulkWrite($namespace,$bulk);
    $read=function(array $projection,?array $map=null) use ($manager,$namespace): mixed {
        $cursor=$manager->executeQuery($namespace,new MongoDB\Driver\Query(['_id'=>701],['projection'=>$projection,'limit'=>1,'maxTimeMS'=>1000]));
        if ($map!==null) { $cursor->setTypeMap($map); }
        $found=$cursor->toArray(); if (count($found)!==1) { throw new RuntimeException('One product expected'); }
        return $found[0];
    };
    $without=$read(['_id'=>0,'sku'=>1,'price'=>1]);
    $with=$read(['_id'=>0,'sku'=>1,'price'=>1,'__pclass'=>1]);
    $explicit=$read(['_id'=>0,'sku'=>1,'price'=>1],['root'=>ScreenProduct120::class]);
    $neutral=$read(['_id'=>0,'sku'=>1,'price'=>1,'__pclass'=>1],['root'=>'array','document'=>'array','array'=>'array']);
    $incompleteRejected=false;
    try { $read(['_id'=>0,'sku'=>1,'__pclass'=>1]); } catch (UnexpectedValueException $e) { $incompleteRejected=true; }
    $rows=[['Без __pclass',get_debug_type($without),$without->sku,$without->price],
        ['С __pclass',get_debug_type($with),$with->sku,$with->price],
        ['Без маркера, явный root',get_debug_type($explicit),$explicit->sku,$explicit->price],
        ['Маркер + root=array',get_debug_type($neutral),$neutral['sku'],$neutral['price']]];
    $checks=['inserted'=>$write->getInsertedCount()===1,'without_marker_plain'=>$without instanceof stdClass,
        'with_marker_typed'=>$with instanceof ScreenProduct120,'explicit_map_typed'=>$explicit instanceof ScreenProduct120,
        'neutral_mapping'=>is_array($neutral),'private_screen_fields_absent'=>!property_exists($without,'internal') && !array_key_exists('internal',$neutral),
        'missing_required_field_rejected'=>$incompleteRejected];
    $details=['Отвергнутая проекция'=>'sku + __pclass без обязательной price',
        'Данные экрана'=>['sku'=>'CUP','price'=>500],'Факт записи'=>'Один insert и ограниченные запросы одного товара'];
} finally { if ($owned) { mo120Command($manager,$database,['drop'=>$collection],true); } }
$checks['owned_collection_removed']=!mo120Exists($manager,$database,$collection);
mo120Report('mongodb120-objects-projected-object',['Проекция/карта', 'PHP-тип', 'Артикул', 'Цена, копейки'],$rows,$checks,$runtime,$details);
echo 'mongodb120-objects-projected-object: saved result.html and result.json
';
Ожидаемый результат
mongodb120-objects-projected-object: saved result.html and result.json

Запустите php example.php; откройте result.html. Все классы находятся в программе, helper и README входят в ZIP. Режим codec-only не проверяет запись на сервер; projected-object выполняет реальный insert/find и удаляет свою коллекцию.

Открыть результат запуска · Таблица JSON

Упражнение с направлением решения. Сделайте класс короткой карточки, которому нужна только sku. Решение: отдельный Unserializable DTO с собственным контрактом и нейтральная/согласованная проекция; нельзя просто отключить проверку price у модели, которая на неё полагается.

Отделить ошибку карты классов от неправильных данных

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

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

Один документ quantity=2 и три ошибочные карты: отсутствующий класс, DateTimeImmutable без нужного интерфейса, сам абстрактный интерфейс. Отдельный документ содержит строковое quantity="2". Ещё два показывают неизвестный и известный __pclass.

Путь данных и вызовов
  1. Проверка карты обнаруживает отсутствующий/неподходящий класс
  2. Допустимая карта передаёт поля в bsonUnserialize
  3. Неверный тип quantity отклоняет прикладной валидатор
  4. Неизвестный Persistable-маркер по умолчанию даёт stdClass
  5. Полностью нейтральная карта читает известный маркер без пользовательского callback

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

$data = $document->toPHP(['root' => 'array', 'document' => 'array', 'array' => 'array']);
// Затем собственная проверка разрешённых полей и типов.
// Имя класса не берём из внешнего запроса или непроверенного __pclass.

Видимый результат

Вход/картаРезультатНаблюдение
MissingClass120ошибка картыобъект не получен
DateTimeImmutableошибка картыобъект не получен
MongoDB\BSON\Unserializableошибка картыобъект не получен
quantity = строка 2ошибка данныхобъект не получен
Неизвестный __pclassstdClassquantity = 2
Известный __pclass, root=arrayarrayвызовов восстановления: 0

Почему получается именно так

InvalidArgumentException драйвера здесь означает недопустимую конфигурацию typeMap. UnexpectedValueException в примере выбрасывает наш метод проверки quantity; это отдельный слой. Неизвестный класс внутри __pclass ведёт себя иначе, чем неизвестный класс явно заданной карты: по умолчанию маркер не восстанавливается в этот класс, используется stdClass. Если имя известно и реализует Persistable, его метод мог бы запуститься. Поэтому для внешнего источника сначала выбирают нейтральные структуры, проверяют форму и только потом явно создают разрешённые модели.

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

Названия классов в этом запуске заданы программой; обработчики не делают внешних действий. Нейтральная карта не является общей песочницей для произвольного PHP: это ограничение конкретного механизма BSON-декодирования. Binary subtype и строковое имя класса не служат доказательством доверия. Не используйте PHP unserialize для недоверенных входных данных.

Полная программа, helper и архив примера
Отделить ошибку карты классов от неправильных данныхСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mo120-helper.php';
$runtime=mo120Runtime();
final class ValidQuantity120 implements MongoDB\BSON\Unserializable {
    public int $quantity;
    public function bsonUnserialize(array $data): void {
        if (!is_int($data['quantity']??null) || $data['quantity']<0) { throw new UnexpectedValueException('Quantity must be a nonnegative int'); }
        $this->quantity=$data['quantity'];
    }
}
final class KnownClass120 implements MongoDB\BSON\Persistable {
    public static int $hydrations=0;
    public function bsonSerialize(): array { return ['quantity'=>2]; }
    public function bsonUnserialize(array $data): void { self::$hydrations++; }
}
$document=MongoDB\BSON\Document::fromPHP(['quantity'=>2]); $rows=[]; $mapErrors=0;
foreach (['MissingClass120',DateTimeImmutable::class,MongoDB\BSON\Unserializable::class] as $class) {
    try { $document->toPHP(['root'=>$class]); throw new RuntimeException('Expected mapping rejection'); }
    catch (MongoDB\Driver\Exception\InvalidArgumentException $e) { $mapErrors++; $rows[]=[$class,'ошибка карты','объект не получен']; }
}
$badData=false;
try { MongoDB\BSON\Document::fromPHP(['quantity'=>'2'])->toPHP(['root'=>ValidQuantity120::class]); }
catch (UnexpectedValueException $e) { $badData=true; $rows[]=['quantity = строка 2','ошибка данных','объект не получен']; }
$unknown=MongoDB\BSON\Document::fromPHP(['quantity'=>2,'__pclass'=>new MongoDB\BSON\Binary('MissingPersistable120',0x80)])->toPHP();
$safe=MongoDB\BSON\Document::fromPHP(['quantity'=>2,'__pclass'=>new MongoDB\BSON\Binary(KnownClass120::class,0x80)])
    ->toPHP(['root'=>'array','document'=>'array','array'=>'array']);
$rows[]=['Неизвестный __pclass',get_debug_type($unknown),'quantity = 2'];
$rows[]=['Известный __pclass, root=array',get_debug_type($safe),'вызовов восстановления: '.KnownClass120::$hydrations];
$checks=['three_mapping_errors'=>$mapErrors===3,'invalid_value_rejected'=>$badData,
    'unknown_marker_fallback'=>$unknown instanceof stdClass && $unknown->quantity===2,
    'neutral_decode_no_user_callback'=>is_array($safe) && $safe['quantity']===2 && KnownClass120::$hydrations===0];
$details=['Разные уровни отказа'=>['нельзя создать указанный класс','класс есть, но значение quantity недопустимо'],
    'Маркер не является разрешением'=>'Не подставляйте имя класса из HTTP или недоверенного файла в typeMap; нейтральное чтение предшествует собственной валидации'];
mo120Report('mongodb120-objects-rejected-mapping',['Вход/карта', 'Результат', 'Наблюдение'],$rows,$checks,$runtime,$details);
echo 'mongodb120-objects-rejected-mapping: saved result.html and result.json
';
Ожидаемый результат
mongodb120-objects-rejected-mapping: saved result.html and result.json

Запустите php example.php; откройте result.html. Все классы находятся в программе, helper и README входят в ZIP. Режим codec-only не проверяет запись на сервер; projected-object выполняет реальный insert/find и удаляет свою коллекцию.

Открыть результат запуска · Таблица JSON

Упражнение с направлением решения. Укажите root=ValidQuantity120 для quantity=0. Решение: объект допустим, поскольку проверяется quantity>=0. Строка "0" должна по-прежнему отклоняться; решение о преобразовании внешнего текста принимается раньше.

Прочитать старую и новую корзину единым объектом

Ранее корзина хранила items/qty, новая версия приложения использует lines/quantity. Нужно читать обе формы, получать одно количество товаров и при следующем сохранении создавать только новую форму.

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

Старая корзина без schemaVersion и новая с schemaVersion=2 содержат два CUP и один LAMP. OrderSnapshot120 реализует обе стороны преобразования, но не Persistable: имя PHP-класса в данные не добавляется.

Путь данных и вызовов
  1. Версия отсутствует → применяем явное правило версии 1
  2. Версия 1 читает items/qty, версия 2 читает lines/quantity
  3. Проверяются список, SKU и положительное количество
  4. Объект хранит единый внутренний список
  5. bsonSerialize всегда выдаёт schemaVersion=2 и lines

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

$order = $oldDocument->toPHP(['root' => OrderSnapshot120::class, 'document' => 'array']);
$currentDocument = MongoDB\BSON\Document::fromPHP($order);
// Старые items/qty прочитаны; новая форма schemaVersion=2 содержит lines/quantity.

Видимый результат

Источник/операцияВерсия формыВсего единиц товара
Старые items/qty13
Новые lines/quantity23
Повторная сериализация23

Почему получается именно так

Восстановление — удобная граница нормализации версии схемы. Важно не молча угадывать любую форму: неизвестная версия 3 в примере отвергается. Сначала собирается полностью проверенный новый список, затем он присваивается объекту. Благодаря document=array элементы вложенного списка приходят в ожидаемой форме. Обе версии дают сумму 3, но повторная сериализация старой создаёт только новые ключи. Исходный BSON-документ при этом неизменен.

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

Это преобразование одной записи в памяти, а не миграция всей коллекции. Реальное обновление требует отдельного фильтра по ID/версии, контроля конкурентных изменений и плана совместимости старого кода. Не удаляйте неизвестные поля без решения об их назначении. В этой учебной модели сериализатор намеренно задаёт разрешённую схему целиком.

Полная программа, helper и архив примера
Прочитать старую и новую корзину единым объектомСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mo120-helper.php';
$runtime=mo120Runtime();
final class OrderSnapshot120 implements MongoDB\BSON\Serializable,MongoDB\BSON\Unserializable {
    private array $lines=[];
    public function bsonUnserialize(array $data): void {
        $version=$data['schemaVersion']??1;
        if (!in_array($version,[1,2],true)) { throw new UnexpectedValueException('Unsupported schema'); }
        $input=$version===1?($data['items']??null):($data['lines']??null);
        if (!is_array($input) || !array_is_list($input)) { throw new UnexpectedValueException('Lines must be a list'); }
        $normalized=[];
        foreach ($input as $line) {
            if (!is_array($line)) { throw new UnexpectedValueException('Line must be a document'); }
            $quantity=$version===1?($line['qty']??null):($line['quantity']??null);
            if (!is_string($line['sku']??null) || $line['sku']==='' || !is_int($quantity) || $quantity<1) { throw new UnexpectedValueException('Invalid line'); }
            $normalized[]=['sku'=>$line['sku'],'quantity'=>$quantity];
        }
        $this->lines=$normalized;
    }
    public function bsonSerialize(): array { return ['schemaVersion'=>2,'lines'=>$this->lines]; }
    public function quantity(): int { return array_sum(array_column($this->lines,'quantity')); }
}
$old=MongoDB\BSON\Document::fromPHP(['items'=>[['sku'=>'CUP','qty'=>2],['sku'=>'LAMP','qty'=>1]]]);
$new=MongoDB\BSON\Document::fromPHP(['schemaVersion'=>2,'lines'=>[['sku'=>'CUP','quantity'=>2],['sku'=>'LAMP','quantity'=>1]]]);
$map=['root'=>OrderSnapshot120::class,'document'=>'array','array'=>'array'];
$oldObject=$old->toPHP($map); $newObject=$new->toPHP($map);
$normalized=MongoDB\BSON\Document::fromPHP($oldObject);
$wrongVersion=false;
try { MongoDB\BSON\Document::fromPHP(['schemaVersion'=>3,'lines'=>[]])->toPHP($map); }
catch (UnexpectedValueException $e) { $wrongVersion=true; }
$rows=[['Старые items/qty',1,$oldObject->quantity()],['Новые lines/quantity',2,$newObject->quantity()],['Повторная сериализация', $normalized->get('schemaVersion'),$normalized->get('lines')->get(0)->get('quantity')+$normalized->get('lines')->get(1)->get('quantity')]];
$checks=['same_business_result'=>$oldObject->quantity()===3 && $newObject->quantity()===3,
    'new_shape_written'=>$normalized->get('schemaVersion')===2 && $normalized->has('lines') && !$normalized->has('items'),
    'source_not_mutated'=>$old->has('items') && !$old->has('schemaVersion'),
    'future_schema_rejected'=>$wrongVersion,'no_class_marker'=>!$normalized->has('__pclass')];
$details=['Новая форма'=>$normalized->toRelaxedExtendedJSON(),
    'Что не произошло'=>'Изменение формы сделано только в памяти; автоматической миграции всех документов базы нет'];
mo120Report('mongodb120-objects-schema-upgrade',['Источник/операция', 'Версия формы', 'Всего единиц товара'],$rows,$checks,$runtime,$details);
echo 'mongodb120-objects-schema-upgrade: saved result.html and result.json
';
Ожидаемый результат
mongodb120-objects-schema-upgrade: saved result.html and result.json

Запустите php example.php; откройте result.html. Все классы находятся в программе, helper и README входят в ZIP. Режим codec-only не проверяет запись на сервер; projected-object выполняет реальный insert/find и удаляет свою коллекцию.

Открыть результат запуска · Таблица JSON

Упражнение с направлением решения. Добавьте необязательное поле note с умолчанием пустой строки в версии 2. Решение: проверить его тип в bsonUnserialize и явно включать в bsonSerialize; не копировать все произвольные поля входа автоматически.

Полные правила: запись объектов, карты типов и приоритет __pclass

Этот раздел собирает правила официального «Serialization and deserialization of PHP variables into MongoDB». Сначала определяется BSON-форма при записи, затем способ восстановления PHP. Вложенный список, корневой документ и PHP-класс — разные уровни решения.

1. PHP-массивы при записи

ВходВложенное BSON-значениеПочему
[] или [0=>"red",1=>"gift"]МассивКлючи образуют список с нуля
[0=>"red",2=>"gift"]Документ с полями "0" и "2"Есть пропуск
[1=>"red",0=>"gift"]ДокументПорядок ключей не соответствует списку
["sku"=>"CUP"]ДокументИменованные поля
Тот же список в корне Document::fromPHPКорневой документ с числовыми именами полейКорень BSON-записи всегда документ

Проверка списка касается порядка и ключей, а не типов значений. Пустой список и пустой документ различаются. Осознанное array_values подходит для позиций, но уничтожит смысл ID в ключах словаря. Подробные примеры — PackedArray и пропуски ключей.

2. PHP-объекты при записи

ОбъектПравило
stdClassBSON-документ с его полями
Обычный PHP-классСохраняются только public-свойства; protected/private пропускаются
Поддерживаемый BSON TypeДрайвер использует встроенное кодирование конкретного типа, например ObjectId или Decimal128
Произвольный класс, напрямую реализовавший только BSON TypeНе становится новым BSON-типом; неизвестное кодирование отклоняется UnexpectedValueException
SerializableДрайвер вызывает bsonSerialize; результат задаёт сохраняемые данные вместо public-свойств
PersistableТе же правила плюс Binary __pclass; итог всегда документ

BSON Type не имеет методов и не предназначен для прямой реализации прикладным классом. Встроенный ObjectId, Binary и другие скалярные BSON-типы обычно являются значениями полей; попытка использовать такой объект вместо корневого документа отклоняется. Современные контейнеры Document/PackedArray и результаты Serializable имеют отдельные правила: прямой PackedArray не разрешён вместо документа, а PackedArray, возвращённый Serializable::bsonSerialize, при корневом использовании преобразуется в документ. Во вложенном поле он остаётся массивом.

Текущий Serializable::bsonSerialize возвращает array|stdClass|Document|PackedArray. У вложенного array список становится BSON-массивом, словарь — документом. Корневой результат всегда документ. Persistable::bsonSerialize возвращает array|stdClass|Document, но не PackedArray: служебное поле идентичности требует документа. Кодирование не является insert; для корневой сущности полезно явно определить _id. При отсутствии _id вставка/upsert может получить созданный драйвером/сервером ObjectId соответственно.

3. Что именно означает __pclass

Драйвер Persistable записывает полностью квалифицированное PHP-имя класса в BSON Binary с subtype 0x80. Он перезаписывает __pclass, который вернул ваш bsonSerialize. Если вам нужен собственный одноимённый бизнес-контракт без автоматического Persistable, не реализуйте Persistable. При восстановлении данные, переданные bsonUnserialize, включают __pclass, если он присутствует; код должен выбирать нужные поля, а не копировать всё.

Значение __pclassПоведение стандартной карты
Нет поляОбычный BSON-документ становится stdClass
Строка либо Binary с subtype не 0x80Обычное поле, без выбора класса
Binary 0x80, доступный конкретный Persistable-классВосстанавливается этот класс; вызывается bsonUnserialize
Binary 0x80, класс отсутствует или не является PersistableПо умолчанию stdClass с сохранёнными данными маркера
Проекция не вернула __pclassАвтоматической информации о классе нет

4. Восстановление: область карты и приоритет

По умолчанию BSON-массив становится PHP array, а документ без применимого __pclass — stdClass. Это сохраняет различие между {"0":"CUP"} и ["CUP"], которое старое расширение mongo могло терять, выдавая массивы в обоих случаях. Карта задаётся Cursor::setTypeMap или аргументом Document::toPHP/PackedArray::toPHP; исторический MongoDB\BSON\toPHP также принимал карту, но удалён в ext-mongodb 2.0.

Ключ картыОбласть
rootТолько верхний BSON-документ
documentВложенные BSON-документы
arrayBSON-массивы
fieldPathsОтдельные пути, например addresses.$.city; $ означает элемент массива
Значение картыЧто получается и что с __pclass
Не задано или nullСтандартное правило: массив → array; документ → stdClass либо применимый Persistable
arrayPHP array; __pclass остаётся данными и не выбирает класс на этом уровне
object или stdClassstdClass; автоматический выбор класса на этом уровне выключен
bsonDocument/PackedArray без пользовательского восстановления; разрешено для root/document/array, не для fieldPaths
Имя конкретного Unserializable-классаДанные передаются в bsonUnserialize этого класса, если корректный Persistable-маркер не выбрал другой

Недоступный, абстрактный класс/интерфейс или класс без Unserializable в явной карте приводит к InvalidArgumentException. Для доступного Persistable из __pclass действует приоритет над пользовательским классом карты, в том числе когда маркер обозначает другой совместимый класс/подкласс. Если маркер не даёт применимого Persistable, корректно заданный класс карты остаётся путём восстановления. Это отличается от специальных array/object/bson, которые подавляют пользовательский выбор. Для нейтрального дерева нужно согласовать все три уровня, а не только root.

Драйвер не вызывает __construct при bsonUnserialize. Метод получает массив полей, обязан инициализировать нужные свойства и проверить бизнес-инварианты; его современный return type — void. Не доверяйте чужому __pclass или имени класса из HTTP: возможны автозагрузка и запуск пользовательского кода. Для внешних файлов сначала используйте нейтральную карту/raw BSON, проверяйте схему и создавайте только разрешённые приложением модели.

5. Ограничения BSON и совместимость старого кода

BSON технически способен содержать повторяющиеся имена полей, потому что хранит последовательность пар ключ/значение. Не создавайте такие документы: поведение сервера/драйвера может быть неопределённым, а PHP array/object не может сохранить два одинаковых ключа без потерь. Нейтральная карта эту проблему не исправляет.

В старом обзорном тексте встречаются сигнатуры bsonSerialize(): object и реализации без возвращаемого типа. Они иллюстрируют старые версии, но не являются готовым кодом для ext-mongodb 2.x. С 1.15 типы стали tentative на PHP 8+, с 1.17 общий object заменён конкретным stdClass и добавлены BSON-контейнеры, с 2.0 возвращаемые типы обязательны. #[ReturnTypeWillChange] был переходным средством подавления предупреждения, а не заменой совместимой сигнатуры в 2.x. Текущие сигнатуры приведены в карточках ниже; допустим суженный тип array, как в наших примерах.

Не смешивайте это с PHP Serializable/serialize/unserialize или JsonSerializable: одинаковое слово «сериализация» не делает протоколы взаимозаменяемыми. BSON хранит данные и при Persistable имя класса, но не код методов и не открытое соединение. Для переноса со старых функций см. миграцию BSON API на современные контейнеры.

Четыре интерфейса и три контракта методов

У интерфейса Type нет методов. У остальных приведены текущие сигнатуры, результаты, ошибки, версии и два применения в полных программах.

КонтрактДля чего нужен
MongoDB\BSON\TypeРазличить базовый BSON-интерфейс и прикладной сериализатор.
MongoDB\BSON\SerializableОпределить сохраняемые данные вместо автоматического набора public-свойств.
MongoDB\BSON\UnserializableСделать класс допустимой целью явной карты BSON-декодирования.
MongoDB\BSON\PersistableСвязать обе стороны преобразования и автоматическую идентичность класса.
MongoDB\BSON\Serializable::bsonSerializeВернуть конкретные данные, которые драйвер закодирует в BSON.
MongoDB\BSON\Unserializable::bsonUnserializeИнициализировать восстановленный объект и проверить его инварианты.
MongoDB\BSON\Persistable::bsonSerializeВернуть данные документа с автоматически добавляемой идентичностью класса.

Выбрать минимально достаточный уровень преобразования

Начните с нейтральных полей и понятной схемы. Serializable добавляет контролируемую запись, Unserializable — проверенное восстановление, Persistable — привязку к PHP-классу в хранилище. Эта привязка полезна только там, где вы контролируете данные, классы и их совместимость. Точная карта нужна и для частичных экранов, и для внешних источников.

Далее: перенос старых BSON-функций на современные контейнеры, безопасная условная правка документа, точные BSON-значения. Создание новой PHP-формы ещё не обновляет базу: сохранение и миграция выполняются явным отдельным действием.

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

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

MongoDB\BSON\TypeРазличить базовый BSON-интерфейс и прикладной сериализатор.

Интерфейс/метод существует с ext-mongodb 1.0.0. Примеры используют PHP 8.4+ и ext-mongodb 2.5+. · mongodb

interface MongoDB\BSON\Type {}

Различить базовый BSON-интерфейс и прикладной сериализатор.

interface MongoDB\BSON\Type {}

Параметры

Параметров нет; у интерфейса правила вызова относятся к объявленным в нём методам.

Результат и поведение

Не вызывается и не создаётся через new. Это общий интерфейс поддерживаемых BSON-типов, у него нет методов.

Ошибки и границы

Не следует реализовывать напрямую: неизвестное драйверу кодирование собственного Type отклоняется UnexpectedValueException. Для своего объекта выбирают Serializable либо Persistable.

Версии

Интерфейс/метод существует с ext-mongodb 1.0.0. Примеры используют PHP 8.4+ и ext-mongodb 2.5+.

Это MongoDB\BSON\Type, не проверка SQL-типа и не произвольная метка класса. Некоторые встроенные значения нельзя использовать вместо корневого BSON-документа. Полные правила и исключение Serializable разобраны в разделе persistence.

Проверить контракт собственного сериализатора

$isBsonType = $product instanceof MongoDB\BSON\Type;

StoredProduct120 является BSON Type через Serializable; кодирование задаёт его bsonSerialize.

Исходные данные и полный сценарий · Результат выполнения

Отличить встроенный идентификатор от обычной строки

$isBsonType = $product->id instanceof MongoDB\BSON\Type;

ObjectId является встроенным BSON-типом с собственной сериализацией и применяется значением поля _id.

Исходные данные и полный сценарий · Результат выполнения

Полная карта правил сериализации и восстановления

MongoDB\BSON\SerializableОпределить сохраняемые данные вместо автоматического набора public-свойств.

Интерфейс/метод существует с ext-mongodb 1.0.0. Примеры используют PHP 8.4+ и ext-mongodb 2.5+. С 1.15.0 возвращаемые типы были tentative на PHP 8+, с 2.0.0 они обязательны. · mongodb

interface MongoDB\BSON\Serializable extends MongoDB\BSON\Type {
    public function bsonSerialize(): array|stdClass|MongoDB\BSON\Document|MongoDB\BSON\PackedArray;
}

Определить сохраняемые данные вместо автоматического набора public-свойств.

interface MongoDB\BSON\Serializable extends MongoDB\BSON\Type {
    public function bsonSerialize(): array|stdClass|MongoDB\BSON\Document|MongoDB\BSON\PackedArray;
}

Параметры

Параметров нет; у интерфейса правила вызова относятся к объявленным в нём методам.

Результат и поведение

Интерфейс не создаёт объект и не пишет в базу. Реализующий класс отдаёт представление через один метод bsonSerialize.

Ошибки и границы

Несовместимая сигнатура реализации в 2.x нарушает контракт PHP. Недопустимое значение возвращаемого типа отклоняется; исключения пользовательского метода не следует маскировать.

Версии

Интерфейс/метод существует с ext-mongodb 1.0.0. Примеры используют PHP 8.4+ и ext-mongodb 2.5+. С 1.15.0 возвращаемые типы были tentative на PHP 8+, с 2.0.0 они обязательны.

С 1.17.0 общий object заменён конкретным stdClass, добавлены Document/PackedArray. Суженный array допустим. Не путайте с глобальным PHP\Serializable и JsonSerializable. Возвращаемый список в корне всё равно является документом, во вложенном поле — массивом.

Сохранить только поля товара

public function bsonSerialize(): array {
    return ['sku'=>$this->sku, 'price'=>$this->price];
}

BSON содержит sku/price; debug и apiToken исключены явным решением класса.

Исходные данные и полный сценарий · Результат выполнения

Всегда выдавать новую схему корзины

public function bsonSerialize(): array {
    return ['schemaVersion'=>2, 'lines'=>$this->lines];
}

Старые items/qty после восстановления сериализуются в согласованные lines/quantity, без __pclass.

Исходные данные и полный сценарий · Результат выполнения

Полная карта правил сериализации и восстановления

MongoDB\BSON\UnserializableСделать класс допустимой целью явной карты BSON-декодирования.

Интерфейс/метод существует с ext-mongodb 1.0.0. Примеры используют PHP 8.4+ и ext-mongodb 2.5+. С 1.15.0 возвращаемые типы были tentative на PHP 8+, с 2.0.0 они обязательны. · mongodb

interface MongoDB\BSON\Unserializable {
    public function bsonUnserialize(array $data): void;
}

Сделать класс допустимой целью явной карты BSON-декодирования.

interface MongoDB\BSON\Unserializable {
    public function bsonUnserialize(array $data): void;
}

Параметры

Параметров нет; у интерфейса правила вызова относятся к объявленным в нём методам.

Результат и поведение

Интерфейс содержит один инициализирующий метод. Класс может быть выбран для root, document, array или конкретного fieldPaths.

Ошибки и границы

Явная карта требует существующий конкретный класс с этим интерфейсом; иначе InvalidArgumentException. Ошибки данных нужно проверять внутри реализации.

Версии

Интерфейс/метод существует с ext-mongodb 1.0.0. Примеры используют PHP 8.4+ и ext-mongodb 2.5+. С 1.15.0 возвращаемые типы были tentative на PHP 8+, с 2.0.0 они обязательны.

__construct не вызывается при BSON-восстановлении. Этот интерфейс сам не сообщает имя класса BSON-документу; для явной карты маркер не нужен. Persistable дополнительно хранит __pclass.

Проверить остаток до выдачи объекта

$row = $document->toPHP(['root'=>StockRow120::class]);

quantity=2 принимается, -1 отклоняется; currency получает RUB, конструктор не вызывается.

Исходные данные и полный сценарий · Результат выполнения

Дать адресам поведение только по нужному пути

$map = ['root'=>'array','document'=>'array',
    'fieldPaths'=>['destinations.$.address'=>Address120::class]];
$delivery = $document->toPHP($map);

Адреса получают label(), а audit остаётся обычным массивом.

Исходные данные и полный сценарий · Результат выполнения

Полная карта правил сериализации и восстановления

MongoDB\BSON\PersistableСвязать обе стороны преобразования и автоматическую идентичность класса.

Интерфейс/метод существует с ext-mongodb 1.0.0. Примеры используют PHP 8.4+ и ext-mongodb 2.5+. · mongodb

interface MongoDB\BSON\Persistable extends MongoDB\BSON\Serializable, MongoDB\BSON\Unserializable {
    public function bsonSerialize(): array|stdClass|MongoDB\BSON\Document;
    public function bsonUnserialize(array $data): void;
}

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

interface MongoDB\BSON\Persistable extends MongoDB\BSON\Serializable, MongoDB\BSON\Unserializable {
    public function bsonSerialize(): array|stdClass|MongoDB\BSON\Document;
    public function bsonUnserialize(array $data): void;
}

Параметры

Параметров нет; у интерфейса правила вызова относятся к объявленным в нём методам.

Результат и поведение

При записи драйвер добавляет Binary __pclass, при чтении может выбрать этот PHP-класс и передать ему данные. Результат сериализации всегда документ.

Ошибки и границы

PackedArray не допускается в возвращаемом типе Persistable::bsonSerialize. Несовместимая сигнатура/возвращаемое значение не являются способом создать массив. Пользовательская проверка восстановления может отказать.

Версии

Интерфейс/метод существует с ext-mongodb 1.0.0. Примеры используют PHP 8.4+ и ext-mongodb 2.5+.

__pclass — полностью квалифицированное имя класса в Binary subtype 0x80, перезаписывающее одноимённое возвращённое поле. Корректный Persistable-маркер приоритетнее пользовательского класса typeMap, но нейтральные array/object/bson подавляют dispatch на своём уровне. Маркер должен входить в проекцию для автоматического выбора.

Восстановить идентичность Product120

$document = MongoDB\BSON\Document::fromPHP($product);
$restored = $document->toPHP();

Маркер Binary 0x80 позволяет восстановить Product120 и его ObjectId без явного root-класса.

Исходные данные и полный сценарий · Результат выполнения

Сохранить тип после ограниченной проекции

$projection = ['_id'=>0,'sku'=>1,'price'=>1,'__pclass'=>1];

Запрос возвращает нужные поля и маркер; результатом становится ScreenProduct120. Неполная price-проекция отклоняется.

Исходные данные и полный сценарий · Результат выполнения

Полная карта правил сериализации и восстановления

MongoDB\BSON\Serializable::bsonSerializeВернуть конкретные данные, которые драйвер закодирует в BSON.

Интерфейс/метод существует с ext-mongodb 1.0.0. Примеры используют PHP 8.4+ и ext-mongodb 2.5+. С 1.15.0 возвращаемые типы были tentative на PHP 8+, с 2.0.0 они обязательны. · mongodb

abstract public MongoDB\BSON\Serializable::bsonSerialize(): array|stdClass|MongoDB\BSON\Document|MongoDB\BSON\PackedArray

Вернуть конкретные данные, которые драйвер закодирует в BSON.

abstract public MongoDB\BSON\Serializable::bsonSerialize(): array|stdClass|MongoDB\BSON\Document|MongoDB\BSON\PackedArray

Параметры

Параметров нет; у интерфейса правила вызова относятся к объявленным в нём методам.

Результат и поведение

array, stdClass, Document или PackedArray. Во вложенном поле список кодируется массивом, ассоциативная форма — документом. Корневой результат всегда документ.

Ошибки и границы

В 2.x неправильная PHP-сигнатура или возврат вне разрешённого union нарушают обязательный тип. Старое описание UnexpectedValueException для любого не-array/stdClass относится к прежним API и не отменяет новые разрешённые контейнеры.

Версии

Интерфейс/метод существует с ext-mongodb 1.0.0. Примеры используют PHP 8.4+ и ext-mongodb 2.5+. С 1.15.0 возвращаемые типы были tentative на PHP 8+, с 2.0.0 они обязательны.

Метод вызывается при BSON-сериализации, не при json_encode. С 1.17 доступны Document/PackedArray и уточнён stdClass вместо object. PackedArray, возвращённый этим методом в корневом контексте, преобразуется в документ; во вложенном контексте остаётся массивом. Для корневой сущности разумно включать _id; если его нет, вставка/upsert могут создать ObjectId. Ни данные соединения, ни внешний ввод не должны произвольно добавлять поля.

Сохранить только поля товара

public function bsonSerialize(): array {
    return ['sku'=>$this->sku, 'price'=>$this->price];
}

BSON содержит sku/price; debug и apiToken исключены явным решением класса.

Исходные данные и полный сценарий · Результат выполнения

Всегда выдавать новую схему корзины

public function bsonSerialize(): array {
    return ['schemaVersion'=>2, 'lines'=>$this->lines];
}

Старые items/qty после восстановления сериализуются в согласованные lines/quantity, без __pclass.

Исходные данные и полный сценарий · Результат выполнения

Полная карта правил сериализации и восстановления

MongoDB\BSON\Unserializable::bsonUnserializeИнициализировать восстановленный объект и проверить его инварианты.

Интерфейс/метод существует с ext-mongodb 1.0.0. Примеры используют PHP 8.4+ и ext-mongodb 2.5+. С 1.15.0 возвращаемые типы были tentative на PHP 8+, с 2.0.0 они обязательны. · mongodb

abstract public MongoDB\BSON\Unserializable::bsonUnserialize(array $data): void

Инициализировать восстановленный объект и проверить его инварианты.

abstract public MongoDB\BSON\Unserializable::bsonUnserialize(array $data): void

Параметры

ПараметрНазначение
$data: arrayПоля исходного BSON-документа/массива; может включать _id и __pclass. Формы вложенных значений определяются картой типов.

Результат и поведение

void. Возвращаемое значение не используется для подмены объекта; метод должен назначить проверенные свойства текущего $this.

Ошибки и границы

Специальный класс исключения для бизнес-проверок интерфейс не навязывает. В примерах UnexpectedValueException означает недопустимые данные; ошибка карты драйвера — отдельный InvalidArgumentException.

Версии

Интерфейс/метод существует с ext-mongodb 1.0.0. Примеры используют PHP 8.4+ и ext-mongodb 2.5+. С 1.15.0 возвращаемые типы были tentative на PHP 8+, с 2.0.0 они обязательны.

Заменяет обычный конструктор на пути восстановления; __construct после него не вызывается. Проверяйте _id там, где он обязателен, не оставляйте типизированные свойства неинициализированными и не выполняйте массовое присвоение всех внешних ключей.

Проверить остаток до выдачи объекта

$row = $document->toPHP(['root'=>StockRow120::class]);

quantity=2 принимается, -1 отклоняется; currency получает RUB, конструктор не вызывается.

Исходные данные и полный сценарий · Результат выполнения

Принять две явные версии схемы

$version = $data['schemaVersion'] ?? 1;
// В реализации: проверить версию, список и каждую строку, затем присвоить normalized lines.

Старые и новые документы дают одинаковый итог 3; версия 3 отклоняется без выдачи неверного объекта.

Исходные данные и полный сценарий · Результат выполнения

Полная карта правил сериализации и восстановления

MongoDB\BSON\Persistable::bsonSerializeВернуть данные документа с автоматически добавляемой идентичностью класса.

Интерфейс/метод существует с ext-mongodb 1.0.0. Примеры используют PHP 8.4+ и ext-mongodb 2.5+. С 1.17.0 допускается Document наряду с array/stdClass; с 2.0.0 возвращаемый тип обязателен. · mongodb

abstract public MongoDB\BSON\Persistable::bsonSerialize(): array|stdClass|MongoDB\BSON\Document

Вернуть данные документа с автоматически добавляемой идентичностью класса.

abstract public MongoDB\BSON\Persistable::bsonSerialize(): array|stdClass|MongoDB\BSON\Document

Параметры

Параметров нет; у интерфейса правила вызова относятся к объявленным в нём методам.

Результат и поведение

array, stdClass или Document, всегда кодируемый документом. Драйвер добавляет поле с именем PHP-класса.

Ошибки и границы

PackedArray исключён из допустимого возвращаемого типа: служебное поле не может быть добавлено к такому представлению. На текущем PHP/драйвере соблюдайте обязательную сигнатуру и return type.

Версии

Интерфейс/метод существует с ext-mongodb 1.0.0. Примеры используют PHP 8.4+ и ext-mongodb 2.5+. С 1.17.0 допускается Document наряду с array/stdClass; с 2.0.0 возвращаемый тип обязателен.

Даже последовательный возвращённый array из-за __pclass сохраняется документом. Рекомендуется определить _id для корневой сущности; без него insert/upsert могут получить созданный ObjectId. Метод не должен выполнять insert самостоятельно: сериализация может вызываться для локального преобразования, повторной отправки и вложенных значений.

Восстановить идентичность Product120

$document = MongoDB\BSON\Document::fromPHP($product);
$restored = $document->toPHP();

Маркер Binary 0x80 позволяет восстановить Product120 и его ObjectId без явного root-класса.

Исходные данные и полный сценарий · Результат выполнения

Сохранить тип после ограниченной проекции

$projection = ['_id'=>0,'sku'=>1,'price'=>1,'__pclass'=>1];

Запрос возвращает нужные поля и маркер; результатом становится ScreenProduct120. Неполная price-проекция отклоняется.

Исходные данные и полный сценарий · Результат выполнения

Полная карта правил сериализации и восстановления

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

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

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

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

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