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:

96 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 - 94
}

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!

  • 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/articles/2...

    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