Skip to content

Commit 3c9d874

Browse files
committed
PHPLIB-1209: Add tutorial to show working with Codecs
1 parent e91e018 commit 3c9d874

File tree

2 files changed

+303
-0
lines changed

2 files changed

+303
-0
lines changed

docs/tutorial.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Tutorials
88
/tutorial/connecting
99
/tutorial/server-selection
1010
/tutorial/crud
11+
/tutorial/codecs
1112
/tutorial/collation
1213
/tutorial/commands
1314
/tutorial/custom-types

docs/tutorial/codecs.txt

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
======
2+
Codecs
3+
======
4+
5+
.. default-domain:: mongodb
6+
7+
.. contents:: On this page
8+
:local:
9+
:backlinks: none
10+
:depth: 2
11+
:class: singlecol
12+
13+
.. versionadded:: 1.17
14+
15+
Overview
16+
--------
17+
18+
Codecs are used to decode BSON documents into PHP objects and vice versa. In contrast to other methods, they allow for
19+
more flexibility and customization of the process and how different data types are handled. They allow separating the
20+
logic for BSON encoding and decoding from the domain classes, allowing to decode BSON into plain old PHP objects (POPOs).
21+
22+
Document Codec Usage
23+
--------------------
24+
25+
The main logic is contained in a document codec. This class implements the ``MongoDB\Codec\DocumentCodec`` interface and
26+
defines what data types can be encoded/decoded and how. The following example defines a ``Person`` class and a codec to
27+
encode/decode it:
28+
29+
.. code-block:: php
30+
31+
<?php
32+
33+
final class Person
34+
{
35+
public MongoDB\BSON\ObjectId $id;
36+
public string $name;
37+
38+
public function __construct(string $name)
39+
{
40+
$this->id = new MongoDB\BSON\ObjectId();
41+
$this->name = $name;
42+
}
43+
}
44+
45+
.. code-block:: php
46+
47+
<?php
48+
49+
final class PersonCodec implements MongoDB\Codec\DocumentCodec
50+
{
51+
// These traits define commonly used functionality to avoid duplication
52+
use MongoDB\Codec\DecodeIfSupported;
53+
use MongoDB\Codec\EncodeIfSupported;
54+
55+
public function canDecode($value): bool
56+
{
57+
return $value instanceof MongoDB\BSON\Document && $value->has('name');
58+
}
59+
60+
public function canEncode($value): bool
61+
{
62+
return $value instanceof Person;
63+
}
64+
65+
public function decode($value): Person
66+
{
67+
if (! $this->canDecode($value)) {
68+
throw UnsupportedValueException::invalidDecodableValue($value);
69+
}
70+
71+
$person = new Person($value->get('name'));
72+
$person->id = $value->get('_id');
73+
74+
return $person;
75+
}
76+
77+
public function encode($value): MongoDB\BSON\Document
78+
{
79+
if (! $this->canEncode($value)) {
80+
throw UnsupportedValueException::invalidEncodableValue($value);
81+
}
82+
83+
return MongoDB\BSON\Document::fromPHP([
84+
'_id' => $value->id,
85+
'name' => $value->name,
86+
]);
87+
}
88+
}
89+
90+
To then use this codec with a collection, specify the ``codec`` option when selecting the collection:
91+
92+
.. code-block:: php
93+
94+
<?php
95+
96+
$collection = (new MongoDB\Client())->selectCollection('test', 'person', ['codec' => new PersonCodec()]);
97+
98+
$person = new Person('Jane Doe');
99+
$collection->insertOne($person);
100+
101+
$person = $collection->findOne();
102+
103+
The example above selects a collection and instructs it to use the ``PersonCodec`` for encoding and decoding documents.
104+
When inserting data, the ``PersonCodec`` is used to encode the document. When retrieving data, the ``PersonCodec`` is
105+
used to decode BSON data into a ``Person`` instance.
106+
107+
.. note::
108+
109+
When working on a collection with a codec, all incoming and outgoing data will be handled with the given codec. This
110+
affects the following methods: ``aggregate``, ``bulkWrite``, ``find``, ``findOne``, ``findOneAndDelete``,
111+
``findOneAndReplace``, ``findOneAndUpdate``, ``insertMany``, ``insertOne``, and ``replaceOne``. To disable the codec
112+
for a specific operation, you can provide ``null`` for the ``codec`` option in the operation.
113+
114+
Generic Codecs
115+
--------------
116+
117+
The previous example showed how to define a codec for a specific class. However, sometimes you want to define codecs to
118+
handle a given type in all documents. For such codecs, you can implement the ``MongoDB\Codec\Codec`` interface. The
119+
following example defines a codec to store ``DateTimeInterface`` instances as BSON dates including the timezone:
120+
121+
.. code-block:: php
122+
123+
<?php
124+
125+
final class DateTimeCodec implements MongoDB\Codec\Codec
126+
{
127+
// These traits define commonly used functionality to avoid duplication
128+
use MongoDB\Codec\DecodeIfSupported;
129+
use MongoDB\Codec\EncodeIfSupported;
130+
131+
public function canDecode($value): bool
132+
{
133+
// For maximum compatibility, this codec supports decoding both UTCDateTime instances and documents with a
134+
// UTCDateTime instance and optional timezone
135+
return
136+
$value instanceof MongoDB\BSON\UTCDateTime ||
137+
$value instanceof MongoDB\BSON\Document && $value->has('utc');
138+
}
139+
140+
public function canEncode($value): bool
141+
{
142+
return $value instanceof DateTimeInterface;
143+
}
144+
145+
public function decode($value): DateTimeImmutable
146+
{
147+
if (! $this->canDecode($value)) {
148+
throw UnsupportedValueException::invalidDecodableValue($value);
149+
}
150+
151+
$utc = $value instanceof MongoDB\BSON\UTCDateTime
152+
? $value
153+
: $value->get('utc');
154+
155+
if (! $utc instanceof UTCDateTime) {
156+
throw UnsupportedValueException::invalidDecodableValue($utc);
157+
}
158+
159+
$dateTime = $utc->toDateTime();
160+
161+
if ($value instanceof MongoDB\BSON\Document && $value->has('tz')) {
162+
$dateTime->setTimeZone(new DateTimeZone($value->get('tz')));
163+
}
164+
165+
return DateTimeImmutable::fromMutable($dateTime);
166+
}
167+
168+
public function encode($value): MongoDB\BSON\Document
169+
{
170+
if (! $this->canEncode($value)) {
171+
throw UnsupportedValueException::invalidEncodableValue($value);
172+
}
173+
174+
return MongoDB\BSON\Document::fromPHP([
175+
'utc' => new UTCDateTime($value),
176+
'tz' => $value->getTimezone()->getName(),
177+
]);
178+
}
179+
}
180+
181+
This codec can now be leveraged by other codecs encode and decode dates. Let's return to the previous example and add a
182+
``createdAt`` field that contains the creation date. The modified encode and decode methods would look like this (other
183+
code omitted for brevity):
184+
185+
.. code-block:: php
186+
187+
<?php
188+
189+
final class PersonCodec implements MongoDB\Codec\DocumentCodec
190+
{
191+
private DateTimeCodec $dateTimeCodec;
192+
193+
public function __construct()
194+
{
195+
$this->dateTimeCodec = new DateTimeCodec();
196+
}
197+
198+
// Other code omitted for brevity
199+
public function decode($value): Person
200+
{
201+
if (! $this->canDecode($value)) {
202+
throw UnsupportedValueException::invalidDecodableValue($value);
203+
}
204+
205+
$person = new Person($value->get('name'));
206+
$person->id = $value->get('_id');
207+
$person->createdAt = $this->dateTimeCodec->decode($value->get('createdAt'));
208+
209+
return $person;
210+
}
211+
212+
public function encode($value): MongoDB\BSON\Document
213+
{
214+
if (! $this->canEncode($value)) {
215+
throw UnsupportedValueException::invalidEncodableValue($value);
216+
}
217+
218+
return MongoDB\BSON\Document::fromPHP([
219+
'_id' => $value->id,
220+
'name' => $value->name,
221+
'createdAt' => $this->dateTimeCodec->encode($value->createdAt),
222+
]);
223+
}
224+
}
225+
226+
Codec Libraries
227+
---------------
228+
229+
If you have a number of codecs that you want to use in multiple places, you can create a codec library. A codec library
230+
contains a list of codecs and checks each codec if it supports a value. If it does, it will use that codec to encode or
231+
decode the given value. The following code snippet changes the PersonCodec to use a codec library instead of a
232+
hard-coded ``DateTimeCodec``:
233+
234+
.. code-block:: php
235+
236+
<?php
237+
238+
final class PersonCodec implements MongoDB\Codec\DocumentCodec
239+
{
240+
private MongoDB\Codec\CodecLibrary $library;
241+
242+
public function __construct(private MongoDB\Codec\CodecLibrary $codecLibrary)
243+
{
244+
}
245+
246+
// Other code omitted for brevity
247+
public function decode($value): Person
248+
{
249+
if (! $this->canDecode($value)) {
250+
throw UnsupportedValueException::invalidDecodableValue($value);
251+
}
252+
253+
$person = new Person($value->get('name'));
254+
$person->id = $value->get('_id');
255+
$person->createdAt = $this->codecLibrary->decode($value->get('createdAt'));
256+
257+
return $person;
258+
}
259+
260+
public function encode($value): MongoDB\BSON\Document
261+
{
262+
if (! $this->canEncode($value)) {
263+
throw UnsupportedValueException::invalidEncodableValue($value);
264+
}
265+
266+
return MongoDB\BSON\Document::fromPHP([
267+
'_id' => $value->id,
268+
'name' => $value->name,
269+
'createdAt' => $this->codecLibrary->encode($value->createdAt),
270+
]);
271+
}
272+
}
273+
274+
This way, you can configure a global codec library and reuse it in multiple places:
275+
276+
.. code-block:: php
277+
278+
<?php
279+
280+
$codecLibrary = new MongoDB\Codec\CodecLibrary([
281+
new DateTimeCodec(),
282+
]);
283+
284+
$personCodec = new PersonCodec($codecLibrary);
285+
286+
.. note::
287+
288+
Be careful when adding ``DocumentCodec`` instances to a codec library. The codec library will only use the first
289+
codec that supports a given value. While this will most likely work fine when encoding to BSON, it can lead to issues
290+
when decoding from BSON as you'll have to inspect the BSON document to detect whether the codec supports it. For this
291+
reason, the ``codec`` option in a collection does not accept a codec library.
292+
293+
Library-aware Codecs
294+
~~~~~~~~~~~~~~~~~~~~
295+
296+
If you have a codec that needs to be aware of the codec library, you can implement the ``MongoDB\Codec\LibraryAware``
297+
interface. When the codec is added to a library, this library is then injected into the codec. This allows you to create
298+
a common library for all value codecs and re-use it in multiple document codecs.
299+
300+
.. note::
301+
302+
The ``attachCodecLibrary`` method will not be called if a library-aware codec is never added to a library.

0 commit comments

Comments
 (0)