Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Deleting an Item from a Collection: orphanRemoval

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

When 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.

Setting the Owning Side: by_reference

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.

Setting the Owning Side: Adder & Remover

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 this Genus, 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 their genus set to null, 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.

Leave a comment!

10
Login or Register to join the conversation
Tavo Avatar

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.

Reply

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!

1 Reply
Tavo Avatar

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.

1 Reply
Tavo Avatar

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

Reply
Default user avatar

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?

Reply

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!

Reply
Default user avatar

Hi Diego, You were right again I made a typo in the addGenusScientist() and typed addGenusScientists() with an extra s at the end. Thanks!

Reply

NP!

I'm glad to hear you could fix your problem :)

Cheers!

Reply
Default user avatar

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.

Reply

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!

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