Symfony2 form events and model transformers

Inheriting $builder from the parent scope using an annonymous function as event handler would also work.

$builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) use ($builder) {
    $data = $event->getData();

    if (empty($data['linkedFoo'])) {
       return;
    }
    $builder->add('linkedFoo', 'choice', [
        'choices' => $this->fooRepo->getListAsArray(
            $data->getLinkedfoo()->getId()
         ),
    ]);
    $builder->get('linkedFoo')
        ->addModelTransformer(
            new FooTransformer()
        );

});

For anyone that is still looking for a better way to add/re-add a Model Transformer inside form events, I think that the best solution is the one from this post all credits go to @Toilal for this brilliant solution

So if you implement the ModelTransformerExtension and define it as a service, and change some code, for example, from

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            array($this, 'onPreSetData')
        );
    $builder->add(
                $builder
                    ->create('customer', TextType::class, [
                        'required' => false,
                        'attr' => array('class' => 'form-control selectize-customer'),
                    ])
                    ->addModelTransformer(new CustomerToId($this->customerRepo))
            )
            ;
}

to something like:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->addEventListener(
                FormEvents::PRE_SET_DATA,
                array($this, 'onPreSetData')
            );
    $builder->add('customer', TextType::class, [
                'required' => false,
                'attr' => array('class' => 'form-control selectize-customer'),
                'model_transformer' => new CustomerToId($this->customerRepo),
            ]
        )
        ;
}

And now if we remove and re-add the desired field inside the eventlistener function, the Model Transformer for the field will not be lost.

protected function onPreSetData(FormEvent $event)
{
    $form = $event->getForm();
    $formFields = $form->all();
    foreach ($formFields as $key=>$value){
        $config = $form->get($key)->getConfig();
        $type = get_class($config->getType()->getInnerType());
        $options = $config->getOptions();

        //you can make changes to options/type for every form field here if you want 

        if ($key == 'customer'){
            $form->remove($key);
            $form->add($key, $type, $options);
        }
    }
}

Note that this is a simple example. I've used this solution to easily process a form to have multiple field states in different places.


Your listener looks (almost :) ) ok.

Just use PRE_SUBMIT. In that case, $event->getData() will be the raw form data (an array) that is sent. $selectedFoo will potentailly contain "other".

If it is the case, you will replace the "short" 'choice' field with a full one, by using formFactory in the listener.

$builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
    $data = $event->getData();
    if (empty($data['linkedFoo']) || $data['linkedFoo'] !== 'other') {
        return;
    }

    // now we know user choose "other"
    // so we'll change the "linkedFoo" field with a "fulllist"


    $event->getForm()->add('linkedFoo', 'choice', array(
        'choices' => $fullList, // $em->getRepository('Foo')->getFullList() ?
    ));
    $event->getForm()->get('linkedFoo')->getConfig()->addModelTransformer(new FooTransformer);
});

You asked so much questions I don't know where to start.

Concerning dataTransformers: until you want to transform raw data into a different representation ("2013-01-01" -> new DateTime("2013-01-01")), then you don't need transformers.


Thanks to the ideas from sstok (on github), I think I've got it working now. The key is to create a customised Form Type and then use that to add the ModelTransformer.

Create the custom Form Type:

namespace Caponica\MagnetBundle\Form\Type;

use ...

class FooShortlistChoiceType extends AbstractType {
    protected $em;

    public function __construct(EntityManager $entityManager)
    {
        $this->em                   = $entityManager;
    }

    public function buildForm(FormBuilderInterface $builder, array $options) {
        $fooTransformer = new FooToStringTransformer($this->em);

        $builder
            ->addModelTransformer($fooTransformer)
        ;
    }

    public function getParent() {
        return 'choice';
    }

    public function getName() {
        return 'fooShortlist';
    }
}

Create a service definition for the new Type:

company_project.form.type.foo_shortlist:
    class: Company\ProjectBundle\Form\Type\FooShortlistChoiceType
    tags:
        - { name: form.type, alias: fooShortlist }
    arguments:
        - @doctrine.orm.entity_manager

The main form's code now looks something like this:

namespace Company\ProjectBundle\Form\Type;

use ...

class FancyFormType extends AbstractType {
    private $fooRepo;

    public function __construct(FooRepository $fooRepo)
    {
        $this->fooRepo = $fooRepo;
    }

    public function buildForm(FormBuilderInterface $builder, array $options) {
        /** @var Bar $bar */
        $bar = $builder->getData();
        $fooTransformer = new FooToStringTransformer($options['em']);

        $builder
            ->add('linkedFoo', 'fooShortlist', array(
                'choices' => $this->fooRepo->getListAsArray(
                    $bar->getLinkedfoo()->getId()
                ),
            ))
        ;

        $builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
            /** @var EntityManager $em */
            $em = $event->getForm()->getConfig()->getOption('em');

            $data = $event->getData();
            if (empty($data['linkedFoo'])) return;
            $selectedFoo = $data['linkedFoo'];

            $event->getForm()->add('linkedFoo', 'fooShortlist', array(
                'choices'       => $em->getRepository('CaponicaMagnetBundle:FooShortlist')->getListAsArray($selectedFoo),
                'label'         => 'label'
            ));
        });

        // ...

    }

    // ...
}

The key is that this method allows you to embed the ModelTransformer within the custom field type so that, whenever you add a new instance of this type it automatically adds the ModelTransformer for you and prevents the previous loop of "can't add a field without a transformer AND can't add a transformer without a field"