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 SubscribeWhen we delete one of the GenusScientist
forms and submit, the CollectionType
is now smart enough to remove that GenusScientist
from the genusScientists
array on Genus
. So, why doesn't that make any difference to the database?
The problem is that the genusScientists
property is now the inverse side of this relationship:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 71 | |
/** | |
* @ORM\OneToMany(targetEntity="GenusScientist", mappedBy="genus", fetch="EXTRA_LAZY") | |
*/ | |
private $genusScientists; | |
... lines 76 - 202 | |
} |
In other words, if we remove or add a GenusScientist
from this array, it doesn't make any difference! Doctrine ignores changes to the inverse side.
How to fix it? We already know how! We did it back with our ManyToMany
relationship! It's a two step process.
First, in GenusFormType
, set the by_reference
option to false
:
... lines 1 - 18 | |
class GenusFormType extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
... lines 24 - 46 | |
->add('genusScientists', CollectionType::class, [ | |
... lines 48 - 49 | |
'by_reference' => false, | |
]) | |
; | |
} | |
... lines 54 - 60 | |
} |
Remember this?
Without this, the form component never calls setGenusScientists()
. In fact, there is no setGenusScientists
method in Genus
. Instead, the form calls getGenusScientists()
and then modifies that ArrayCollection
object by reference:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 195 | |
/** | |
* @return ArrayCollection|GenusScientist[] | |
*/ | |
public function getGenusScientists() | |
{ | |
return $this->genusScientists; | |
} | |
} |
But by setting it to false, it's going to give us the flexibility we need to set the owning side of the relationship.
With just that change, submit the form. Error! But look at it closely: the error happens when the form system calls removeGenusScientist()
. That's perfect! Well, not the error, but when we set by_reference
to false, the form started using our adder and remover methods. Now, when we delete a GenusScientist
form, it calls removeGenusScientist()
:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 184 | |
public function removeGenusScientist(User $user) | |
{ | |
if (!$this->genusScientists->contains($user)) { | |
return; | |
} | |
$this->genusScientists->removeElement($user); | |
// not needed for persistence, just keeping both sides in sync | |
$user->removeStudiedGenus($this); | |
} | |
... lines 195 - 202 | |
} |
The only problem is that those methods are totally outdated: they're still written for our old ManyToMany
setup.
In removeGenusScientist()
, change the argument to accept a GenusScientist
object. Then update $user
to $genusScientist
in one spot, and then the other:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 184 | |
public function removeGenusScientist(GenusScientist $genusScientist) | |
{ | |
if (!$this->genusScientists->contains($genusScientist)) { | |
return; | |
} | |
$this->genusScientists->removeElement($genusScientist); | |
... lines 192 - 193 | |
} | |
... lines 195 - 202 | |
} |
For the last line, use $genusScientist->setGenus(null)
. Let's update the note to say the opposite:
Needed to update the owning side of the relationship!
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 184 | |
public function removeGenusScientist(GenusScientist $genusScientist) | |
{ | |
if (!$this->genusScientists->contains($genusScientist)) { | |
return; | |
} | |
$this->genusScientists->removeElement($genusScientist); | |
// needed to update the owning side of the relationship! | |
$genusScientist->setGenus(null); | |
} | |
... lines 195 - 202 | |
} |
Now, when we remove one of the embedded GenusScientist
forms and submit, it will call removeGenusScientist()
and that will set the owning side: $genusScientist->setGenus(null)
.
If you're a bit confused how this will ultimately delete that GenusScientist
, hold on! Because you're right! But, submit the form again.
Yay! Another error!
UPDATE genus_scientist SET genus_id = NULL
Huh... that makes perfect sense. Our code is not deleting that GenusEntity
. Nope, it's simply setting its genus
property to null
. This update query makes sense!
But... it's not what we want! We want to say:
No no no. If the
GenusScientist
is no longer set to thisGenus
, it should be deleted entirely from the database.
And Doctrine has an option for exactly that. In Genus
, find your genusScientists
property. Let's reorganize the OneToMany
annotation onto multiple lines: it's getting a bit long. Then, add one magical option: orphanRemoval = true
:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 71 | |
/** | |
* @ORM\OneToMany( | |
* targetEntity="GenusScientist", | |
* mappedBy="genus", | |
* fetch="EXTRA_LAZY", | |
* orphanRemoval=true | |
* ) | |
*/ | |
private $genusScientists; | |
... lines 81 - 207 | |
} |
That's the key. It says:
If one of these
GenusScientist
objects suddenly has theirgenus
set tonull
, just delete it entirely.
Tip
If the GenusScientist.genus
property is set to a different Genus
,
instead of null
, it will still be deleted. Use orphanRemoval
only
when that's not going to happen.
Give it a try! Refresh the form to start over. We have four genus scientists. Remove one and hit save.
Woohoo! That fourth GenusScientist
was just deleted from the database.
I know this was a bit tricky, but we didn't write a lot of code to get here. There are just two things to remember.
First, if you're ever modifying the inverse side of a relationship in a form, set by_reference
to false, create adder and remover methods, and set the owning side in each. And second, for a OneToMany
relationship like this, use orphanRemoval
to delete that related entity for you.
This was a big success! Next: we need to be able to add new genus scientists in the form.
Gey Gustavo,
I'm not sure Doctrine has such annotation :) How will Doctrine know to whom assign it? It's only possible to set genus to NULL on deleting. I *think* you can add a listener to listen to preRemove event and implement a custom logic in it to assign it to another Genus.
Cheers!
Thank you very much for your response. What I was referring to wasn't to assign the $genusScientist to another $genus when performing the delete action. I just want to free the $genusScientist so that another $genus can add it from the edit action, for example. When I use OrphanRemoval the $genusScientist is completely removed, so if a different $genus wants to add it, it needs to re-enter all the $ genusScientist information again. I apologize for my English, I'm learning it too.
but you have given me the answer. In the $genusScientist entity, I only have to write *@ORM\JoinColumn(nullable=true) in the field $genus, and that's it! again, thank you very much for your time
I get an error when I added 'by_reference' =>false to the GenusFormType
->add('genusScientists', CollectionType::class, [
'entry_type' => GenusScientistEmbeddedForm::class,
'allow_delete' => true,
'by_reference' =>false
])
CRITICAL - Uncaught PHP Exception Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException: "Neither the property "genusScientists" nor one of the methods "addGenusScientist()"/"removeGenusScientist()", "setGenusScientists()", "genusScientists()", "__set()" or "__call()" exist and have public access in class "AppBundle\Entity\Genus"." at /var/www/aqua_note/vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php line 614
The genus entity has the method:
public function removeGenusScientist(GenusScientist $genusScientist)
{
if (!$this->genusScientists->contains($genusScientist)) {
return;
}
$this->genusScientists->removeElement($genusScientist);
// Needed to update the owning site of the relationship
$genusScientist->setGenus(null);
}
What could be wrong?
Hey Hank!
Do you also have the method "addGenusScientist()"? you need both if you are going the "remove" way, instead of using a setter
Cheers!
Hi Diego, You were right again I made a typo in the addGenusScientist() and typed addGenusScientists() with an extra s at the end. Thanks!
This solution to remove a genusScientist from the Genus will only remove ONE genusScientist when the form is submitted, regardless of how many you removed from the form.
Hey Tony C!
Are you sure? I just backed up my code to this exact step in the tutorial and tried it - I removed 4 genus scientists of the 7 on a page and saved - all 4 I selected were removed. Are you seeing something different?
Cheers!
// 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
}
}
Which doctrine annotation should I use if I really wanted to assign a $genusScientist to another $genus instead of completely removing it from the database? Thanks.