Doctrine allows you to create new mapping types. This can come in handy when you're missing a specific mapping type or when you want 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.
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.
<?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;
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);
}
/** @return array{utc: UTCDateTime, tz: string} */
public function convertToDatabaseValue($value): array
{
if (!$value instanceof DateTimeImmutable) {
throw new \RuntimeException(
sprintf(
'Expected instance of \DateTimeImmutable, got %s',
gettype($value)
)
);
}
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. You don't need to check for NULL values. - The
UnitOfWorknever passes values to the database convert method that did not change in the request.
When you have implemented the type you still need to let Doctrine
know about it. You can create a TypeRegistry and inject it into the
DocumentManager:
<?php
// in bootstrapping code
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\Types\TypeRegistry;
$typeRegistry = new TypeRegistry();
// Adds a type. This results in an exception if type with given name is already registered
$typeRegistry->register('date_with_timezone', new \My\Project\Types\DateTimeWithTimezoneType());
// Overrides a type. This replaces any existing type with given name
$typeRegistry->register('date_immutable', new \My\Project\Types\DateTimeWithTimezoneType();
// Initialize DocumentManager with the TypeRegistry
$config = new Configuration();
$config->setTypeRegistry($typeRegistry);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
use DateTimeImmutable;
class Thing
{
#[Field(type: 'date_with_timezone')]
public DateTimeImmutable $date;
}
.. code-block:: xml
<field field-name="field" type="date_with_timezone" />
You can create a custom mapping type for your own value objects or classes. For
example, to map a Money value object using the moneyphp/money library, you can
implement a type that converts between this class and a BSON embedded document format.
This approach works for any custom class by adapting the conversion logic to your needs.
Example Implementation (using Money\Money):
.. code-block:: php
<?php
namespace AppMongoDBTypes;
use DoctrineODMMongoDBTypesClosureToPHP; use DoctrineODMMongoDBTypesType; use InvalidArgumentException; use MoneyMoney; use MoneyCurrency;
final class MoneyType extends Type {
// This trait provides a default closureToPHP used during data hydration use ClosureToPHP;
public function convertToPHPValue(mixed $value): ?Money {
- if (null === $value) {
- return null;
}
- if (is_array($value) && isset($value['amount'], $value['currency'])) {
- return new Money($value['amount'], new Currency($value['currency']));
}
throw new InvalidArgumentException(sprintf('Could not convert database value from "%s" to %s', get_debug_type($value), Money::class));
}
public function convertToDatabaseValue(mixed $value): ?array {
- if (null === $value) {
- return null;
}
- if ($value instanceof Money) {
- return [
- 'amount' => $value->getAmount(), 'currency' => $value->getCurrency()->getCode(),
];
}
throw new InvalidArgumentException(sprintf('Could not convert database value from "%s" to array', get_debug_type($value)));
}
}
Register the type in your bootstrap code:
.. code-block:: php
$typeRegistry = new TypeRegistry(); $typeRegistry->register(Money::class, new AppMongoDBTypesMoneyType());
By using the |FQCN| of the value object class as the type name, the type is
automatically used when encountering a property of that class. This means you
can omit the type option when defining the field mapping:
.. code-block:: php
#[Field] public ?MoneyMoney $price;
Note
This implementation of MoneyType is kept simple for illustration purposes
and does not handle all edge cases, but it should give you a good starting
point for implementing your own custom types.