Buy

The big question is: who is the best superhero of all time? Um, I mean, how can we insert things into this join table? How can we join a Genus and a User together?

Doctrine makes this easy... and yet... at the same time... kind of confusing! First, you need to completely forget that a join table exists. Stop thinking about the database! Stop it! Instead, your only job is to get a Genus object, put one or more User objects onto its genusScientists property and then save. Doctrine will handle the rest.

Setting Items on the Collection

Let's see this in action! Open up GenusController. Remember newAction()? This isn't a real page - it's just a route where we can play around and test out some code. And hey, it already creates and saves a Genus. Cool! Let's associate a user with it!

First, find a user with $user = $em->getRepository('AppBundle:User') then findOneBy() with email set to aquanaut1@example.org:

117 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 18
public function newAction()
{
... lines 21 - 38
$user = $em->getRepository('AppBundle:User')
->findOneBy(['email' => 'aquanaut1@example.org']);
... lines 41 - 51
}
... lines 53 - 115
}

That'll work thanks to our handy-dandy fixtures file! We have scientists with emails aquanaut, 1-10@example.org:

37 lines src/AppBundle/DataFixtures/ORM/fixtures.yml
... lines 1 - 22
AppBundle\Entity\User:
... lines 24 - 28
user.aquanaut_{1..10}:
email: aquanaut<current()>@example.org
... lines 31 - 37

We've got a User, we've got a Genus... so how can we smash them together? Well, in Genus, the genusScientists property is private. Add a new function so we can put stuff into it: public function: addGenusScientist() with a User argument:

180 lines src/AppBundle/Entity/Genus.php
... lines 1 - 14
class Genus
{
... lines 17 - 174
public function addGenusScientist(User $user)
{
... line 177
}
}

Very simply, add that User to the $genusScientists property. Technically, that property is an ArrayCollection object, but we can treat it like an array:

180 lines src/AppBundle/Entity/Genus.php
... lines 1 - 14
class Genus
{
... lines 17 - 174
public function addGenusScientist(User $user)
{
$this->genusScientists[] = $user;
}
}

Then back in the controller, call that: $genus->addGenusScientist() and pass it $user:

117 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 18
public function newAction()
{
... lines 21 - 38
$user = $em->getRepository('AppBundle:User')
->findOneBy(['email' => 'aquanaut1@example.org']);
$genus->addGenusScientist($user);
... lines 42 - 51
}
... lines 53 - 115
}

We're done! We don't even need to persist anything new, because we're already persisting the $genus down here.

Try it out! Manually go to /genus/new. Ok, genus Octopus15 created. Next, head to your terminal to query the join table. I'll use:

./bin/console doctrine:query:sql "SELECT * FROM genus_scientist"

Oh yeah! The genus id 11 is now joined - by pure coincidence - to a user who is also id 11. This successfully joined the Octopus15 genus to the aquanaut1@example.org user.

If adding new items to a ManyToMany relationship is confusing... it's because Doctrine does all the work for you: add a User to your Genus, and just save. Don't over-think it!

Avoiding Duplicates

Let's do some experimenting! What if I duplicated the addGenusScientist() line?

118 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 18
public function newAction()
{
... lines 21 - 40
$genus->addGenusScientist($user);
$genus->addGenusScientist($user); // duplicate is ignored!
... lines 43 - 52
}
... lines 54 - 116
}

Could this one new Genus be related to the same User two times? Let's find out!

Refresh the new page again. Alright! I love errors!

Duplicate entry '12-11' for key 'PRIMARY'

So this is saying:

Yo! You can't insert two rows into the genus_scientist table for the same genus and user.

And this is totally by design - it doesn't make sense to relate the same Genus and User multiple times. So that's great... but I would like to avoid this error in case this happens accidentally in the future.

To do that, we need to make our addGenusScientist() method a little bit smarter. Add if $this->genusScientists->contains()... remember, the $genusScientists property is actually an ArrayCollection object, so it has some trendy methods on it, like contains. Then pass $user. If genusScientists already has this User, just return:

184 lines src/AppBundle/Entity/Genus.php
... lines 1 - 14
class Genus
{
... lines 17 - 174
public function addGenusScientist(User $user)
{
if ($this->genusScientists->contains($user)) {
return;
}
$this->genusScientists[] = $user;
}
}

Now when we go back and refresh, no problems. The genus_scientist table now holds the original entry we created and this one new entry: no duplicates for us.

Next mission: if I have a Genus, how can I get and print of all of its related Users? AND, what if I have a User, how can I get its related Genuses? This will take us down the magical - but dangerous - road of inverse relationships.

Leave a comment!

  • 2017-09-27 Diego Aguiar

    Hey maxii123

    You mean checking if the user is a scientist instead of checking if exists in the genusScientists collection?

  • 2017-09-27 maxii123

    Can't help but thinking using the isScientist restriction could have been relevant here.

  • 2017-05-10 Victor Bocharsky

    Hey Chea,

    First of all, you're missing comma before 'abstract' and have some extra chars from example, the correct example should be:


    avatarUri: <imageUrl(100, 100, 'abstract')>

    Also notice capitalized "U" in "imageUrl()".

    I bet it should work now, but if not - please, double check that you have User::$avatarUri property and proper setter for it. You also can dump($this->avatarUri) inside its setter to see what value is assigned by Alice on loading fixtures.

    Cheers!

  • 2017-05-09 sokphea chea (ជា សុខភា)

    Hello Diego, here the the code and it should be correct since I just copy from project code
    AppBundle\Entity\User:
    user_{1..10}:
    email: admin+<current()>@gmail.com
    plainPassword: '123456'
    roles: ['ROLE_ADMIN']
    avatarUri: <imageurl(100, 100,="" 'abstract')="">

  • 2017-05-08 Diego Aguiar

    Hey Sokphea!

    Look's like you have some extra characters, try this:

    AppBundle\Entity\User:
    avatarUri: <imageurl(100, 100,="" 'abstract')="">

    Update:
    Oh wait, look's like those characters are added by "Disquss"
    Can you show me how your fixtures.yml and user class look's like ?

    Cheers!

  • 2017-05-07 sokphea chea (ជា សុខភា)

    Hello, I got this error when run fixture:load "Could not determine how to assign avatarUri to a AppBundle\Entity\User object".
    I'm coding along from the '"Getting Crazy with Form Themes"..and I already add new avatarUri property and add get and set for it. Is there's something to do with <imageurl(100, 100,="" 'abstract')=""> ?

  • 2017-01-26 Michael

    It works perfect!! Thanks a lot!!

  • 2017-01-26 Victor Bocharsky

    Yo Michael,

    Yes, it totally makes sense! Good research. Then we can easily fix it I think. When you create a form, pass the entity with assigned member ID to it as the 2nd argument, I mean something like this:


    $user = $this->getUser();
    $member = $user->getMember();

    $weight = new Weight();
    $weight->setMember($member);

    $form = $this->createForm(WeightType::class, $weight);
    // And only then validate your form
    $form->handleRequest($request);
    if ($form->isValid()) {
    // persist and flush it!
    }

    Let me know if it works.

    Cheers!

  • 2017-01-25 Michael

    Yes, the form holds only the field weight and I won't get and other field.. But I found the mistake(?).. in the entity of the weight-table I seted the field member_id to "@Assert\NotBlank()" (I think you showed this in the tutorials, because its the "linked" field to the table memeber) and because of this the form wasn't valid. Is this ok so?
    I like Symfony and your tutorials so much!! Keep on going! :-)

  • 2017-01-25 Victor Bocharsky

    Hm, probably I misunderstand you... you say you have a form type, which holds only one field, i.e. weight and that's it, but when you render this form - you'll get an extra field, i.e. member_id, right? Are you sure you don't extend any other form type except the AbstractType in you custom form type? Could you show me what type has your weight field?

    Cheers!

  • 2017-01-24 Michael

    Hi Victor
    Thanks a lot for your answers!! I didn't add the field member_id to the form, it's just there. When I have a look in the Debug-tool > Forms > Submitted Data, there is field id, member and weight. But in my form I just added weight.

  • 2017-01-24 Victor Bocharsky

    Hey Michael,

    Looks like you don't need member_id in your form at all, so just try to remove this field from form. Then before saving a new weight-data, set the proper user and member IDs, which, as you said, you can get. I think it's the easiest solution of your problem. Does it help you? The another one solution is to "inject" member ID into the form type, for example with a hidden field, but since member is an entity - you will need Data Transformer for it. However, you have to do more extra work with validation in this case, so try the first suggestion in the first place.

    P.S. Hello to Switzerland! ;)

    Cheers!

  • 2017-01-23 Michael

    In my first project;-) I have a the table "user", "member" and "weight". Each user is linked to only one member and each member is linked to many datas in the weight-table (how you have explained it).
    Now, I'm trying to save a new weight-data from a form:
    With "$this->getUser()->getId();" I can get the Id of the logged user and find the related member_id. Now, the problem is, the form isn't valid, because of the missing member_id. How can I get the member_id to the form and store it in the database?
    Sorry for my bad english and my poor beginner question;-)
    Cheers from a Knp-University-Fan from Switzerland

  • 2016-11-09 Victor Bocharsky

    Hey Johan,

    Do you mean addGenusScientist() method on Genus entity here? I think it'll be better to check this rule in a controller, probably using Symfony validator with custom callback. But if we talk about Value Objects - I think it's fine to hold this business logic inside them.

    Cheers!

  • 2016-11-08 Johan

    Let's say in my application, I have a business rule that says that a Genus can only be studied by at most 5 scientists. Would it be fine to check for this in the addGenusScientist method? Wouldn't I be violating the SRP? I'm mixing storing/retrieving data with business logic.