Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Saving the Inverse Side of a ManyToMany

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Back on the main part of the site, click one of the genuses, and then click one of the users that studies it. This page has a little edit button: click that. Welcome to a very simple User form.

Building the Field

Ok, same plan: add checkboxes so that I can choose which genuses are being studied by this User. Open the controller: UserController and find editAction():

... lines 1 - 5
use AppBundle\Form\UserEditForm;
... lines 7 - 11
class UserController extends Controller
{
... lines 14 - 57
public function editAction(User $user, Request $request)
{
$form = $this->createForm(UserEditForm::class, $user);
... lines 61 - 78
}
}

This uses UserEditForm, so go open that as well.

In buildForm(), we'll do the exact same thing we did on the genus form: add a new field called studiedGenuses - that's the property name on User that we want to modify:

... lines 1 - 6
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
... lines 8 - 14
class UserEditForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 20 - 24
->add('studiedGenuses', EntityType::class, [
... lines 26 - 29
])
;
}
... lines 33 - 39
}

Keep going: use EntityType::class and then set the options: class set now to Genus::class to make Genus checkboxes. Then, multiple set to true, expanded set to true, and choice_label set to name to display that field from Genus:

... lines 1 - 6
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
... lines 8 - 14
class UserEditForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 20 - 24
->add('studiedGenuses', EntityType::class, [
'class' => Genus::class,
'multiple' => true,
'expanded' => true,
'choice_label' => 'name',
])
;
}
... lines 33 - 39
}

Next! Open the template: user/edit.html.twig. At the bottom, use form_row(userForm.studiedGenuses):

... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-8">
... lines 7 - 8
{{ form_start(userForm) }}
... lines 10 - 16
{{ form_row(userForm.studiedGenuses) }}
... lines 18 - 19
{{ form_end(userForm) }}
</div>
</div>
</div>
{% endblock %}

That's it.

Try it! Refresh! Cool! This User is studying five genuses: good for them! Let's uncheck one genus, check a new one and hit Update.

It didn't Work!!! Inverse Relationships are Read-Only

Wait! It didn't work! The checkboxes just reverted back! What's going on!?

This is the moment where someone who doesn't know what we're about to learn, starts to hate Doctrine relations.

Earlier, we talked about how every relationship has two sides. You can start with a Genus and talk about the genus scientist users related to it:

... lines 1 - 14
class Genus
{
... lines 17 - 71
/**
* @ORM\ManyToMany(targetEntity="User", inversedBy="studiedGenuses", fetch="EXTRA_LAZY")
* @ORM\JoinTable(name="genus_scientist")
*/
private $genusScientists;
... lines 77 - 195
}

Or, you can start with a User and talk about its studied genuses:

... lines 1 - 16
class User implements UserInterface
{
... lines 19 - 77
/**
* @ORM\ManyToMany(targetEntity="Genus", mappedBy="genusScientists")
* @ORM\OrderBy({"name" = "ASC"})
*/
private $studiedGenuses;
... lines 83 - 222
}

Only one of these side - in this case the Genus - is the owning side. So far, that hasn't meant anything: we can easily read data from either direction. BUT! The owning side has one special power: it is the only side that you're allowed to change.

What I mean is, if you have a User object and you add or remove genuses from its studiedGenuses property and save... Doctrine will do nothing. Those changes are completely ignored.

And it's not a bug! Doctrine is built this way on purpose. The data about which Genuses are linked to which Users is stored in two places. So Doctrine needs to choose one of them as the official source when it saves. It uses the owning side.

For a ManyToMany relationship, we chose the owning side when we set the mappedBy and inversedBy options. The owning side is also the only side that's allowed to have the @ORM\JoinTable annotation.

This is a long way of saying that if we want to update this relationship, we must add and remove users from the $genusScientists property on Genus:

... lines 1 - 14
class Genus
{
... lines 17 - 71
/**
* @ORM\ManyToMany(targetEntity="User", inversedBy="studiedGenuses", fetch="EXTRA_LAZY")
* @ORM\JoinTable(name="genus_scientist")
*/
private $genusScientists;
... lines 77 - 195
}

Adding and removing genuses from the User object will do nothing. And that's exactly what our form just did.

No worries! We can fix this, with just a little bit of really smart code.

Leave a comment!

6
Login or Register to join the conversation
Alessandro-D Avatar
Alessandro-D Avatar Alessandro-D | posted 3 years ago | edited

I am having such a hard time trying to fix this issue.
Basically I have a "User" and "Profession" entity and I have a ManyToMany relationship between the two.
The user has the ability to edit their own profile, which they can do it via the form. The form at the moment display all basic information contained in the User table.
I now want to allow users to edit their own professions too, therefore I added the following to my ProfileFormType class:


            ->add('profession', EntityType::class, [
                'class'=> Profession::class,
                'multiple'=> true
            ])

When I try now to edit the profile, I can see the "profession" field with all the options and the one already assigned to the user are selected. The problem starts when I try to add/remove additional professions. When I try to save them, it doesn't do anything and I don't know why.
Inside the "User" class I have this:


    /**
     * @ORM\ManyToMany(targetEntity="App\Entity\Profession", mappedBy="user")
     */
    private $professions;
    ...
    ...

    public function addShowreel(Showreel $showreel): self
    {
        if (!$this->showreels->contains($showreel)) {
            $this->showreels[] = $showreel;
            $showreel->setUser($this);
        }

        return $this;
    }

    public function removeShowreel(Showreel $showreel): self
    {
        if ($this->showreels->contains($showreel)) {
            $this->showreels->removeElement($showreel);
            // set the owning side to null (unless already changed)
            if ($showreel->getUser() === $this) {
                $showreel->setUser(null);
            }
        }

        return $this;
    }

    public function getProfessions(): Collection
    {
        return $this->professions;
    }

    /**
     * @return Collection|Profession[]
     */
    public function getProfession(): Collection
    {
        return $this->professions;
    }

and inside the Profession class, I have this:


    /**
     * @ORM\ManyToMany(targetEntity="App\Entity\User", inversedBy="professions")
     */
    private $user;
    ...
    ...
    /**
     * @return Collection|User[]
     */
    public function getUser(): Collection
    {
        return $this->user;
    }

    public function addUser(User $user): self
    {
        if (!$this->user->contains($user)) {
            $this->user[] = $user;
        }

        return $this;
    }

    public function removeUser(User $user): self
    {
        if ($this->user->contains($user)) {
            $this->user->removeElement($user);
        }

        return $this;
    }

My Profile controller (in the edit function) I have this:


$user = $this->getUser();
$form = $this->createForm(ProfileFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
            $em->persist($user);
            $em->flush();
            return $this->redirectToRoute('grm_profile');
}

Sorry for the length of this post, but I really need some help. I have tried slack, IRC but I can't find anyone who can help me understand what is wrong with it.

Thanks,
Alessandro

Reply

Hey Alessandro!

Hmm, ok. Here are the things that I'm looking at to make sure this works correctly:

1) You added a profession field to your form - but the property on your entity is called professions (with an "s"). This should have caused an error. If there is no error, it must mean that you have (I'm guessing?) a getProfession() and setProfession() method in your User class? Those should not be there. Basically, make sure that in your form, the field is called professions (with an s).

2) Assuming the name of the field in the form is professions, I would next look to make sure that your User class has addProfession() and removeProfession() methods. These should have been generated for you if you created the relationship via make:entity. You should not have a setProfessions method. Basically, because the field in your form is called professions, when you submit the form, Symfony will first look for a setProfessions() method. If it exists, it will call it. We actually don't want that :). If it does not find this method, it will then look for addProfession() and removeProfession: it will call each method one time for all the professions that were added/removed by the user in the form. We WANT this.

3) Side note: in Profession, it looks like the name of the property is "user". It should be "users" for clarity: it will hold an array of User objects :).

4) Here is the key: ultimately, because the Profession.user (which really should be Profession.user for clarity) is the "owning" side of the relationship, we ultimately need that side of the relation to be set. If the new data were only set onto User.professions, Doctrine will not save the updated data in the database. This is almost definitely what's happening to you. To make that happen, if you used make:entity to generate the relationship, the code inside the addProfession() and removeProfession methods will already contain code to do this. That will be enough to updating the Profession.user property and everything will save.

The easiest way to get things cleaned up might be to:

A) remove the relationship entirely from Profession and User (remove all the "user" code from Profession - the property, methods, etc and then remove all the "profession" code from User).

B) Re-generate the relationship with make:entity so that all the properties and methods are set up correctly.

Let me know if this helps!

Cheers!

1 Reply
Paul Z. Avatar
Paul Z. Avatar Paul Z. | posted 5 years ago

Where is this in the ''finish' code folder?

Reply

Hay Dave,

What exactly can't you find in the "finish/" folder?

Cheers!

Reply
Paul Z. Avatar

I couldn't find any of the code to save the inverse many-to-many. I understand now that the finish folder is the end of this whole course, not this chapter. At the end of this course the many-to-many is turned into a one-to-many / many-to-one so that's why it's not there. Sorry for my misunderstanding, although it would be nice to have the code after each chapter since things keep changing.

Reply

Hey Dave,

Sorry for misleading you. Actually, we have an issue about this, but it's not prioritized yet. But for now, you can use our handy expandable code block below each video. You can even expand the entire file by pressing "Show All Lines" button (double arrows in the left top corner of each code block) to get more context. Plus, keep in mind "Copy" button in the right top corner, it prevent you to select the code manually.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "symfony/symfony": "3.4.*", // v3.4.49
        "doctrine/orm": "^2.5", // 2.7.5
        "doctrine/doctrine-bundle": "^1.6", // 1.12.13
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.47
        "nelmio/alice": "^2.1", // v2.3.6
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice