编解码器
1.17 版本中的新增功能。
Overview
编解码器用于将 BSON 文档解码为 PHP 对象,以及将 PHP 对象编码为 BSON 文档。 与其他方法(例如 类型映射),编解码器允许更好地自定义和处理不同的数据类型。 它们将 BSON 编码和解码的逻辑与域类分开,这也使 BSON 能够解码为普通的旧 PHP 对象。
处理文档
主要逻辑包含在文档编解码器中。 此类实现 MongoDB\Codec\DocumentCodec
接口,并定义可以编码/解码哪些数据类型以及如何编码/解码。 以下示例定义了一个Person
类和用于转换该类的编解码器:
use MongoDB\BSON\ObjectId; final class Person { public function __construct( public string $name, public readonly ObjectId $id = new ObjectId(), ) { } }
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
选项:
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
一起使用,或者在解码嵌入式文档时使用。
当使用一个collection与编解码器一起时,编解码器将仅为某些操作接受和返回该类型的数据。插入和替换操作(例如 insertOne
、 `findOneAndReplace
和某些bulkWrite
操作)将尝试使用提供的编解码器对给定数据进行编码。 尝试插入或替换无法编码的文档将导致异常。 读取操作(例如 aggregate
、 find
和findOneAndUpdate
)将尝试使用提供的编解码器对返回的文档进行解码。 如果编解码器不支持返回的数据,则会抛出异常。
您可以通过为任何操作的codec
选项指定null
,禁用特定操作的编解码器使用或使用不同的编解码器(例如,解码聚合管道的结果)。 或者,使用typeMap
操作指定类型映射也将覆盖collection级编解码器:
// 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
。
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(), ]); } }
注意
编写编解码器时,在处理数据方面应尽可能宽松。 在这种情况下,编解码器在编码为 BSON 时会处理任何DateTimeInterface
,因为可以从任何此类对象创建UTCDateTime
实例。 从 BSON 解码数据时,它将始终解码为DateTimeImmutable
实例。
现在,处理日期字段的其他编解码器可以利用此编解码器。
首先,我们向Person
类添加一个createdAt
字段:
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(), ) { } }
最后但并非最不重要的一点是,我们修改编解码器以处理新字段:
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
文档中。 为了确保一致性,我们将其设为只读类:
final readonly class Address { public function __construct( public string $street, public string $postCode, public string $city, public string $country, ) { } }
我们现在可以为此类创建文档编解码器:
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
类获得一个新的字段,但我们将其保留为可选项:
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
字段:
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); } }