Docs Menu
Docs Home
/ /

Codificar datos con códecs de tipo

Nuevo en la versión 1.17.

Los códecs se utilizan para decodificar documentos BSON en objetos PHP y codificar objetos PHP en documentos BSON. A diferencia de otros métodos (por ejemplo, los mapas de tipos), los códecs permiten una mayor personalización y gestión de diferentes tipos de datos. Separan la lógica de codificación y decodificación de BSON de las clases de dominio, lo que también permite decodificar BSON en objetos PHP estándar.

La lógica principal está contenida en un códec de documento. Esta clase implementa el MongoDB\Codec\DocumentCodec Interfaz que define qué tipos de datos se pueden codificar/decodificar y cómo. El siguiente ejemplo define una clase Person y un códec para transformarla:

<?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,
]);
}
}

Para luego usar este códec con una colección, especifique la opción codec al seleccionar la colección:

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

El ejemplo anterior selecciona una colección y le indica que use PersonCodec para codificar y decodificar documentos. Al insertar datos, se usa PersonCodec para codificar el documento. Al recuperar datos, se usa el mismo PersonCodec para decodificar datos BSON en una instancia Person. Tenga en cuenta que, si bien PersonCodec podría decodificar técnicamente cualquier documento BSON que contenga un campo de nombre, no se usaría para ningún otro documento. Los códecs de documentos están diseñados para usarse con un MongoDB\Collection, o al decodificar documentos incrustados.

Al usar una colección con un códec, este solo aceptará y devolverá datos de ese tipo para ciertas operaciones. Las operaciones de inserción y reemplazo (p. ej., insertOne, `findOneAndReplace y algunas bulkWrite) intentarán codificar los datos proporcionados con el códec proporcionado. Intentar insertar o reemplazar un documento que no se pueda codificar generará una excepción. Las operaciones de lectura (p. ej., aggregate, find y findOneAndUpdate) intentarán decodificar los documentos devueltos con el códec proporcionado. Si el códec no admite los datos devueltos, se generará una excepción.

Puedes deshabilitar el uso de codec para una operación específica o utilizar un codec diferente (por ejemplo, para decodificar el resultado de un pipeline de agregación) especificando null para la opción codec en cualquier operación. De forma alternativa, especificar un mapa de tipos mediante la operación typeMap también anulará el codec a nivel de colección:

<?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']]);

El ejemplo anterior mostró cómo definir un códec para una clase específica. Sin embargo, es posible que desee crear un códec que gestione un tipo de datos específico en cualquier documento. Esto se puede lograr implementando la interfaz MongoDB\Codec\Codec.

El siguiente ejemplo define un códec que almacena instancias DateTimeInterface como un documento incrustado que contiene una fecha BSON y la cadena de zona horaria correspondiente. Estos mismos documentos incrustados pueden traducirse de nuevo a DateTimeImmutable durante la decodificación BSON.

<?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(),
]);
}
}

Nota

Al escribir un códec, se debe ser lo más flexible posible con el manejo de datos. En este caso, el códec maneja cualquier DateTimeInterface al codificar a BSON, ya que se puede crear una instancia UTCDateTime a partir de dicho objeto. Al decodificar datos de BSON, siempre se decodificará a una instancia DateTimeImmutable.

Este códec ahora puede ser aprovechado por otros códecs que manejan campos de fecha.

Primero, agregamos un campo createdAt a la clase Person:

<?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(),
) {
}
}

Por último, pero no menos importante, modificamos el códec para manejar el nuevo campo:

<?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),
]);
}
}

Un ejemplo anterior mostró cómo gestionar un solo documento. Sin embargo, a veces es necesario gestionar campos que contienen documentos incrustados. Lo demostraremos con un documento Address, que incrustaremos en un documento Person. Para garantizar la coherencia, la clase será de solo lectura:

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

Ahora podemos crear un códec de documento para esta clase:

<?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,
]);
}
}

La clase Person obtiene un nuevo campo address, pero lo dejaremos opcional:

<?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 ahora puede manejar el campo opcional address al transformar datos:

<?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);
}
}

Volver

Tipos personalizados

En esta página