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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeAfter 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 forGenusScientist
.
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 aGenusScientist
that you have not told me to save. You never calledpersist()
on thisGenusScientist
! 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 theGenusScientist
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.
Hello super person,
So in my project one product can have many pictures with titles and descriptions. And I'm able to get the remover method to work.
BUT it seems that I can't get the adder to work. if I dump $form->getData(); it seems that everything I add gets ignored. While the multiple pictures I manually added in the database doesn't get ignored, and I can edit them in the form. In "inspect element" in the HTML form. I can see that embedded forms gets the necessary [0], [1] etc in the name, but it isn't visible when I post.
Any ideas what I accidentally missed?