Via G. Falcone 5, Pollenza (MC), Italy
+39 0733 203595

Orphan removal, forms collection and collection’s elements swapping: pick your poison

Today we are going to analyze a very common situation if you use Symfony framework: let’s talk about collection of objects, forms used to handle them and “collection swapping” (a.k.a. changing owner onto owning side of a collection relationship in doctrine).

Scenario

What we get here is a trivial situation: let’s say a client hired us to write the next super cool events booking application. So a user – that must be registered into the system – can buy its tickets through this app. Nothing easier!

Now, our client comes with an idea

Samuele, why don’t we add a killing feature where a super admin can login and handle tickets for a user that needs to be helped?

After this confusing sentence we come up with this reply

What’s the meaning of “handle” and “needs to be helped”?

We all know that is very difficult to deal with clients and after a sentence-reply-tennis-game – that will be omitted, of course! – we end up with these definitions:

  • Handle means that a super admin can buy and refund tickets from a recap form (keep this in mind) on behalf of a user
  • Needs to be helped means that a user can buy tickets and have a refund as well by email or by phone

By introducing this “killing feature” (wow) we need to build a form. This form will be based onto user and will show, among other things, a collection of tickets (with adding/deleting tickets capability).

Cool! We have our form. It’s important to note that TicketType will not be reported as it’s not interesting for this scenario. UserController will be not reported as well: it takes the Request, binds it to the form, performs isValid() control and writes e onto db. Furthermore we set by_reference to false to be sure that User‘s setters will be called for the collection.

After we have placed some client-side magic (to add and remove tickets) the application fulfills starting requirements.

As soon as this form is used to delete a ticket from a user collection, software does not work as expected! As a matter of fact collection’s element are not removed!

Here you can easily figure out why this is happening.

When removing objects in this way, you may need to do a little bit more work to ensure that the relationship between the Task (ed: User) and the removed Tag (ed: Ticket) is properly removed.

In Doctrine, you have two sides of the relationship: the owning side and the inverse side. Normally in this case you’ll have a many-to-many relationship and the deleted tags will disappear and persist correctly (adding new tags also works effortlessly).

But if you have a one-to-many relationship (ed: that’s our situation) or a many-to-many relationship with a mappedBy on the Task entity (meaning Task is the “inverse” side), you’ll need to do more work for the removed tags to persist correctly.

#1 poison

In this case, you can modify the controller to remove the relationship on the removed ticket. This assumes that you have some editAction which is handling the “update” of your User:

Wait a minute … why should I manually do this

when I can go like …

#2 poison

this

(we added orphanRemoval to the collection)

without touching a line of code into controllers? Cool, isn’t it?

Yeah, a sort of. Because now we need to add an extra feature to admin dashboard as, if a user wants to unsubscribe from our site, it’s mandatory for him to give away all tickets he purchased to a user (by specifying its username in a form that will not be reported here).

Ok, so we come up with this solution

What do you expect from this snippet of code?

If the answer is something like

Tickets should now be related to $beneficiaryUser

Well, you’re just wrong!

The reason behind this – at first glance – strange behavior is that

When using the orphanRemoval=true option Doctrine makes the assumption that the entities are privately owned and will NOT be reused by other entities. If you neglect this assumption your entities will get deleted by Doctrine even if you assigned the orphaned entity to another one.

BINGO!

Let me say that use cascade={"remove"} is not a solution at all for our situation: when you remove an entity from a collection you, probably, don’t want to remove the collection holder as well (so you can return to #1 poison solution if you’re going to use cascading operation).

What really happens here is that, since we are removing a user, we are also removing all its tickets, despite of the fact we are assigning them to another user.

#3 poison

Ok, so we have a brilliant idea! Just remove those tickets from $yieldingUser before assign them to beneficiaryUser and all should work!

So we slightly modify previous code …

We are close to make it works but not yet: as a matter of fact, tickets are still removed!

The solution here is to modify setUser function onto Ticket entity as follows (we do not show as it was originally but it looked like a setter without any logic)

And this is going to work (as long as you set cascade={"persist"} into User of course!)

BUT … 

If you expect this to keep tickets ids … well, you are wrong!

In fact what happens here is that doctrine will remove old instances of ticket from original collection and it will re-create them into new one.

You can understand yourself that, if you have a foreign key somewhere, this could drive you straight into big troubles!

RECAP

So, you can act knowingly now that you know this three situations and pick what fits you the most!

With #1 poison (standard scenario) you have to write code into controller (or, better, into a dedicated manager) just to handle correct persistence between old collection values and new ones.

With #2 poison (orphanRemoval) you do not need to write any line of code BUT you simply can’t assign an element of a collection to another holder if you set orphanRemoval

With #3 poison (orphanRemoval + custom logic) you can avoid writing extra code logic but, if you need to switch collection’s element holder, you need to implement – again – some extra logic and you are going to loose original collection’s element ids.

So … pick your poison now!

1 comment

Arthur Hundiak

+1 on using the term poison.

Doctrine 2 ORM works great on a small number of simple use cases but it is nonsense like this that made me go back to just plain sql queries and simple object factories. I would rather write a few extra lines of code than try to maintain the options given above.

Leave a reply