Symfony2 Forms: Entity as hidden field

April 13th 2013

Update: I have created a component that you can install via composer to add a hidden entity form type to your symfony2 project, here is the post about it and the git repository. When I started my first big project in Symfony2 I very quickly ran into an issue trying to get entities as hidden fields like I used to in Symfony1.4.

A bit of background

The situation arose when a client needed a system that could handle multiple resorts and multiple properties per resort.  The way they wanted the admin system to work was to click into a resort and then manage properties for that resort.  This meant I needed the system to have a property form with a manyToOne relationship to a resort.

My initial work around solution

I ended up with a work around solution that added the resort field as an entity type and passed through a query builder object that selected just the resort that the admin was currently managing.  Something like:

//Controller create method
$query_builder = $em->getRepository('ResortBundle:Resort')->createQueryBuilder('r')->where('r.id = :resort_id')->setParameter('resort_id', $resort_id)
//FormType build form method
$builder->add('resort', 'entity', array(
    'class' => 'ResortBundle:Resort',
    'query_builder' => $this->query_builder
));

This actually worked remarkably well but from a user perspective was confusing as there was a select to choose the resort but only ever one option so why have it.  This is not a great way to handle this situation though.

The new, much more awesome, solution: DataTransformer

After some digging I came across a forum post that suggested someone with my above issue just use a data transformer.  So, Megatrons nerdy cousin to the rescue!  I looked up data transformers and they are very well documented and make sense.  With this new found knowledge I wrote a base entity to id transformer that I could use for all entities in my projects:

<?php 

namespace Lrotherfield\MegatronBundle\Form\DataTransformer

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;

class EntityToIntTransformer implements DataTransformerInterface
{
    /**
     * @var \Doctrine\Common\Persistence\ObjectManager
     */
    private $om;
    private $entityClass;
    private $entityType;
    private $entityRepository;

    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om)
    {
        $this->om = $om;
    }

    /**
     * @param mixed $entity
     *
     * @return integer
     */
    public function transform($entity)
    {
        // Modified from comments to use instanceof so that base classes or interfaces can be specified
        if (null === $entity ||!$entity instanceof $this->entityClass) {
            // updated due to https://github.com/LRotherfield/Form/commit/140742b486352a5c9ac97590ae09f6d8b7f5be7f
            return '';
        }

        return $entity->getId();
    }

    /**
     * @param mixed $id
     *
     * @throws \Symfony\Component\Form\Exception\TransformationFailedException
     *
     * @return mixed|object
     */
    public function reverseTransform($id)
    {
        if (!$id) {
            //updated due to https://github.com/LRotherfield/Form/commit/2be11d1c239edf57de9f6e418a067ea9f1f8c2ed
            return null;
        }

        $entity = $this->om->getRepository($this->entityRepository)->findOneBy(array("id" => $id));

        if (null === $entity) {
            throw new TransformationFailedException(sprintf(
                'A %s with id "%s" does not exist!',
                $this->entityType,
                $id
            ));
        }

        return $entity;
    }

    public function setEntityType($entityType)
    {
        $this->entityType = $entityType;
    }

    public function setEntityClass($entityClass)
    {
        $this->entityClass = $entityClass;
    }

    public function setEntityRepository($entityRepository)
    {
        $this->entityRepository = $entityRepository;
    }

}

This class above requires 3 values to be set.

  1. The type of the entity (user, role, post etc).  This is used to provide a slightly more intuitive error message if the entity passed to transform is not correct
  2. The entity class (Lrotherfield\ExampleBundle\Entity\Example).  This is used to check that the entity passed to the transform function is of the provided class.
  3. The entity repository (LrotherfieldExampleBundle:Example).  This is used to fetch the entity using the id passed to reverseTransform.

Values 1 and 2 are just additional features that are not really needed and could be stripped out if you want.  They only add two lines of code when you call the class though so its not a big issue to keep them in and it can help with debugging and entity integrity.

Using the data transformer

Some how we need to set the required values, this can be done in two simple ways:

1.Create an extension class for each entity that you want to transform and set the values in the constructor:

<?php

namespace LRotherfield\UserBundle\Form\DataTransformer;

use Doctrine\Common\Persistence\ObjectManager;

class UserToIntTransformer extends EntityToIntTransformer
{
    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om)
    {
        parent::__construct($om);
        $this->setEntityClass("LRotherfield\\UserBundle\\Entity\\User");
        $this->setEntityRepository("LRotherfieldUserBundle:User");
        $this->setEntityType("user");
    }

}

2. As the setters are public methods, you can just instantiate the transformer class and then call the methods one after the other:

//In a form class
public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $userTransformer = new EntityToIntTransformer($options["em"]);
        $userTransformer->setEntityClass("LRotherfield\\UserBundle\\Entity\\User");
        $userTransformer->setEntityRepository("LRotherfieldUserBundle:User");
        $userTransformer->setEntityType("user");
    }
//...

Then, once you have used your preferred method, you can simply use the data transformer to make your hidden field:

//In your form class
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $userTransformer = new UserToIntTransformer($options["em"]);

        $builder
            ->add($builder->create('user', 'hidden')->addModelTransformer($userTransformer));
        //...

Remember you must pass an entity manager through to the form class options argument with the key "em"

//In your controller
$form = $this->createForm(new YourFormType(), $yourEntity, array(
    'em' => $this->getDoctrine()->getManager()
));

Closing notes

Hopefully this little class will be of some help to you if you were/are in a similar situation to me when I started.  As I mentioned, most of the above is clearly documented on the Symfony2 website. If you have any feedback, please let me know with the comment form below.

Luke Rotherfield

Symfony2 Developer in CT USA. Luke is a Symfony2 wizard and has written some sweet libraries of his own. Luke loves Jesus, his gorgeous wife and his two beautiful daughters :)