Symfony2 collection of Entities - how to add/remove association with existing entities?

I've come to the same conclusion that there's something wrong with the Form component and can't see an easy way to fix it. However, I've come up with a slightly less cumbersome workaround solution that is completely generic; it doesn't have any hard-coded knowledge of entities/attributes so will fix any collection it comes across:

Simpler, generic workaround method

This doesn't require you to make any changes to your entity.

use Doctrine\Common\Collections\Collection;
use Symfony\Component\Form\Form;

# In your controller. Or possibly defined within a service if used in many controllers

/**
 * Ensure that any removed items collections actually get removed
 *
 * @param \Symfony\Component\Form\Form $form
 */
protected function cleanupCollections(Form $form)
{
    $children = $form->getChildren();

    foreach ($children as $childForm) {
        $data = $childForm->getData();
        if ($data instanceof Collection) {

            // Get the child form objects and compare the data of each child against the object's current collection
            $proxies = $childForm->getChildren();
            foreach ($proxies as $proxy) {
                $entity = $proxy->getData();
                if (!$data->contains($entity)) {

                    // Entity has been removed from the collection
                    // DELETE THE ENTITY HERE

                    // e.g. doctrine:
                    // $em = $this->getDoctrine()->getEntityManager();
                    // $em->remove($entity);

                }
            }
        }
    }
}

Call the new cleanupCollections() method before persisting

# in your controller action...

if($request->getMethod() == 'POST') {
    $form->bindRequest($request);
    if($form->isValid()) {

        // 'Clean' all collections within the form before persisting
        $this->cleanupCollections($form);

        $em->persist($user);
        $em->flush();

        // further actions. return response...
    }
}

So a year has passed, and this question has become quite popular. Symfony has changed since, my skills and knowledge have also improved, and so has my current approach to this problem.

I've created a set of form extensions for symfony2 (see FormExtensionsBundle project on github) and they include a form type for handleing One/Many ToMany relationships.

While writing these, adding custom code to your controller to handle collections was unacceptable - the form extensions were supposed to be easy to use, work out-of-the-box and make life easier on us developers, not harder. Also.. remember.. DRY!

So I had to move the add/remove associations code somewhere else - and the right place to do it was naturally an EventListener :)

Have a look at the EventListener/CollectionUploadListener.php file to see how we handle this now.

PS. Copying the code here is unnecessary, the most important thing is that stuff like that should actually be handled in the EventListener.


1. The workaround solution

The workaround solution suggested by Jeppe Marianger-Lam is at the moment the only one working I know of.

1.1 Why did it stop working in my case?

I changed my RoleNameType (for other reasons) to:

  • ID (hidden)
  • name (custom type - label)
  • module & description (hidden, read-only)

The problem was my custom type label rendered NAME property as


    <span> role name </span>

And since it was not "read only" the FORM component expected to get NAME in POST.

Instead only ID was POSTed, and thus FORM component assumed NAME is NULL.

This lead to CASE 2 (3.2) -> creating association, but overwriting ROLE NAME with an empty string.

2. So, what exacly is this workaround about?

2.1 Controller

This workaround is very simple.

In your controller, before you VALIDATE the form, you have to fetch the posted entity identyficators and get matching entities, then set them to your object.

// example action
public function createAction(Request $request)
{      
    $em = $this->getDoctrine()->getEntityManager();

    // the workaround code is in updateUser function
    $user = $this->updateUser($request, new User());

    $form = $this->createForm(new UserType(), $user);

    if($request->getMethod() == 'POST') {
        $form->bindRequest($request);

        if($form->isValid()) {
            /* Persist, flush and redirect */
            $em->persist($user);
            $em->flush();
            $this->setFlash('avocode_user_success', 'user.flash.user_created');
            $url = $this->container->get('router')->generate('avocode_user_show', array('id' => $user->getId()));

            return new RedirectResponse($url);
        }
    }

    return $this->render('AvocodeUserBundle:UserManagement:create.html.twig', array(
      'form' => $form->createView(),
      'user' => $user,
    ));
}

And below the workaround code in updateUser function:

protected function updateUser($request, $user)
{
    if($request->getMethod() == 'POST')
    {
      // getting POSTed values
      $avo_user = $request->request->get('avo_user');

      // if no roles are posted, then $owned_roles should be an empty array (to avoid errors)
      $owned_roles = (array_key_exists('avoRoles', $avo_user)) ? $avo_user['avoRoles'] : array();

      // foreach posted ROLE, get it's ID
      foreach($owned_roles as $key => $role) {
        $owned_roles[$key] = $role['id'];
      }

      // FIND all roles with matching ID's
      if(count($owned_roles) > 0)
      {
        $em = $this->getDoctrine()->getEntityManager();
        $roles = $em->getRepository('AvocodeUserBundle:Role')->findById($owned_roles);

        // and create association
        $user->setAvoRoles($roles);
      }

    return $user;
}

For this to work your SETTER (in this case in User.php entity) must be:

public function setAvoRoles($avoRoles)
{
    // first - clearing all associations
    // this way if entity was not found in POST
    // then association will be removed

    $this->getAvoRoles()->clear();

    // adding association only for POSTed entities
    foreach($avoRoles as $role) {
        $this->addAvoRole($role);
    }

    return $this;
}

3. Final thoughts

Still, I think this workaround is doing the job that

$form->bindRequest($request);

should do! It's either me doing something wrong, or symfony's Collection form type is not complete.

There are some major changes in Form component comeing in symfony 2.1, hopefully this will be fixed.

PS. If it's me doing something wrong...

... please post the way it should be done! I'd be glad to see a quick, easy and "clean" solution.

PS2. Special thanks to:

Jeppe Marianger-Lam and userfriendly (from #symfony2 on IRC). You've been very helpful. Cheers!


This is what I have done before - I don't know if it's the 'right' way to do it, but it works.

When you get the results from the submitted form (i.e., just before or right after if($form->isValid())), simply ask the list of the roles, then remove them all from the entity (saving the list as a variable). With this list, simply loop through them all, ask the repository for the role entity that matches the ID's, and add these to your user entity before you persist and flush.

I just searched through the Symfony2 documentation because I remembered something about prototype for form collections, and this turned up: http://symfony.com/doc/current/cookbook/form/form_collections.html - It has examples of how to deal correctly with javascript add and remove of collection types in forms. Perhaps try this approach first, and then try what I mentioned above afterwards if you cannot get it to work :)