This tutorial has a new version, check it out!

ManyToOne Doctrine Relationships

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

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!

  • 2019-12-03 Diego Aguiar

    NP! my pleasure :)

  • 2019-12-02 Dung Le

    Absolutely agree, they are great when you started out with an existing database rather than having to write entities - which can be time consuming especially for a big database, instead we can just use this tool as in my case. But we should say that this Reverse Engineering tool is not perfect that we need to clean up such as "NUL" or NULL without double quotes - this job however is very minimal comparing to writing all entities but one must be aware and clean up syntax a bit after the entities are auto generated. Thanks for your opinion Diego Aguiar

  • 2019-12-02 Diego Aguiar

    Hey Dung Le

    I'm glad to know that you could fix your problem, and about auto generated entities, I don't think there is anything wrong using them but you should check them to assure they got generated correctly

    Cheers!

  • 2019-12-02 Dung Le

    Good news weaverryan ,

    You are right :), the change can be written in 2 value formats

    server_version: 'mariadb-10.3.16'

    or

    server_version: '10.3.16-mariadb'

    Doctrine understands both (clever).

    In addition, I also had to fix / change

    FROM:


    /**
    * @var int|null
    *
    * @ORM\Column(name="num", type="integer", nullable=true, options={"default"="NULL"})
    */
    private $num = NULL;

    TO:


    /**
    * @var int|null
    *
    * @ORM\Column(name="num", type="integer", nullable=true, options={"default"=NULL})
    */
    private $num = NULL;

    Notice that: "NULL" in double quotes is wrong, there should not be quotes. I think I got this auto generated syntax from generating entities from existing database.

    All is good now :), In the future, I will not use generate entities from database and I will not use MariaDB. Thank you for your support!

    For the very first time ever I got this feeling good message

    The database schema and the application mapping information are already in sync.
  • 2019-12-02 weaverryan

    Hey Dung Le !

    Thanks for re-posting the links! I'm still having trouble loading them (it just loads forever?) but I *think* I might know what's going on here. And, you are definitely *not* doing anything wrong :). Exactly what database are you using? And what version - e.g. MySQL 5.7? Maria DB 10.2.21?

    If you look in your config/packages/doctrine.yaml file, you should see a server_version config key - like https://symfonycasts.com/sc... - this is meant to be set to *your* server version - e.g. 5.7 or something like mariadb-10.2.12 if you're using Mariadb. If you do *not* set this correctly, probably everything will work ok... but maybe not ;). Doctrine uses this information to know which features your database engine supports or doesn't support. It then generates different SQL queries based on it. In some cases, setting the wrong server_version can lead to the *exact* issue you're describing. Doctrine thinks the database engine is out of date, but it's not. Check this, and let me know.

    If I'm wrong, try posting the photos to something like imgur - I use them a lot for random screenshot stuff :).

    Cheers!

  • 2019-11-30 Dung Le

    Still I can not find out why I get all these unnecessary MYSQL CHANGE queries https://uofc-my.sharepoint.... the only thing I can think of is because I built the database first then generated entities using existing databasev https://symfony.com/doc/4.4... but since I took this course and learnt and adopted from these tutorials I started to create the database from entities. Hence problem?

    Without making any changes to the entities when I run "php bin/console make:migration" I always get these CHANGE statements in a newd migration version and after manually executed these statements within the database, I still did not see what changes were made :).

  • 2019-11-30 Dung Le

    weaverryan I think something is not perfectly right with my database designing, I am digging will update, it is truly weird as you said :)

  • 2019-11-30 Dung Le

    Good Morning weaverryan I have updated new links, can you look at it again?
    Best regards,

  • 2019-11-27 Dung Le

    Hi @weaverryan I reposted the links sorry, correct! that onDelete="CASCADE" will require 2 - drop and add queries as seen in tutorial. I believe this is the case of something being weird (I built the database first when started out project but now I have learnt and used doctrine annotations in entities to modify the database) is this what causes weird extra no harm queries now?

    the database still always works using make migration and migrate, it is just extra queries always there that do no harm that bother me to ask for your advice?

    Thank you for your time.

  • 2019-11-27 weaverryan

    Hi Dung Le!

    Oh, boo - I think those links stopped working for some reason :/ - I'm particularly interested in what all the queries are that you did not expect? If I remember correctly, setting onDelete="CASCADE" to a relation property will require a few queries - it needs to drop the foreign key then re-add it with the constraint. But if you're seeing a lot more queries, that could be something weird.

    Cheers!

  • 2019-11-26 Dung Le

    Hello SymfonyCasts,

    ***Re-posted links only***

    I am following this tutorial chapter 02 as seen here https://uofc-my.sharepoint.... , and add

    onDelete="CASCADE"

    to a property as seen here https://uofc-my.sharepoint.... however when I ran the command

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

    I will get a lot more queries that I think I should NOT get and all of these queries are

    Alter Table ... Change

    as seen here https://uofc-my.sharepoint....

    Could you please help point out why this is? Thank you all!

  • 2017-03-28 Diego Aguiar

    Hey Hermen!
    If you will have a *Person Class* with two fields, Father and Mother, you will need a *ManyToOne* association in both fields, because a father or a mother might have more than one son
    You can read more information about relationships here:
    http://docs.doctrine-projec...

    Have a nice day!

  • 2017-03-25 Hermen

    Hey Ryan, anything to consider when trying to set a relation on the same table (like a Person has a mother and father in the same Person table... that would mean a field User with ManyToOne and a field User with OneToMany. Is it just that or is it a little bit more difficult?

  • 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/sc... - 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/sha.... I think this is at the root of your problem, specifically the line where you set the creditCards field: https://gist.github.com/sha....

    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/s... - 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/sha...

    This is my This is my Creditcard Entity

    https://gist.github.com/sha...

    This is my survey.orm.yml

    https://gist.github.com/sha...

    This is my survey Entity

    https://gist.github.com/sha...

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

    https://gist.github.com/sha...

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

    https://gist.github.com/sha...

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

    http://symfony.com/doc/curr...

    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/sc...

    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