编解码器
在版本中新增1.17.
概述
编解码器用于将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
或在解码嵌套文档时一起使用。
在使用带有编解码器的集合时,编解码器只会对特定操作接受和返回该类型的数据。插入和替换操作(例如 insertOne
,`findOneAndReplace`
以及一些 bulkWrite
操作)将尝试使用提供的编解码器对给定数据进行编码。尝试插入或替换无法编码的文档将导致异常。读取操作(例如 aggregate
,find
和 findOneAndUpdate
)将尝试使用提供的编解码器对返回的文档进行解码。如果编解码器不支持返回的数据,将抛出异常。
您可以通过指定任何操作的 codec
选项为 null
来禁用特定操作的编解码器使用,或者使用不同的编解码器(例如,解码聚合管道的结果)。或者,通过使用 typeMap
操作指定类型映射也可以覆盖集合级别的编解码器。
// 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(), ]); } }
注意
在编写编解码器时,您应该尽可能宽松地处理数据。在这种情况下,编解码器处理任何 DateTimeInterface
,当编码到 BSON 时,因为可以从任何此类对象创建一个 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); } }