Symfony3 使用事件动态修改表单

Symfony3 Dynamically Modify Forms with events

问题是市政当局字段中没有加载任何内容,它未定义。在 AJAX 代码中我很好地获取了省份的值。但是在classaddMunicipioField.php不取$province的值,总是nul

我正在尝试制作一个注册表单,其中部分常用字段 (name, nick, password, ...) 我还添加了两个相关字段 MunicipalityProvince.

编解码控制器:

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.
    ));
}

创建表单的 EntityDbUsuario,它有 idMunicipio 字段。

/** @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;
}

然后 Entity of DbMunicipio 连接 'province' 和 :

/** @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;
}

Entity DbProvincia 只有字段 (id (integer), slug (String) and province (String)).

我定义的形式如下:

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'; }
}

我在 AppBundle\Form\eventListener\AddProvinciaField 中定义了 classes 调用的形式:

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);
    }  
}

然后我定义 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);
  }
 }

最后 AJAX 请求:

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

我在代码中添加了var_dumpalert()。这是输出的方式。

在这种情况下,省的值始终为空

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

另一种观点 实体是一样的。 在这种情况下它可以工作,但我必须按下发送按钮。怎么能不按按钮呢,那是自动换的?

class RegistreUserType extends AbstractType 我添加下面几行。

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

新增 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;
        }
    ));
 }

编解码器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'));
            }
        });
    });
});

我没有在您的实体或主窗体中注意到一个名为 'select_provincia' 的字段或 属性,所以我会尝试猜测,它可能应该简单地称为 'provincia',因为这是市政实体中 属性 和市政当局订户形式的名称。同样在 AddMunicipioField.php 中,您应该更改此代码:

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

对此:

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

因为在查询时您会将省份与省份 ID 进行比较。

此外,确保你已经在市政实体中实现了 __toString() 方法,这样 symfony 就会知道如何将该对象转换为字符串以便在 select列表。

希望这对您有所帮助:)


看到你补充了新的信息我会更新我的回答:

首先,在AddMunicipioField.php你仍然有基本相同的错误: 数组键的调用方式与您命名字段的方式相同,在本例中不是 'provincia_input',而是 'provincia'。在检查数组键是否存在之前,您可以通过调用 "dump($data); die;" 查看 posted 给您的数据(检查键名 "provincia",因为您可以看到名称匹配将字段添加到表单时指定的内容 (AddProvinciaField.php):

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

我在您 posted 的第一个 js 片段中注意到的另一件事是,在这部分代码中:

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

您正在从 $(".provincia-input") 获取输入并将其作为值发送给名为 "idMunicipio" 的字段,我认为在您的情况下这没有任何意义。


最后,我将讨论您 post 编辑的最后一段 JS 中出现的错误:

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

首先,class 名称不应该用于识别您正在使用的字段。根据定义,它们应该在文档中多次使用并且仅描述样式,这可能会随着代码库的增长而导致一些意外行为。请为您将要查询尤其是替换的输入分配正确的 ID 值,以便您可以正确识别它们。

其次,请参考postSymfony官方教程中的JS代码link。如您所见,将 post 数据返回服务器的正确方法不是像您在这一行中尝试做的那样发送单独的 属性:

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

而是通过发送 属性 作为表单数据的一部分。因此,在我 post 编辑的教程中,请先创建一个空数据对象:

var data = {};

然后加上省份值:

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

第三,这部分代码明显不正确:

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

如您所见,html 变量在那部分代码中未定义。这当然是因为在这种情况下您应该使用的变量称为数据(您从服务器获得的响应)。所以请将其更改为:

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

最后,如果您仍在学习 SF 和 Web 编程,我建议您采用自下而上的方法来提高您的编程知识,因为这种情况非常复杂,并且阻止您的代码工作的问题仍然需要更深入地了解您正在使用的技术。我个人建议阅读 HTML 属性用法,Symfony 表单处理,阅读每个 Symfony 表单事件期间可用的数据,并尝试使用 symfony 的转储器组件来调试代码,因为 var_dump 确实是一种非常低效的调试 SF 代码的方法(会为你解决很多问题)。

解决了!!

在我的表单中,我添加了对两个新 classes 的调用:

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

第一个select是省,这是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);
}
}

第二个class是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);
} 
}

控件添加此功能:

    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);
}

在函数 findByProvinceId 中,我将其创建为实体 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();
} 
}

和解码器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>');
            }
        }
    });
});
});