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
}
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));
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();
}
}
After executing line $addrName = $address->getAddress() you will get the following error:
Call to a member function getAddress() on null
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))
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();
}
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)
You did not illustrate it in the your classes, but I assume if there is a
User::getAddress
method, then there is aUser::$address
property of typeUserAddress
.This would mean you have a bi-directional relationship between the
User::$address
andUserAddress::$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.
When you call
UserAddress::setUser
you need to write logic to assign itself toUser::$address
. You can then avoid having to callEntityManager::refresh
.It would actually get a cached copy of the user, where
$user::getAddress()
would still returnnull
. This would only not happen if you had calledEntityManger::refresh
as per your first suggestion, orEntityManager::clear
had been called, or your event subscriber has a different instance of theEntityManager
.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 !
Didn't know about refresh(). Learned something new today ๐
Thanks