Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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'));
            }
        });
    });
});
like image 960
Merche Avatar asked Mar 12 '17 20:03

Merche


People also ask

What is the formevent object in Symfony?

So, when the form submits, Symfony will call this function, but the $event object will only have information about the location field - not the entire form. Let's actually see this! Refresh to re-submit the form. There it is! The FormEvent contains the raw, submitted data - the solar_system string - and the entire Form object for this one field.

Where can I find the list of form events in Symfony?

You can view the full list of form events via the Symfony\Component\Form\FormEvents class. For better reusability or if there is some heavy logic in your event listener, you can also move the logic for creating the name field to an event subscriber:

How do I modify a Symfony form based on product data?

To do this, you can rely on Symfony’s EventDispatcher component system to analyze the data on the object and modify the form based on the Product object’s data. In this article, you’ll learn how to add this level of flexibility to your forms.

How do I pass a callback as a second argument in Symfony?

Pass a callback as a second argument: Symfony will pass that a FormEvent object. Let's dd () the $event so we can see what it looks like. But before we check it out, two important things. First, when you build a form, it's actually a big form tree. We've seen this inside of the form profiler.


Video Answer


1 Answers

I did not notice a field or a property called 'select_provincia' in neither your entity, nor the main form, so I will try guessing, that it probably should be called simply 'provincia', as that is the name for both the property in municipality entity and in the form subscriber for municipality. Also in AddMunicipioField.php you should change this code:

if ($provincia instanceof DbProvincia) {
   $qb->where('idMunicipio.provincia = :provincia')
      >setParameter('provincia', $provincia);
} 

to this:

if ($provincia instanceof DbProvincia) {
   $qb->where('idMunicipio.provincia = :provincia')
      >setParameter('provincia', $provincia->getId());
} 

since when querying you will be comparing provincia to the ID of province.

Further more, make sure you have implemented the __toString() method in the municipality entity, so that symfony would know how to convert that object to a string in order to show it in the select list.

Hope this helps :)


Seeing that you have added new information i will update my answer:

Firstly, In the AddMunicipioField.php you still have basically the same error: the array key is going to be called the same way you name your field, in this case not 'provincia_input', but 'provincia'. You can see the data that was posted to you by calling "dump($data); die;" just before you check if the array key exists (check for a key name "provincia", as you can see the name matches what you have specified when adding the field to the form (AddProvinciaField.php):

$form -> add('provincia', EntityType::class

Another thing I have noticed in the first js snippet you have posted is that in this part of code:

$(".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>');
            }
        }
    });
});

you are taking the input from $(".provincia-input") and sending it as a value for a field called "idMunicipio", which in you situation I don't think makes any sense.


Lastly, I will discus the errors that were made in the last piece of the JS you've posted:

  $(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'));
            }
        });
    });
  });

First of all, class names are not supposed to be used for identifying the fields that you are using. By definition they are supposed to be used multiple time in the document and describe only style, which might lead to some unexpected behaviour as your codebase grows. Please assign proper ID values to the inputs that you are going to be querying and especially replacing so that you could identify them correctly.

Secondly, please refer to the JS code posted in the official Symfony tutorial by following this link. As you can see the proper way to post data back to the server is not by sending a lone property like you are trying to do in this line:

var data = $('.provincia-input').serialize();

but rather by sending the property as a part of the forms data. So as in the tutorial I've posted, please first create an empty data object:

var data = {};

then add the province value to it:

data[$(this).attr('name')] = $(this).val();

Thirdly, this part of code is clearly incorrect:

success: function (data) {
        $('.municipio-input').replaceWith($(html).find('.municipio-input'));
}

As you can see the html variable is undefined in that part of code. This of course is because the variable that you are supposed to be using in this case is called data (the response that you have gotten from the server). So please change it to this:

success: function (data) {
        $('.municipio-input').replaceWith($(data).find('.municipio-input'));
}

Lastly, if you are still learning SF and web programming, I would like to suggest taking the bottom up approach to advance your programming knowledge instead, since this case is pretty complex and issues that prevented your code from working still require deeper understanding of the technologies you are using. I would personally suggest reading up on HTML attribute usage, Symfony form handling, read up on what data is available to you during each Symfony form event and maybe try using the dumper component of symfony more to debug your code, since var_dump is really a very inefficient way to debug SF code (would have solved many problems for you).

like image 113
grssn Avatar answered Oct 09 '22 23:10

grssn