Implementing a TypedFieldMapper

New in version 2.14

You can specify custom typed field mapping between PHP type and DBAL type using Doctrine\ORM\Configuration and a custom Doctrine\ORM\Mapping\TypedFieldMapper implementation.

1<?php $configuration->setTypedFieldMapper(new CustomTypedFieldMapper());
2

DefaultTypedFieldMapper

By default the Doctrine\ORM\Mapping\DefaultTypedFieldMapper is used, and you can pass an array of PHP type => DBAL type mappings into its constructor to override the default behavior or add new mappings.

1<?php use App\CustomIds\CustomIdObject; use App\DBAL\Type\CustomIdObjectType; use Doctrine\ORM\Mapping\DefaultTypedFieldMapper; $configuration->setTypedFieldMapper(new DefaultTypedFieldMapper([ CustomIdObject::class => CustomIdObjectType::class, ]));
2
3
4
5
6
7
8

Then, an entity using the CustomIdObject typed field will be correctly assigned its DBAL type (CustomIdObjectType) without the need of explicit declaration.

1<?php #[ORM\Entity] #[ORM\Table(name: 'cms_users_typed_with_custom_typed_field')] class UserTypedWithCustomTypedField { #[ORM\Column] public CustomIdObject $customId; // ... }
2
3
4
5
6
7
8
9
10

It is perfectly valid to override even the "automatic" mapping rules mentioned above:

1<?php use App\DBAL\Type\CustomIntType; use Doctrine\ORM\Mapping\DefaultTypedFieldMapper; $configuration->setTypedFieldMapper(new DefaultTypedFieldMapper([ 'int' => CustomIntType::class, ]));
2
3
4
5
6
7

If chained, once the first TypedFieldMapper assigns a type to a field, the DefaultTypedFieldMapper will ignore its mapping and not override it anymore (if it is later in the chain). See below for chaining type mappers.

TypedFieldMapper interface

The interface Doctrine\ORM\Mapping\TypedFieldMapper allows you to implement your own typed field mapping logic. It consists of just one function

1<?php /** * Validates & completes the given field mapping based on typed property. * * @param array{fieldName: string, enumType?: string, type?: mixed} $mapping The field mapping to validate & complete. * @param \ReflectionProperty $field * * @return array{fieldName: string, enumType?: string, type?: mixed} The updated mapping. */ public function validateAndComplete(array $mapping, ReflectionProperty $field): array;
2
3
4
5
6
7
8
9
10

ChainTypedFieldMapper

The class Doctrine\ORM\Mapping\ChainTypedFieldMapper allows you to chain multiple TypedFieldMapper instances. When being evaluated, the TypedFieldMapper::validateAndComplete is called in the order in which the instances were supplied to the ChainTypedFieldMapper constructor.

1<?php use App\DBAL\Type\CustomIntType; use Doctrine\ORM\Mapping\ChainTypedFieldMapper; use Doctrine\ORM\Mapping\DefaultTypedFieldMapper; $configuration->setTypedFieldMapper( new ChainTypedFieldMapper( new DefaultTypedFieldMapper(['int' => CustomIntType::class,]), new CustomTypedFieldMapper() ) );
2
3
4
5
6
7
8
9
10
11

Implementing a TypedFieldMapper

If you want to assign all BackedEnum fields to your custom BackedEnumDBALType or you want to use different DBAL types based on whether the entity field is nullable or not, you can achieve this by implementing your own typed field mapper.

You need to create a class which implements Doctrine\ORM\Mapping\TypedFieldMapper.

1<?php final class CustomEnumTypedFieldMapper implements TypedFieldMapper { /** * {@inheritDoc} */ public function validateAndComplete(array $mapping, ReflectionProperty $field): array { $type = $field->getType(); if ( ! isset($mapping['type']) && ($type instanceof ReflectionNamedType) ) { if (! $type->isBuiltin() && enum_exists($type->getName())) { $mapping['type'] = BackedEnumDBALType::class; } } return $mapping; } }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Note that this case checks whether the mapping is already assigned, and if yes, it skips it. This is up to your implementation. You can make a "greedy" mapper which will always override the mapping with its own type, or one that behaves like the DefaultTypedFieldMapper and does not modify the type once its set prior in the chain.