Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony2 "This form should not contain extra fields" error with collections & dynamic field

So, going into day 5 of troubleshooting this one and still can't work it out. I've even created a special form just for troubleshooting this, still haven't been able to make any headway. Basic issue: I have a Symfony2 form built via FormBuilderInterface using Form Modifiers to dynamically populate a field based on user selection of the first entity in the form. The form also has 3 collections in it (which represent One-To-One associations with the form's main class).

No matter what I do, on if ($form->isValid()) I get the all-too-familiar and fun "ERROR: This form should not contain extra fields." error on form submission (3 times, 1 for each collection).

Notable and probably related to the fix: If I remove the collection entities from the form, it validates correctly. With any of the collections in the form, or any combination of 2 of the 3 collections (I've tried all), it throws the "This form should not contain extra fields" error.

Here is the code for the form type:

class ThisDataClassType extends AbstractType
{

protected $user_id;

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

/**
 * @param FormBuilderInterface $builder
 * @param array $options
 */
public function buildForm(FormBuilderInterface $builder, array $options)
{

    $user_id = $this->user_id;

    $builder->add('firstChoice', 'entity', array(
            'class' => 'MyBundle:FirstChoice',
            'query_builder' => function(firstChoiceRepository $repository) use ($user_id) {
                    return $repository->createQueryBuilder('f')
                        ->where('f.user = ?1')
                        ->andWhere('f.isActive = 1')
                        ->setParameter(1, $user_id);
                },
            'property' => 'firstChoice_name',
            'required' => true,
            'mapped' => false,
            'label' => 'block.firstChoice.name'
        ))
        ->add('sub_block_name','text', array(
            'label' => 'subblock.block.name',
            'max_length' => 50,
            'required' => true,
            'attr' => array(
                'placeholder' => 'subblock.phv.name',
                'pattern' => 'alpha_numeric'
            )
        ))

        // ... bunch of other standard form types (text, etc.) ... //

        ->add('subdata1', 'collection', array(
            'type' => new SubData1Type()
        ))
        ->add('subdata2', 'collection', array(
            'type' => new SubData2Type()
        ))
        ->add('subdata3', 'collection', array(
            'type' => new SubData3Type()
        ));

    $formModifier = function(FormInterface $form, $firstChoice_id) {

        $form->add('secondChoice', 'entity', array(
            'class'         => 'MyBundle:SecondChoice',
            'query_builder' => function(secondChoiceRepository $S_repository) use ($firstChoice_id) {
            return $S_repository->createQueryBuilder('s')
                ->where('s.firstChoice_id = ?1')
                ->andWhere('s.isActive = 1')
                ->setParameter(1, $firstChoice_id);
            },
            'property'    => 'secondChoice_name',
            'label'       => 'block.secondChoice.name'
        ));
    };

    $builder->addEventListener(
        FormEvents::PRE_SET_DATA,
        function(FormEvent $event) use ($formModifier) {
            $data = $event->getData()->getId();
            $formModifier($event->getForm(), $data);
        }
    );

    $builder->get('firstChoice')->addEventListener(
        FormEvents::POST_SUBMIT,
        function(FormEvent $event) use ($formModifier) {
            $data = $event->getForm()->getData();
            $formModifier($event->getForm()->getParent(), $data->getId());
        }
    );

    $builder->setMethod('POST');
}

/**
 * @param OptionsResolverInterface $resolver
 */
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'MyBundle\Entity\ThisDataClass',
        'user_id' => null,
        'translation_domain' => 'block',
        'cascade_validation' => true
    ));
}

/**
 * @return string
 */
public function getName()
{
    return 'ThisDataForm';
}
}

...and the basic controller (just used for Testing at the moment):

public function TestFormAction(Request $request, $whichSize)
{
    $user_id = $this->getUser()->getId();
    $success = 'Not submitted';

    $dataclass = new Entity\ThisDataClass();
    $subdata1 = new Entity\SubData1();
    $subdata2 = new Entity\SubData2();
    $subdata3 = new Entity\SubData3();

    if (!$request->isMethod('POST')) {
        $dataclass->getSubData1()->add($subdata1);
        $dataclass->getSubData2()->add($subdata2);
        $dataclass->getSubData3()->add($subdata3);
    }

    $form = $this->createForm(new Form\ThisDataClassType($user_id), $dataclass, array(
        'action' => $this->generateUrl('my_custom_test_route', array('whichSize' => $whichSize)),
        'user_id' => $user_id
    ));

    $form->handleRequest($request);

    if ($form->isValid()) {
        $success = 'Success!!';
        return $this->render('MyBundle:DataClassTestForm.html.twig', array('dataClassTestForm' => $form->createView(), 'whichSize' => $whichSize, 'success' => $success));

    } else {
        $success = $form->getErrorsAsString();
    }

        return $this->render('MyBundle:DataClassTestForm.html.twig', array('dataClassTestForm' => $form->createView(), 'whichSize' => $whichSize, 'success' => $success));

}

To mention a couple of other important points:

  • the form displays correctly (all entities show correct values and update as desired)
  • all of the expected POST variables are in the post data (checked via Firebug and Symfony's Profiler)
  • there are no extra variables in the post data which are not mapped to the classes (except for the firstChoice field which has 'mapped' => false set, and the CSRF _token, but I get the error whether I explicitly enable the CSRF _token or not, and again the error disappears when I remove the collections)

Greatly appreciate anyone's insight on what it is I'm missing.

like image 689
Chris_Alexander Avatar asked Mar 29 '14 14:03

Chris_Alexander


1 Answers

After taking a day and a half off and getting away, the answer popped when I sat down and is quite obvious. The controller as written only adds the subdata classes to the form when it is first created (ie when no submit/POST event has occurred). Hence the form created AFTER the submit event has all of the post data, but the classes are not associated with it. The controller has now been rewritten as below and the form now validates as expected:

public function TestFormAction(Request $request, $whichSize)
{
    $user_id = $this->getUser()->getId();
    $success = 'Not submitted';

    $dataclass = new Entity\ThisDataClass();
    $subdata1 = new Entity\SubData1();
    $subdata2 = new Entity\SubData2();
    $subdata3 = new Entity\SubData3();
    $dataclass->getSubData1()->add($subdata1);
    $dataclass->getSubData2()->add($subdata2);
    $dataclass->getSubData3()->add($subdata3);

    $form = $this->createForm(new Form\ThisDataClassType($user_id), $dataclass, array(
        'action' => $this->generateUrl('my_custom_test_route', array('whichSize' => $whichSize)),
        'user_id' => $user_id
    ));

    $form->handleRequest($request);

    if ($form->isValid()) {
        $success = 'Success!!';
    } else {
        $success = 'Invalid!!';
    }

} // RESULT::Success!!
like image 189
Chris_Alexander Avatar answered Oct 07 '22 08:10

Chris_Alexander