Skip to content

Latest commit

 

History

History
502 lines (383 loc) · 19.3 KB

serialization.rst

File metadata and controls

502 lines (383 loc) · 19.3 KB

Persistence in Hippo and Phongo

This document discusses the methods how compound structures (documents, arrays, objects) are persisted through the drivers. And how they are brought back into PHP land.

Serialization to BSON

Arrays

If an array is a packed array — i.e. the keys start at 0 and are sequential without gaps: BSON array.

If the array is not packed — i.e. having associative (string) keys, the keys don't start at 0, or when there are gaps:: BSON object

A top-level (root) document, always serializes as a BSON document.

Examples

These serialize as a BSON array:

[ 8, 5, 2, 3 ] => [ 8, 5, 2, 3 ]
[ 0 => 4, 1 => 9 ] => [ 4, 9 ]

These serialize as a BSON document:

[ 0 => 1, 2 => 8, 3 => 12 ] => { "0" : 1, "2" : 8, "3" : 12 }
[ "foo" => 42 ] => { "foo" : 42 }
[ 1 => 9, 0 => 10 ] => { "1" : 9, "0" : 10 }

Note that the five examples are extracts of a full document, and represent only one value inside a document.

Objects

If an object is of the stdClass, serialize as a BSON document.

If an object is a supported class that implements MongoDB\BSON\Type, then use the BSON serialization logic for that specific type. MongoDB\BSON\Type instances (excluding MongoDB\BSON\Serializable) may only be serialized as a document field value. Attempting to serialize such an object as a root document will throw a MongoDB\Driver\Exception\UnexpectedValueException.

If an object is of an unknown class implementing the MongoDB\BSON\Type interface, then throw a MongoDB\Driver\Exception\UnexpectedValueException.

If an object is of any other class, without implementing any special interface, serialize as a BSON document. Keep only public properties, and ignore protected and private properties.

If an object is of a class that implements the MongoDB\BSON\Serializable interface, call bsonSerialize and use the returned array or stdClass to serialize as a BSON document or array. The BSON type will be determined by the following:

  1. Root documents must be serialized as a BSON document.
  2. MongoDB\BSON\Persistable objects must be serialized as a BSON document.
  3. If bsonSerialize returns a packed array, serialize as a BSON array.
  4. If bsonSerialize returns a non-packed array or stdClass, serialize as a BSON document.
  5. If bsonSerialize did not return an array or stdClass, throw an MongoDB\Driver\Exception\UnexpectedValueException exception.

If an object is of a class that implements the MongoDB\BSON\Persistable interface (which implies MongoDB\BSON\Serializable), obtain the properties in a similar way as in the previous paragraphs, but also add an additional property __pclass as a Binary value, with subtype 0x80 and data bearing the fully qualified class name of the object that is being serialized.

The __pclass property is added to the array or object returned by bsonSerialize, which means it will overwrite any __pclass key/property in the bsonSerialize return value. If you want to avoid this behaviour and set your own __pclass value, you must not implement MongoDB\BSON\Persistable and should instead implement MongoDB\BSON\Serializable directly.

Examples

stdClass {
  public $foo = 42;
} => { "foo" : 42 }

MyClass {
  public $foo = 42;
  protected $prot = "wine";
  private $fpr = "cheese";
} => { "foo" : 42 }

AnotherClass1 implements MongoDB\BSON\Serializable {
  public $foo = 42;
  protected $prot = "wine";
  private $fpr = "cheese";
  function bsonSerialize() {
      return [ 'foo' => $this->foo, 'prot' => $this->prot ];
  }
} => { "foo" : 42, "prot" : "wine" }

AnotherClass2 implements MongoDB\BSON\Serializable {
  public $foo = 42;
  function bsonSerialize() {
      return $this;
  }
} => MongoDB\Driver\Exception\UnexpectedValueException("bsonSerialize() did not return an array or stdClass")

AnotherClass3 implements MongoDB\BSON\Serializable {
  private $elements = [ 'foo', 'bar' ];
  function bsonSerialize() {
      return $this->elements;
  }
} => { "0" : "foo", "1" : "bar" }

ContainerClass implements MongoDB\BSON\Serializable {
  public $things = AnotherClass4 implements MongoDB\BSON\Serializable {
    private $elements = [ 0 => 'foo', 2 => 'bar' ];
    function bsonSerialize() {
      return $this->elements;
    }
  }
  function bsonSerialize() {
      return [ 'things' => $this->things ];
  }
} => { "things" : { "0" : "foo", "2" : "bar" } }

ContainerClass implements MongoDB\BSON\Serializable {
  public $things = AnotherClass5 implements MongoDB\BSON\Serializable {
    private $elements = [ 0 => 'foo', 2 => 'bar' ];
    function bsonSerialize() {
      return array_values($this->elements);
    }
  }
  function bsonSerialize() {
      return [ 'things' => $this->things ];
  }
} => { "things" : [ "foo", "bar" ] }

ContainerClass implements MongoDB\BSON\Serializable {
  public $things = AnotherClass6 implements MongoDB\BSON\Serializable {
    private $elements = [ 'foo', 'bar' ];
    function bsonSerialize() {
      return (object) $this->elements;
    }
  }
  function bsonSerialize() {
      return [ 'things' => $this->things ];
  }
} => { "things" : { "0" : "foo", "1" : "bar" } }

UpperClass implements MongoDB\BSON\Persistable {
  public $foo = 42;
  protected $prot = "wine";
  private $fpr = "cheese";
  function bsonSerialize() {
      return [ 'foo' => $this->foo, 'prot' => $this->prot ];
  }
} => { "foo" : 42, "prot" : "wine", "__pclass" : { "$type" : "80", "$binary" : "VXBwZXJDbGFzcw==" } }

Deserialization from BSON

Compound Types

For compound types, there are three data types:

  • root: refers to the top-level BSON document only
  • document: refers to embedded BSON documents only
  • array: refers to a BSON array

Each of those three data types can be mapped against different PHP types. The possible mapping values are:

  • not set or NULL — this is the default.

    • A BSON array will be deserialized as a PHP array.

    • A BSON document (root or embedded) without a __pclass property [1] becomes a PHP stdClass object, with each BSON document key set as a public stdClass property.

    • A BSON document (root or embedded) with a __pclass property [1] becomes a PHP object of the class name as defined by the __pclass property.

      If the named class implements the MongoDB\BSON\Persistable interface, then the properties of the BSON document, including the __pclass property, are sent as an associative array to the bsonUnserialize function to initialise the object's properties.

      If the named class does not exist or does not implement the MongoDB\BSON\Persistable interface, stdClass will be used and each BSON document key (including __pclass) will be set as a public stdClass property.

  • "array" — turns a BSON array or BSON document into a PHP array. There will be no special treatment of a __pclass property [1], but it may be set as an element in the returned array if it was present in the BSON document.

  • "object" or "stdClass" — turns a BSON array or BSON document into a stdClass object. There will be no special treatment of a __pclass property [1], but it may be set as a public property in the returned object if it was present in the BSON document.

  • any other string — defines the class name that the BSON array or BSON object should be deserialized as. For BSON objects that include __pclass properties, that class will take priority.

    If the named class does not exist, is not concrete (i.e. it is abstract or an interface), or does not implement MongoDB\BSON\Unserializable, then an MongoDB\Driver\Exception\InvalidArgumentException exception is thrown.

    If the BSON object has a __pclass property and that class exists and implements MongoDB\BSON\Persistable, it will supersede the class provided in the type map.

    The properties of the BSON document, including the __pclass property if it exists, will be sent as an associative array to the bsonUnserialize function to initialise the object's properties.

Scalar Types

For types that the driver has a wrapper class for, it is possible to define a map from each supported data type to a class of your own, providing it implements the MongoDB\BSON\TypeWrapper interface.

The supported data types are Binary, Decimal128, Javascript, MaxKey, MinKey, ObjectID, Regex, Timestamp, and UTCDateTime.

The MongoDB\BSON\TypeWrapper interface defines two functions: createFromBSONType(), a factory method which takes a MongoDB\BSON\Type argument, and toBSONType().

As an example, a wrapped UTCDateTime class, could look like:

class UTCDateTimeWrapper implements \MongoDB\BSON\TypeWrapper
{
    private $intern;

    public function __construct( \MongoDB\BSON\UTCDateTime $type )
    {
        $this->intern = $type->toDateTime();
    }

    static function createFromBSONType(\MongoDB\BSON\Type $type)
    {
        if (! $type instanceof \MongoDB\BSON\UTCDateTime) {
            throw new UnexpectedValueException;
        }

        return new self( $type );
    }

    function toBSONType()
    {
        return new \MongoDB\BSON\UTCDateTime( $this->intern );
    }
}

If the defined class wraps (composes) an original MongoDB\BSON\* class, then it SHOULD also implement the accompanying \MongoDB\BSON\<classname>Interface interface.

The type interfaces include all the type-specific methods from the original class, with the exact same arguments and return types. For example, they will not include the __construct() and __debugInfo() methods.

For example, a user-defined UTCDateTimeWrapper class needs to implement the MongoDB\BSON\UTCDateTimeInterface and MongoDB\BSON\TypeWrapper interfaces.

The toBSONType() method must return a value that can be serialized to BSON, e.g. a scalar value, or an object that implements MongoDB\BSON\Serializable. If you return an object that implements MongoDB\BSON\TypeWrapper, the driver will not call the toBSONType() method on this new object.

Returning a different type from the original one could be used as a way to migrate Decimal128 values to plain strings, or downgrade them to floating points or integers. Likewise, you could implement a UTCDateTime wrapper that converts dates to a formatted string; however, that would prevent round-tripping of the original date value.

TypeMaps

TypeMaps can be set through the setTypeMap() method on a MongoDB\Driver\Cursor object, or the $typeMap argument of MongoDB\BSON\toPHP() (previously, MongoDB\BSON\toArray()). Each of the three classes (root, document and array) can be individually set.

Additionally, you can specify the scalar type mappings through a types element. Each element in that types array maps a MongoDB data type to a user-defined class name.

If the named class does not exist, is not concrete (i.e. it is abstract or an interface), or does not implement MongoDB\BSON\TypeWrapper, then an MongoDB\Driver\Exception\InvalidArgumentException exception is thrown.

Examples

These examples use the following classes:

  • MyClass, which does not implement any interface
  • YourClass, which implements MongoDB\BSON\Unserializable
  • OurClass, which implements MongoDB\BSON\Persistable
  • TheirClass, which extends OurClass

The bsonUnserialize() method of YourClass, OurClass, TheirClass iterate over the array and set the properties without modifications. It also sets the $unserialized property to true:

function bsonUnserialize( array $map )
{
    foreach ( $map as $k => $value )
    {
        $this->$k = $value;
    }
    $this->unserialized = true;
}
/* typemap: [] (all defaults) */
{ "foo": "yes", "bar" : false }
  -> stdClass { $foo => 'yes', $bar => false }

{ "foo": "no", "array" : [ 5, 6 ] }
  -> stdClass { $foo => 'no', $array => [ 5, 6 ] }

{ "foo": "no", "obj" : { "embedded" : 3.14 } }
  -> stdClass { $foo => 'no', $obj => stdClass { $embedded => 3.14 } }

{ "foo": "yes", "__pclass": "MyClass" }
  -> stdClass { $foo => 'yes', $__pclass => 'MyClass' }

{ "foo": "yes", "__pclass": { "$type" : "80", "$binary" : "MyClass" } }
  -> stdClass { $foo => 'yes', $__pclass => Binary(0x80, 'MyClass') }

{ "foo": "yes", "__pclass": { "$type" : "80", "$binary" : "YourClass") }
  -> stdClass { $foo => 'yes', $__pclass => Binary(0x80, 'YourClass') }

{ "foo": "yes", "__pclass": { "$type" : "80", "$binary" : "OurClass") }
  -> OurClass { $foo => 'yes', $__pclass => Binary(0x80, 'OurClass'), $unserialized => true }

{ "foo": "yes", "__pclass": { "$type" : "44", "$binary" : "YourClass") }
  -> stdClass { $foo => 'yes', $__pclass => Binary(0x44, 'YourClass') }
/* typemap: [ "root" => "MissingClass" ] */
{ "foo": "yes" }
  -> MongoDB\Driver\Exception\InvalidArgumentException("MissingClass does not exist")

/* typemap: [ "root" => "MyClass" ] */
{ "foo": "yes", "__pclass" : { "$type": "80", "$binary": "MyClass" } }
  -> MongoDB\Driver\Exception\InvalidArgumentException("MyClass does not implement Unserializable interface")

/* typemap: [ "root" => "MongoDB\BSON\Unserializable" ] */
{ "foo": "yes" }
  -> MongoDB\Driver\Exception\InvalidArgumentException("Unserializable is not a concrete class")

/* typemap: [ "root" => "YourClass" ] */
{ "foo": "yes", "__pclass" : { "$type": "80", "$binary": "MongoDB\BSON\Unserializable" } }
  -> YourClass { $foo => "yes", $__pclass => Binary(0x80, "MongoDB\BSON\Unserializable"), $unserialized => true }

/* typemap: [ "root" => "YourClass" ] */
{ "foo": "yes", "__pclass" : { "$type": "80", "$binary": "MyClass" } }
  -> YourClass { $foo => "yes", $__pclass => Binary(0x80, "MyClass"), $unserialized => true }

/* typemap: [ "root" => "YourClass" ] */
{ "foo": "yes", "__pclass" : { "$type": "80", "$binary": "OurClass" } }
  -> OurClass { $foo => "yes", $__pclass => Binary(0x80, "OurClass"), $unserialized => true }

/* typemap: [ "root" => "YourClass" ] */
{ "foo": "yes", "__pclass" : { "$type": "80", "$binary": "TheirClass" } }
  -> TheirClass { $foo => "yes", $__pclass => Binary(0x80, "TheirClass"), $unserialized => true }

/* typemap: [ "root" => "OurClass" ] */
{ foo: "yes", "__pclass" : { "$type": "80", "$binary": "TheirClass" } }
  -> TheirClass { $foo => "yes", $__pclass => Binary(0x80, "TheirClass"), $unserialized => true }
/* typemap: [ 'root' => 'YourClass' ] */
{ foo: "yes", "__pclass" : { "$type": "80", "$binary": "YourClass" } }
  -> YourClass { $foo => 'yes', $__pclass => Binary(0x80, 'YourClass'), $unserialized => true }
/* typemap: [ 'root' => 'array', 'document' => 'array' ] */
{ "foo": "yes", "bar" : false }
  -> [ "foo" => "yes", "bar" => false ]

{ "foo": "no", "array" : [ 5, 6 ] }
  -> [ "foo" => "no", "array" => [ 5, 6 ] ]

{ "foo": "no", "obj" : { "embedded" : 3.14 } }
  -> [ "foo" => "no", "obj" => [ "embedded => 3.14 ] ]

{ "foo": "yes", "__pclass": "MyClass" }
  -> [ "foo" => "yes", "__pclass" => "MyClass" ]

{ "foo": "yes", "__pclass" : { "$type": "80", "$binary": "MyClass" } }
  -> [ "foo" => "yes", "__pclass" => Binary(0x80, "MyClass") ]

{ "foo": "yes", "__pclass" : { "$type": "80", "$binary": "OurClass" } }
  -> [ "foo" => "yes", "__pclass" => Binary(0x80, "OurClass") ]
/* typemap: [ 'root' => 'object', 'document' => 'object' ] */
{ "foo": "yes", "__pclass": { "$type": "80", "$binary": "MyClass" } }
  -> stdClass { $foo => "yes", "__pclass" => Binary(0x80, "MyClass") }
/* typemap: [ 'types' => [ 'UTCDateTime' => 'UTCDateTimeWrapper' ] ] */
{ "date" : ISODate("2016-07-19T16:49:54") }
  -> stdClass { $date => UTCDateTimeWrapper(…) }

/* typemap: [ 'types' => [ 'UTCDateTime' => 'UTCDateTimeAsUnixTimestamp' ] ] */
{ "date" : ISODate("2016-07-19T16:49:54") }
  -> stdClass { $date => 1468946994 }

Related Tickets

  • PHPC-248: Allow ->setTypeMap() to set 'array' and 'stdclass'
  • PHPC-249: empty array should be serialized as array
  • PHPC-260: Allow "object" as an alias of "stdClass" for setTypeMap()
  • PHPC-274: zval_to_bson() ignores BSONSerializable interface
  • PHPC-275: object_to_bson() should throw exception if bsonSerialize() returns non-array
  • PHPC-288: ODS (Object Document Serializer) support and integrations
  • PHPC-311: Rename BSON from/toArray() methods to from/toPHP()
  • PHPC-315: Support explicit type mapping for top-level documents
  • PHPC-318: Cursor type map should apply to top-level document
  • PHPC-319: Top level documents should be deserialized as stdClass by default
  • PHPC-329: Determine if ODM class should always supersede the type map
  • HHVM-55: Implement BSONPeristable interface
  • HHVM-56: Implement BSONSerializable interface
  • HHVM-57: Implement BSONUnserializable interface
  • HHVM-63: Empty array should be serialized as empty array, and empty object should be serialized as empty object
  • HHVM-64: Allow ->setTypeMap() to set 'array' and 'stdclass'
  • HHVM-67: ODM should only match field of specific name (__pclass)
  • HHVM-84: Implement MongoDBBSONSerializable
  • HHVM-85: Implement MongoDBBSONUnserializable / MongoDBBSONPersistable
  • HHVM-214 Implement interfaces for userland BSON type classes
  • PHP-1457: MongoCollection::insert() Non-public properties of objects.

Unrelated Tickets

  • PHPC-314: Prototype type map syntax for documents within field paths
[1](1, 2, 3, 4) A __pclass property is only deemed to exist if there exists a property with that name, and it is a Binary value, and the sub-type of the Binary value is 0x80. If any of these three conditions is not met, the __pclass property does not exist and should be treated as any other normal property.