Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc: Update custom mapping example #2654

Merged
merged 1 commit into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a suggestion, but this could benefit from some value checks:

if (! is_array($value) || ! isset($value['utc'], $value['tz'])) {
    throw new Exception(); // TODO: throw correct exception class
}

The end result will be the same, but I feel like there can be more useful information in the exception message rather than letting PHP throw its type errors. Not sure if there's prior art for this in our type, so feel free to disregard and leave this as an exercise to the user.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, it's easy to be sure of a property's value, especially with typing, but there can always be surprises with database content, especially when there's no schema (which is certainly very common).

$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;
}
Loading