Skip to content

Commit 8b542f4

Browse files
authored
Update custom mapping example (#2654)
The previous example was a simplified copy of the date type. In order to present something more useful, the new example is inspired by MongoDB's codec tutorial.
1 parent e40c8b1 commit 8b542f4

File tree

4 files changed

+146
-29
lines changed

4 files changed

+146
-29
lines changed

docs/en/reference/custom-mapping-types.rst

+39-29
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,58 @@ to replace the existing implementation of a mapping type.
77

88
In order to create a new mapping type you need to subclass
99
``Doctrine\ODM\MongoDB\Types\Type`` and implement/override
10-
the methods. Here is an example skeleton of such a custom type
11-
class:
10+
the methods.
11+
12+
The following example defines a custom type that stores ``DateTimeInterface``
13+
instances as an embedded document containing a BSON date and accompanying
14+
timezone string. Those same embedded documents are then be translated back into
15+
a ``DateTimeImmutable`` when the data is read from the database.
1216

1317
.. code-block:: php
1418
1519
<?php
1620
1721
namespace My\Project\Types;
1822
23+
use DateTimeImmutable;
24+
use DateTimeZone;
1925
use Doctrine\ODM\MongoDB\Types\ClosureToPHP;
2026
use Doctrine\ODM\MongoDB\Types\Type;
2127
use MongoDB\BSON\UTCDateTime;
2228
23-
/**
24-
* My custom datatype.
25-
*/
26-
class MyType extends Type
29+
class DateTimeWithTimezoneType extends Type
2730
{
2831
// This trait provides default closureToPHP used during data hydration
2932
use ClosureToPHP;
3033
31-
public function convertToPHPValue($value): \DateTime
34+
public function convertToPHPValue($value): DateTimeImmutable
3235
{
33-
// This is called to convert a Mongo value to a PHP representation
34-
return $value->toDateTime();
36+
$timeZone = new DateTimeZone($value['tz']);
37+
$dateTime = $value['utc']
38+
->toDateTime()
39+
->setTimeZone($timeZone);
40+
41+
return DateTimeImmutable::createFromMutable($dateTime);
3542
}
3643
37-
public function convertToDatabaseValue($value): UTCDateTime
44+
public function convertToDatabaseValue($value): array
3845
{
39-
// This is called to convert a PHP value to its Mongo equivalent
40-
return new UTCDateTime($value);
46+
if (! isset($value['utc'], $value['tz'])) {
47+
throw new RuntimeException('Database value cannot be converted to date with timezone. Expected array with "utc" and "tz" keys.');
48+
}
49+
50+
return [
51+
'utc' => new UTCDateTime($value),
52+
'tz' => $value->getTimezone()->getName(),
53+
];
4154
}
4255
}
4356
4457
Restrictions to keep in mind:
4558

4659
-
47-
If the value of the field is *NULL* the method
48-
``convertToDatabaseValue()`` is not called.
60+
If the value of the field is *NULL* the method ``convertToDatabaseValue()``
61+
is not called. You don't need to check for *NULL* values.
4962
-
5063
The ``UnitOfWork`` never passes values to the database convert
5164
method that did not change in the request.
@@ -59,41 +72,38 @@ know about it:
5972
6073
// in bootstrapping code
6174
62-
// ...
63-
6475
use Doctrine\ODM\MongoDB\Types\Type;
6576
66-
// ...
67-
6877
// Adds a type. This results in an exception if type with given name is already registered
69-
Type::addType('mytype', \My\Project\Types\MyType::class);
78+
Type::addType('date_with_timezone', \My\Project\Types\DateTimeWithTimezoneType::class);
7079
7180
// Overrides a type. This results in an exception if type with given name is not registered
72-
Type::overrideType('mytype', \My\Project\Types\MyType::class);
81+
Type::overrideType('date_immutable', \My\Project\Types\DateTimeWithTimezoneType::class);
7382
7483
// Registers a type without checking whether it was already registered
75-
Type::registerType('mytype', \My\Project\Types\MyType::class);
84+
Type::registerType('date_immutable', \My\Project\Types\DateTimeWithTimezoneType::class);
7685
77-
As can be seen above, when registering the custom types in the
78-
configuration you specify a unique name for the mapping type and
79-
map that to the corresponding |FQCN|. Now you can use your new
80-
type in your mapping like this:
86+
As can be seen above, when registering the custom types in the configuration you
87+
specify a unique name for the mapping type and map that to the corresponding
88+
|FQCN|. Now you can use your new type in your mapping like this:
8189

8290
.. configuration-block::
8391

8492
.. code-block:: php
8593
8694
<?php
8795
88-
class MyPersistentClass
96+
use DateTimeImmutable;
97+
98+
class Thing
8999
{
90-
#[Field(type: 'mytype')]
91-
private \DateTime $field;
100+
#[Field(type: 'date_with_timezone')]
101+
public DateTimeImmutable $date;
92102
}
93103
94104
.. code-block:: xml
95105
96-
<field field-name="field" type="mytype" />
106+
<field field-name="field" type="date_with_timezone" />
97107
98108
.. |FQCN| raw:: html
99109
<abbr title="Fully-Qualified Class Name">FQCN</abbr>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Documentation\CustomMapping;
6+
7+
use DateTimeImmutable;
8+
use DateTimeZone;
9+
use Doctrine\ODM\MongoDB\Tests\BaseTestCase;
10+
use Doctrine\ODM\MongoDB\Types\Type;
11+
12+
class CustomMappingTest extends BaseTestCase
13+
{
14+
public function testTest(): void
15+
{
16+
Type::addType('date_with_timezone', DateTimeWithTimezoneType::class);
17+
Type::overrideType('date_immutable', DateTimeWithTimezoneType::class);
18+
19+
$thing = new Thing();
20+
$thing->date = new DateTimeImmutable('2021-01-01 00:00:00', new DateTimeZone('Africa/Tripoli'));
21+
22+
$this->dm->persist($thing);
23+
$this->dm->flush();
24+
$this->dm->clear();
25+
26+
$result = $this->dm->find(Thing::class, $thing->id);
27+
$this->assertEquals($thing->date, $result->date);
28+
$this->assertEquals('Africa/Tripoli', $result->date->getTimezone()->getName());
29+
30+
// Ensure we don't need to handle null values
31+
$nothing = new Thing();
32+
33+
$this->dm->persist($nothing);
34+
$this->dm->flush();
35+
$this->dm->clear();
36+
37+
$result = $this->dm->find(Thing::class, $nothing->id);
38+
$this->assertNull($result->date);
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Documentation\CustomMapping;
6+
7+
use DateTimeImmutable;
8+
use DateTimeInterface;
9+
use DateTimeZone;
10+
use Doctrine\ODM\MongoDB\Types\ClosureToPHP;
11+
use Doctrine\ODM\MongoDB\Types\Type;
12+
use MongoDB\BSON\UTCDateTime;
13+
use RuntimeException;
14+
15+
class DateTimeWithTimezoneType extends Type
16+
{
17+
// This trait provides default closureToPHP used during data hydration
18+
use ClosureToPHP;
19+
20+
/** @param array{utc: UTCDateTime, tz: string} $value */
21+
public function convertToPHPValue($value): DateTimeImmutable
22+
{
23+
if (! isset($value['utc'], $value['tz'])) {
24+
throw new RuntimeException('Database value cannot be converted to date with timezone. Expected array with "utc" and "tz" keys.');
25+
}
26+
27+
$timeZone = new DateTimeZone($value['tz']);
28+
$dateTime = $value['utc']
29+
->toDateTime()
30+
->setTimeZone($timeZone);
31+
32+
return DateTimeImmutable::createFromMutable($dateTime);
33+
}
34+
35+
/**
36+
* @param DateTimeInterface $value
37+
*
38+
* @return array{utc: UTCDateTime, tz: string}
39+
*/
40+
public function convertToDatabaseValue($value): array
41+
{
42+
return [
43+
'utc' => new UTCDateTime($value),
44+
'tz' => $value->getTimezone()->getName(),
45+
];
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Documentation\CustomMapping;
6+
7+
use DateTimeImmutable;
8+
use Doctrine\ODM\MongoDB\Mapping\Annotations\Document;
9+
use Doctrine\ODM\MongoDB\Mapping\Annotations\Field;
10+
use Doctrine\ODM\MongoDB\Mapping\Annotations\Id;
11+
12+
#[Document]
13+
class Thing
14+
{
15+
#[Id]
16+
public string $id;
17+
18+
#[Field(type: 'date_with_timezone')]
19+
public ?DateTimeImmutable $date = null;
20+
}

0 commit comments

Comments
 (0)