user3631654
user3631654

Reputation: 1785

Doctrine: Get auto increment ID before persist/flush

I'm having trouble in Doctrine-Fixtures. I'd like to add a user and a email in another entity, but in relation to the user. So here is my process:

    // Create user
    $user1 = new User();

    // Create user email and add the foreign key to the user
    $user1Mail = new UserEmail();
    $user1Mail->setEmail('[email protected]');
    $user1Mail->setUser($user1);

    // Add attributes
    $user1->setEmail($user1Mail);
    // ...

    $manager->persist($user1Mail);
    $manager->persist($user1);
    $manager->flush();

I add the user of the email in $user1Mail->setUser($user1); before the persist, but the problem is, the user has no primary key --> the ID (auto increment). So to create the relation between the email and the user, the user needs to have a primary key to refer to.

I know the solution to create a unique token before and set this to the ID of the user, but I think this is a uncomfortable way because I need to check if the user ID is already in use.

Is there a good way to handle this?

// EDIT: Here is the necessary entity relation:

User:

class User implements UserInterface, \Serializable
{
    // ...

    /**
     * @var Application\CoreBundle\Entity\UserEmail
     * 
     * @ORM\OneToOne(
     *  targetEntity="UserEmail",
     *  cascade={"persist"}
     * )
     * @ORM\JoinColumn(
     *  name="primaryEmail",
     *  referencedColumnName="id",
     *  nullable=false,
     *  onDelete="restrict"
     * )
     */
    private $email;

    // ...

}

UserEmail:

class UserEmail
{

    // ...

    /**
     * @var Application\CoreBundle\Entity\User
     * @ORM\ManyToOne(
     *  targetEntity="User",
     *  cascade={"persist", "remove"}
     * )
     * @ORM\JoinColumn(
     *  name="userID",
     *  referencedColumnName="id",
     *  nullable=false
     * )
     */
    private $user;


    // ...

}

As you can see, if you add a user you have to add a UserEmail also. But the UserEmail requires that the userID is already set, but it is only set if you persist the user into the db. How can I realize a fix for it?

Upvotes: 0

Views: 10003

Answers (2)

Jasper N. Brouwer
Jasper N. Brouwer

Reputation: 21817

I find it strange to see that your User has a OneToOne association towards UserEmail, and UserEmail has a ManyToOne association towards User, and those are 2 separate associations.

I think you'd rather have a single bidirectional OneToMany association:

class User implements UserInterface, \Serializable
{
    // ...

    /**
     * @var ArrayCollection
     * 
     * @ORM\OneToMany(targetEntity="UserEmail", mappedBy="user", cascade={"persist", "remove"})
     */
    private $emails;

    public function __construct()
    {
        $this->emails = new ArrayCollection();
    }

    /**
     * @param UserEmail $email
     */
    public function addEmail(UserEmail $email)
    {
        $this->emails->add($email);
        $email->setUser($this);
    }

    /**
     * @param UserEmail $email
     */
    public function removeEmail(UserEmail $email)
    {
        $this->emails->removeElement($email);
        $email->setUser(null);
    }

    /**
     * @return UserEmail[]
     */
    public function getEmails()
    {
        return $this->emails->toArray();
    }

    // ...
}

class UserEmail
{
    // ...

    /**
     * @var User
     *
     * @ORM\ManyToOne(targetEntity="User", inversedBy="emails")
     * @ORM\JoinColumn(name="userID", referencedColumnName="id", nullable=FALSE)
     */
    private $user;

    /**
     * @param User $user
     */
    public setUser(User $user = null)
    {
        $this->user = $user;
    }

    /**
     * @return User[]
     */
    public function getUser()
    {
        return $this->user;
    }

    // ...
}

I've put a cascade on User::$emails, so any changes to User get cascaded towards UserEmail. This will make managing them easier.

Using this would look something like this:

$email = new UserEmail();

$user = new User();
$user->addEmail($email);

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

About foreign keys

Doctrine will manage the foreign keys of your entities for you. You don't need to manually set them on your entities when using associations.

Primary email

Personally I would add a property to UserEmail to mark it as primary. You'll need a bit more logic in the entities, but managing them will become effortless.

Here's the additional code you need:

class User
{

    // ...

    /**
     * @param UserEmail $email
     */
    public function addEmail(UserEmail $email)
    {
        $this->emails->add($email);
        $email->setUser($this);

        $this->safeguardPrimaryEmail();
    }

    /**
     * @param UserEmail $email
     */
    public function removeEmail(UserEmail $email)
    {
        $this->emails->removeElement($email);
        $email->setUser(null);

        $this->safeguardPrimaryEmail();
    }

    /**
     * @param UserEmail $email
     */
    public function setPrimaryEmail(UserEmail $newPrimaryEmail)
    {
        if (!$this->emails->contains($newPrimaryEmail)) {
            throw new \InvalidArgumentException('Unknown email given');
        }

        foreach ($this->emails as $email) {
            if ($email === $newPrimaryEmail) {
                $email->setPrimary(true);
            } else {
                $email->setPrimary(false);
            }
        }
    }

    /**
     * @return UserEmail|null
     */
    public function getPrimaryEmail()
    {
        foreach ($this->emails as $email) {
            if ($email->isPrimary()) {
                return $email;
            }
        }

        return null;
    }

    /**
     * Make sure there's 1 and only 1 primary email (if there are any emails)
     */
    private function safeguardPrimaryEmail()
    {
        $primaryFound = false;

        foreach ($this->emails as $email) {
            if ($email->isPrimary()) {
                if ($primaryFound) {
                    // make sure there's no more than 1 primary email
                    $email->setPrimary(false);
                } else {
                    $primaryFound = true;
                }
            }
        }

        if (!$primaryFound and !$this->emails->empty()) {
            // make sure there's at least 1 primary email
            $this->emails->first()->setPrimary(true);
        }
    }

    // ...
}

class UserEmail
{
    // ...

    /**
     * @var boolean
     *
     * @ORM\Column(type="boolean")
     */
    private $isPrimary = false;

    /**
     * @internal Use 
     * @param bool $isPrimary
     */
    public function setPrimary($isPrimary)
    {
        $this->isPrimary = (bool)$isPrimary;
    }

    /**
     * @return bool
     */
    public function isPrimary()
    {
        return $this->isPrimary;
    }

    // ...
}

You'll probably notice safeguardPrimaryEmail(). This will make sure the primary-mark will remain consistent when adding/removing emails.

Using this is very simple:

  • An email that's created is not primary by default.
  • If it's the first email added to a user, it will automatically become primary.
  • Additionally added emails will remain not primary.
  • When the primary email is removed, the first remaining email will become primary.
  • You can manually set another primary email by calling User::setPrimaryEmail().

There are many variations to this concept possible, so just view this as an example and refine it to your needs.

Upvotes: 1

vobence
vobence

Reputation: 523

It's because Doctrine will generate the entity id when it's inserted into the database. You can do it with an extra flush():

$user1 = new User();
$manager->persist($user1);
$manager->flush();

// Create user email and add the foreign key to the user
$user1Mail = new UserEmail();
$user1Mail->setEmail('[email protected]');
$user1Mail->setUser($user1);

// Add attributes
$user1->setEmail($user1Mail);
// ...

$manager->persist($user1Mail);
$manager->persist($user1);
$manager->flush();

Or you can set the email mapping in your User class to cascade persist. It means that if a new not persisted object is added to that object, then the new object will be saved as well.

I don't know the exact structure of the entites, but it would look like

/**
 * @ORM\OneToOne(targetEntity="user", cascade={"persist"})
 * @ORM\JoinColumn(name="user_email_id", referencedColumnName="id")
 */
private $userEmail

So if you set a new e-mail to the user it will be auto-persisted if you persist the User entity.

I would prefer the second method if it works. I hope it will help.

Doctrine reference: Transitive persistence / Cascade Operations

Upvotes: 0

Related Questions