Search code examples
phpsymfonyeventssymfony-formssymfony-3.1

Symfony3 Dynamically Modify Forms with events


Problem is that nothing is loaded in the municipality field, it goes undefined. In the AJAX code I get the value of the province well. But in the class addMunicipioField.php does not take the value of the $province, it is always nul

enter image description here

I am trying to make a registration form where part of the usual fields (name, nick, password, ...) I also add two dependent fields Municipality and Province.

The codec Controler:

class UserController extends Controller {

private $session;

public function __construct() {
    $this->session = new Session();
}

public function registerAction(Request $request) {

    if (is_object($this->getUser())) {
        return $this->redirect('home');
    }

    $user = new DbUsuario();

    $form = $this->createForm(RegistreUserType::class, $user);

    $form->handleRequest($request);
    if ($form->isSubmitted()) {
        if ($form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $query = $em->createQuery('SELECT u FROM BackendBundle:DbUsuario u WHERE u.email = :email OR u.nick = :nick')
                    ->setParameter('email', $form->get("email")->getData())
                    ->setParameter('nick', $form->get("nick")->getData());

            $user_isset = $query->getResult();

            if (count($user_isset) == 0) {
                $factory = $this->get("security.encoder_factory");
                $encoder = $factory->getEncoder($user);

                $password = $encoder->encodePassword($form->get("password")->getData(), $user->getSalt());

                $user->setPassword($password);
                $user->setRole("ROLE_USER");
                $user->setImagen(null);

                $em->persist($user);
                $flush = $em->flush();

                if ($flush == null) {
                    $status = "Te has registrado correctamente";
                    $this->session->getFlashBag()->add("status", $status);
                    return $this->redirect("login");
                } else {
                    $status = "No te has registrado correctamente";
                }
            } else {
                $status = "Usuario ya esta registrado.";
            }
        } else {
            $status = "No te has registrado correctamente.";
        }
        $this->session->getFlashBag()->add("status", $status);
    }
    return $this->render('AppBundle:User:register.html.twig', array(
                "form" => $form->createView() # Genera el html del formulario.
    ));
}

The Entity that creates the form is DbUsuario, which has the idMunicipio field.

/** @var \BackendBundle\Entity\DbMunicipios */
private $idMunicipio;

/**
 * Set idMunicipio
 * @param \BackendBundle\Entity\DbMunicipio $idMunicipio
 * @return DbUsuario
 */
public function setIdMunicipio(\BackendBundle\Entity\DbMunicipio $idMunicipio = null) {
    $this->idMunicipio = $idMunicipio;
    return $this;
}

/**
 * Get idMunicipio
 * @return \BackendBundle\Entity\DbMunicipio
 */
public function getIdMunicipio() {
    return $this->idMunicipio;
}

Then the Entity Of DbMunicipio that connects with 'province' with :

/** @var \BackendBundle\Entity\DbProvincia */
private $provincia;

/**@param \BackendBundle\Entity\DbProvincia $provincia
 * @return DbMunicipio
 */
public function setProvincia(\BackendBundle\Entity\DbProvincia $provincia = null){
    $this->provincia = $provincia;
    return $this;
}

// And implement this function.
public function __toString(){
    return $this->getMunicipio();
}

/**@return \BackendBundle\Entity\DbProvincia */
public function getProvincia(){
    return $this->provincia;
}

And the Entity DbProvincia that only has the fields (id (integer), slug (String) and province (String)).

I define the form as follows:

namespace AppBundle\Form;
use ....

class RegistreUserType extends AbstractType {

public function buildForm(FormBuilderInterface $builder, array $options) {
     $factory = $builder->getFormFactory(); 

    $builder->add('nombre', TextType::class, array('label' => 'Nombre',
        'required' => 'required',
        'attr' => array('class' => 'form-nombre form-control')
    ));
    $builder->add('apellido', TextType::class, array('label' => 'Apellido',
        'required' => 'required',
        'attr' => array('class' => 'form-apellido form-control')
    ));
    $builder->add('nick', TextType::class, array('label' => 'Nick',
        'required' => 'required',
        'attr' => array('class' => 'form-nick form-control nick-input')
    ));

    $provinSubscriber = new AddProvinciaField($factory);
    $builder->addEventSubscriber($provinSubscriber);

    $muniSubscriber = new AddMunicipioField($factory);
    $builder->addEventSubscriber($muniSubscriber);

    $builder->add('email', EmailType::class, array('label' => 'Correo electrónico',
        'required' => 'required',
        'attr' => array('class' => 'form-email form-control')
    ));
    $builder->add('password', PasswordType::class, array('label' => 'Password',
        'required' => 'required',
        'attr' => array('class' => 'form-password form-control')
    ));

    $builder->add('Registrarse', SubmitType::class, array("attr" => array("class" => "form-submit btn btn-success")));

}

public function configureOptions(OptionsResolver $resolver) {
    $resolver->setDefaults(array(
        'data_class' => 'BackendBundle\Entity\DbUsuario'
    ));
}

public function getBlockPrefix() { return 'backendbundle_dbusuario'; }
}

I define within the AppBundle \ Form \ eventListener \ AddProvinciaField the classes called in the form:

namespace AppBundle\Form\EventListener;

use ....
use BackendBundle\Entity\DbProvincia;

class AddProvinciaField implements EventSubscriberInterface {
     private $factory;

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

    public static function getSubscribedEvents() {
        return array(
            FormEvents::PRE_SET_DATA => 'preSetData',
            FormEvents::PRE_SUBMIT     => 'preSubmit'
        );
    }

    private function addProvinciaForm($form, $provincia) {

       $form -> add('provincia', EntityType::class, array(
            'class'         => 'BackendBundle:DbProvincia',
            'label'         => 'Provincia',
            'placeholder'   => '_ Elegir _',
            'auto_initialize' => false,
            'mapped'        => false,
            'attr'=> array('class' => 'form-provincia form-control provincia-input'),
            'query_builder' => function (EntityRepository $repository) {
                $qb = $repository->createQueryBuilder('provincia');
                return $qb;
            }
        ));
    }

    public function preSetData(FormEvent $event){
        $data = $event->getData();
        $form = $event->getForm();

        if (null === $data) {return;}

        $provincia = ($data->getIdMunicipio()) ? $data->getIdMunicipio()->getProvincia() : null ;
        $this->addProvinciaForm($form, $provincia);
    }

    public function preSubmit(FormEvent $event) {
        $data = $event->getData();
        $form = $event->getForm();

        if (null === $data) { return;}

        $provincia = array_key_exists('provincia-input', $data) ? $data['provincia-input'] : null;
        $this->addProvinciaForm($form, $provincia);
    }  
}

And later I define AddMunicipioField.php:

namespace AppBundle\Form\EventListener;

 use ....
 use BackendBundle\Entity\DbProvincia;


 class AddMunicipioField implements EventSubscriberInterface {
    private $factory;

    public function _construct(FormFactoryInterface $factory) {
        $this->factory = $factory;
    }

    public static function getSubscribedEvents() {
        return array(
            FormEvents::PRE_SET_DATA => 'preSetData',
            FormEvents::PRE_SUBMIT     => 'preSubmit'
        );
    }

    private function addMunicipioForm($form, $provincia) {
        $form->add('idMunicipio', EntityType::class, array(
            'class'         => 'BackendBundle:DbMunicipio',
            'label'         => 'Municipio',
            'placeholder'   => '_ Elegir _',
            'auto_initialize' => false,
            'attr'=> array('class' => 'form-municipio form-control municipio-input'),
            'query_builder' => function (EntityRepository $repository) use ($provincia) {
            $qb = $repository->createQueryBuilder('idMunicipio')
                ->innerJoin('idMunicipio.provincia', 'provincia');
            if ($provincia instanceof DbProvincia) {
                $qb->where('idMunicipio.provincia = :provincia')
                ->setParameter('provincia', $provincia);
            
            } elseif (is_numeric($provincia)) {
                $qb->where('provincia.id = :provincia')
                ->setParameter('provincia', $provincia);
          
            } else {
                $qb->where('provincia.provincia = :provincia')
                ->setParameter('provincia', null);
          
            }
            return $qb;
        }
    ));
}

public function preSetData(FormEvent $event){
    $data = $event->getData();
    $form = $event->getForm();

    if (null === $data) { return; }

    $provincia = ($data->getIdMunicipio()) ? $data->getIdMunicipio()->getProvincia() : null ;
    $this->addMunicipioForm($form, $provincia);
}

public function preSubmit(FormEvent $event){
    $data = $event->getData();
    $form = $event->getForm();

    if (null === $data) { return; }

    $provincia = array_key_exists('provincia_input', $data) ? $data['provincia_input'] : null;
    $this->addMunicipioForm($form, $provincia);
  }
 }

And finally the AJAX request:

$(document).ready(function(){
    var $form = $(this).closest('form');
    $(".provincia-input").change(function(){
        var data = { idMunicipio: $(this).val() };
        $.ajax({
            type: 'POST',
            url: $form.attr('action'),
            data: data,
            success: function(data) {
                for (var i=0, total = data.length; i < total; i++) {
                    $('.municipio-input').append('<option value="' + data[i].id + '">' + data[i].municipio + '</option>');
                }
            }
        });
    });
});

I added var_dump and alert() in the code. This is the way output.

In this case, the value of province is always null.

 addMunicipioField.php
 public function preSetData(FormEvent $event){
    $data = $event->getData();
    $form = $event->getForm();

    if (null === $data) {
        return;
    }

    $provincia = ($data->getIdMunicipio()) ? $data->getIdMunicipio()->getProvincia() : null ;
    var_dump('presetdata');
    var_dump($provincia);
    $this->addMunicipioForm($form, $provincia);
}

AJAX:

$(document).ready(function(){
    var $form = $(this).closest('form');
    $(".provincia-input").change(function(){
        alert($('.provincia-input').val()); // THIS IS CORRECT VALUE, INTEGER.
        var data = { idMunicipio: $(this).val() };
        $.ajax({
            type: 'POST',
            url: $form.attr('action'),
            data: data,
            success: function(data) {
                alert(data);
                alert(data.length); // THIS IS INCORRECT.
                for (var i=0, total = data.length; i < total; i++) {
                    $('.municipio-input').append('<option value="' + data[i].id + '">' + data[i].municipio + '</option>');
                }
            }
        });
    });
});

enter image description here

Another point of view The entities are the same. In this case it works but I must press the send button. How could I do it without pressing the button, that it was automatic change?

The class RegistreUserType extends AbstractType I add the following lines.

$builder -> add('provincia', EntityType::class, array(
        'class'         => 'BackendBundle:DbProvincia',
        'label'         => 'Provincia',
        'placeholder'   => '_ Elegir _',
        'auto_initialize' => false,
        'mapped'        => false,
        'attr'=> array('class' => 'form-provincia form-control provincia-input'),
        'query_builder' => function (EntityRepository $repository) {
            $qb = $repository->createQueryBuilder('provincia');
            return $qb;
        }
    ));
 
    $builder->add('idMunicipio', EntityType::class, array(
        'class' => 'BackendBundle:DbMunicipio',
        'label'         => 'Municipio',
        'placeholder'   => '_ Elegir _',
        'auto_initialize' => false,
        'mapped'        => false,
        'attr'=> array('class' => 'form-municipio form-control municipio-input')
    ));
    
    $builder->addEventSubscriber(new AddMunicipioField());

The new class AddMunicpioField():

class AddMunicipioField implements EventSubscriberInterface {

public static function getSubscribedEvents() {
    return array(
        FormEvents::PRE_SUBMIT => 'preSubmit',
        FormEvents::PRE_SET_DATA => 'preSetData',
    );
}

public function preSubmit(FormEvent $event){
    $data = $event->getData();
    $this->addField($event->getForm(), $data['provincia']);
}

protected function addField(Form $form, $provincia){
    $form->add('idMunicipio', EntityType::class, array(
        'class'         => 'BackendBundle:DbMunicipio',
        'label'         => 'Municipio',
        'placeholder'   => '_ Elegir _',
        'auto_initialize' => false,
        'mapped'        => false,
        'attr'=> array('class' => 'form-municipio form-control municipio-input'),
        'query_builder' => function(EntityRepository $er) use ($provincia){
            $qb = $er->createQueryBuilder('idMunicipio')
                    ->where('idMunicipio.provincia = :provincia')
                    ->setParameter('provincia', $provincia);
            
            return $qb;
        }
    ));
 }

Codec Ajax:

$(document).ready(function () {
$('.provincia-input').change(function () {       
    var $form = $(this).closest('form');
    var data = $('.provincia-input').serialize();
    $.ajax({
        url: $form.attr('action'),
        type: 'POST',
        data: data,
        success: function (data) {
            $('.municipio-input').replaceWith($(html).find('.municipio-input'));
            }
        });
    });
});

Solution

  • Solved!!

    In my form I added the call to the two new classes:

    $builder -> addEventSubscriber(new AddMunicipioFieldSubscriber('idMunicipio'));
    $builder -> addEventSubscriber(new AddProvinceFieldSubscriber('idMunicipio'));
    

    The firth select is province, this is the class:

    class AddProvinceFieldSubscriber implements EventSubscriberInterface {
    private $propertyPathToMunicipio;
    
    public function __construct($propertyPathToMunicipio) {
        $this->propertyPathToMunicipio = $propertyPathToMunicipio;
    }
    
    public static function getSubscribedEvents() {
        return array(
            FormEvents::PRE_SET_DATA => 'preSetData',
            FormEvents::PRE_SUBMIT   => 'preSubmit'
        );
    }
    
    private function addProvinceForm($form, $Province = null) {
        $formOptions = array(
            'class'         => 'BackendBundle:DbProvincia',
            'mapped'        => false,
            'label'         => 'Provincia',
            'attr'          => array(
                'class' => 'class_select_provincia',
            ),
        );
    
        if ($Province) {
            $formOptions['data'] = $Province;
        }
    
        $form->add('provincia', EntityType::class, $formOptions);
    }
    
    public function preSetData(FormEvent $event){
        $data = $event->getData();
        $form = $event->getForm();
    
        if (null === $data) {
            return;
        }
    
        $accessor = PropertyAccess::createPropertyAccessor();
    
        $municipio    = $accessor->getValue($data, $this->propertyPathToMunicipio);
        $provincia = ($municipio) ? $municipio->getIdMunicipio()->getProvincia() : null;
    
        $this->addProvinceForm($form, $provincia);
    }
    
    public function preSubmit(FormEvent $event){
        $form = $event->getForm();
    
        $this->addProvinceForm($form);
    }
    }
    

    The second class is Municipi:

    class AddMunicipioFieldSubscriber implements EventSubscriberInterface {
    //put your code here
    
    private $propertyPathToMunicipio;
    
    public function __construct($propertyPathToMunicipio){
        $this->propertyPathToMunicipio = $propertyPathToMunicipio;
    }
    
    public static function getSubscribedEvents(){
        return array(
            FormEvents::PRE_SET_DATA  => 'preSetData',
            FormEvents::PRE_SUBMIT    => 'preSubmit'
        );
    }
    
    private function addCityForm($form, $province_id){
        $formOptions = array(
            'class'         => 'BackendBundle:DbMunicipio',
            'label'         => 'Municipio',
            'attr'          => array(
                'class' => 'class_select_municipio',
            ),
            'query_builder' => function (EntityRepository $repository) use ($province_id) {
                $qb = $repository->createQueryBuilder('municipio')
                    ->innerJoin('municipio.provincia', 'provincia')
                    ->where('provincia.id = :provincia')
                    ->setParameter('provincia', $province_id)
                ;
    
                return $qb;
            }
        );
    
        $form->add($this->propertyPathToMunicipio, EntityType::class, $formOptions);
    }
    
    public function preSetData(FormEvent $event){
        $data = $event->getData();
        $form = $event->getForm();
    
        if (null === $data) {
            return;
        }
    
        $accessor    = PropertyAccess::createPropertyAccessor();
    
        $municipio        = $accessor->getValue($data, $this->propertyPathToMunicipio);
        $province_id = ($municipio) ? $municipio->getIdMunicipio()->getProvincia()->getId() : null;
    
        $this->addCityForm($form, $province_id);
    }
    
    public function preSubmit(FormEvent $event){
        $data = $event->getData();
        $form = $event->getForm();
    
        $province_id = array_key_exists('provincia', $data) ? $data['provincia'] : null;
    
        $this->addCityForm($form, $province_id);
    } 
    }
    

    The controled add this function:

        public function municipioTestAction(Request $request){
        $provincia_id = $request->get('provincia_id');
    
        $em = $this->getDoctrine()->getManager();
        $provincia = $em->getRepository('BackendBundle:DbMunicipio')->findByProvinceId($provincia_id);
    
        return new JsonResponse($provincia);
    }
    

    Where the function findByProvinceId, I create it as a repository of the entity DbMunicipio.

    class DbMunicipioRepository extends EntityRepository{
    
    public function findByProvinceId($provincia_id){
    
        $query = $this->getEntityManager()->createQuery("
            SELECT muni
            FROM BackendBundle:DbMunicipio muni
            LEFT JOIN muni.provincia provin
            WHERE provin.id = :provincia_id
        ")->setParameter('provincia_id', $provincia_id);
    
        return $query->getArrayResult();
    } 
    }
    

    And de codec AJAX.

    $(document).ready(function () {
    $(".class_select_provincia").change(function(){
        var data = {
            provincia_id: $(this).val()
        };
    
        $.ajax({
            type: 'POST',
            url: URL+'/municipio-test',
            data: data,
            success: function(data) {
    
                var $muni_selector = $('.class_select_municipio');
                alert(data);
                $muni_selector.html('<option>Ciudad</option>');
    
                for (var i=0, total = data.length; i < total; i++) {
                    $muni_selector.append('<option value="' + data[i].id + '">' + data[i].municipio + '</option>');
                }
            }
        });
    });
    });