---
title: "Symfony UUID, Doctrine and type-hinting : everything you should know"
date: 2023-05-04T13:44:04.000Z
canonical: https://www.ekino.fr/publications/symfony-uuid-doctrine-and-type-hinting-everything-you-should-know/
---

### Symfony UUID, Doctrine and type-hinting: everything you should know

#### And why you should look at how everything work under the hood

![](https://www.ekino.fr/media/original/0000/00/57a9282c-44dc-35a8-51bb-138b664c654a.jpg)
*Image from Joshua Woroniecki on Pixabay*

In this article, we will talk about wrong type-hinting and how using Doctrine ORM can lead to some *unexpected* behavior. We’ll start from *finding* of the bug, then trying to *understand why *it happens and finally trying to *resolve* it.

### **The beginning**

First, let’s set the technical context and everything you need to know to understand the rest of the article.

The tech stack is a classic one: Symfony 6.2, Doctrine ORM with PostgreSQL, PHPUnit and finally Foundry.

Now, let me introduce you the base code we’ll need:

```
namespace AppEntity;

use DoctrineORMMapping as ORM;

#[ORMEntity]
#[ORMHasLifecycleCallbacks]
final class Member
{
    #[ORMId]
    #[ORMColumn(type: 'uuid', unique: true)]
    #[ORMGeneratedValue(strategy: 'CUSTOM')]
    #[ORMCustomIdGenerator(class: 'doctrine.uuid_generator')]
    public string $id;

    #[ORMColumn(type: 'datetime_immutable')]
    public DateTimeImmutable $createdAt;

    #[ORMColumn(type: 'datetime_immutable')]
    public DateTimeImmutable $updatedAt;

    #[ORMPrePersist]
    public function prePersist(): void
    {
        $this->createdAt = $this->createdAt ?? new DateTimeImmutable();
        $this->updatedAt = $this->createdAt;
    }

    #[ORMPreUpdate]
    public function preUpdate(): void
    {
        $this->updatedAt = new DateTimeImmutable();
    }
}
```

```
namespace AppEntity;

use DoctrineORMMapping as ORM;

#[ORMEntity]
class Message
{
    #[ORMId]
    #[ORMColumn(type: 'uuid', unique: true)]
    #[ORMGeneratedValue(strategy: 'CUSTOM')]
    #[ORMCustomIdGenerator(class: 'doctrine.uuid_generator')]
    public string $id;

    #[ORMManyToOne(targetEntity: Member::class)]
    #[ORMJoinColumn(nullable: false)]
    public Member $author;
}
```

> For the sake of simplicity, I use public fields. The real code use the good old getter/setter methods.

Nothing really fancy with those snippets.

We have an entity Member that represents a kind of user in our application. It has 3 simple fields: id, createdAt and updatedAt. This entity has Doctrine callback on prePersist and preUpdate events.

The second entity Message has two fields: id and author. This entity represents a simple message in the application.

### The investigation

In our functionnal tests, assertions like this failed:

```
self::assertSame($member, $message->author)
```

Because $member->updatedAt is different from $message->getAuthor->updatedAt, even we don’t modified $member in the tests.

After investigations, we found that preUpdate doctrine event is fired even if we don’t modify $member. Our first guess was that Foundry fires the event, but we had no idea *why* and *when.*

The first step was to find a minimal block of code to reproduce the issue. So then it is easier to understand why the issue is produced.

After some time, we managed to reduce the code to the following snippet:

```
$member = MemberFactory::createOne()->object();

MessageFactory::createOne([
    'author' => $member
]);

self::assertEquals($member->createdAt, $member->updatedAt);
```

The snippet uses some Foundry functionnalities, especialy the createOne method.

So we need to get rid of it, and translate the Foundry code to plain Doctrine instructions. And after reading the code of Foundry and lot of trials and errors, the following minimal code reproduces the issue with only doctrine calls.

```
$member = new Member();

$recommendationrequest = new Message();
$recommendationrequest->author = $member;

$em = $this->getEntityManager()

$em->persist($member);
$em->flush();

$em->refresh($member);

$em->persist($recommendationrequest);
$em->flush();

self::assertEquals($member->createdAt, $member->updatedAt);
```

Now, we try to find which line produces the bug. It appears when we call the refresh method the event is fired. So we answer to the question *when.*

Let’s find now *why*!

A quick and simple way to find it, is to dump the event received in preUpdate method. Let see what we found:

```
^ DoctrineORMEventPreUpdateEventArgs^ {#185
  -objectManager: DoctrineORMEntityManager^ {#109 …11}
  -object: AppEntityMember^ {#312
    +id: "1edac68e-0f54-6828-b519-0b4d85d941b5"
    +createdAt: DateTimeImmutable @1676380196 {#167
      date: 2023-02-14 13:09:56.0 UTC (+00:00)
    }
    +updatedAt: DateTimeImmutable @1676380196 {#169
      date: 2023-02-14 13:09:56.0 UTC (+00:00)
    }
  }
  -entityChangeSet: & array:1 [
    "id" => array:2 [
      0 => SymfonyComponentUidUuidV6^ {#549
        #uid: "1edac68e-0f54-6828-b519-0b4d85d941b5"
        toBase58: "4oz2m7gHUaB5R1EjB45meL"
        toBase32: "0YVB38W3TMD0MBA68B9P2XJGDN"
        time: "2023-02-14 13:09:56.910084 UTC"
      }
      1 => "1edac68e-0f54-6828-b519-0b4d85d941b5"
    ]
  ]
}
```

So according the entityChangeSet Doctrine find that the id type has changed from SymfonyComponentUidUuidV6 to string. Then Doctrine fires the event.

### The resolution

How to solve it and *why* this happens?

We found that the event is fired because the id type is changed. But* why*?

Let’s see what’s happening when we first create the member entity:

```
 AppEntityMember^ {#630
  +id: "1edae008-54be-67ae-a260-d1c92065d6bf"
  +createdAt: DateTimeImmutable @1676555277 {#609
    date: 2023-02-16 13:47:57.793062 UTC (+00:00)
  }
  +updatedAt: DateTimeImmutable @1676555277 {#609}
}
```

The id field is a string, as we expected. But doctrine.uuid\_generator returns an Uuid object. So this object must be able to be cast down to a string. And the answer is yes, Uuid inherits from SymfonyComponentUidAbstractUid which implements a \_\_toString function.

And then, to save the data to the database, Doctrine will call the convertToDatabaseValue of AbstractUiType (which is inherit by Uuid class) which will re-cast our string to a new Uuid object and then to a string.

Here the snippet of convertToDatabaseValue function:

```
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
{
    $toString = $this->hasNativeGuidType($platform) ? 'toRfc4122' : 'toBinary';

    if ($value instanceof AbstractUid) {
        return $value->$toString();
    }

    if (null === $value || '' === $value) {
        return null;
    }

    if (!is_string($value)) {
        throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]);
    }

    try {
        return $this->getUidClass()::fromString($value)->$toString();
    } catch (InvalidArgumentException) {
        throw ConversionException::conversionFailed($value, $this->getName());
    }
}
```

> source: [https://github.com/symfony/symfony/blob/feeeaf5295f4a42477262ed7143c76d886ba0f55/src/Symfony/Bridge/Doctrine/Types/AbstractUidType.php#L65](https://github.com/symfony/symfony/blob/feeeaf5295f4a42477262ed7143c76d886ba0f55/src/Symfony/Bridge/Doctrine/Types/AbstractUidType.php#L65)

Everything happens in the return instruction. $value is converted to AbstractUuid then converted to string.

If we resume what happen during the creation of the member entity:

- doctrine.uuid\_generator return a freshly generated Uuid object
- This object is cast down to a string by AbstractUiType

After the creation, refresh function is called on the entity, here what happens:

- Doctrine compares the current value of id which is a string to the PHP value obtain after calling convertToPHPValue of AbstractUuid class. It was what we obtain from the PreUpdateEventArgs argument from preUpdate method !
- Then the id field is updated with the string value and the event preUpdate is fired

Code of convertToPHPValue:

```
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?AbstractUid
{
    if ($value instanceof AbstractUid || null === $value) {
        return $value;
    }

    if (!is_string($value)) {
        throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]);
    }

    try {
        return $this->getUidClass()::fromString($value);
    } catch (InvalidArgumentException $e) {
        throw ConversionException::conversionFailed($value, $this->getName(), $e);
    }
}
```

> source: [https://github.com/symfony/symfony/blob/feeeaf5295f4a42477262ed7143c76d886ba0f55/src/Symfony/Bridge/Doctrine/Types/AbstractUidType.php#L43](https://github.com/symfony/symfony/blob/feeeaf5295f4a42477262ed7143c76d886ba0f55/src/Symfony/Bridge/Doctrine/Types/AbstractUidType.php#L43)

![](https://www.ekino.fr/media/original/0000/00/43d90398-8a4f-e99e-ba81-e2b9abb34240.png)
*Sequence diagram to understand why doctrine dectect a change*

Finally, as we deeply understand what happens, we can fix our code. The fix is simple: we have to change the type-hinting of id from string to Uuid|string.

### End of the journey

As simple as the resolution is, keep in mind the investigation of this kind of bug take roughtly 2–3 months, dealing with day-to-day dev tasks.

This article tries to hightlight that with a good undestanding of how works Doctrine, this bug could be prevent. But we have to keep in mind, this is with this kind of experience we improve ourself and the most important is to have a good debug method to solve those bugs.

#### About ekino

> The ekino group has been supporting major groups and start-ups in their transformation for over 10 years, helping them to imagine and implement their digital services and deploying new methodologies within their project teams. A pioneer in its holistic approach, ekino draws on the synergy of its expertise to build coherent, long-term solutions.

> To find out more, visit our website — [ekino.com](https://www.ekino.com/).

---

[Symfony UUID, Doctrine and type-hinting : everything you should know](https://medium.com/ekino-france/symfony-uuid-doctrine-and-type-hinting-everything-you-should-know-b846519a9927) was originally published in [ekino-france](https://medium.com/ekino-france) on Medium, where people are continuing the conversation by highlighting and responding to this story.
