Adding to a Collection: Cascade Persist

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

After adding a new GenusScientist sub-form and submitting, we're greeted with this wonderful error!

Expected argument of type User, GenusScientist given

Updating the Adder Method

But, like always, look closely. Because if you scroll down a little, you can see that the form is calling the addGenusScientist() method on our Genus object:

... lines 1 - 14
class Genus
{
... lines 17 - 178
public function addGenusScientist(User $user)
{
... lines 181 - 187
}
... lines 189 - 207
}

Oh yea, we expected that! But, the code in this method is still outdated.

Change the argument to accept a GenusScientist object. Then, I'll refactor the variable name to $genusScientist:

... lines 1 - 14
class Genus
{
... lines 17 - 179
public function addGenusScientist(GenusScientist $genusScientist)
{
if ($this->genusScientists->contains($genusScientist)) {
return;
}
$this->genusScientists[] = $genusScientist;
}
... lines 188 - 206
}

As you guys know, we always need to set the owning side of the relationship in these methods. But, don't do that... yet. For now, only make sure that the new GenusScientist object is added to our array.

With that fixed, go back, and refresh to resubmit the form. Yay! New error! Ooh, this is an interesting one:

A new entity was found through the relationship Genus.genusScientists that was not configured to cascade persist operations for GenusScientist.

Umm, what? Here's what's going on: when we persist the Genus, Doctrine sees the new GenusScientist on the genusScientists array... and notices that we have not called persist on it. This error basically says:

Yo! You told me that you want to save this Genus, but it's related to a GenusScientist that you have not told me to save. You never called persist() on this GenusScientist! This doesn't make any sense!

Cascade Persist

So what's the fix? It's simple! We just need to call persist() on any new GenusScientist objects. We could add some code to our controller to do that after the form is submitted:

... lines 1 - 15
class GenusAdminController extends Controller
{
... lines 18 - 34
public function newAction(Request $request)
{
$form = $this->createForm(GenusFormType::class);
// only handles data on POST
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
... lines 42 - 43
$em = $this->getDoctrine()->getManager();
$em->persist($genus);
$em->flush();
... lines 47 - 53
}
... lines 55 - 58
}
... lines 60 - 87
}

Or... we could do something fancier. In Genus, add a new option to the OneToMany: cascade={"persist"}:

... lines 1 - 14
class Genus
{
... lines 17 - 71
/**
* @ORM\OneToMany(
* targetEntity="GenusScientist",
* mappedBy="genus",
* fetch="EXTRA_LAZY",
* orphanRemoval=true,
* cascade={"persist"}
* )
*/
private $genusScientists;
... lines 82 - 206
}

This says:

When we persist a Genus, automatically call persist on each of the GenusScientist objects in this array. In other words, cascade the persist onto these children.

Alright, refresh now. This is the last error, I promise! And this makes perfect sense: it is trying to insert into genus_scientist - yay! But with genus_id set to null.

The GenusScientistEmbeddedForm creates a new GenusScientist object and sets the user and yearsStudied fields:

... lines 1 - 12
class GenusScientistEmbeddedForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('user', EntityType::class, [
... lines 19 - 23
])
->add('yearsStudied')
;
}
... lines 28 - 36
}

But, nobody is ever setting the genus property on this GenusScientist.

This is because I forced you - against your will - to temporarily not set the owning side of the relationship in addGenusScientist. I'll copy the same comment from the remover, and then add $genusScientist->setGenus($this):

... lines 1 - 14
class Genus
{
... lines 17 - 179
public function addGenusScientist(GenusScientist $genusScientist)
{
... lines 182 - 186
// needed to update the owning side of the relationship!
$genusScientist->setGenus($this);
}
... lines 190 - 208
}

Owning side handled!

Ok, refresh one last time. Boom! We now have four genuses: this new one was just inserted.

And yea, that's about as complicated as you can get with this stuff.

Don't Purposefully Make your Life Difficult

Oh, but before we move on, go back to /genus, click a genus, go to one of the user show pages, and then click the pencil icon. This form is still totally broken: it's still built as if we have a ManyToMany relationship to Genus. But with our new-found knowledge, we could easily fix this in the exact same way that we just rebuilt the GenusForm. But, since that's not too interesting, instead, open UserEditForm and remove the studiedGenuses field:

... lines 1 - 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',
'by_reference' => false,
])
;
}
... lines 34 - 40
}

Then, open the user/edit.html.twig template and kill the render:

... 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 %}

Finally, find the User class and scroll down to the adder and remover methods. Get these outta here:

... lines 1 - 16
class User implements UserInterface
{
... lines 19 - 222
public function addStudiedGenus(Genus $genus)
{
if ($this->studiedGenuses->contains($genus)) {
return;
}
$this->studiedGenuses[] = $genus;
$genus->addGenusScientist($this);
}
public function removeStudiedGenus(Genus $genus)
{
if (!$this->studiedGenuses->contains($genus)) {
return;
}
$this->studiedGenuses->removeElement($genus);
$genus->removeGenusScientist($this);
}
}

Go back to refresh the form. Ok, better! This last task was more than just some cleanup: it illustrates an important point. If you don't need to edit the genusesStudied from this form, then you don't need all the extra code, especially the adder and remover methods. Don't make yourself do extra work. At first, whenever I map the inverse side of a relationship, I only add a "getter" method. It's only later, if I need to update things from this side, that I get fancy.

Oh, and also, remember that this entire side of the relationship is optional. The owning side of the relationship is in GenusScientist. So unless you need to be able to easily fetch the GenusScientist instances for a User - in other words, $user->getStudiedGenuses() - don't even bother mapping this side. We are using that functionality on the user show page, so I'll leave it.

Leave a comment!

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": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1", // 1.1.1
        "stof/doctrine-extensions-bundle": "^1.2" // v1.2.2
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}