Docs Menu

Docs HomePHP Library Manual

Codecs

On this page

  • Overview
  • Handling Documents
  • Handling Fields and Data Types
  • Handling Embedded Documents

New in version 1.17.

Codecs are used to decode BSON documents into PHP objects, and encode PHP objects into BSON documents. In contrast to other methods (e.g. type maps), codecs allow for greater customization and handling of different data types. They separate logic for BSON encoding and decoding from the domain classes, which also enables BSON to be decoded into plain old PHP objects.

The main logic is contained in a document codec. This class implements the MongoDB\Codec\DocumentCodec interface and defines what data types can be encoded/decoded and how. The following example defines a Person class and a codec to transform it:

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

To then use this codec with a collection, specify the codec option when selecting the collection:

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

The example above selects a collection and instructs it to use the PersonCodec for encoding and decoding documents. When inserting data, the PersonCodec is used to encode the document. When retrieving data, the same PersonCodec is used to decode BSON data into a Person instance. Note that while the PersonCodec could technically decode any BSON document that contains a name field, we wouldn't want to use it for any other documents. Document codecs are meant to be used with a MongoDB\Collection, or when decoding embedded documents.

When using a collection with a codec, the codec will only accept and return data of that type for certain operations. Insert and replace operations (e.g. insertOne, `findOneAndReplace, and some bulkWrite operations) will attempt to encode the given data using the provided codec. Trying to insert or replace a document that cannot be encoded will result in an exception. Read operations (e.g. aggregate, find, and findOneAndUpdate) will attempt to decode returned documents using the provided codec. If the codec does not support the data returned, an exception will be thrown.

You can disable codec usage for a specific operation or use a different codec (e.g. to decode the result of an aggregation pipeline) by specifying null for the codec option for any operation. Alternatively, specifying a type map using the typeMap operation will also override the collection-level codec:

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

The previous example showed how to define a codec for a specific class. However, you may want to create a codec that handles a particular data type in any document. This can be achieved by implementing the MongoDB\Codec\Codec interface.

The following example defines a codec that stores DateTimeInterface instances as an embedded document containing a BSON date and accompanying timezone string. Those same embedded documents can then be translated back into a DateTimeImmutable during BSON decoding.

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

Note

When writing a codec, you should be as lenient as possible when it comes to handling data. In this case, the codec handles any DateTimeInterface when encoding to BSON, as a UTCDateTime instance can be created from any such object. When decoding data from BSON, it will always decode to a DateTimeImmutable instance.

This codec can now be leveraged by other codecs that handle date fields.

First, we add a createdAt field to the Person class:

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

Last but not least, we modify the codec to handle the new field:

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

A previous example showed how to handle a single document. However, sometimes you want to handle fields that contain embedded documents. We will demonstrate this using an Address document, which we will embed within a Person document. To ensure consistency, we're going to make this a read-only class:

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

We can now create a document codec for this class:

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

The Person class gets a new address field, but we'll leave this optional:

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

The PersonCodec can now handle the optional address field when transforming data:

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