From 1678dd9c607976f53021d1963287d91767b58cd1 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Thu, 6 Jul 2023 11:15:10 +0200 Subject: [PATCH 01/16] Add psalm stubs for new BSON classes --- psalm.xml.dist | 5 ++++ stubs/BSON/Document.stub.php | 47 +++++++++++++++++++++++++++++++++ stubs/BSON/Iterator.stub.php | 32 ++++++++++++++++++++++ stubs/BSON/PackedArray.stub.php | 38 ++++++++++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 stubs/BSON/Document.stub.php create mode 100644 stubs/BSON/Iterator.stub.php create mode 100644 stubs/BSON/PackedArray.stub.php diff --git a/psalm.xml.dist b/psalm.xml.dist index 0acafb77f..7ca91ba36 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -15,4 +15,9 @@ + + + + + diff --git a/stubs/BSON/Document.stub.php b/stubs/BSON/Document.stub.php new file mode 100644 index 000000000..3d0fec66b --- /dev/null +++ b/stubs/BSON/Document.stub.php @@ -0,0 +1,47 @@ + + */ +final class Document implements \IteratorAggregate, \Serializable +{ + private function __construct() {} + + final static public function fromBSON(string $bson): Document {} + + final static public function fromJSON(string $json): Document {} + + /** @param array|object $value */ + final static public function fromPHP($value): Document {} + + /** @return TValue */ + final public function get(string $key) {} + + /** @return Iterator */ + final public function getIterator(): Iterator {} + + final public function has(string $key): bool {} + + /** @return array|object */ + final public function toPHP(?array $typeMap = null) {} + + final public function toCanonicalExtendedJSON(): string {} + + final public function toRelaxedExtendedJSON(): string {} + + final public function __toString(): string {} + + final public static function __set_state(array $properties): Document {} + + final public function serialize(): string {} + + /** @param string $serialized */ + final public function unserialize($serialized): void {} + + final public function __unserialize(array $data): void {} + + final public function __serialize(): array {} +} diff --git a/stubs/BSON/Iterator.stub.php b/stubs/BSON/Iterator.stub.php new file mode 100644 index 000000000..7a1de9699 --- /dev/null +++ b/stubs/BSON/Iterator.stub.php @@ -0,0 +1,32 @@ + + */ +final class Iterator implements \Iterator +{ + final private function __construct() {} + + /** @return TValue */ + final public function current() {} + + /** @return TKey */ + final public function key() {} + + final public function next(): void {} + + final public function rewind(): void {} + + final public function valid(): bool {} + + final public function __wakeup(): void {} +} diff --git a/stubs/BSON/PackedArray.stub.php b/stubs/BSON/PackedArray.stub.php new file mode 100644 index 000000000..3c00c614e --- /dev/null +++ b/stubs/BSON/PackedArray.stub.php @@ -0,0 +1,38 @@ + + */ +final class PackedArray implements \IteratorAggregate, \Serializable +{ + private function __construct() {} + + final static public function fromPHP(array $value): PackedArray {} + + /** @return TValue */ + final public function get(int $index) {} + + /** @return Iterator */ + final public function getIterator(): Iterator {} + + final public function has(int $index): bool {} + + /** @return array|object */ + final public function toPHP(?array $typeMap = null) {} + + final public function __toString(): string {} + + final public static function __set_state(array $properties): PackedArray {} + + final public function serialize(): string {} + + /** @param string $serialized */ + final public function unserialize($serialized): void {} + + final public function __unserialize(array $data): void {} + + final public function __serialize(): array {} +} From e6810d1d2a7e283400b72e2e73dcf88aaa2906ec Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Thu, 6 Jul 2023 11:19:14 +0200 Subject: [PATCH 02/16] Add basic codec infrastructure --- src/Codec/Codec.php | 28 +++++++++++ src/Codec/Decoder.php | 48 ++++++++++++++++++ src/Codec/DocumentCodec.php | 28 +++++++++++ src/Codec/Encoder.php | 48 ++++++++++++++++++ src/Codec/architecture.md | 99 +++++++++++++++++++++++++++++++++++++ 5 files changed, 251 insertions(+) create mode 100644 src/Codec/Codec.php create mode 100644 src/Codec/Decoder.php create mode 100644 src/Codec/DocumentCodec.php create mode 100644 src/Codec/Encoder.php create mode 100644 src/Codec/architecture.md diff --git a/src/Codec/Codec.php b/src/Codec/Codec.php new file mode 100644 index 000000000..37d80a19c --- /dev/null +++ b/src/Codec/Codec.php @@ -0,0 +1,28 @@ + + * @template-extends Encoder + */ +interface Codec extends Decoder, Encoder +{ +} diff --git a/src/Codec/Decoder.php b/src/Codec/Decoder.php new file mode 100644 index 000000000..2fd3e46ef --- /dev/null +++ b/src/Codec/Decoder.php @@ -0,0 +1,48 @@ + + */ +interface DocumentCodec extends Codec +{ +} diff --git a/src/Codec/Encoder.php b/src/Codec/Encoder.php new file mode 100644 index 000000000..5d19cd90b --- /dev/null +++ b/src/Codec/Encoder.php @@ -0,0 +1,48 @@ + + * @template-extends Encoder + */ +interface Codec extends Decoder, Encoder +{ +} +``` + +## Document codec + +The document codec is special as it is guaranteed to always encode to a BSON document instance and decode to a PHP +object. Document codecs can be provided to a `MongoDB\Collection` instance to have it automatically decode data read +from the database. Likewise, any supported value is encoded before writing to the database in `insert` and `replace` +operations. + +```php +namespace MongoDB\Codec; + +use MongoDB\BSON\Document; + +/** + * @template ObjectType of object + * @extends Codec + */ +interface DocumentCodec extends Codec +{ +} +``` + +## Using codecs + +The `MongoDB\Collection` class and all operations that work with documents now take a `codec` option. This can be +an instance of a `DocumentCodec` that will be used to encode documents (for insert and replace operations) and decode +them into PHP objects when reading data. + +### Codecs and type maps + +When providing a value for the `codec` option, it takes precedence over the `typeMap` option. An exception is made +when the `codec` option was specified on the collection level, but an operation is given a `typeMap` option. In +this case, the type map is used. The precedence order is as follows: + +* operation-level `codec` option +* operation-level `typeMap` option +* collection-level `codec` option +* collection-level `typeMap` option + +Codecs are not inherited from the client or the database object, as they are purely used for operations that return +documents. However, database- or client-level aggregation commands will take an operation-level codec option to +decode the resulting documents. + From 8e818a39c496620ad2c220ee51e0ca9e8fd0115c Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Thu, 6 Jul 2023 11:23:38 +0200 Subject: [PATCH 03/16] Introduce codec library --- psalm-baseline.xml | 10 ++ src/Codec/CodecLibrary.php | 149 ++++++++++++++++++++++++++ src/Codec/DecodeIfSupported.php | 50 +++++++++ src/Codec/EncodeIfSupported.php | 50 +++++++++ src/Codec/KnowsCodecLibrary.php | 23 ++++ tests/Codec/CodecLibraryTest.php | 174 +++++++++++++++++++++++++++++++ 6 files changed, 456 insertions(+) create mode 100644 src/Codec/CodecLibrary.php create mode 100644 src/Codec/DecodeIfSupported.php create mode 100644 src/Codec/EncodeIfSupported.php create mode 100644 src/Codec/KnowsCodecLibrary.php create mode 100644 tests/Codec/CodecLibraryTest.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 1e29faf7f..27dc4743b 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -75,6 +75,16 @@ $mergedDriver['platform'] + + + ($value is BSONType ? NativeType : $value) + + + + + ($value is NativeType ? BSONType : $value) + + $cmd[$option] diff --git a/src/Codec/CodecLibrary.php b/src/Codec/CodecLibrary.php new file mode 100644 index 000000000..fa9bbfbe3 --- /dev/null +++ b/src/Codec/CodecLibrary.php @@ -0,0 +1,149 @@ + */ + private $decoders = []; + + /** @var array */ + private $encoders = []; + + /** @param Decoder|Encoder $items */ + public function __construct(...$items) + { + foreach ($items as $item) { + if (! $item instanceof Decoder && ! $item instanceof Encoder) { + throw InvalidArgumentException::invalidType('$items', $item, [Decoder::class, Encoder::class]); + } + + if ($item instanceof Codec) { + // Use attachCodec to avoid multiple calls to attachLibrary + $this->attachCodec($item); + + continue; + } + + if ($item instanceof Decoder) { + $this->attachDecoder($item); + } + + if ($item instanceof Encoder) { + $this->attachEncoder($item); + } + } + } + + /** @return static */ + final public function attachCodec(Codec $codec): self + { + $this->decoders[] = $codec; + $this->encoders[] = $codec; + if ($codec instanceof KnowsCodecLibrary) { + $codec->attachLibrary($this); + } + + return $this; + } + + /** @return static */ + final public function attachDecoder(Decoder $decoder): self + { + $this->decoders[] = $decoder; + if ($decoder instanceof KnowsCodecLibrary) { + $decoder->attachLibrary($this); + } + + return $this; + } + + /** @return static */ + final public function attachEncoder(Encoder $encoder): self + { + $this->encoders[] = $encoder; + if ($encoder instanceof KnowsCodecLibrary) { + $encoder->attachLibrary($this); + } + + return $this; + } + + /** @param mixed $value */ + final public function canDecode($value): bool + { + foreach ($this->decoders as $decoder) { + if ($decoder->canDecode($value)) { + return true; + } + } + + return false; + } + + /** @param mixed $value */ + final public function canEncode($value): bool + { + foreach ($this->encoders as $encoder) { + if ($encoder->canEncode($value)) { + return true; + } + } + + return false; + } + + /** + * @param mixed $value + * @return mixed + */ + final public function decode($value) + { + foreach ($this->decoders as $decoder) { + if ($decoder->canDecode($value)) { + return $decoder->decode($value); + } + } + + throw new UnexpectedValueException(sprintf('No decoder found for value of type "%s"', get_debug_type($value))); + } + + /** + * @param mixed $value + * @return mixed + */ + final public function encode($value) + { + foreach ($this->encoders as $encoder) { + if ($encoder->canEncode($value)) { + return $encoder->encode($value); + } + } + + throw new UnexpectedValueException(sprintf('No encoder found for value of type "%s"', get_debug_type($value))); + } +} diff --git a/src/Codec/DecodeIfSupported.php b/src/Codec/DecodeIfSupported.php new file mode 100644 index 000000000..a2331e0a2 --- /dev/null +++ b/src/Codec/DecodeIfSupported.php @@ -0,0 +1,50 @@ +canDecode($value) ? $this->decode($value) : $value; + } +} diff --git a/src/Codec/EncodeIfSupported.php b/src/Codec/EncodeIfSupported.php new file mode 100644 index 000000000..1f1142fcc --- /dev/null +++ b/src/Codec/EncodeIfSupported.php @@ -0,0 +1,50 @@ +canEncode($value) ? $this->encode($value) : $value; + } +} diff --git a/src/Codec/KnowsCodecLibrary.php b/src/Codec/KnowsCodecLibrary.php new file mode 100644 index 000000000..f1d4ce8b6 --- /dev/null +++ b/src/Codec/KnowsCodecLibrary.php @@ -0,0 +1,23 @@ +getCodecLibrary(); + + $this->assertTrue($codec->canDecode('cigam')); + $this->assertFalse($codec->canDecode('magic')); + + $this->assertSame('magic', $codec->decode('cigam')); + } + + public function testDecodeIfSupported(): void + { + $codec = $this->getCodecLibrary(); + + $this->assertSame('magic', $codec->decodeIfSupported('cigam')); + $this->assertSame('magic', $codec->decodeIfSupported('magic')); + } + + public function testDecodeNull(): void + { + $codec = $this->getCodecLibrary(); + + $this->assertFalse($codec->canDecode(null)); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('No decoder found for value of type "null"'); + + $this->assertNull($codec->decode(null)); + } + + public function testDecodeUnsupportedValue(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('No decoder found for value of type "string"'); + + $this->getCodecLibrary()->decode('foo'); + } + + public function testEncode(): void + { + $codec = $this->getCodecLibrary(); + + $this->assertTrue($codec->canEncode('magic')); + $this->assertFalse($codec->canEncode('cigam')); + + $this->assertSame('cigam', $codec->encode('magic')); + } + + public function testEncodeIfSupported(): void + { + $codec = $this->getCodecLibrary(); + + $this->assertSame('cigam', $codec->encodeIfSupported('magic')); + $this->assertSame('cigam', $codec->encodeIfSupported('cigam')); + } + + public function testEncodeNull(): void + { + $codec = $this->getCodecLibrary(); + + $this->assertFalse($codec->canEncode(null)); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('No encoder found for value of type "null"'); + + $codec->encode(null); + } + + public function testEncodeUnsupportedValue(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('No encoder found for value of type "string"'); + + $this->getCodecLibrary()->encode('foo'); + } + + public function testLibraryAttachesToCodecs(): void + { + $codec = $this->getTestCodec(); + $library = $this->getCodecLibrary(); + + $library->attachCodec($codec); + $this->assertSame($library, $codec->library); + } + + public function testLibraryAttachesToCodecsWhenCreating(): void + { + $codec = $this->getTestCodec(); + $library = new CodecLibrary($codec); + + $this->assertSame($library, $codec->library); + } + + private function getCodecLibrary(): CodecLibrary + { + return new CodecLibrary( + /** @template-implements Codec */ + new class implements Codec + { + use DecodeIfSupported; + use EncodeIfSupported; + + public function canDecode($value): bool + { + return $value === 'cigam'; + } + + public function canEncode($value): bool + { + return $value === 'magic'; + } + + public function decode($value) + { + return 'magic'; + } + + public function encode($value) + { + return 'cigam'; + } + } + ); + } + + private function getTestCodec(): Codec + { + return new class implements Codec, KnowsCodecLibrary { + use DecodeIfSupported; + use EncodeIfSupported; + + public $library; + + public function attachLibrary(CodecLibrary $library): void + { + $this->library = $library; + } + + public function canDecode($value): bool + { + return false; + } + + public function canEncode($value): bool + { + return false; + } + + public function decode($value) + { + return null; + } + + public function encode($value) + { + return null; + } + }; + } +} From 3134ef00a4d6941f9835eab37a6f19555efa5c81 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Fri, 7 Jul 2023 11:25:22 +0200 Subject: [PATCH 04/16] Exclude methods inherited from traits in PedantryTest This commit also uses dataset names for better visibility during debugging --- tests/PedantryTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/PedantryTest.php b/tests/PedantryTest.php index 2885d82a7..a46477d94 100644 --- a/tests/PedantryTest.php +++ b/tests/PedantryTest.php @@ -33,7 +33,8 @@ public function testMethodsAreOrderedAlphabeticallyByVisibility($className): voi $methods = array_filter( $methods, function (ReflectionMethod $method) use ($class) { - return $method->getDeclaringClass() == $class; + return $method->getDeclaringClass() == $class // Exclude inherited methods + && $method->getFileName() === $class->getFileName(); // Exclude methods inherited from traits } ); @@ -86,7 +87,8 @@ public function provideProjectClassNames() continue; } - $classNames[][] = 'MongoDB\\' . str_replace(DIRECTORY_SEPARATOR, '\\', substr($file->getRealPath(), strlen($srcDir) + 1, -4)); + $className = 'MongoDB\\' . str_replace(DIRECTORY_SEPARATOR, '\\', substr($file->getRealPath(), strlen($srcDir) + 1, -4)); + $classNames[$className][] = $className; } return $classNames; From fb5a7a74b1705b12bbc7df31f55cff09144fd793 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Fri, 7 Jul 2023 13:55:24 +0200 Subject: [PATCH 05/16] Exclude stubs from git exports --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index eb9a2b8f3..a7f276e7d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,6 +4,7 @@ tests export-ignore docs export-ignore examples export-ignore mongo-orchestration export-ignore +stubs export-ignore tools export-ignore Makefile export-ignore phpcs.xml.dist export-ignore From 5f530b9c16a6ccbdd42081f9b443b657bb9b0a2a Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Fri, 7 Jul 2023 14:07:46 +0200 Subject: [PATCH 06/16] Improve architecture document --- src/Codec/architecture.md | 57 ++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/src/Codec/architecture.md b/src/Codec/architecture.md index 632e17a1a..41e76e45f 100644 --- a/src/Codec/architecture.md +++ b/src/Codec/architecture.md @@ -1,14 +1,17 @@ # Converting BSON data through codecs -The codec system is a more advanced way to convert BSON data to native types and back, designed for libraries with more -advanced use cases, e.g. object mappers. It is designed to decouple the serialisation logic from the data model, -allowing for more flexible implementations. +Codecs provide a more flexible way to convert BSON data to native types and back and address most of the shortcomings of +the previous type map system. The codec system is designed to be used by libraries that need to convert BSON data into +native types, for example object mappers. Unlike the type map system, codecs allow converting any BSON type to a native +type directly when reading data from the database. Together with lazy decoding of BSON structures, this allows +for a more flexible and efficient way to handle BSON data. ## Encoders and Decoders -The codec interface is split into two smaller interfaces: encoders and decoders. Both interfaces are marked as internal, -as users are only expected to interact with the Codec interface. The interfaces are typed using Psalm generics, allowing -for better type checking when they are used. Without type annotations, the interfaces are equivalent to the following: +The codec interface is comprised by two smaller interfaces: encoders and decoders. Both interfaces are marked as +internal, as users are only expected to interact with the Codec interface. The interfaces are typed as generics through +`@template` annotations, allowing for better type checking when they are used. Without type annotations, the interfaces +are equivalent to the following: ```php namespace MongoDB\Codec; @@ -32,7 +35,7 @@ interface Encoder } ``` -## Codec interface +## Codec Interface The `Codec` interface combines decoding and encoding into a single interface. This will be used for most values except for documents where a more specific `DocumentCodec` is provided. @@ -44,23 +47,16 @@ encode or decode a value only if it is supported. If it is not supported, the or ```php namespace MongoDB\Codec; -/** - * @psalm-template BSONType - * @psalm-template NativeType - * @template-extends Decoder - * @template-extends Encoder - */ interface Codec extends Decoder, Encoder { } ``` -## Document codec +## Document Codec The document codec is special as it is guaranteed to always encode to a BSON document instance and decode to a PHP -object. Document codecs can be provided to a `MongoDB\Collection` instance to have it automatically decode data read -from the database. Likewise, any supported value is encoded before writing to the database in `insert` and `replace` -operations. +object. Document codecs will be used by `MongoDB\Collection` instances to automatically decode BSON data into PHP +objects when reading data, and to encode PHP objects when inserting or replacing data. ```php namespace MongoDB\Codec; @@ -76,11 +72,29 @@ interface DocumentCodec extends Codec } ``` -## Using codecs +## Built-in codecs + +By default, two codecs are provided: an `ArrayCodec` and an `ObjectCodec`. These two codecs are used to recursively +encode and decode values in arrays and `stdClass` instances, respectively. When encoding or decoding an object, +`ObjectCodec` only handles public properties of the object and ignores private and protected properties. + +## Future Work + +### Using Codecs + +The `MongoDB\Collection` class and all operations that work with documents now take a `codec` option. This option is +passed along to the various operations that already take a `typeMap` option. Collections only support a `DocumentCodec` +instance to guarantee that data always encodes to a BSON document and decodes to a PHP object. + +All operations that return documents will use the codec to decode the documents into PHP objects. This includes +the various `find` and `findAndModify` operations in collections as well as the `aggregate` and `watch` operations in +collections, databases, and the client object itself. + +When writing data, any operation that takes an entire document will use the codec to automatically encode the document. +This is limited to `insertOne`, `insertMany`, `replaceOne`, and `findOneAndReplace` operations in collections. `update` +operations will not use the codec to encode documents, as they only support update operators and can't work with the +entire document. -The `MongoDB\Collection` class and all operations that work with documents now take a `codec` option. This can be -an instance of a `DocumentCodec` that will be used to encode documents (for insert and replace operations) and decode -them into PHP objects when reading data. ### Codecs and type maps @@ -96,4 +110,3 @@ this case, the type map is used. The precedence order is as follows: Codecs are not inherited from the client or the database object, as they are purely used for operations that return documents. However, database- or client-level aggregation commands will take an operation-level codec option to decode the resulting documents. - From 2fe40d2a44507c4aee6dc92992847444d68aa9d5 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Fri, 7 Jul 2023 14:21:45 +0200 Subject: [PATCH 07/16] Use native types for DocumentCodec interface --- src/Codec/DocumentCodec.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Codec/DocumentCodec.php b/src/Codec/DocumentCodec.php index bee6b8ebc..7189a7780 100644 --- a/src/Codec/DocumentCodec.php +++ b/src/Codec/DocumentCodec.php @@ -25,4 +25,16 @@ */ interface DocumentCodec extends Codec { + /** + * @param mixed $value + * @psalm-param Document $value + * @psalm-return ObjectType + */ + public function decode($value): object; + + /** + * @param mixed $value + * @psalm-param ObjectType $value + */ + public function encode($value): Document; } From 9e93ba9c2c8f8ed0724e5d027645fafaeaa9bcdb Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Fri, 7 Jul 2023 14:21:52 +0200 Subject: [PATCH 08/16] Add description to codec interfaces --- src/Codec/Codec.php | 3 +++ src/Codec/Decoder.php | 13 +++++++++++++ src/Codec/DocumentCodec.php | 3 +++ src/Codec/Encoder.php | 13 +++++++++++++ src/Codec/KnowsCodecLibrary.php | 5 +++++ 5 files changed, 37 insertions(+) diff --git a/src/Codec/Codec.php b/src/Codec/Codec.php index 37d80a19c..26fddf2cb 100644 --- a/src/Codec/Codec.php +++ b/src/Codec/Codec.php @@ -18,6 +18,9 @@ namespace MongoDB\Codec; /** + * The Codec interface allows decoding BSON data to native PHP types and back + * to BSON. + * * @psalm-template BSONType * @psalm-template NativeType * @template-extends Decoder diff --git a/src/Codec/Decoder.php b/src/Codec/Decoder.php index 2fd3e46ef..dce6b0569 100644 --- a/src/Codec/Decoder.php +++ b/src/Codec/Decoder.php @@ -17,6 +17,8 @@ namespace MongoDB\Codec; +use MongoDB\Exception\InvalidArgumentException; + /** * @internal * @psalm-template BSONType @@ -25,20 +27,31 @@ interface Decoder { /** + * Checks if the decoder supports a given value. + * * @param mixed $value * @psalm-assert-if-true BSONType $value */ public function canDecode($value): bool; /** + * Decodes a given value. If the decoder does not support the value, it + * should throw an exception. + * * @param mixed $value * @psalm-param BSONType $value * @return mixed * @psalm-return NativeType + * @throws InvalidArgumentException if the decoder does not support the value */ public function decode($value); /** + * Decodes a given value if supported, otherwise returns the value as-is. + * + * The DecodeIfSupported trait provides a default implementation of this + * method. + * * @param mixed $value * @psalm-param mixed $value * @return mixed diff --git a/src/Codec/DocumentCodec.php b/src/Codec/DocumentCodec.php index 7189a7780..e4daa9f47 100644 --- a/src/Codec/DocumentCodec.php +++ b/src/Codec/DocumentCodec.php @@ -20,6 +20,9 @@ use MongoDB\BSON\Document; /** + * The DocumentCodec interface allows decoding BSON document data to native PHP + * objects and back to BSON documents. + * * @psalm-template ObjectType of object * @template-extends Codec */ diff --git a/src/Codec/Encoder.php b/src/Codec/Encoder.php index 5d19cd90b..7d678ae07 100644 --- a/src/Codec/Encoder.php +++ b/src/Codec/Encoder.php @@ -17,6 +17,8 @@ namespace MongoDB\Codec; +use MongoDB\Exception\InvalidArgumentException; + /** * @internal * @psalm-template BSONType @@ -25,20 +27,31 @@ interface Encoder { /** + * Checks if the encoder supports a given value. + * * @param mixed $value * @psalm-assert-if-true NativeType $value */ public function canEncode($value): bool; /** + * Encodes a given value. If the encoder does not support the value, it + * should throw an exception. + * * @param mixed $value * @psalm-param NativeType $value * @return mixed * @psalm-return BSONType + * @throws InvalidArgumentException if the decoder does not support the value */ public function encode($value); /** + * Encodes a given value if supported, otherwise returns the value as-is. + * + * The EncodeIfSupported trait provides a default implementation of this + * method. + * * @param mixed $value * @psalm-param mixed $value * @return mixed diff --git a/src/Codec/KnowsCodecLibrary.php b/src/Codec/KnowsCodecLibrary.php index f1d4ce8b6..d49c2e7d7 100644 --- a/src/Codec/KnowsCodecLibrary.php +++ b/src/Codec/KnowsCodecLibrary.php @@ -17,6 +17,11 @@ namespace MongoDB\Codec; +/** + * This interface is used to indicate that a class is aware of the CodecLibrary + * it was added to. The library will be injected when the codec is added to the + * library. This allows codecs to recursively encode its nested values. + */ interface KnowsCodecLibrary { public function attachLibrary(CodecLibrary $library): void; From 1d2859bc792d4c00dcc9a3b8d59c489690b49bea Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Tue, 11 Jul 2023 09:49:26 +0200 Subject: [PATCH 09/16] Remove internal designation from Decoder and Encoder interfaces --- src/Codec/Decoder.php | 1 - src/Codec/Encoder.php | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Codec/Decoder.php b/src/Codec/Decoder.php index dce6b0569..6ec1992e5 100644 --- a/src/Codec/Decoder.php +++ b/src/Codec/Decoder.php @@ -20,7 +20,6 @@ use MongoDB\Exception\InvalidArgumentException; /** - * @internal * @psalm-template BSONType * @psalm-template NativeType */ diff --git a/src/Codec/Encoder.php b/src/Codec/Encoder.php index 7d678ae07..8c25fc3e3 100644 --- a/src/Codec/Encoder.php +++ b/src/Codec/Encoder.php @@ -20,7 +20,6 @@ use MongoDB\Exception\InvalidArgumentException; /** - * @internal * @psalm-template BSONType * @psalm-template NativeType */ From 3ae9681f8fe4367091c449193b820251f35522ce Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Tue, 11 Jul 2023 10:12:24 +0200 Subject: [PATCH 10/16] Remove unnecessary psalm-param annotations --- src/Codec/DecodeIfSupported.php | 1 - src/Codec/Decoder.php | 1 - src/Codec/EncodeIfSupported.php | 1 - src/Codec/Encoder.php | 1 - 4 files changed, 4 deletions(-) diff --git a/src/Codec/DecodeIfSupported.php b/src/Codec/DecodeIfSupported.php index a2331e0a2..aa26b5b74 100644 --- a/src/Codec/DecodeIfSupported.php +++ b/src/Codec/DecodeIfSupported.php @@ -39,7 +39,6 @@ abstract public function decode($value); /** * @param mixed $value - * @psalm-param mixed $value * @return mixed * @psalm-return ($value is BSONType ? NativeType : $value) */ diff --git a/src/Codec/Decoder.php b/src/Codec/Decoder.php index 6ec1992e5..d6e07b0e3 100644 --- a/src/Codec/Decoder.php +++ b/src/Codec/Decoder.php @@ -52,7 +52,6 @@ public function decode($value); * method. * * @param mixed $value - * @psalm-param mixed $value * @return mixed * @psalm-return ($value is BSONType ? NativeType : $value) */ diff --git a/src/Codec/EncodeIfSupported.php b/src/Codec/EncodeIfSupported.php index 1f1142fcc..7e579bc6b 100644 --- a/src/Codec/EncodeIfSupported.php +++ b/src/Codec/EncodeIfSupported.php @@ -39,7 +39,6 @@ abstract public function encode($value); /** * @param mixed $value - * @psalm-param mixed $value * @return mixed * @psalm-return ($value is NativeType ? BSONType : $value) */ diff --git a/src/Codec/Encoder.php b/src/Codec/Encoder.php index 8c25fc3e3..ba5343f80 100644 --- a/src/Codec/Encoder.php +++ b/src/Codec/Encoder.php @@ -52,7 +52,6 @@ public function encode($value); * method. * * @param mixed $value - * @psalm-param mixed $value * @return mixed * @psalm-return ($value is NativeType ? BSONType : $value) */ From b6db63548d0f611f58ad915922fef304c6d55045 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Tue, 11 Jul 2023 10:12:59 +0200 Subject: [PATCH 11/16] Rename attachLibrary method in KnowsCodecLibrary interface The new method name is more specific and helps prevent naming conflicts --- src/Codec/CodecLibrary.php | 6 +++--- src/Codec/KnowsCodecLibrary.php | 2 +- tests/Codec/CodecLibraryTest.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Codec/CodecLibrary.php b/src/Codec/CodecLibrary.php index fa9bbfbe3..c4dca9a92 100644 --- a/src/Codec/CodecLibrary.php +++ b/src/Codec/CodecLibrary.php @@ -65,7 +65,7 @@ final public function attachCodec(Codec $codec): self $this->decoders[] = $codec; $this->encoders[] = $codec; if ($codec instanceof KnowsCodecLibrary) { - $codec->attachLibrary($this); + $codec->attachCodecLibrary($this); } return $this; @@ -76,7 +76,7 @@ final public function attachDecoder(Decoder $decoder): self { $this->decoders[] = $decoder; if ($decoder instanceof KnowsCodecLibrary) { - $decoder->attachLibrary($this); + $decoder->attachCodecLibrary($this); } return $this; @@ -87,7 +87,7 @@ final public function attachEncoder(Encoder $encoder): self { $this->encoders[] = $encoder; if ($encoder instanceof KnowsCodecLibrary) { - $encoder->attachLibrary($this); + $encoder->attachCodecLibrary($this); } return $this; diff --git a/src/Codec/KnowsCodecLibrary.php b/src/Codec/KnowsCodecLibrary.php index d49c2e7d7..e98e845d1 100644 --- a/src/Codec/KnowsCodecLibrary.php +++ b/src/Codec/KnowsCodecLibrary.php @@ -24,5 +24,5 @@ */ interface KnowsCodecLibrary { - public function attachLibrary(CodecLibrary $library): void; + public function attachCodecLibrary(CodecLibrary $library): void; } diff --git a/tests/Codec/CodecLibraryTest.php b/tests/Codec/CodecLibraryTest.php index ce35bed15..00998f900 100644 --- a/tests/Codec/CodecLibraryTest.php +++ b/tests/Codec/CodecLibraryTest.php @@ -145,7 +145,7 @@ private function getTestCodec(): Codec public $library; - public function attachLibrary(CodecLibrary $library): void + public function attachCodecLibrary(CodecLibrary $library): void { $this->library = $library; } From 6476edecbc1a9aafaafbd9e3c539d0baa1baf85f Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Tue, 11 Jul 2023 10:23:34 +0200 Subject: [PATCH 12/16] Use better values in test codecs --- tests/Codec/CodecLibraryTest.php | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/Codec/CodecLibraryTest.php b/tests/Codec/CodecLibraryTest.php index 00998f900..cb3b92517 100644 --- a/tests/Codec/CodecLibraryTest.php +++ b/tests/Codec/CodecLibraryTest.php @@ -16,18 +16,18 @@ public function testDecode(): void { $codec = $this->getCodecLibrary(); - $this->assertTrue($codec->canDecode('cigam')); - $this->assertFalse($codec->canDecode('magic')); + $this->assertTrue($codec->canDecode('encoded')); + $this->assertFalse($codec->canDecode('decoded')); - $this->assertSame('magic', $codec->decode('cigam')); + $this->assertSame('decoded', $codec->decode('encoded')); } public function testDecodeIfSupported(): void { $codec = $this->getCodecLibrary(); - $this->assertSame('magic', $codec->decodeIfSupported('cigam')); - $this->assertSame('magic', $codec->decodeIfSupported('magic')); + $this->assertSame('decoded', $codec->decodeIfSupported('encoded')); + $this->assertSame('decoded', $codec->decodeIfSupported('decoded')); } public function testDecodeNull(): void @@ -54,18 +54,18 @@ public function testEncode(): void { $codec = $this->getCodecLibrary(); - $this->assertTrue($codec->canEncode('magic')); - $this->assertFalse($codec->canEncode('cigam')); + $this->assertTrue($codec->canEncode('decoded')); + $this->assertFalse($codec->canEncode('encoded')); - $this->assertSame('cigam', $codec->encode('magic')); + $this->assertSame('encoded', $codec->encode('decoded')); } public function testEncodeIfSupported(): void { $codec = $this->getCodecLibrary(); - $this->assertSame('cigam', $codec->encodeIfSupported('magic')); - $this->assertSame('cigam', $codec->encodeIfSupported('cigam')); + $this->assertSame('encoded', $codec->encodeIfSupported('decoded')); + $this->assertSame('encoded', $codec->encodeIfSupported('encoded')); } public function testEncodeNull(): void @@ -116,22 +116,22 @@ private function getCodecLibrary(): CodecLibrary public function canDecode($value): bool { - return $value === 'cigam'; + return $value === 'encoded'; } public function canEncode($value): bool { - return $value === 'magic'; + return $value === 'decoded'; } public function decode($value) { - return 'magic'; + return 'decoded'; } public function encode($value) { - return 'cigam'; + return 'encoded'; } } ); From 31c5414580ba4c77ba80405784614ce81f186f0f Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Tue, 11 Jul 2023 10:32:30 +0200 Subject: [PATCH 13/16] Add comment explaining stub files are temporary --- stubs/BSON/Document.stub.php | 2 ++ stubs/BSON/Iterator.stub.php | 7 ++----- stubs/BSON/PackedArray.stub.php | 2 ++ 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/stubs/BSON/Document.stub.php b/stubs/BSON/Document.stub.php index 3d0fec66b..b1b3c60fb 100644 --- a/stubs/BSON/Document.stub.php +++ b/stubs/BSON/Document.stub.php @@ -3,6 +3,8 @@ namespace MongoDB\BSON; /** + * This stub file is temporary and can be removed when using Psalm 5 + * * @template TValue * @template-implements \IteratorAggregate */ diff --git a/stubs/BSON/Iterator.stub.php b/stubs/BSON/Iterator.stub.php index 7a1de9699..cc8f699e4 100644 --- a/stubs/BSON/Iterator.stub.php +++ b/stubs/BSON/Iterator.stub.php @@ -1,13 +1,10 @@ diff --git a/stubs/BSON/PackedArray.stub.php b/stubs/BSON/PackedArray.stub.php index 3c00c614e..231a55019 100644 --- a/stubs/BSON/PackedArray.stub.php +++ b/stubs/BSON/PackedArray.stub.php @@ -3,6 +3,8 @@ namespace MongoDB\BSON; /** + * This stub file is temporary and can be removed when using Psalm 5 + * * @template TValue * @template-implements \IteratorAggregate */ From 00592aedfb1356c6828fdaeef6c96b6be1bb6edf Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Tue, 11 Jul 2023 10:32:47 +0200 Subject: [PATCH 14/16] Add comment explaining potential mocking when using PHPUnit 10 --- tests/Codec/CodecLibraryTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Codec/CodecLibraryTest.php b/tests/Codec/CodecLibraryTest.php index cb3b92517..6fd1643f6 100644 --- a/tests/Codec/CodecLibraryTest.php +++ b/tests/Codec/CodecLibraryTest.php @@ -90,6 +90,7 @@ public function testEncodeUnsupportedValue(): void public function testLibraryAttachesToCodecs(): void { + // TODO PHPUnit >= 10: use createMockForIntersectionOfInterfaces instead $codec = $this->getTestCodec(); $library = $this->getCodecLibrary(); From dd35ad44453416be1911ba6816eedf1c91bb042d Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Tue, 11 Jul 2023 10:49:50 +0200 Subject: [PATCH 15/16] Introduce domain exceptions for decoding and encoding --- src/Codec/CodecLibrary.php | 9 ++-- src/Codec/DecodeIfSupported.php | 3 ++ src/Codec/Decoder.php | 4 +- src/Codec/DocumentCodec.php | 3 ++ src/Codec/EncodeIfSupported.php | 3 ++ src/Codec/Encoder.php | 4 +- src/Exception/UnsupportedValueException.php | 55 +++++++++++++++++++++ tests/Codec/CodecLibraryTest.php | 20 +++----- 8 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 src/Exception/UnsupportedValueException.php diff --git a/src/Codec/CodecLibrary.php b/src/Codec/CodecLibrary.php index c4dca9a92..82acb2187 100644 --- a/src/Codec/CodecLibrary.php +++ b/src/Codec/CodecLibrary.php @@ -18,10 +18,7 @@ namespace MongoDB\Codec; use MongoDB\Exception\InvalidArgumentException; -use MongoDB\Exception\UnexpectedValueException; - -use function get_debug_type; -use function sprintf; +use MongoDB\Exception\UnsupportedValueException; class CodecLibrary implements Codec { @@ -129,7 +126,7 @@ final public function decode($value) } } - throw new UnexpectedValueException(sprintf('No decoder found for value of type "%s"', get_debug_type($value))); + throw UnsupportedValueException::invalidDecodableValue($value); } /** @@ -144,6 +141,6 @@ final public function encode($value) } } - throw new UnexpectedValueException(sprintf('No encoder found for value of type "%s"', get_debug_type($value))); + throw UnsupportedValueException::invalidEncodableValue($value); } } diff --git a/src/Codec/DecodeIfSupported.php b/src/Codec/DecodeIfSupported.php index aa26b5b74..56dcfb9ec 100644 --- a/src/Codec/DecodeIfSupported.php +++ b/src/Codec/DecodeIfSupported.php @@ -17,6 +17,8 @@ namespace MongoDB\Codec; +use MongoDB\Exception\UnsupportedValueException; + /** * @psalm-template BSONType * @psalm-template NativeType @@ -34,6 +36,7 @@ abstract public function canDecode($value): bool; * @psalm-param BSONType $value * @return mixed * @psalm-return NativeType + * @throws UnsupportedValueException if the decoder does not support the value */ abstract public function decode($value); diff --git a/src/Codec/Decoder.php b/src/Codec/Decoder.php index d6e07b0e3..904e097fe 100644 --- a/src/Codec/Decoder.php +++ b/src/Codec/Decoder.php @@ -17,7 +17,7 @@ namespace MongoDB\Codec; -use MongoDB\Exception\InvalidArgumentException; +use MongoDB\Exception\UnsupportedValueException; /** * @psalm-template BSONType @@ -41,7 +41,7 @@ public function canDecode($value): bool; * @psalm-param BSONType $value * @return mixed * @psalm-return NativeType - * @throws InvalidArgumentException if the decoder does not support the value + * @throws UnsupportedValueException if the decoder does not support the value */ public function decode($value); diff --git a/src/Codec/DocumentCodec.php b/src/Codec/DocumentCodec.php index e4daa9f47..ba4488b08 100644 --- a/src/Codec/DocumentCodec.php +++ b/src/Codec/DocumentCodec.php @@ -18,6 +18,7 @@ namespace MongoDB\Codec; use MongoDB\BSON\Document; +use MongoDB\Exception\UnsupportedValueException; /** * The DocumentCodec interface allows decoding BSON document data to native PHP @@ -32,12 +33,14 @@ interface DocumentCodec extends Codec * @param mixed $value * @psalm-param Document $value * @psalm-return ObjectType + * @throws UnsupportedValueException if the decoder does not support the value */ public function decode($value): object; /** * @param mixed $value * @psalm-param ObjectType $value + * @throws UnsupportedValueException if the encoder does not support the value */ public function encode($value): Document; } diff --git a/src/Codec/EncodeIfSupported.php b/src/Codec/EncodeIfSupported.php index 7e579bc6b..c4aebac6b 100644 --- a/src/Codec/EncodeIfSupported.php +++ b/src/Codec/EncodeIfSupported.php @@ -17,6 +17,8 @@ namespace MongoDB\Codec; +use MongoDB\Exception\UnsupportedValueException; + /** * @psalm-template BSONType * @psalm-template NativeType @@ -34,6 +36,7 @@ abstract public function canEncode($value): bool; * @psalm-param NativeType $value * @return mixed * @psalm-return BSONType + * @throws UnsupportedValueException if the encoder does not support the value */ abstract public function encode($value); diff --git a/src/Codec/Encoder.php b/src/Codec/Encoder.php index ba5343f80..dba58d9d5 100644 --- a/src/Codec/Encoder.php +++ b/src/Codec/Encoder.php @@ -17,7 +17,7 @@ namespace MongoDB\Codec; -use MongoDB\Exception\InvalidArgumentException; +use MongoDB\Exception\UnsupportedValueException; /** * @psalm-template BSONType @@ -41,7 +41,7 @@ public function canEncode($value): bool; * @psalm-param NativeType $value * @return mixed * @psalm-return BSONType - * @throws InvalidArgumentException if the decoder does not support the value + * @throws UnsupportedValueException if the encoder does not support the value */ public function encode($value); diff --git a/src/Exception/UnsupportedValueException.php b/src/Exception/UnsupportedValueException.php new file mode 100644 index 000000000..33635d8a0 --- /dev/null +++ b/src/Exception/UnsupportedValueException.php @@ -0,0 +1,55 @@ +value; + } + + /** @param mixed $value */ + public static function invalidDecodableValue($value): self + { + return new self(sprintf('Could not decode value of type "%s".', get_debug_type($value)), $value); + } + + /** @param mixed $value */ + public static function invalidEncodableValue($value): self + { + return new self(sprintf('Could not encode value of type "%s".', get_debug_type($value)), $value); + } + + /** @param mixed $value */ + private function __construct(string $message, $value) + { + parent::__construct($message); + + $this->value = $value; + } +} diff --git a/tests/Codec/CodecLibraryTest.php b/tests/Codec/CodecLibraryTest.php index 6fd1643f6..489e1d1d8 100644 --- a/tests/Codec/CodecLibraryTest.php +++ b/tests/Codec/CodecLibraryTest.php @@ -7,7 +7,7 @@ use MongoDB\Codec\DecodeIfSupported; use MongoDB\Codec\EncodeIfSupported; use MongoDB\Codec\KnowsCodecLibrary; -use MongoDB\Exception\UnexpectedValueException; +use MongoDB\Exception\UnsupportedValueException; use MongoDB\Tests\TestCase; class CodecLibraryTest extends TestCase @@ -36,17 +36,13 @@ public function testDecodeNull(): void $this->assertFalse($codec->canDecode(null)); - $this->expectException(UnexpectedValueException::class); - $this->expectExceptionMessage('No decoder found for value of type "null"'); - - $this->assertNull($codec->decode(null)); + $this->expectExceptionObject(UnsupportedValueException::invalidDecodableValue(null)); + $codec->decode(null); } public function testDecodeUnsupportedValue(): void { - $this->expectException(UnexpectedValueException::class); - $this->expectExceptionMessage('No decoder found for value of type "string"'); - + $this->expectExceptionObject(UnsupportedValueException::invalidDecodableValue('foo')); $this->getCodecLibrary()->decode('foo'); } @@ -74,17 +70,13 @@ public function testEncodeNull(): void $this->assertFalse($codec->canEncode(null)); - $this->expectException(UnexpectedValueException::class); - $this->expectExceptionMessage('No encoder found for value of type "null"'); - + $this->expectExceptionObject(UnsupportedValueException::invalidEncodableValue(null)); $codec->encode(null); } public function testEncodeUnsupportedValue(): void { - $this->expectException(UnexpectedValueException::class); - $this->expectExceptionMessage('No encoder found for value of type "string"'); - + $this->expectExceptionObject(UnsupportedValueException::invalidEncodableValue('foo')); $this->getCodecLibrary()->encode('foo'); } From 6a700321e61ffa58c8306c43b7ac47fb0a8c7cb0 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Tue, 11 Jul 2023 12:49:42 +0200 Subject: [PATCH 16/16] Improve codec architecture documentation --- src/Codec/architecture.md | 89 +++++++++++---------------------------- 1 file changed, 24 insertions(+), 65 deletions(-) diff --git a/src/Codec/architecture.md b/src/Codec/architecture.md index 41e76e45f..b72b5a80c 100644 --- a/src/Codec/architecture.md +++ b/src/Codec/architecture.md @@ -8,76 +8,36 @@ for a more flexible and efficient way to handle BSON data. ## Encoders and Decoders -The codec interface is comprised by two smaller interfaces: encoders and decoders. Both interfaces are marked as -internal, as users are only expected to interact with the Codec interface. The interfaces are typed as generics through -`@template` annotations, allowing for better type checking when they are used. Without type annotations, the interfaces -are equivalent to the following: +The `Codec` interface is comprised of two smaller interfaces: `Decoder` and `Encoder`. The interfaces are typed as +generics through `@template` annotations, allowing for better type checking when they are used. -```php -namespace MongoDB\Codec; +Each decoder and encoder may support a limited set of types or even values. Before calling `decode` or `encode`, the +`canDecode` and `canEncode` methods can be used to determine whether a value is supported. If a value is not supported, +the `decode` and `encode` methods will throw a `UnsupportedValueException` exception. The `decodeIfSupported` and +`encodeIfSupported` methods are useful to encode or decode a value only if it is supported. For unsupported values, the +original value is returned. -interface Decoder -{ - public function canDecode(mixed $value): bool; - - public function decode(mixed $value): mixed; - - public function decodeIfSupported(mixed $value): mixed; -} - -interface Encoder -{ - public function canEncode(mixed $value): bool; - - public function encode(mixed $value): mixed; - - public function encodeIfSupported(mixed $value): mixed; -} -``` - -## Codec Interface +## Codec Interfaces The `Codec` interface combines decoding and encoding into a single interface. This will be used for most values except -for documents where a more specific `DocumentCodec` is provided. - -The base interface supports encoding from a `NativeType` to a `BSONType` and back. Helper methods to determine whether a -value is supported are provided. The `decodeIfSupported` and `encodeIfSupported` methods are useful to have a codec -encode or decode a value only if it is supported. If it is not supported, the original value is returned. - -```php -namespace MongoDB\Codec; - -interface Codec extends Decoder, Encoder -{ -} -``` +for documents where a more specific `DocumentCodec` is provided. The `DocumentCodec` interface overrides the `decode` +and `encode` methods from the base `Codec` interface to narrow the return types. Document codecs guarantee to always +encode to a BSON document instance and decode to a PHP object. -## Document Codec - -The document codec is special as it is guaranteed to always encode to a BSON document instance and decode to a PHP -object. Document codecs will be used by `MongoDB\Collection` instances to automatically decode BSON data into PHP -objects when reading data, and to encode PHP objects when inserting or replacing data. - -```php -namespace MongoDB\Codec; - -use MongoDB\BSON\Document; - -/** - * @template ObjectType of object - * @extends Codec - */ -interface DocumentCodec extends Codec -{ -} -``` - -## Built-in codecs +## Built-in Codecs By default, two codecs are provided: an `ArrayCodec` and an `ObjectCodec`. These two codecs are used to recursively encode and decode values in arrays and `stdClass` instances, respectively. When encoding or decoding an object, `ObjectCodec` only handles public properties of the object and ignores private and protected properties. +## Codec Libraries + +The `CodecLibrary` class is able to combine several `Decoder`, `Encoder`, and `Codec` instances into a single codec. +When decoding or encoding a value, the library will use the first instance that supports the value. This allows for +easier composition of codecs. A `Decoder`, `Encoder`, or `Codec` implementation may choose to implement the +`KnowsCodecLibrary` interface. In this case, when the codec is added to a library, the library is injected into the +instance using the `attachCodecLibrary` method. This allows the codec to use the library to decode or encode values. + ## Future Work ### Using Codecs @@ -91,10 +51,9 @@ the various `find` and `findAndModify` operations in collections as well as the collections, databases, and the client object itself. When writing data, any operation that takes an entire document will use the codec to automatically encode the document. -This is limited to `insertOne`, `insertMany`, `replaceOne`, and `findOneAndReplace` operations in collections. `update` -operations will not use the codec to encode documents, as they only support update operators and can't work with the -entire document. - +This is limited to `insertOne`, `insertMany`, `replaceOne`, and `findOneAndReplace` operations in collections as well +as `insertOne` and `replaceOne` operations in bulk writes. `update` operations will not use the codec to encode +documents, as they only support update operators and can't work with the entire document. ### Codecs and type maps @@ -108,5 +67,5 @@ this case, the type map is used. The precedence order is as follows: * collection-level `typeMap` option Codecs are not inherited from the client or the database object, as they are purely used for operations that return -documents. However, database- or client-level aggregation commands will take an operation-level codec option to +documents. However, database- or client-level aggregation commands will take an operation-level `codec` option to decode the resulting documents.