DEV Community

Nacho Colomina Torregrosa
Nacho Colomina Torregrosa

Posted on

Take this into account when using doctrine

I would like to share with you a common error which can happen when using doctrine and it can be a nightmare until you notice why it's happening.

Let's see it using an example. Imagine we are working with these entities:

#[ORM\Entity(repositoryClass: UserRepository::class)]
class User {

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 150)]
    private string $name;

    #[ORM\Column(length: 255)]
    private string $pass;

    #[ORM\Column(length: 100)]
    private string $email;

    // getters & setters
}

#[ORM\Entity(repositoryClass: UserAddressRepository::class)]
class UserAddress {

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\ManyToOne]
    private User $user;

    #[ORM\Column(length: 150)]
    private string $address;

    // getters & setters
}

Enter fullscreen mode Exit fullscreen mode

Now, when you get a request for a new register you do something like this:

$user = new User();
$user->setName('Peter');
$user->setPass('xxxxx');
$user->setEmail('peter.morris@gmail.com');

$address = new UserAddress();
$address->setUser($user);
$address->setAddress('Down street 25B');

$em->persist($user);
$em->persist($address);
$em->flush();

$eventDispatcher->dispatch(new UserRegisteredEvent($user));
Enter fullscreen mode Exit fullscreen mode

So far, this code seems right. It creates a user and an Address, persists them on the database and dispatches and event. Let's see now the subscriber:


class UserSubscriber implements EventSubscriberInterface {

    public static function getSubscribedEvents(): array
    {
        return [
            UserRegisteredEvent::class => 'onUserRegistered'
        ];
    }

    public function onUserRegistered(UserRegisteredEvent $e){
       $user = $e->getUser();
       $address = $user->getAddress();
       $addrName = $address->getAddress();
    }
}

Enter fullscreen mode Exit fullscreen mode

After executing line $addrName = $address->getAddress() you will get the following error:

  Call to a member function getAddress() on null  
Enter fullscreen mode Exit fullscreen mode

That happens because we've persisted both entities but we have not refreshed user entity.

To avoid this kind of errors, we can follow the next steps (among others of course):

Refresh user object before passing it to the event

$em->persist($user);
$em->persist($address);
$em->flush();

$em->refresh($user);

$eventDispatcher->dispatch(new UserRegisteredEvent($user))
Enter fullscreen mode Exit fullscreen mode

This allows us to having user re-hydrated before using in the subscriber.

Passing user identifier to the event

$em->persist($user);
$em->persist($address);
$em->flush();

$eventDispatcher->dispatch(new UserRegisteredEvent($user->getId()));

// -------------------------------------------------------

public function onUserRegistered(UserRegisteredEvent $e){

    $user = $em->getRepository(User::class)->find($e->getId());

    $user = $e->getUser();
    $address = $user->getAddress();
    $addrName = $address->getAddress();
}
Enter fullscreen mode Exit fullscreen mode

This way pass the user identifier to the event so it's the subscriber who gets a fresh user from the database

Conclusion

This can seem a really simple error but it can be a nightmare when you have a project working on production and you start getting this kind of errors and your code is syntactically correct but not semantically.

Top comments (3)

Collapse
 
courtneymiles profile image
Courtney Miles

You did not illustrate it in the your classes, but I assume if there is a User::getAddress method, then there is a User::$address property of type UserAddress.

This would mean you have a bi-directional relationship between the User::$address and UserAddress::$user.

The problem you are experiencing is because doctrine won't keep the two properties on the entities in sync for you. If the assertions in the following code fails, it means you have not completed the bi-directional logic.

$user = new User();
$address = new UserAddress();
$address->setUser($user);

assert($user->getAddress() === $address);
Enter fullscreen mode Exit fullscreen mode

When you call UserAddress::setUser you need to write logic to assign itself to User::$address. You can then avoid having to call EntityManager::refresh.


This way pass the user identifier to the event so it's the subscriber who gets a fresh user from the database

It would actually get a cached copy of the user, where $user::getAddress() would still return null. This would only not happen if you had called EntityManger::refresh as per your first suggestion, or EntityManager::clear had been called, or your event subscriber has a different instance of the EntityManager.

Collapse
 
icolomina profile image
Nacho Colomina Torregrosa

Hi Courtney, yes, you are right. User has an address property and its setter and you can set address to the user to avoid using refresh
About passing id instead of object, event subscriber has a different entity manager.

Many thanks for your comments !

Collapse
 
jeremymoorecom profile image
Jeremy Moore

Didn't know about refresh(). Learned something new today 👍
Thanks