Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony2 form events and model transformers

I'm getting tied in knots trying to wrestle with Symfony2's form builders, events and transformers... hopefully somebody here is more experienced and can help out!

I have a form field (select drop-down) which contains some values (a shortlist) which maps to an Entity. One of these options is "other". Assume there's no AJAX for now and when a user submits the form I want to detect if they chose 'other' (or any other option not in the shortlist). If they chose one of these options then the full list of options should be shown, otherwise just show the shortlist. Should be easy, right? ;)

So, I have my Form Type and it displays the basic shortlist just fine. The code looks something like this:

namespace Company\ProjectBundle\Form\Type;

use ...

class FancyFormType extends AbstractType {
    private $fooRepo;

    public function __construct(EntityManager $em, 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($builder
                ->create('linkedFoo', 'choice', array(
                    'choices' => $this->fooRepo->getListAsArray(
                        $bar->getLinkedfoo()->getId()
                    ),
                ))
                ->addModelTransformer($fooTransformer)
            )
        ;

        // ...

    }

    // ...
}

Now, I want to check the value submitted so I use a Form Event Listener as follows.

public function buildForm(FormBuilderInterface $builder, array $options) {
    // ... This code comes just after the snippet shown above

    $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', 'choice', array(
            'choices' => $em
                ->getRepository('CompanyProjectBundle:FooShortlist')
                ->getListAsArray($selectedFoo)
            ,
        ));
        //@todo - needs transformer?
    });
}

However, it fails with an error message like this:

Notice: Object of class Proxies\__CG__\Company\ProjectBundle\Entity\Foo could not be converted to int in \path\to\project\symfony\symfony\src\Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList.php line 458 

I presume this error is because when the linkedFoo was over-written it removed the modelTransformer? I tried various ways of accessing a builder in the event's closure but this didn't seem to work (the return values were unexpected). Is there some other method I should be using in the event other than $event->getForm()->add()? Or is there a more fundamental problem with my approach here?

Basically I don't want to mess with the linkedFoo field's config/transformers/labels except to change the choices available... is there some other way to do that? E.g. something like $form->getField()->updateChoices()?

Thanks in advance for any help you can offer!

C

P.S. is there any better documentation or discussion of the forms, events, etc than on the Symfony website? E.g. what's the difference between PRE_SET_DATA, PRE_SUBMIT, SUBMIT, etc? When are they fired? What should they be used for? How does inheritance work with custom form fields? What is a Form and a Builder, how do they interact and when should you deal with each? How, when and why should you use the FormFactory you can access through $form->getConfig()->getFormFactory()? Etc..


Edit: In response to Florian's suggestion here's some more info about things that were tried but do not work:

If you try to get the FormBuilder within the event like this:

/** @var FormBuilder $builder */
$builder = $event->getForm()->get('linkedFoo')->getConfig();

$event->getForm()->add($builder
    ->create('linkedFoo', 'choice', array(
        'choices' => $newChoices,
        'label'   =>'label',
    ))
    ->addModelTransformer(new FooToStringTransformer($em))
);

Then you get the error:

FormBuilder methods cannot be accessed anymore once the builder is turned
into a FormConfigInterface instance.

So then you try something like Florian suggested, i.e.

$event->getForm()->add('linkedFoo', 'choice', array(
    'choices' => $newChoices,
));
$event->getForm()->get('linkedFoo')->getConfig()->addModelTransformer(new FooToStringTransformer($em));

...but you get this error instead:

Notice: Object of class Proxies\__CG__\Company\ProjectBundle\Entity\Foo could not be converted to int 
in C:\path\to\vendor\symfony\symfony\src\Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList.php line 458

Which seems to suggest that the second line (which adds the ModelTransformer) is never called because the ->add() call is failing before you can get there.

like image 491
caponica Avatar asked Oct 15 '13 13:10

caponica


2 Answers

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"

like image 190
caponica Avatar answered Nov 08 '22 15:11

caponica


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.

like image 1
Florian Klein Avatar answered Nov 08 '22 14:11

Florian Klein