文档菜单
文档首页
/ / /
PHP库手册
/

编解码器

本页内容

  • 概述
  • 处理文档
  • 处理字段和数据类型
  • 处理嵌套文档

在版本中新增1.17.

编解码器用于将BSON文档解码成PHP对象,并将PHP对象编码成BSON文档。与其他方法(例如类型映射)相比,编解码器允许进行更大的定制和处理不同数据类型。它们将BSON编码和解码的逻辑与域类分开,这也使得BSON可以解码成普通的PHP对象。

主要逻辑包含在一个文档编解码器中。该类实现了MongoDB\Codec\DocumentCodec 接口并定义了可以编码/解码的数据类型以及如何进行解码。以下示例定义了一个 Person 类和一个编解码器来转换它

<?php
use MongoDB\BSON\ObjectId;
final class Person
{
public function __construct(
public string $name,
public readonly ObjectId $id = new ObjectId(),
) {
}
}
<?php
use MongoDB\BSON\Document;
use MongoDB\Codec\DecodeIfSupported;
use MongoDB\Codec\DocumentCodec;
use MongoDB\Codec\EncodeIfSupported;
use MongoDB\Exception\UnsupportedValueException;
/** @template-implements DocumentCodec<Person> */
final class PersonCodec implements DocumentCodec
{
// These traits define commonly used functionality to avoid duplication
use DecodeIfSupported;
use EncodeIfSupported;
public function canDecode(mixed $value): bool
{
return $value instanceof Document && $value->has('name');
}
public function canEncode(mixed $value): bool
{
return $value instanceof Person;
}
public function decode(mixed $value): Person
{
if (! $this->canDecode($value)) {
throw UnsupportedValueException::invalidDecodableValue($value);
}
return new Person(
$value->get('name'),
$value->get('_id'),
);
}
public function encode(mixed $value): Document
{
if (! $this->canEncode($value)) {
throw UnsupportedValueException::invalidEncodableValue($value);
}
return Document::fromPHP([
'_id' => $value->id,
'name' => $value->name,
]);
}
}

要使用这个编解码器与集合一起使用,当选择集合时指定 codec 选项

<?php
use MongoDB\Client;
$client = new Client();
$collection = $client->selectCollection('test', 'person', [
'codec' => new PersonCodec(),
]);
$person = new Person('Jane Doe');
$collection->insertOne($person);
$person = $collection->findOne();

上面的示例选择了一个集合并指示它使用 PersonCodec 对文档进行编码和解码。当插入数据时,使用 PersonCodec 对文档进行编码。当检索数据时,使用相同的 PersonCodec 将BSON数据解码成 Person 实例。请注意,虽然从技术上讲,PersonCodec 可以解码包含名称字段的任何BSON文档,但我们不希望将其用于任何其他文档。文档编解码器应该与MongoDB\Collection 或在解码嵌套文档时一起使用。

在使用带有编解码器的集合时,编解码器只会对特定操作接受和返回该类型的数据。插入和替换操作(例如 insertOne`findOneAndReplace` 以及一些 bulkWrite 操作)将尝试使用提供的编解码器对给定数据进行编码。尝试插入或替换无法编码的文档将导致异常。读取操作(例如 aggregatefindfindOneAndUpdate)将尝试使用提供的编解码器对返回的文档进行解码。如果编解码器不支持返回的数据,将抛出异常。

您可以通过指定任何操作的 codec 选项为 null 来禁用特定操作的编解码器使用,或者使用不同的编解码器(例如,解码聚合管道的结果)。或者,通过使用 typeMap 操作指定类型映射也可以覆盖集合级别的编解码器。

<?php
// Overrides the collection codec, falling back to the default type map
$collection->aggregate($pipeline, ['codec' => null]);
// Overrides the collection codec, using the specified type map
$collection->findOne($filter, ['typeMap' => ['root' => 'stdClass']]);

前面的示例展示了如何为特定类定义编解码器。然而,您可能希望创建一个处理任何文档中特定数据类型的编解码器。这可以通过实现 MongoDB\Codec\Codec 接口来实现。

以下示例定义了一个编解码器,该编解码器将 DateTimeInterface 实例存储为包含一个 BSON 日期和相应的时区字符串的嵌入文档。然后,可以将这些相同的嵌入文档在 BSON 解码过程中转换回 DateTimeImmutable

<?php
use MongoDB\BSON\Document;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Codec\Codec;
use MongoDB\Codec\DecodeIfSupported;
use MongoDB\Codec\EncodeIfSupported;
use MongoDB\Exception\UnsupportedValueException;
/** @template-implements Codec<Document, DateTimeImmutable> */
final class DateTimeCodec implements Codec
{
use DecodeIfSupported;
use EncodeIfSupported;
public function canDecode(mixed $value): bool
{
/* This codec inspects the BSON document to ensure it has the fields it expects, and that those fields are of
* the correct type. This is a robust approach to avoid decoding document that are not supported and would cause
* exceptions.
*
* For large documents, this can be inefficient as we're inspecting the entire document four times (once for
* each call to has() and get()). For small documents, this is not a problem.
*/
return $value instanceof Document
&& $value->has('utc') && $value->get('utc') instanceof UTCDateTime
&& $value->has('tz') && is_string($value->get('tz'));
}
public function canEncode(mixed $value): bool
{
return $value instanceof DateTimeInterface;
}
public function decode(mixed $value): DateTimeImmutable
{
if (! $this->canDecode($value)) {
throw UnsupportedValueException::invalidDecodableValue($value);
}
$timeZone = new DateTimeZone($value->get('tz'));
$dateTime = $value->get('utc')
->toDateTime()
->setTimeZone($timeZone);
return DateTimeImmutable::createFromMutable($dateTime);
}
public function encode(mixed $value): Document
{
if (! $this->canEncode($value)) {
throw UnsupportedValueException::invalidEncodableValue($value);
}
return Document::fromPHP([
'utc' => new UTCDateTime($value),
'tz' => $value->getTimezone()->getName(),
]);
}
}

注意

在编写编解码器时,您应该尽可能宽松地处理数据。在这种情况下,编解码器处理任何 DateTimeInterface,当编码到 BSON 时,因为可以从任何此类对象创建一个 UTCDateTime 实例。当从 BSON 解码数据时,它始终解码为 DateTimeImmutable 实例。

现在,其他处理日期字段的编解码器可以利用这个编解码器。

首先,我们在 Person 类中添加一个 createdAt 字段

<?php
use MongoDB\BSON\ObjectId;
final class Person
{
public function __construct(
public string $name,
public readonly DateTimeImmutable $createdAt = new DateTimeImmutable(),
public readonly ObjectId $id = new ObjectId(),
) {
}
}

最后但同样重要的是,我们修改编解码器来处理新字段

<?php
use MongoDB\BSON\Document;
use MongoDB\Codec\DecodeIfSupported;
use MongoDB\Codec\DocumentCodec;
use MongoDB\Codec\EncodeIfSupported;
use MongoDB\Exception\UnsupportedValueException;
/** @template-implements DocumentCodec<Person> */
final class PersonCodec implements DocumentCodec
{
use DecodeIfSupported;
use EncodeIfSupported;
public function __construct(
private readonly DateTimeCodec $dateTimeCodec = new DateTimeCodec(),
) {
}
public function canDecode(mixed $value): bool
{
return $value instanceof Document && $value->has('name');
}
public function canEncode(mixed $value): bool
{
return $value instanceof Person;
}
public function decode(mixed $value): Person
{
if (! $this->canDecode($value)) {
throw UnsupportedValueException::invalidDecodableValue($value);
}
return new Person(
$value->get('name'),
$this->dateTimeCodec->decode($value->get('createdAt')),
$value->get('_id'),
);
}
public function encode(mixed $value): Document
{
if (! $this->canEncode($value)) {
throw UnsupportedValueException::invalidEncodableValue($value);
}
return Document::fromPHP([
'_id' => $value->id,
'name' => $value->name,
'createdAt' => $this->dateTimeCodec->encode($value->createdAt),
]);
}
}

前面的例子展示了如何处理单个文档。然而,有时您需要处理包含嵌入文档的字段。我们将使用一个Address文档来演示这一点,该文档将嵌入到一个Person文档中。为了确保一致性,我们将将其作为一个只读类来实现

<?php
final readonly class Address
{
public function __construct(
public string $street,
public string $postCode,
public string $city,
public string $country,
) {
}
}

现在我们可以为这个类创建一个文档编解码器

<?php
use MongoDB\BSON\Document;
use MongoDB\Codec\DecodeIfSupported;
use MongoDB\Codec\DocumentCodec;
use MongoDB\Codec\EncodeIfSupported;
use MongoDB\Exception\UnsupportedValueException;
/** @template-implements DocumentCodec<Address> */
final class AddressCodec implements DocumentCodec
{
use DecodeIfSupported;
use EncodeIfSupported;
public function canDecode(mixed $value): bool
{
return $value instanceof Document
&& $value->has('street')
&& $value->has('postCode')
&& $value->has('city')
&& $value->has('country');
}
public function canEncode(mixed $value): bool
{
return $value instanceof Address;
}
public function decode(mixed $value): Address
{
if (! $this->canDecode($value)) {
throw UnsupportedValueException::invalidDecodableValue($value);
}
return new Address(
$value->get('street'),
$value->get('postCode'),
$value->get('city'),
$value->get('country'),
);
}
public function encode(mixed $value): Document
{
if (! $this->canEncode($value)) {
throw UnsupportedValueException::invalidEncodableValue($value);
}
return Document::fromPHP([
'street' => $value->street,
'postCode' => $value->postCode,
'city' => $value->city,
'country' => $value->country,
]);
}
}

Person类得到了一个新的address字段,但我们将其设置为可选

<?php
use MongoDB\BSON\ObjectId;
final class Person
{
public ?Address $address = null;
public function __construct(
public string $name,
public readonly ObjectId $id = new ObjectId(),
) {
}
}

PersonCodec现在可以在转换数据时处理可选的address字段

<?php
use MongoDB\BSON\Document;
use MongoDB\Codec\DecodeIfSupported;
use MongoDB\Codec\DocumentCodec;
use MongoDB\Codec\EncodeIfSupported;
use MongoDB\Exception\UnsupportedValueException;
/** @template-implements DocumentCodec<Person> */
final class PersonCodec implements DocumentCodec
{
use DecodeIfSupported;
use EncodeIfSupported;
public function __construct(
private readonly AddressCodec $addressCodec = new AddressCodec(),
) {
}
public function canDecode(mixed $value): bool
{
return $value instanceof Document && $value->has('name');
}
public function canEncode(mixed $value): bool
{
return $value instanceof Person;
}
public function decode(mixed $value): Person
{
if (! $this->canDecode($value)) {
throw UnsupportedValueException::invalidDecodableValue($value);
}
$person = new Person(
$value->get('name'),
$value->get('_id'),
);
// Address is optional, so only decode if it exists
if ($value->has('address')) {
$person->address = $this->addressCodec->decode($value->get('address'));
}
return $person;
}
public function encode(mixed $value): Document
{
if (! $this->canEncode($value)) {
throw UnsupportedValueException::invalidEncodableValue($value);
}
$data = [
'_id' => $value->id,
'name' => $value->name,
];
// Don't add a null value to the document if address is not set
if ($value->address) {
$data['address'] = $this->addressCodec->encode($value->address);
}
return Document::fromPHP($data);
}
}

返回

自定义数据类型