Skip to content

Commit

Permalink
Update custom mapping example
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
GromNaN committed Jun 27, 2024
1 parent 1408466 commit 911fe56
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 30 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@
"psr-4": {
"Doctrine\\ODM\\MongoDB\\Benchmark\\": "benchmark",
"Doctrine\\ODM\\MongoDB\\Tests\\": "tests/Doctrine/ODM/MongoDB/Tests",
"Documentation\\": "tests/Documentation",
"Documents\\": "tests/Documents",
"Documents81\\": "tests/Documents81",
"Stubs\\": "tests/Stubs",
"TestDocuments\\" :"tests/Doctrine/ODM/MongoDB/Tests/Mapping/Driver/fixtures"
}
Expand Down
68 changes: 39 additions & 29 deletions docs/en/reference/custom-mapping-types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,58 @@ to replace the existing implementation of a mapping type.

In order to create a new mapping type you need to subclass
``Doctrine\ODM\MongoDB\Types\Type`` and implement/override
the methods. Here is an example skeleton of such a custom type
class:
the methods.

The following example defines a custom type that stores ``DateTimeInterface``
instances as an embedded document containing a BSON date and accompanying
timezone string. Those same embedded documents are then be translated back into
a ``DateTimeImmutable`` when the data is read from the database.

.. code-block:: php
<?php
namespace My\Project\Types;
use DateTimeImmutable;
use DateTimeZone;
use Doctrine\ODM\MongoDB\Types\ClosureToPHP;
use Doctrine\ODM\MongoDB\Types\Type;
use MongoDB\BSON\UTCDateTime;
/**
* My custom datatype.
*/
class MyType extends Type
class DateTimeWithTimezoneType extends Type
{
// This trait provides default closureToPHP used during data hydration
use ClosureToPHP;
public function convertToPHPValue($value): \DateTime
public function convertToPHPValue($value): DateTimeImmutable
{
// This is called to convert a Mongo value to a PHP representation
return $value->toDateTime();
$timeZone = new DateTimeZone($value['tz']);
$dateTime = $value['utc']
->toDateTime()
->setTimeZone($timeZone);
return DateTimeImmutable::createFromMutable($dateTime);
}
public function convertToDatabaseValue($value): UTCDateTime
public function convertToDatabaseValue($value): array
{
// This is called to convert a PHP value to its Mongo equivalent
return new UTCDateTime($value);
if (! isset($value['utc'], $value['tz'])) {
throw new RuntimeException('Database value cannot be converted to date with timezone. Expected array with "utc" and "tz" keys.');
}
return [
'utc' => new UTCDateTime($value),
'tz' => $value->getTimezone()->getName(),
];
}
}
Restrictions to keep in mind:

-
If the value of the field is *NULL* the method
``convertToDatabaseValue()`` is not called.
If the value of the field is *NULL* the method ``convertToDatabaseValue()``
is not called. You don't need to check for *NULL* values.
-
The ``UnitOfWork`` never passes values to the database convert
method that did not change in the request.
Expand All @@ -59,41 +72,38 @@ know about it:
// in bootstrapping code
// ...
use Doctrine\ODM\MongoDB\Types\Type;
// ...
// Adds a type. This results in an exception if type with given name is already registered
Type::addType('mytype', \My\Project\Types\MyType::class);
Type::addType('date_with_timezone', \My\Project\Types\DateTimeWithTimezoneType::class);
// Overrides a type. This results in an exception if type with given name is not registered
Type::overrideType('mytype', \My\Project\Types\MyType::class);
Type::overrideType('date_immutable', \My\Project\Types\DateTimeWithTimezoneType::class);
// Registers a type without checking whether it was already registered
Type::registerType('mytype', \My\Project\Types\MyType::class);
Type::registerType('date_immutable', \My\Project\Types\DateTimeWithTimezoneType::class);
As can be seen above, when registering the custom types in the
configuration you specify a unique name for the mapping type and
map that to the corresponding |FQCN|. Now you can use your new
type in your mapping like this:
As can be seen above, when registering the custom types in the configuration you
specify a unique name for the mapping type and map that to the corresponding
|FQCN|. Now you can use your new type in your mapping like this:

.. configuration-block::

.. code-block:: php
<?php
class MyPersistentClass
use DateTimeImmutable;
class Thing
{
#[Field(type: 'mytype')]
private \DateTime $field;
#[Field(type: 'date_with_timezone')]
public DateTimeImmutable $date;
}
.. code-block:: xml
<field field-name="field" type="mytype" />
<field field-name="field" type="date_with_timezone" />
.. |FQCN| raw:: html
<abbr title="Fully-Qualified Class Name">FQCN</abbr>
40 changes: 40 additions & 0 deletions tests/Documentation/CustomMapping/CustomMappingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Documentation\CustomMapping;

use DateTimeImmutable;
use DateTimeZone;
use Doctrine\ODM\MongoDB\Tests\BaseTestCase;
use Doctrine\ODM\MongoDB\Types\Type;

class CustomMappingTest extends BaseTestCase
{
public function testTest(): void
{
Type::addType('date_with_timezone', DateTimeWithTimezoneType::class);
Type::overrideType('date_immutable', DateTimeWithTimezoneType::class);

$thing = new Thing();
$thing->date = new DateTimeImmutable('2021-01-01 00:00:00', new DateTimeZone('Africa/Tripoli'));

$this->dm->persist($thing);
$this->dm->flush();
$this->dm->clear();

$result = $this->dm->find(Thing::class, $thing->id);
$this->assertEquals($thing->date, $result->date);
$this->assertEquals('Africa/Tripoli', $result->date->getTimezone()->getName());

// Ensure we don't need to handle null values
$nothing = new Thing();

$this->dm->persist($nothing);
$this->dm->flush();
$this->dm->clear();

$result = $this->dm->find(Thing::class, $nothing->id);
$this->assertNull($result->date);
}
}
47 changes: 47 additions & 0 deletions tests/Documentation/CustomMapping/DateTimeWithTimezoneType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Documentation\CustomMapping;

use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use Doctrine\ODM\MongoDB\Types\ClosureToPHP;
use Doctrine\ODM\MongoDB\Types\Type;
use MongoDB\BSON\UTCDateTime;
use RuntimeException;

class DateTimeWithTimezoneType extends Type
{
// This trait provides default closureToPHP used during data hydration
use ClosureToPHP;

/** @param array{utc: UTCDateTime, tz: string} $value */
public function convertToPHPValue($value): DateTimeImmutable
{
if (! isset($value['utc'], $value['tz'])) {
throw new RuntimeException('Database value cannot be converted to date with timezone. Expected array with "utc" and "tz" keys.');
}

$timeZone = new DateTimeZone($value['tz']);
$dateTime = $value['utc']
->toDateTime()
->setTimeZone($timeZone);

return DateTimeImmutable::createFromMutable($dateTime);
}

/**
* @param DateTimeInterface $value
*
* @return array{utc: UTCDateTime, tz: string}
*/
public function convertToDatabaseValue($value): array
{
return [
'utc' => new UTCDateTime($value),
'tz' => $value->getTimezone()->getName(),
];
}
}
20 changes: 20 additions & 0 deletions tests/Documentation/CustomMapping/Thing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Documentation\CustomMapping;

use DateTimeImmutable;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Field;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Id;

#[Document]
class Thing
{
#[Id]
public string $id;

#[Field(type: 'date_with_timezone')]
public ?DateTimeImmutable $date = null;
}

0 comments on commit 911fe56

Please sign in to comment.