Buy

OneToMany: Inverse Side of the Relation

We have the Genus object. So how can we get the collection of related GenusNote Well, the simplest way is just to make a query - in fact, you could fetch the GenusNote repository and call findBy(['genus' => $genus]). It's really that simple.

Tip

You can also pass the Genus's ID in queries, instead of the entire Genus object.

But what if we could be even lazier? What if we were able to just say $genus->getNotes()? That'd be cool! Let's hook it up!

Setting up the OneToMany Side

Open up GenusNote. Remember, there are only two types of relationships: ManyToOne and ManyToMany. For this, we needed ManyToOne.

But actually, you can think about any relationship in two directions: each GenusNote has one Genus. Or, each Genus has many GenusNote. And in Doctrine, you can map just one side of a relationship, or both. Let me show you.

Open Genus and add a new $notes property:

111 lines src/AppBundle/Entity/Genus.php
... lines 1 - 11
class Genus
{
... lines 14 - 48
private $notes;
... lines 50 - 109
}

This is the inverse side of the relationship. Above this, add a OneToMany annotation with targetEntity set to GenusNote and a mappedBy set to genus - that's the property in GenusNote that forms the main, side of the relation:

111 lines src/AppBundle/Entity/Genus.php
... lines 1 - 11
class Genus
{
... lines 14 - 45
/**
* @ORM\OneToMany(targetEntity="GenusNote", mappedBy="genus")
*/
private $notes;
... lines 50 - 109
}

But don't get confused: there's still only one relation in the database: but now there are two ways to access the data on it: $genusNote->getGenus() and now $genus->getNotes().

Add an inversedBy set to notes on this side: to point to the other property:

101 lines src/AppBundle/Entity/GenusNote.php
... lines 1 - 10
class GenusNote
{
... lines 13 - 39
/**
* @ORM\ManyToOne(targetEntity="Genus", inversedBy="notes")
* @ORM\JoinColumn(nullable=false)
*/
private $genus;
... lines 45 - 99
}

I'm not sure why this is also needed - it feels redundant - but oh well.

Next, generate a migration! Not! This is super important to understand: this didn't cause any changes in the database: we just added some sugar to our Doctrine setup.

Add the ArrayCollection

Ok, one last detail: in Genus, add a __construct() method and initialize the notes property to a new ArrayCollection:

111 lines src/AppBundle/Entity/Genus.php
... lines 1 - 11
class Genus
{
... lines 14 - 50
public function __construct()
{
$this->notes = new ArrayCollection();
}
... lines 55 - 109
}

This object is like a PHP array on steroids. You can loop over it like an array, but it has other super powers we'll see soon. Doctrine always returns one of these for relationships instead of a normal PHP array.

Finally, go to the bottom of the class and add a getter for notes:

111 lines src/AppBundle/Entity/Genus.php
... lines 1 - 11
class Genus
{
... lines 14 - 105
public function getNotes()
{
return $this->notes;
}
}

Time to try it out! In getNotesAction() - just for now - loop over $genus->getNotes() as $note and dump($note):

112 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 12
class GenusController extends Controller
{
... lines 15 - 94
public function getNotesAction(Genus $genus)
{
foreach ($genus->getNotes() as $note) {
dump($note);
}
... lines 100 - 109
}
}

Head back and refresh! Let the AJAX call happen and then go to /_profiler to find the dump. Yes! A bunch of GenusNote objects.

Oh, and look at the Doctrine section: you can see the extra query that was made to fetch these. This query doesn't happen until you actually call $genus->getNotes(). Love it!

Owning and Inverse Sides

That was pretty easy: if you want this shortcut, just add a few lines to map the other side of the relationship.

But actually, you just learned the hardest thing in Doctrine. Whenever you have a relation: start by figuring out which entity should have the foreign key column and then add the ManyToOne relationship there first. This is the only side of the relationship that you must have - it's called the "owning" side.

Mapping the other side - the OneToMany inverse side - is always optional. I don't map it until I need to - either because I want a cute shortcut like $genus->getNotes() or because I want to join in a query from Genus to GenusNote - something we'll see in a few minutes.

Tip

ManyToMany relationships - the only other real type of relationship - also have an owning and inverse side, but you can choose which is which. We'll save that topic for later.

Now, there is one gotcha. Notice I did not add a setNotes() method to Genus. That's because you cannot set data on the inverse side: you can only set it on the owning side. In other words, $genusNote->setGenus() will work, but $genus->setNotes() would not work: Doctrine will ignore that when saving.

So when you setup the inverse side of a relation, do yourself a favor: do not generate the setter function.

Leave a comment!

  • 2017-05-31 Victor Bocharsky

    Hey Chris,

    Thanks for sharing it! And glad you found the problem by yourself. Yes, to be able to parse annotations with PHP you need to write doc-block comments which should start with "/**".

    Cheers!

  • 2017-05-23 Chris

    EDIT: I found my error! It was in the first line.. I forgot an * in the mapping setup. Should Be /** instead of /*. (Leaving this comment here in case someone else has the same error.)

    Hello, thank you for these tutorials, they are a great help! However, I have encountered a mapping error: The association AppBundle\Entity\ProfileComment#profile refers to the inverse side field AppBundle\Entity\Profile#comments which does not exist.

    I'm using Profile for Genus and Comments for Notes, but $profile->getComments() is returning null. Here's my code:

    Profile.php


    /*
    * @ORM\OneToMany(targetEntity="ProfileComment", mappedBy="profile")
    */
    private $comments;

    public function __construct(){
    $this->comments = new ArrayCollection();
    }
    ...
    public function getComments(){
    return $this->comments;
    }

    ProfileComment.php


    /**
    * @ORM\ManyToOne(targetEntity="Profile", inversedBy="comments")
    * @ORM\JoinColumn(nullable=false)
    */
    private $profile;

    public function setProfile(Profile $profile){
    $this->profile = $profile;
    }

    ProfileController.php


    public function getCommentsAction(Profile $profile){
    $com = $profile->getComments();
    dump($com); //returns null??
    }

    Thank you in advance, I hope you can help me figure out what's going on. I have searched online to no avail.

  • 2017-04-11 Victor Bocharsky

    Hey maxii123 ,

    Yes, you always need to avoid bidirectional relationship, but you don't have to. Sometimes it could be useful in query builder, or when you need to fetch some data from the inverse side of the relationship. So the main idea is to use bidirectional relashinship only when you really need it. If you can do without it - then do not add it at all! But this feature is exist, and why don't use it if it helps you and make your life easier? :) And of course, you should have strong understanding of mappedBy/inversedBy concepts, otherwise you can do some mess in your code, but it's not the reason do not use it when it perfectly fit your business. ;)

    Cheers!

  • 2017-04-10 maxii123

    Surely the notes to genus is, at the end of the day, a unidirectional relationship? The notes refer to the Genus. Are we not complicating things by mangling it into "bidirectional" and thus meaning we now have to juggle mappedBy and inversedBy because of it being bidirectional all of a sudden. A quick google suggests there is a lot of confusion out there with regards to mappedBy and inversedBy.

  • 2017-04-03 Victor Bocharsky

    Hey Roberto,

    Sorry for the delay, your message was in spam box. So as I see, the Restaurant entity relates to the RestaurantAddress as OneToMany, i.e. each Restaurant entity may have one or *more* address. If it's on purpose - then when you call $restaurant->getAddress() - you get ArrayCollection of RestaurantAddress entities, so you have to iterate over them.


    public function listAction(): Response
    {
    $restaurants = $this->entityManager->getRepository(Restaurant::class)->findAll();
    foreach ($restaurants as $restaurant){
    // because $restaurant->getAddress() returns another ArrayCollection!
    foreach ($restaurant->getAddress() as $address)
    $id = $address->getId();
    }
    }
    }

    So you better to rename your Restaurant::$address property to Restaurant::$addresses, because it holds collection! But if you want that Restaurant may have one and only one address - use the OneToOne relationship instead.

    P.S. You can always dump things like "dump($restaurant->getAddress()); die;" - it will help you to understand what the getAddress() method returns.

    Cheers!

  • 2017-03-23 Roberto Briones Arg├╝elles

    I'm trying to setup something like this, but without success, just with restaurants instead of genus and with addresses instead of notes.

    I have this:


    public function listAction(): Response
    {
    $restaurants = $this->entityManager->getRepository(Restaurant::class)->findAll();
    foreach ($restaurants as $restaurant){
    $address = $restaurant->getAddress();
    $id = $address->getId();
    }

    And it seems that the $address object is a PersistentCollection, not an RestaurantAddress object like my Entity, so I get an exception:
    `Call to undefined method Doctrine\ORM\PersistentCollection::getId()`.

    I'm not using annotations, just yaml configurations, this for my RestaurantEntity:


    AppBundle\Entity\Restaurant:
    type: entity
    table: null
    repositoryClass: AppBundle\Repository\RestaurantRepository
    id:
    id:
    type: integer
    id: true
    generator:
    strategy: AUTO
    fields:
    name:
    type: string
    length: 255
    oneToMany:
    address:
    targetEntity: AppBundle\Entity\RestaurantAddress
    mappedBy: restaurant
    lifecycleCallbacks: { }


    and this for my RestaurantAddress entity:


    AppBundle\Entity\RestaurantAddress:
    type: entity
    table: null
    repositoryClass: AppBundle\Repository\RestaurantAddressRepository
    id:
    id:
    type: integer
    id: true
    generator:
    strategy: AUTO
    fields:
    street:
    type: string
    length: 255
    number:
    type: string
    length: 255
    postalCode:
    type: string
    length: '10'
    column: postal_code
    interiorNumber:
    type: string
    length: '10'
    column: interior_number
    neighborhood:
    type: string
    length: 255
    city:
    type: string
    length: 255
    manyToOne:
    restaurant:
    targetEntity: AppBundle\Entity\Restaurant
    inversedBy: address
    lifecycleCallbacks: { }

    My DB is created by Symfony and the data is fake using fixtures. Any idea what could be wrong?

  • 2017-03-13 weaverryan

    Ah, Eureka indeed! Of course it would be caching ;). Good debugging and have a great week yourself!

  • 2017-03-12 Yehuda Am-Baruch

    Eureka!

    weaverryan thank you very much for your help!
    The configuration was ok, yet annotaion were ignored due to cache of Redis (https://github.com/snc/SncR...

    I did flush the DB but I guess i had a connection problem to the redis-server or something so it didn't actually flushed.

    Everything is seems to work just fine now, and that event was written in the Book-Of-Days of our company :)

    Again, thank you very much for the wonderful and informative answers (and tips!)

    Have a lovely weekend! :)

  • 2017-03-10 weaverryan

    Hey Yehuda Am-Baruch!

    Hmm, yes, I agree with your assessment! I also think that Symfony/Doctrine is still not reading the annotations! Your configuration in config.yml should be ok, but let's change it back to how it looked originally: https://github.com/symfony/.... You should not have needed to change this config, and while your new config should work, I want to eliminate this as a possible cause.

    Also, one way to know for SURE whether or not the annotations are being ignored, is to add a new field + @ORM\Column to your entity. Then, run bin/console doctrine:schema:update --dump-sql. If you see the SQL to add the new column, then your annotations ARE being read correctly. If you do no, then they are NOT being read.

    Let's determine whether or not the annotations are being read for sure first! If they are NOT being read, I believe there are only 2 causes: (A) There are .orm.xml or .orm.yml files in your bundle, and Doctrine is reading these inead or (B) your configuration in config.yml is incorrect (which we will definitely have fixed by changing the settings back to the default).

    I'm sure we're close! Cheers!

  • 2017-03-10 Yehuda Am-Baruch

    Hi weaverryan ,

    Again, thank you for your informative feedback.

    There is no doubt that a logical conclusion would be that the annotation is not applied.

    I've also applied a repository for that entity that its methods are not working -which makes that thought stronger.

    Yet it is only for Symfony, the phpStorm refers to the new repository methods (and autocomplete).

    The import was done precisely according to that manual you referred.
    The xml files removed before contacting, and in the config.yml, under doctrine.orm I put:

    auto_mapping: false
    mappings:
    AppBundle:
    type: annotation

    I also tried:
    -updating symfony to 3.2.5
    -purge all composer cache and reinstall everything (after cloning the project to a brand new directory).
    -Of course, purging cache

    Again, the phpStorm recognize the repository reference for that entity but I get error on symfony.

    I still convinced that, for some reason, Symfony ignores the annotation -could you think of a reason?

    Thanks for your help :)

  • 2017-03-09 weaverryan

    Hi Yehuda Am-Baruch!

    Hmm. I didn't quite explain myself well :). The most important idea from my first message was this:

    > When you query for a Genus, the genusNotes property will be a PersistentCollection, never null. So if you're seeing null, something is misconfigured!

    So let's get down to debugging this! First, you mentioned that you did an "import DB". Are you referring to this: http://symfony.com/doc/curr... If so, make sure that you have deleted the orm.xml files that were created as part of this process! If you don't delete these, then Doctrine continues to read the metadata from those XML files, and ignores your annotations. This could explain why Doctrine is not seeing your OneToMany relationship!

    Other than that, I don't see any issues. So, let me know if the XML files are the problem... and if not, we'll think of something else to debug!

    Cheers!

  • 2017-03-09 Yehuda Am-Baruch

    Hey weaverryan ,

    Thank you for your informative answer.

    If I understood correctly that means I need to generate a new Instance with "new" so the __construct() will set the genusNotes to ArrayCollection ? (Because basically I don't see a reason to create one if not, as I get the Instance from the Query).

    Anyway, This also doesn't work, I failed to mention, that I've build another project under symfony 3.0.1, doctrine/orm 2.5.6 where everything works just fine..

    The thing I did:
    To be fair the doctrine did most of it when I import DB :)

    At the child Entity I set:

    /**
    * @var \AppBundle\Entity\Genus
    *
    * @Serializer\Groups({"Default"})
    * @Serializer\Expose()
    * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Genus", inversedBy="genusNotes")
    * @ORM\JoinColumns({
    * @ORM\JoinColumn(name="fk_genus_id", referencedColumnName="genus_id")
    * })
    */
    private $fkGenus;

    At the parent Entity I set:
    /**
    * @var \AppBundle\Entity\genusNotes
    *
    * @Serializer\Groups({"Default"})
    * @Serializer\Expose()
    * @ORM\OneToMany(targetEntity="AppBundle\Entity\genusNotes",mappedBy="fkGenus")
    */
    private $genusNotes;

    public function __construct()
    {
    $this->genusNotes= new ArrayCollection();
    }
    public funtion getGenusNotes(){
    $this->genusNotes;

    }

    Results are showing All Genus Data & Parent Data as well, but the child data is Null..

    I really fail to see the problem (Could it be anything local after I've cleared cache?).
    Will appreciate any feedbacks.

    *Also, need to mention that DB relations was verified and the $em->getRepository(GenusNotes::class)->findBy(['fkGenus'=>$genus]);
    (where $genus is result of last query) returns the notes with the Genus Data under fkGenus.

    Regards,
    Yehuda.

  • 2017-03-08 weaverryan

    Hey Yehuda Am-Baruch!

    Very good detective work! When you query for an entity (doesn't matter if it's find(), findAll(), custom query, etc) - Doctrine does *not* call your object's __construct method. That can be weird at first, but it's by design. Doctrine's philosophy is that an object is really only instantiated *one* time - when you originally say new Genus. When you query for this later, Doctrine says that you are not really "re-creating" it. You are simply "waking it up" from sleeping (in the database). So, it skips the constructor.

    So let's look at what happens with relationships. There are 2 different scenarios:

    1) When we create a new Genus, the __construct() method is called and the genusNotes property is set to a new ArrayCollection.

    2) When we query for a Genus, the __construct() method is *not* called. However, this does *not* mean that genusNotes is null. Obviously, when you query for a Genus, Doctrine puts all of the data from the database onto its properties - e.g. id, name, etc. It does the *same* thing for the genusNotes property - it sets this to a PersistentCollection object (side note: PersistentCollection and ArrayCollection implement the same Collection interface). This object holds your GenusNote objects. Actually, it's a bit more complex / interesting than that: the PersistentCollection is empty at first, but as soon as you reference the genusNotes property and try to loop over the items in it, PersistentCollection performs a (lazy) query for the GenusNote objects.

    Phew! So, you should *never* see null as the value for the genusNotes OneToMany field. If you are, there must be some misconfiguration somewhere! If you are still having issues, you can definitely post some code here!

    Cheers!

  • 2017-03-07 Yehuda Am-Baruch

    Hi, i'm running symfony 3.2.4 with doctrine/orm 2.5.6
    And this doesn't seems to work.. I get null for OneToMany field when $em->getRepository(TestEntity::class)->findAll();

    It seems that the __construct() is not called at all.

  • 2016-12-12 Victor Bocharsky

    Hey Cristian,

    It depends on the field's type. If you add a simple string or integer field to the Genus - it won't create any additional fields for Note. But if you create a complex field type, like manyToOne bidirectional relation, etc - you also need to add a new field to the Note entity.

    Cheers!

  • 2016-12-11 Cristian Merli

    Hi, what if you needed to add a new column to Genus, and therefore run a new migration.
    Would it then create also the Notes column?

  • 2016-10-31 Terry Caliendo

    No... I highly appreciate the detailed explanation. Thanks much!

  • 2016-10-30 weaverryan

    Yo Terry!

    Oh yea, it's super magic :). Here's how it works behind the scenes:

    1) We query for a Genus.
    2) Doctrine populates all of the normal (non-relationship) fields onto the Genus object
    3) Since we have only queried for a Genus, we don't have the GenusNotes data yet. So, Doctrine assigns a PersistentCollection object to the notes property. This and the ArrayCollection object have all the same methods, so most people don't notice that sometimes notes is an ArrayCollection and sometimes it's a PersistentCollection. The point is, don't get too hung up on this point :).
    4) Later (e.g. maybe in our templates) we try to access the data on our notes property - e.g. we call $genus->getNotes() to fetch that PersistentCollection object and we start looping over it. At that moment, the PersistentCollection object makes a query to the database for the GenusNotes and populates itself with that data. We don't realize this is happening, and we happily loop over those GenusNotes as if there were always there.

    This is called "lazy loading" and when you look under the hood like this, it's pretty obvious why :). Also, it turns out that the __construct() we added, is not important *at all* for any of this to happen. I mean, if you remove the $this->notes = new ArrayCollection(), everything I just described will work perfectly. The *only* reason we do this, is so that - under all circumstances - the notes property is a "collection" object (either PersistentCollection or ArrayCollection - they implement the same interface). Without this code, if you *query* for a Genus notes will be a collection. But if you create a *new* Genus, then notes would actually be null. That might not be an issue... until you perhaps call getNotes() on a new Genus or try to add a GenusNote to the notes property. Suddenly, when you're expecting a collection (i.e. something that acts like an array), you're dealing with null.

    Let me know if that helps! Probably it was more explanation than you wanted ;).

    Cheers!

  • 2016-10-28 Terry Caliendo

    Seems to me the line should be more like:
    $this->notes = new ArrayCollection($this->Genus);

  • 2016-10-28 Terry Caliendo

    How do all of Genus' notes get assigned to the Genus? I don't see anything that seems to explicitly state that a lookup is happening. Is something magical happening in the __construct function with the ArrayCollection? Does Symfony know that if you assign a new ArrayCollection to a GenusNotes type (ie. $this->notes) that it should find all the associated GenusNotes?

  • 2016-10-28 Terry Caliendo

    Nevermind... its the first thing you say in the video! Deleting this post!

  • 2016-08-06 3amprogrammer

    There is a very useful shortcut - which I use daily - fore -> tab. It creates foreach loop and generates as variable name automatically. You should definitely check this out!

  • 2016-06-29 Victor Bocharsky

    BTW, please, do the "bin/console doctrine:schema:validate" command to ensure your mapping is correct. You should get 2x "OK" on Mapping and Database checks

  • 2016-06-29 Victor Bocharsky

    Hey there!

    Hm, you did it right when typehinting to the Collection interface to fix it. Anyways, you will get a collection and you can iterate over it.

    Did you update you entity constructor to set an `ArrayCollection` by default on entity creation?


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

    P.S. You can use `length` filter instead in Twig templates:
    {{ mainCategory.categories|length }}. This code will work with both ArrayCollection and PersistentCollection and even with simple PHP arrays.

  • 2016-06-29 the_nuts

    Hello,
    Is it normal that the return type of the getter function is sometimes PersistentCollection instead of ArrayCollection?

    I followed your instructions, plus I added a php7 return type to the getter function:
    public function getCategories() : ArrayCollection
    {
    return $this->categories;
    }

    But I get the type error (Return value of getCategories() must be an instance of ArrayCollection, instance of PersistentCollection returned) when in twig I do:
    {{ mainCategory.categories.count() }}

    PS Since they both implement Collection, I changed the return type to Collection to solve the error

  • 2016-06-01 Ruben Bijker

    Hi Ryan,
    Thank you for your answer. In the end I got an experienced Symfony programmer to build the form for me, and it ended up being a quite complex solution. The lesson I can draw from this is like you said: "use them when they help, don't use them when they don't help.". I think in many cases it is better to just create (parts of) a form custom rather then making it really complex with the Sf form builder. I look forward to more tutorials on forms for Sf3.
    Cheers!

  • 2016-05-31 weaverryan

    Yo Ruben!

    Ok - first thing: your relationship setup looks perfect. Because of the "amount" property on the "join table", this is not a true ManyToMany relationship - it's actually 2 ManyToOne relationships, and you nailed that :).

    This form will be very tricky, especially because you want to display *all* of the amenities, even if the amount is 0. After thinking about this, I believe this is your best option:

    1) Don't use the forms system :). You would just render these text boxes yourself, and perhaps save them via AJAX or fetch them off of the Request object manually on save. No shame in this - it's a fairly "simple" form - so you don't need a lot of help from Symfony. But the data modeling is complex, so it will be harder to fit this into Symfony forms. This is the cardinal rule of Symfony forms: use them when they help, don't use them when they don't help.

    I *was* going to also give you an option (2) that uses the form framework, but it's quite advanced - honestly, something we could have a screencast on all by itself :). So, I'll save that for later.

    Let me know if this helps clear things up!

    Cheers!

  • 2016-05-27 Ruben Bijker

    Hi,
    I have 2 entities: Boat and Amenity. There needs to be a ManyToMany relationship between Boat and Amenity. I also need to be able to store the amount of Amenities a Boat has. Eg. A Boat can have 3 Kayaks. Therefor I have created to join table JoinBoatAmentity that stores: boat_id, amenity_id, amount.

    My setup of entities looks like this:
    Boat <onetomany>JoinBoatAmentity<manytoone>Amenity

    I have created this relationship according to this article http://future500.nl/article...

    Now I have created a form to edit a Boat. My problem is the following; How do I add a field/section in the boat form builder that displays all the Amenities that are stored in the Amenity table, and that will have a field next to it displaying the amount this boat has, displaying 0 if it doesn't have any.
    eg.:
    [3] Kayak
    [0] Surfboard

    Your help would be very much appreciated.
    Thank you!
    Ruben