Search code examples
symfonydoctrine-ormsymfony-forms

Update two entities sharing the primary key with a Symfony2 form


I have two entities: User and CompanyInfo.

The relationship between both is oneToOne, the User can have zero or one CompanyInfo, and one CompanyInfo belongs to one User.

Therefore I setup them to have the same primary key (User's id):

class User extends BaseUser
{
/**
 * @ORM\Id
 * @ORM\Column(type="integer")
 * @ORM\GeneratedValue(strategy="AUTO")
 */
protected $id;
/**
 * @ORM\OneToOne(targetEntity="CompanyInfo", mappedBy="user", cascade={"persist"})
 * @var CompanyInfo
 */
protected $companyInfo;
...
}

class CompanyInfo
{

/**
 * @ORM\OneToOne(targetEntity="User", inversedBy="companyInfo", cascade={"persist"})
 * @ORM\JoinColumn(name="user_id", referencedColumnName="id")
 * @ORM\Id
 * @var User
 */
protected $user;
....
 }

I'm having an issue trying to expose the them at the same time so that they can be updated by submitting only one form:

In the UserFormType I have the following line:

    $builder->add('companyInfo', new CompanyInfoFormType(), ['required' => false, 'by_reference' => false])

The CompanyInfoFormType has the following:

    /**
 * @param OptionsResolverInterface $resolver
 */
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => '....\Entity\CompanyInfo',
        'intention'  => 'registration'
    ));
}

It all works fine, the form is rendered with both the user and the company info fields. When creating a new user & companyInfo it works but only because I did the following in the onSuccess of the UserFormHandler (Basically persist the User in first place, looks a bit hackie but couldn't find a nicer way):

    if ($user->getCompanyInfo() instanceof CompanyInfo) {
        $companyInfo = $user->getCompanyInfo()->setUser($user);
        $user->setCompanyInfo(null);
        $this->entityManager->beginTransaction();
        $this->entityManager->persist($user);
        $this->entityManager->flush();
        $this->entityManager->persist($companyInfo);
        $this->entityManager->flush();
        $this->entityManager->commit();
        $this->entityManager->refresh($user);
    }

Now, the issue is when I'm trying to update a user that already has a companyInfo. For some weird reason, doctrine is thinking that the CompanyInfo entity doesn't exist and it's trying to do an INSERT rather than an UPDATE. It's like if the entity CompanyInfo it's not managed by Doctrine and therefore when doing the cascade persist, tries to create a new one.


Solution

  • I finally managed to find a solution for this, it's not ideal though, but it works. Basically, the process method of my UpdateFormHandler looks like follows:

    /**
     * @param UserInterface $user
     * @return bool
     */
    public function process(UserInterface $user)
    {
        $this->form->setData($user);
    
        $method = $this->request->getMethod();
        if (in_array($method, ['PUT', 'PATCH'])) {
            $this->form->submit($this->request, 'PATCH' !== $method);
    
            if ($this->form->isValid()) {
                /** @var User $user */
                $this->saveCompanyInfo($user);
                $this->onSuccess($user);
                $this->userManager->reloadUser($user);
                return true;
            }
        }
    
        return false;
    }
    
    /**
     * Create link companyInfo -> User when persisting CompanyInfo for first time
     *
     * @param User $user
     */
    protected function saveCompanyInfo(User $user)
    {
        if ($user->getCompanyInfo() instanceof CompanyInfo && !$user->getCompanyInfo()->getUser()) {
    
            $user->getCompanyInfo()->setUser($user);
    
        } elseif ($user->getCompanyInfo() instanceof CompanyInfo) {
            /**
             * If companyInfo exists and has been modified, doctrine things that it's a new entity
             * detached from the manager, and will try to insert a new row on the company_info table (which crashes, as the PK (user_id) already exists).
             * By calling ->merge, we obtain an attached/managed instance,
             * so that it will be properly cascade persisted when the user is saved.
             */
            /** @var CompanyInfo $companyInfo */
            $companyInfo = $this->getEntityManager()->merge($user->getCompanyInfo());
            $user->setCompanyInfo($companyInfo);
        }
    }
    

    I'm sure (I hope) there are better ways, but I'm looking forward to hear them, as this is the only thing that worked for me.

    I hope it helps.

    Regards, Javier