Codecs
Nesta página
Novidade na versão 1.17.
Visão geral
Os codecs são usados para decodificar documentos BSON em objetos PHP e codificar objetos PHP em documentos BSON. Em contraste com outros métodos (por exemplo, mapas de tipo), codecs permitem maior personalização e manuseio de diferentes tipos de dados. Eles separam a lógica para codificação e decodificação BSON das classes de domínio, o que também permite que o BSON seja decodificado em objetos PHP antigos e simples.
Manipulação de documentos
A lógica principal está contida em um codec de documento. Esta classe implementa a interface MongoDB\Codec\DocumentCodec
e define quais tipos de dados podem ser codificados/decodificados e como. O exemplo a seguir define uma classe Person
e um codec para transformá-la:
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, ]); } }
Para usar este codec com uma collection, especifique a opção codec
ao selecionar a collection:
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();
O exemplo acima seleciona uma coleção e a instrui a usar o PersonCodec
para codificar e decodificar documentos. Ao inserir dados, o PersonCodec
é utilizado para codificar o documento. Ao recuperar dados, o mesmo PersonCodec
é usado para decodificar dados BSON em uma instância Person
. Observe que, embora o PersonCodec
possa teoricamente decodificar qualquer documento BSON que contenha um campo de nome , não queremos usá-lo para nenhum outro documento. Os codecs de documentos devem ser usados com MongoDB\Collection
ou ao decodificar documentos incorporados.
Ao usar uma collection com um codec, o codec só aceitará e retornará dados desse tipo para determinadas operações. Inserir e substituir operações (por exemplo insertOne
, `findOneAndReplace
e algumas operações bulkWrite
) tentarão codificar os dados fornecidos usando o codec fornecido. Tentar inserir ou substituir um documento que não pode ser codificado resultará em uma exceção. Operações de leitura (por exemplo aggregate
, find
e findOneAndUpdate
) tentarão decodificar os documentos devolvidos usando o codec fornecido. Se o codec não suportar os dados retornados, será lançada uma exceção.
Você pode desativar o uso do codec para uma operação específica ou usar um codec diferente (por exemplo, para decodificar o resultado de um pipeline de agregação) especificando null
para a opção codec
para qualquer operação. Como alternativa, especificar um mapa de tipos usando a operação typeMap
também substituirá o codec no nível da 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']]);
Manipulação de campos e tipos de dados
O exemplo anterior mostra como definir um codec para uma classe específica. No entanto, você pode querer criar um codec que lide com um tipo de dados específico em qualquer documento. Isto pode ser feito implementando a interface do MongoDB\Codec\Codec
.
O exemplo a seguir define um codec que armazena instâncias DateTimeInterface
como um documento incorporado contendo uma data BSON e a cadeia de caracteres de fuso horário que a acompanha. Esses mesmos documentos incorporados podem ser traduzidos de volta para um DateTimeImmutable
durante a decodificação BSON.
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(), ]); } }
Observação
Ao escrever um codec, você deve ser o mais transitório possível no que se refere ao tratamento de dados. Nesse caso, o codec lida com qualquer DateTimeInterface
ao codificar para BSON, pois uma instância UTCDateTime
pode ser criada a partir de qualquer objeto desse tipo. Ao decodificar dados do BSON, ele sempre decodificará para uma instância DateTimeImmutable
.
Este codec agora pode ser aproveitado por outros codecs que lidam com campos de data.
Primeiro, adicionamos um campo createdAt
à classe Person
:
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(), ) { } }
Por último, mas não menos importante, modificamos o codec para lidar com o novo campo:
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), ]); } }
Tratamento de documentos incorporados
Um exemplo anterior mostra como lidar com um único documento. No entanto, às vezes, você deseja lidar com campos que contêm documentos incorporados. Demonstraremos isso usando um documento Address
, que incorporaremos dentro de um documento Person
. Para garantir consistência, vamos fazer desta uma classe somente leitura:
final readonly class Address { public function __construct( public string $street, public string $postCode, public string $city, public string $country, ) { } }
Agora podemos criar um codec de documento para esta classe:
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, ]); } }
A classe Person
obtém um novo campo address
, mas deixaremos isso opcional:
use MongoDB\BSON\ObjectId; final class Person { public ?Address $address = null; public function __construct( public string $name, public readonly ObjectId $id = new ObjectId(), ) { } }
O PersonCodec
agora pode lidar com o campo address
opcional ao transformar dados:
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); } }