Inconsistent behavior of symfony2′s 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

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

21 thoughts on “Inconsistent behavior of symfony2′s form framework?

    • It’s already using cascading persists right now (I didn’t use $em->persist($tag) ), they got persisted automatically because they belong to the product being persisted.
      However the owning side of the relation doesn’t know about it so they’re persisted with no product…

  1. You already found the correct solution. Any linking in your data model should be done by yourself, as you did in setTags(). The form does not care about that.

    In order for setTags() to be called in your case, you need to set the option “by_reference” of the “tags” field to false. You don’t always want a setTags() method, which is why the default value of “by_reference” is true. (BTW this is not only the case for collections, but for any other associated object, like product->category etc.)

    I agree that it would be nice if the framework could automatically call an addTag() method if available, but this is currently not possible.

    • Victory! Thanks much!

      Does the ‘by_reference’ means that the forms get a reference to the private property and then modify it directly which circumvents the fact that it’s private?

      Anyway, I understand how it’s done now and will make a post to explain the final things later today.

    • hi webmozart,

      Regarding your comment below… do you know of an example of using by_reference that is NOT a collection type? i am trying to understand this. Thanks

      You already found the correct solution. Any linking in your data model should be done by yourself, as you did in setTags(). The form does not care about that.

      In order for setTags() to be called in your case, you need to set the option “by_reference” of the “tags” field to false. You don’t always want a setTags() method, which is why the default value of “by_reference” is true. (BTW this is not only the case for collections, but for any other associated object, like product->category etc.)

        • Yes. When you have a model Article with a related Author and you want to edit the title of the article and the name of the author in the same form, basically two methods get called:

          1. $article->setTitle()
          2. $article->getAuthor()->setName()

          Note that setAuthor() is not called. If you set by_reference of ‘author’ to false, the method calls slightly differ:

          1. $article->setTitle()
          2. $author = $article->getAuthor()
          3. $author->setName()
          4. $article->setAuthor($author)

  2. No. “by_reference” means that the form calls getTags() to obtain the collection object and then directly modifies that collection object without writing it back using setTags().

    Glad it worked :)

    • This actually didn’t feel to me like a form related thing but more the way setters should be done. I’ve had the case where I need to set this cross-reference and this was nowhere close to a form.
      If you generate your entities getter and setter through Doctrine, you can add the tag to the product, but the way they do the setter doesn’t let the Tag know about it. However in the Doctrine documentation, they suggest doing it this way (in the setter):

      • Ah, I don’t recall seeing that in the D2 docs.

        Previously I’ve just been aware of the owning side of my relationships when it comes time to persist stuff. I guess for me a post bind listener felt more “right” than hardcoding knowledge of a bi-directional relationship within the setter of a POPO. But that’s open to debate :)

  3. I’m experiencing problems about persisting the many-side of an entity.
    I have twi entities: Candidate and Interview. A candidate can have many Interview.

    On a form, I want to add (with jquery) as many interviews as i want. So, when i push on a button , this adds a new div ,etc. See the picture :

    http://uppix.net/a/3/3/c4f549b09e8c290959d0e77c573fd.jpg

    My problem is that I can’t get back the value of all of my textarea.
    I have no script erro. I have tested my code with Netbeans (xdebug) in order to debug my code and find the solution but nothing work.

    The code of the function in my controller:

    getRequest();
    $form = $this->createForm(new CandidateTypeInterview(), $entity);
    $form->bindRequest($request);

    if ($form->isValid())
    {
    $em = $this->getDoctrine()->getEntityManager();
    $em->persist($entity);

    foreach ($entity->getInterviews() as $value)
    {
    $em-> persist ($value);
    }
    }

    $em->flush();

    return $this->render('AdlHiringBundle:Candidate:saveinterview.html.twig',array(
    'entity' => $entity,
    'form' => $form->createView()
    ));
    }

    I have no idea why I can’t persist any of my Interview ?
    Could you help me ? Thanks

      • Hi,

        I looked at your code but unfortunately, it doesn’t work. I can’t persist the sub entity.
        The foreach loop can’t be access neither. I put a simple echo inside of t to see the result but nothing displays.

        I have the same structure than you. Simply two entities (see above). I really don’t understand.

  4. This solved my problem too !

    Nb : The ‘by_reference’ => false is put in your form builder in the array of $builder->add(xxx , collection , array( X ) )

Comments are closed.