Buy

ManyToOne Doctrine Relationships

Right now, if I creat an Event, there’s no database link back to my user. We don’t know which user created each Event.

To fix this, we need to create a OneToMany relationship from User to Event. In the database, this will mean a user_id foreign key column on the yoda_event table.

In Doctrine, relationships are handled by creating links between objects. Start by creating an owner property inside Event:

// src/Yoda/EventBundle/Entity/Event.php
// ...

class Event
{
    // ...

    protected $owner;
}

For normal fields, we use the @ORM\Column annotation. But for relationships, we use @ORM\ManyToOne, @ORM\ManyToMany or @ORM\OneToMany. This is a ManyToOne relationship because many events may have the same one User. I’ll talk about the other 2 relationships later (OneToMany, ManyToMany).

Add the @ORM\ManyToOne relationship and pass in the entity that forms the other side:

// src/Yoda/EventBundle/Entity/Event.php
// ...

/**
 * @ORM\ManyToOne(targetEntity="Yoda\UserBundle\Entity\User")
 */
protected $owner;

Next, create the getter and setter for the the new property:

// src/Yoda/EventBundle/Entity/Event.php
// ...

use Yoda\UserBundle\Entity\User;

class Event
{
    // ...

    public function getOwner()
    {
        return $this->owner;
    }

    public function setOwner(User $owner)
    {
        $this->owner = $owner;
    }
}

Notice that when we call setOwner, we’ll pass it an actual User object, not the id of a user. But when you save an Event, Doctrine will use the owner’s id value to populate an owner_id column on the yoda_event table. So we link objects to objects in PHP, and Doctrine takes care of setting the foreign key id value for us. If you’re newer to an ORM, this is one of the toughest things to understand about Doctrine.

Updating the Database

How can we update our database with the new column and foreign key? Why, with the doctrine:schema:update command of course! I’ll dump the SQL to the terminal first to see it:

php app/console doctrine:schema:update --dump-sql
php app/console doctrine:schema:update --force

As expected, the SQL that’s generated will add a new owner_id field to yoda_event along with the foreign key constraint.

ManyToOne Options

Since I’m feeling fancy, let’s configure a few things. Whenever you have a ManyToOne annotation, you can optionally add an @ORM\JoinColumn annotation to control some database options.

JoinColumn onDelete

To add a database-level “ON DELETE” cascade behavior, add the onDelete option:

// src/Yoda/EventBundle/Entity/Event.php
// ...

/**
 * @ORM\ManyToOne(targetEntity="Yoda\UserBundle\Entity\User")
 * @ORM\JoinColumn(onDelete="CASCADE")
 */
protected $owner;

Now, let’s run the doctrine:schema:update command again:

php app/console doctrine:schema:update --dump-sql
php app/console doctrine:schema:update --force

The SQL tells us that this actually re-creates the foreign key with the “on delete” behavior. So if we delete a User, the database will automatically delete all rows in the yoda_event table that link to that user and ship them off into hyper space.

The cascade Option

Another common option is cascade on the actual ManyToOne part:

// src/Yoda/EventBundle/Entity/Event.php
// ...

/**
 * @ORM\ManyToOne(targetEntity="Yoda\UserBundle\Entity\User", cascade={"remove"})
 * @ORM\JoinColumn(onDelete="CASCADE")
 */
protected $owner;

This is like onDelete, but in the opposite direction. With this, if we delete an Event, it will cascade the remove onto the owner. In other words, If I delete an Event, it will also delete the User who is the owner.

Run doctrine:schema:update again:

php app/console doctrine:schema:update --dump-sql

Now, it doesn’t want to change our database at all. Unlike onDelete, this behavior is enforced entirely by Doctrine in PHP, not in the database layer.

Tip

You can also cascade persist, which is useful at times with ManyToMany relationship where you’re creating new items in the relationship.

Remove the cascade option because it’s dangerous in our situation:

// src/Yoda/EventBundle/Entity/Event.php
// ...

/**
 * @ORM\ManyToOne(targetEntity="Yoda\UserBundle\Entity\User")
 * @ORM\JoinColumn(onDelete="CASCADE")
 */
protected $owner;

If we delete an Event, we definitely don’t want that to delete the Event’s owner. Darth would be so angry.

Linking an Event to its owner on creation

Time to put our shiny relationship to the test. When a new Event object is created, let’s associate it with the User object for whoever is logged in:

// src/Yoda/EventBundle/Controller/EventController.php
// ...

public function createAction(Request $request)
{
    // ...

    if ($form->isValid()) {
        $user = $this->getUser();

        // ...
    }
}

To complete the link, just call setOwner on the Event and pass in the whole User object:

// src/Yoda/EventBundle/Controller/EventController.php
// ...

public function createAction(Request $request)
{
    // ...

    if ($form->isValid()) {
        $user = $this->getUser();

        $entity->setOwner($user);

        // ... the existing save logic
    }
}

Yep, that’s it. When we save the Event, Doctrine will automatically grab the id of the User object and place it on the owner_id field.

Time to test! Login as Wayne. Remember, he has ROLE_ADMIN, which also means he has ROLE_EVENT_CREATE because of the role_hierarchy section in security.yml.

Now, fill in some basic data and submit it. To see the result, use the query tool to list the events:

php app/console doctrine:query:sql "SELECT * FROM yoda_event"

Sure enough, our newest event is linked back to our user! #Winning

Leave a comment!

  • 2015-09-21 Shairyar Baig

    Thanks Ryan, Really appreciate the help.....

  • 2015-09-21 weaverryan

    Hey again!

    1) Yep, I agree with the OneToMany because cards cannot have multiple surveys.

    2) The easiest way to delete a card is to do it just like you said: have an action, with the {id} of the card in the URL, and delete it that way. The "collection" form type also has an "allow_delete" option, but it can be a bit tricky, as you need to make sure you "unlink" the relationship in the removeXX method and also probably need to use the orphanRemoval option so that Doctrine fully deletes the removed Creditcards (not just unlink them from the Survey). Your way is much easier.

    3) We talk about this a little bit here: http://knpuniversity.com/scree... - I rarely use the cascade option. Basically, it says "If I save a Survey, automatically call $em->persist() on all of the related Creditcards entities linked to it". In your case, in your controller, you're never looping over $survey->getCreditcards() and calling $em->persist() on each, so without the cascade, Doctrine says "You told me to persist this Survey, and this survey is related to these 2 Creditcards, but you did not tell me to persist them. What the heck?". I usually call persist() manually on all of my entities, to avoid any wtf issues later. But, this is probably the best situation for cascade persist. I would still probably remove it and manually loop over the linked creditCards in your controller and persist them.

    Thanks for the nice comment - I love it!

    Cheers!

  • 2015-09-21 Shairyar Baig

    Wow, that did fix the problem, I did not know when collectionType is used we do not need to worry about getting the linked data saved, thats great to know.

    I have couple of queries to get the concept straight

    1)
    The relationship I am using here between Survey and Creditcard is OneToMany bidirectional, is this is correct relationship? A survey can have multiple cards linked to it, but the cards cannot have multiple surveys they can be linked with only one survey. I dont think ManyToMany relationship goes here, correct?

    2)
    How will the delete work here if I need to delete one card? will that be linked with an action with get parameter card id and based on the id run the delete query or Symfony has a shortcut to that?

    3)
    My survey.orm.yml where I have defined OneToMany relationship, I had to use cascade: ["persist"], what is the purpose of this?

    You asked me the reason for manual data setting, its actually an extremely long survey which is broken down is different forms, i was doing the manual data setting as i did not want one section to overwrite the data of other section while saving and now i realize i do not need to do that as symfony will do it for me automatically and all I need to do is $em->flush(); i have no idea why i did that :) may be i was going crazy making the relationship work and save data :)

    Coming across this website is such a blessing :)

  • 2015-09-21 weaverryan

    Hey Shairyar!

    Hmm, I can see a few things initially. First, what's the purpose of all the manual data setting here? https://gist.github.com/shairy.... I think this is at the root of your problem, specifically the line where you set the creditCards field: https://gist.github.com/shairy....

    When you have a collectionType, it looks for an addCreditCard method and calls that for all the new credit cards added. You *do* have this method, and so it's being called. But then you're immediately re-setting this whole property in the controller, but you're now skipping the addCreditCard method by saying getCreditCards()->add(...);. That may or may not be part if the problem, but I'd get rid of that: you *do* want to use your adder* function :).

    The real problem is that the "owning" side (a concept I talk about about here https://knpuniversity.com/scre... - because it *is* tricky) if your relationship is never set, which directly leads to your first error. Your "adder" function would need to look like this:


    public function addCreditCard(CreditCard $card)
    {
    if (!$this->creditcards->contains($card)) {
    $this->creditcards->add($card);
    }


    // THIS IS THE KEY PART
    $card->setSurvey($this);
    }

    That sets the owning side, and is likely the key to your issue. This stuff is hard: only setting the "owning" side (e.g. CreditCard::setSurvey) does anything for saving to Doctrine.

    Good luck!

  • 2015-09-21 Shairyar Baig

    Hi Ryan,

    I am working with oneToMany bidirectional Entity and I am having problem saving the data

    I have an entity called survey and I have an entity called Creditcards

    Creditcard is linked with survey using manyToOne

    survey is linked with Creditcard using oneToMany

    The survey has bunch of financial related questions which gets saved in survey table and it also asks user for their creditcard related details which then gets saved in Creditcard table, since user can have more than 1 card so they can add multiple card details and this is where the manyToOne andoneToMany comes into play.

    There are two errors that I run into and I just cant seem to get past them, its probably my lack of Symfony knowledge,

    The first error I see is


    A new entity was found through the relationship 'ExampleBundle\Entity\survey#creditcards' that was not configured to cascade persist operations for entity: ExampleBundle\Entity\Creditcards@000000001bbd76da000000008bbf1e28. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'ExampleBundle\Entity\Creditcards#__toString()' to get a clue.
    500 Internal Server Error - ORMInvalidArgumentException


    So while trying to fix this I then run into


    The class 'Doctrine\ORM\PersistentCollection' was not found in the chain configured namespaces ExampleBundle\Entity, UserBundle\Entity


    At this stage I am pretty cluelss what to do and I am hoping if you can help me figure this out.

    This the code is too long, rather than pasting it here I have created a gist.

    This is my Creditcard.orm.yml

    https://gist.github.com/shairy...

    This is my This is my Creditcard Entity

    https://gist.github.com/shairy...

    This is my survey.orm.yml

    https://gist.github.com/shairy...

    This is my survey Entity

    https://gist.github.com/shairy...

    The Form that gets called in controller and passed to the twig

    https://gist.github.com/shairy...

    Finally my debtAction that processes the form and tries to save the data

    https://gist.github.com/shairy...

    I have gone through the following step by step article but I just cant get the save to work

    http://symfony.com/doc/current...

    I will really appreciate if you can help me understand what am i doing wrong?

  • 2015-07-30 weaverryan

    In PhpStorm, goto the "Code" menu on top and then select "Generate". I'm using the Mac shortcut for this - which is cmd+n.

    Yea, having this is HUGE for productivity. We're releasing a screencast all about being super fast in PhpStorm - http://knpuniversity.com/scree...

    Cheers!

  • 2015-07-30 Scarlett Pratt

    How did you automatically create the getter and setter in your text editor? Thanks!

  • 2014-10-04 Arno

    Doh! Had an error in the UserRepository:refreshUser() function: I was returning the old $user not the new $refreshedUser obect.
    Well, at least I learned something about the inner workings of Symfony & Doctrine while debugging this :o) With this fix my "solution" from the previous comment is no longer necessary (basically it is what refreshUser() does).

  • 2014-10-04 Arno

    When I try this tutorial, I get an error about "A new entity was found through the relationship". I finally tracked down the cause: the $user-object from $this->getUser() is reconstructed from the security.context (unserialized from the token). The entity manager does not know about this object and thus fails.

    I tried various ways (e.g. $em->merge() instead of $em->persist, or adding cascade=PERSIST to the relationship), but none worked, partly because (a) as part of a former episode we don't serialize the complete $user-object - and even if we did (I tried it) - we would create another user of the same name & email address.

    Conclusio: The entity manager does not know about the $user-object which is unserialized from the token. Instead I modified the code like this:

    $user = $this->getUser(); // user reconstructed from token
    $em = $this->getDoctrine()->getManager();
    $user = $em->getRepository('UserBundle:User')->find($user->getId());

    This way we get the complete user object (e.g. including isActive & email address). The entity manager knows about it and persisting the event object finally works.

    Versions:
    doctrine/common: v2.4.2
    symfony/symfony: v2.5.4