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

Дерево руководства
Глава 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 означает, что класс обязуется предоставить метод интерфейса; метод — функция, принадлежащая объекту или классу.
- PHP-объект товара
- bsonSerialize выбирает sku и price
- BSON-документ хранит только выбранные данные
- Карта типов либо __pclass выбирает класс чтения
- bsonUnserialize проверяет поля и назначает свойства
- Готовый 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. Токен в примере синтетический.
- Обычный объект → только public sku/debug
- Serializable → вызов bsonSerialize()
- Метод возвращает sku/price и исключает debug/apiToken
- Document::fromPHP кодирует выбранный результат
- Короткая таблица показывает реально вошедшие поля
Короткая полезная операция
public function bsonSerialize(): array {
return ['sku' => $this->sku, 'price' => $this->price];
} // Фрагмент класса: debug и apiToken в результат не включены.Видимый результат
| Способ | Сохранённые поля | Артикул |
|---|---|---|
| Обычный объект | sku, debug | CUP |
| Serializable | sku, price | CUP |
Почему получается именно так
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 в карте типов указывает его имя. Счётчик в конструкторе помогает увидеть, запускался ли он при восстановлении.
- toPHP читает BSON и карту root=StockRow120
- Драйвер выделяет объект без обычного конструктора
- bsonUnserialize получает ассоциативный массив полей
- Проверки разрешают quantity=2 и назначают currency=RUB
- Неверное количество прерывает восстановление исключением
Короткая полезная операция
$document = MongoDB\BSON\Document::fromPHP(['sku' => 'CUP', 'quantity' => 2]);
$row = $document->toPHP(['root' => StockRow120::class]);
echo $row->quantity; // 2; проверка выполнена в bsonUnserialize, не в __construct.Видимый результат
| Артикул/ситуация | Количество | Валюта/результат | Вызовов конструктора |
|---|---|---|---|
| CUP | 2 | RUB | 0 |
| 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; знак $ означает любой элемент этого списка.
- Корень и обычные документы → PHP array
- По пути destinations.$.address выбран Address120
- Каждый адрес проходит bsonUnserialize
- Метод label() формирует строку для доставки
- Соседний audit не превращается в адрес
Короткая полезная операция
$map = ['root' => 'array', 'document' => 'array',
'fieldPaths' => ['destinations.$.address' => Address120::class]];
$delivery = $document->toPHP($map); // Address120 только в адресах назначений.Видимый результат
| Назначение | PHP-класс адреса | Метка для доставки |
|---|---|---|
| home | Address120 | 300000, Тула |
| office | Address120 | 101000, Москва |
Почему получается именно так
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 нужен для наблюдения приоритета карты.
- bsonSerialize выбирает данные товара
- Драйвер добавляет Binary __pclass с именем Product120 и subtype 0x80
- toPHP по умолчанию восстанавливает Product120
- Валидный маркер перекрывает custom root=Fallback120
- Явные 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-тип | Артикул |
|---|---|---|
| По умолчанию | Product120 | CUP |
| root = Fallback120 | Product120 | CUP |
| root = array | array | CUP |
| root = object | stdClass | CUP |
| root = bson | MongoDB\BSON\Document | CUP |
Почему получается именно так
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 и архив примера
<?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.
- insert сохраняет товар и служебный маркер
- Фильтр _id=701 и limit=1 ограничивают выборку
- Проекция без __pclass → обычный stdClass
- Проекция с маркером либо явной картой → ScreenProduct120
- Отсутствующая price нарушает контракт восстановления и отклоняется
Короткая полезная операция
$query = new MongoDB\Driver\Query(['_id' => 701], [
'projection' => ['_id' => 0, 'sku' => 1, 'price' => 1, '__pclass' => 1], 'limit' => 1,
]); // __pclass сохраняет автоматический выбор класса; price нужен его проверке.Видимый результат
| Проекция/карта | PHP-тип | Артикул | Цена, копейки |
|---|---|---|---|
| Без __pclass | stdClass | CUP | 500 |
| С __pclass | ScreenProduct120 | CUP | 500 |
| Без маркера, явный root | ScreenProduct120 | CUP | 500 |
| Маркер + root=array | array | CUP | 500 |
Почему получается именно так
Автоматическое восстановление работает только по данным, которые действительно вернул запрос. Если 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.
- Проверка карты обнаруживает отсутствующий/неподходящий класс
- Допустимая карта передаёт поля в bsonUnserialize
- Неверный тип quantity отклоняет прикладной валидатор
- Неизвестный Persistable-маркер по умолчанию даёт stdClass
- Полностью нейтральная карта читает известный маркер без пользовательского callback
Короткая полезная операция
$data = $document->toPHP(['root' => 'array', 'document' => 'array', 'array' => 'array']);
// Затем собственная проверка разрешённых полей и типов.
// Имя класса не берём из внешнего запроса или непроверенного __pclass.Видимый результат
| Вход/карта | Результат | Наблюдение |
|---|---|---|
| MissingClass120 | ошибка карты | объект не получен |
| DateTimeImmutable | ошибка карты | объект не получен |
| MongoDB\BSON\Unserializable | ошибка карты | объект не получен |
| quantity = строка 2 | ошибка данных | объект не получен |
| Неизвестный __pclass | stdClass | quantity = 2 |
| Известный __pclass, root=array | array | вызовов восстановления: 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 читает items/qty, версия 2 читает lines/quantity
- Проверяются список, SKU и положительное количество
- Объект хранит единый внутренний список
- 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/qty | 1 | 3 |
| Новые lines/quantity | 2 | 3 |
| Повторная сериализация | 2 | 3 |
Почему получается именно так
Восстановление — удобная граница нормализации версии схемы. Важно не молча угадывать любую форму: неизвестная версия 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-объекты при записи
| Объект | Правило |
|---|---|
| stdClass | BSON-документ с его полями |
| Обычный 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-документы |
| array | BSON-массивы |
| fieldPaths | Отдельные пути, например addresses.$.city; $ означает элемент массива |
| Значение карты | Что получается и что с __pclass |
|---|---|
| Не задано или null | Стандартное правило: массив → array; документ → stdClass либо применимый Persistable |
| array | PHP array; __pclass остаётся данными и не выбирает класс на этом уровне |
| object или stdClass | stdClass; автоматический выбор класса на этом уровне выключен |
| bson | Document/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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


