2011S

Inconsistent Behavior of Symfony2s Form Framework

Updated Post: Things can work! It’s not as intuitive as I first thought, but I was on the right track and almost had the solution. The complete code for this bundle is now on github: https://github.com/khepin/ProductBundle if you want to review it.

I’m still trying to get to understand more about symfony2′s form framework. And especially the CollectionType, which should be a powerful and interesting part of the framework.

I’ve “almost” done it now, but when it comes to saving content to the database, I still have some issues. Trying to find where those come from, I found some things that I find inconsistent in the forms for symfony2 so far. I’ll first explain quickly how I’m now able to use the CollectionType, and then explain where I find it not consistent and why it’s blocking me now.

New Bundle: Product – Tag

o make things simple, I created a bundle only to try this with Products and Tags. That’s all I have in the bundle now. A Product has a name, that’s all. A Tag has a name and a Product attached to it. The idea is to have a form much like in this sample bundle where you can dynamically add sub-forms to your original form. But for now I’ll let the dynamic javascript funk to rest and will get a default form with 3 tags for each product.

Product Type

For a product, we want the name, and a collection of N tags. The options on the collection are what will later allow us to add things dynamically through javascript. Let’s forget about them for now.

Then I set my default options in order to tell that from this I want to get back an object of the Product class and not just an array.

Update: setting the ‘by_reference’ option as false will call a setTags function. This is the correct way to proceed as discussed in the comments.

Tag Type

Not much more to talk about here, just that it’s important to set the data_class option for things to start working.

Controller

The ‘new’ action creates a form based on a product that has 3 empty tags. Therefore, when we display the form and the tag collection, we will indeed see 3 tag widgets.

The ‘create’ action receives data from this form and tries to save it. The product is saved in the database, no problem there. The tags are also saved in the database. Almost no problem! The tags are saved without any product_id. This is normal since there is no data in the form for each tag to say what is the product they are attached to.

However if we inspect the Product entity before saving it, it actually has its “tags” property set to an ArrayCollection containing all the tags. All seems fine but those Tags have a Product that is null instead of being the Product we’re about to save.

Inconsistensies

I’ve tried a few things and been digging inside Form->bind(), DataMappers and property paths and other goodness of the internals of forms that I absolutely do not understand for now but have been unable to see at which point those things are set. I thought maybe I needed to provide my own DataMapper or something.

I then took some time to take a look at the PizzaBundle I talked about earlier. In this bundle, they use a Factory to be given as “data” to the form instead of using the entity class directly. This factory implements all of the same getters and setters as the entity itself. The factory is for the Order class which contains a collection of OrderItems (= a pizza type and a quantity).

It implements a setItems($items) method and then when it actually creates the order, it properly sets the Order property of each of these items and in the end everything is saved correctly.

First try

So I realized that in my form, when I send the name of the product, then the form calls Product::setName($receivedData). To try this, I set the setName method so that it would repeat the given name twice. Indeed when I save something to the database, the name is repeated twice.

If I comment the setName method, since name is a private property, the form breaks and tells me that I should create that method because it can’t directly set the name by itself. I think I have my solution!

Second try

However for collections, when we generate the entities, they come with an ‘addXxx’ method only that allows to add things to a collection one at a time. So here’s what I wanted to do:

FAIL! Never ever ever are any of those methods called! And although the ‘tags’ property is supposed to be private, it is set with an ArrayCollection of 3 tags without ever calling the public methods intended to create this collection!

I can’t find exactly where this is done. And have no idea how this is done.

I don’t understand why giving a factory solves the issue but I don’t feel like writing a complete factory class just to correctly set the references.

I don’t get why collections here are treated differently than other attributes of the final object and don’t use a setter method.

Update: As discussed in the comments below, after adding the ‘by_reference’ option to my tags collection, the Product entity setter is called and all works just as I first expected.