0

I'm using symfony 2.3, so apparently, I can't use the 'allow_extra_fields' option discussed here.

I have a main Form Type, RegistrationStep1UserType :

/**
 * Class RegistrationStep1UserType
 * @package Evo\DeclarationBundle\Form\Type
 */
class RegistrationStep1UserType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('customer', new RegistrationStep1CustomerType(), [
                'label' => false,
                'data_class' => 'Evo\UserBundle\Entity\Customer',
                'cascade_validation' => true,
            ])
            ->add('declaration', 'evo_declaration_bundle_registration_step1_declaration_type', [
                'label' => false,
                'cascade_validation' => true,
            ])
        ;
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Evo\UserBundle\Entity\User',
            'validation_groups' => false,
        ));
    }

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

This form type includes an embedded Form Type (on "declaration" field), RegistrationStep1DeclarationType, registered as a service :

/**
 * Class RegistrationStep1DeclarationType
 * @package Evo\DeclarationBundle\Form\Type
 */
class RegistrationStep1DeclarationType extends AbstractType
{
    /**
     * @var EntityManagerInterface
     */
    private $em;

    /**
     * @var EventSubscriberInterface
     */
    private $addBirthCountyFieldSubscriber;

    /**
     * RegistrationStep1DeclarationType constructor.
     * @param EntityManagerInterface $em
     * @param EventSubscriberInterface $addBirthCountyFieldSubscriber
     */
    public function __construct(EntityManagerInterface $em, EventSubscriberInterface $addBirthCountyFieldSubscriber)
    {
        $this->em = $em;
        $this->addBirthCountyFieldSubscriber = $addBirthCountyFieldSubscriber;
    }

    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('birthLastname', null, [
                'label' => 'Nom de naissance',
                'attr' => [
                    'required' => true,
                ],
            ])
            ->add('nationality', 'entity', [
                'label' => 'Nationalité',
                'class' => 'Evo\GeoBundle\Entity\Country',
                'property' => 'nationalityFr',
                'attr' => [
                    'required' => true,
                    'class' => 'selectpicker',
                ],
                'preferred_choices' => $this->fillPreferredNationalities(),
            ])
            ->add('birthCountry', 'entity', [
                'label' => 'Pays de naissance',
                'class' => 'Evo\GeoBundle\Entity\Country',
                'property' => 'nameFr',
                'empty_value' => '',
                'empty_data' => null,
                'attr' => [
                    'required' => true,
                    'class' => 'trigger-form-modification selectpicker',
                ],
                'preferred_choices' => $this->fillPreferredBirthCountries(),
            ])
            ->add('birthCity', null, [
                'label' => 'Ville de naissance',
                'attr' => [
                    'required' => true,
                ],
            ])
        ;

        $builder->get("birthCountry")->addEventSubscriber($this->addBirthCountyFieldSubscriber);
    }

    /**
     * @return array
     */
    private function fillPreferredNationalities()
    {
        $nationalities = $this->em->getRepository("EvoGeoBundle:Country")->findBy(["isDefault" => true]);

        return $nationalities;
    }

    /**
     * @return array|\Evo\GeoBundle\Entity\Country[]
     */
    private function fillPreferredBirthCountries()
    {
        $countries = $this->em->getRepository("EvoGeoBundle:Country")->findBy(["isDefault" => true]);

        return $countries;
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'required' => false,
            'data_class' => 'Evo\DeclarationBundle\Entity\Declaration',
            'validation_groups' => false,
        ));
    }

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

This embedded Form Type adds a Subscriber (registered as a service too, because it needs injection of EntityManager) on the "birthCountry" field.

The goal is to dynamically add or remove an extra field (called "birthCounty") depending on the value of the birthCountry choice list (note the 2 fields are different here, "birthCountry" and "birthCounty").

Here is the Subscriber :

/**
 * Class AddBirthCountyFieldSubscriber
 * @package Evo\CalculatorBundle\Form\EventListener
 */
class AddBirthCountyFieldSubscriber  implements EventSubscriberInterface
{
    /**
     * @var EntityManagerInterface
     */
    private $em;

    /**
     * AddBirthCountyFieldSubscriber constructor.
     * @param EntityManagerInterface $em
     */
    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
    }

    /**
     * @return array
     */
    public static function getSubscribedEvents()
    {
        return array(
            FormEvents::POST_SET_DATA => 'postSetData',
            FormEvents::POST_SUBMIT => 'postSubmit',
        );
    }

    /**
     * @param FormEvent $event
     */
    public function postSetData(FormEvent $event)
    {
        $birthCountry = $event->getData();
        $form = $event->getForm()->getParent();

        $this->removeConditionalFields($form);

        if ($birthCountry instanceof Country && true === $birthCountry->getIsDefault()) {
            $this->addBirthCountyField($form);
        }
    }

    /**
     * @param FormEvent $event
     */
    public function postSubmit(FormEvent $event)
    {
        $birthCountry = $event->getData();
        $form = $event->getForm()->getParent();

        if (!empty($birthCountry)) {
            $country = $this->em->getRepository("EvoGeoBundle:Country")->find($birthCountry);

            $this->removeConditionalFields($form);

            if ($country instanceof Country && true === $country->getIsDefault()) {
                $this->addBirthCountyField($form);
            }
        }
    }

    /**
     * @param FormInterface $form
     */
    private function addBirthCountyField(FormInterface $form)
    {
        $form
            ->add('birthCounty', 'evo_geo_bundle_autocomplete_county_type', [
                'label' => 'Département de naissance',
                'attr' => [
                    'required' => true,
                ],
            ])
        ;
    }

    /**
     * @param FormInterface $form
     */
    private function removeConditionalFields(FormInterface $form)
    {
        $form->remove('birthCounty');
    }
}

In the view, when the "birthCountry" choice list changes, it trigger an AJAX request to the controller, which handles the request and render the view again (as explained in the documentation about dynamic form submission) :

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

if ($request->isXmlHttpRequest()) {
    $form->handleRequest($request);
} elseif ("POST" == $request->getMethod()) {
    [...]
}

The problem is the following :

When I make a change on the birthCountry choice list and select a Country supposed to hide the "birthCounty" field, the form correctly render without that field, but it shows an error message :

Ce formulaire ne doit pas contenir des champs supplémentaires.

or

this form should not contain extra fields (in english)

I tried many different solutions :

  • adding a 'validation_groups' option to RegistrationStep1UserType and RegistrationStep1DeclarationType
  • adding a preSubmit event to AddBirthCountyFieldSubscriber replicating the logic of preSetData and postSubmit methods
  • even adding 'mapped' => false, to the birthCounty field triggers the error. very surprising

Even $form->getExtraData() is empty if I dump it just after $form->handleRequest($request);

But in vendor\symfony\symfony\src\Symfony\Component\Form\Extension\Validator\Constraints\FormValidator, I can see an extra field

array(1) {
  ["birthCounty"]=>
  string(0) ""
}

here :

// Mark the form with an error if it contains extra fields
    if (count($form->getExtraData()) > 0) {
        echo '<pre>';
        \Doctrine\Common\Util\Debug::dump($form->getExtraData());
        echo '</pre>';
        die();

        $this->context->addViolation(
            $config->getOption('extra_fields_message'),
            array('{{ extra_fields }}' => implode('", "', array_keys($form->getExtraData()))),
            $form->getExtraData()
        );
    }

Did I miss something about form dynamic extra fields ?

Community
  • 1
  • 1
VaN
  • 2,180
  • 4
  • 19
  • 43

1 Answers1

0

I did not analyzed all the question but, I guess, that you can invert the logic: always add that field and remove it when the condition is not satisfied.

That way you don't need to perform operations in postSubmit (that is where the issue is)

DonCallisto
  • 29,419
  • 9
  • 72
  • 100