本文概述
使用Symfony Forms非常简单, 功能强大且功能强大。它们已经解决了许多你理论上可以忽略的问题, 因为你只需要注意构建它们。对于像我这样的懒人来说, 还有一些很棒的事情, 那就是从实体生成CRUD表单(创建, 更新和删除)。这将使用控制器构建整个模块, 并正确查看准备使用的模块。尽管自动构建解决了很多问题, 但可惜的是, Symfony默认不存在自动依赖选择功能, 这意味着你必须自己实现。
根据数据库的设计, 你将需要表单中的从属选择, 以仅向用户显示与同一表单中的另一个选择相关的行。现实生活中(最容易理解的)最典型的例子是人, 城市和邻里关系。考虑以下实体, 第一个是Person.php:
注意
我们将跳过该类中的getter和setter来使文章简短, 但是显然它们必须存在。
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Person
*
* @ORM\Table(name="person", indexes={@ORM\Index(name="city_id", columns={"city_id"}), @ORM\Index(name="neighborhood_id", columns={"neighborhood_id"})})
* @ORM\Entity
*/
class Person
{
/**
* @var integer
*
* @ORM\Column(name="id", type="bigint")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=255, nullable=false)
*/
private $name;
/**
* @var string
*
* @ORM\Column(name="last_name", type="string", length=255, nullable=false)
*/
private $lastName;
/**
* @var \AppBundle\Entity\City
*
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\City")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="city_id", referencedColumnName="id")
* })
*/
private $city;
/**
* @var \AppBundle\Entity\Neighborhood
*
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\Neighborhood")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="neighborhood_id", referencedColumnName="id")
* })
*/
private $neighborhood;
}
可以在系统中注册的每个人都需要从我们的数据库中居住在城市中, 并且还必须居住在数据库的特定邻域中, 因此City.php实体:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* City
*
* @ORM\Table(name="city")
* @ORM\Entity
*/
class City
{
/**
* @var integer
*
* @ORM\Column(name="id", type="bigint")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=255, nullable=false)
*/
private $name;
}
还有Neighborhood.php实体:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Neighborhood
*
* @ORM\Table(name="neighborhood", indexes={@ORM\Index(name="city_id", columns={"city_id"})})
* @ORM\Entity
*/
class Neighborhood
{
/**
* @var integer
*
* @ORM\Column(name="id", type="bigint")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=255, nullable=false)
*/
private $name;
/**
* @var \AppBundle\Entity\City
*
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\City")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="city_id", referencedColumnName="id")
* })
*/
private $city;
}
实体是完全有效的, 在本教程中我们根本不需要修改它们, 但是它们对于了解我们将要做的事情很有用。如果你是根据这些实体自动生成的表单, 则在”人员表单”的”邻域”字段中, 将找到所有邻域, 而不会仅过滤属于所选城市的邻域:
这就是为什么我们需要实现一个依赖选择的原因, 因此, 当用户选择例如旧金山作为他的城市时, 在邻居选择中, 他应该只找到属于旧金山的两个邻居(金银岛和旧金山的普雷西迪奥)。在FormType中过滤查询很容易, 但是这也应该使用JavaScript进行动态分析, 因此可以通过以下步骤轻松实现:
1.正确配置FormType
创建选择依赖项的逻辑如下:首先, 邻居(依赖项)的选择为空, 直到用户选择城市为止, 使用所选城市的ID, 你应该将新选项加载到邻居选择中。但是, 如果你正在编辑”个人”形式, 则选定的邻域应自动显示为选中状态, 而无需在编辑视图中使用JavaScript。这就是为什么你需要修改表单的FormType(在这种情况下为PersonType)的原因。首先, 你需要将2个事件侦听器附加到触发表单的事件PRE_SET_DATA和PRE_SUBMIT时执行的表单。在事件内部, 你将验证表单中是否存在选定的城市, 如果存在, 则将其作为参数发送到addElements方法。
addElements方法期望City实体(或null)作为第二个参数来决定将在”邻域选择”中呈现哪些数据:
注意
FormType需要在构造函数中接收实体管理器, 因为你需要在内部进行一些查询。取决于你的Symfony版本, 这是通过自动装配自动完成的, 如果不是自动完成的, 则可能需要将其作为传递参数的参数传递给构造该类的控制器中的构造函数。
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
// 1. Include Required Namespaces
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityManagerInterface;
// Your Entity
use AppBundle\Entity\City;
class PersonType extends AbstractType
{
private $em;
/**
* The Type requires the EntityManager as argument in the constructor. It is autowired
* in Symfony 3.
*
* @param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// 2. Remove the dependent select from the original buildForm as this will be
// dinamically added later and the trigger as well
$builder->add('name')
->add('lastName');
// 3. Add 2 event listeners for the form
$builder->addEventListener(FormEvents::PRE_SET_DATA, array($this, 'onPreSetData'));
$builder->addEventListener(FormEvents::PRE_SUBMIT, array($this, 'onPreSubmit'));
}
protected function addElements(FormInterface $form, City $city = null) {
// 4. Add the province element
$form->add('city', EntityType::class, array(
'required' => true, 'data' => $city, 'placeholder' => 'Select a City...', 'class' => 'AppBundle:City'
));
// Neighborhoods empty, unless there is a selected City (Edit View)
$neighborhoods = array();
// If there is a city stored in the Person entity, load the neighborhoods of it
if ($city) {
// Fetch Neighborhoods of the City if there's a selected city
$repoNeighborhood = $this->em->getRepository('AppBundle:Neighborhood');
$neighborhoods = $repoNeighborhood->createQueryBuilder("q")
->where("q.city = :cityid")
->setParameter("cityid", $city->getId())
->getQuery()
->getResult();
}
// Add the Neighborhoods field with the properly data
$form->add('neighborhood', EntityType::class, array(
'required' => true, 'placeholder' => 'Select a City first ...', 'class' => 'AppBundle:Neighborhood', 'choices' => $neighborhoods
));
}
function onPreSubmit(FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
// Search for selected City and convert it into an Entity
$city = $this->em->getRepository('AppBundle:City')->find($data['city']);
$this->addElements($form, $city);
}
function onPreSetData(FormEvent $event) {
$person = $event->getData();
$form = $event->getForm();
// When you create a new person, the City is always empty
$city = $person->getCity() ? $person->getCity() : null;
$this->addElements($form, $city);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Person'
));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'appbundle_person';
}
}
仅通过正确配置FormType, 如果你尝试创建一个新的Person, Neighborhood字段将为空, 你将无法保存任何内容, 并且如果你尝试编辑一个Person, 你将看到仅Neighborhood字段加载与所选城市相关的社区。
2.创建一个端点, 以动态获取视图中城市的邻域
下一步, 你需要创建一个可访问Ajax的端点, 该端点将返回城市的邻域(城市的ID通过get参数(即cityid)发送), 因此你可以随意在任意位置和方式创建它, 在此示例中, 我们决定将其编写在同一个人控制器中:
<?php
namespace AppBundle\Controller;
use AppBundle\Entity\Person;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
// Include JSON Response
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* Person controller.
*
*/
class PersonController extends Controller
{
// Rest of your original controller
/**
* Returns a JSON string with the neighborhoods of the City with the providen id.
*
* @param Request $request
* @return JsonResponse
*/
public function listNeighborhoodsOfCityAction(Request $request)
{
// Get Entity manager and repository
$em = $this->getDoctrine()->getManager();
$neighborhoodsRepository = $em->getRepository("AppBundle:Neighborhood");
// Search the neighborhoods that belongs to the city with the given id as GET parameter "cityid"
$neighborhoods = $neighborhoodsRepository->createQueryBuilder("q")
->where("q.city = :cityid")
->setParameter("cityid", $request->query->get("cityid"))
->getQuery()
->getResult();
// Serialize into an array the data that we need, in this case only name and id
// Note: you can use a serializer as well, for explanation purposes, we'll do it manually
$responseArray = array();
foreach($neighborhoods as $neighborhood){
$responseArray[] = array(
"id" => $neighborhood->getId(), "name" => $neighborhood->getName()
);
}
// Return array with structure of the neighborhoods of the providen city id
return new JsonResponse($responseArray);
// e.g
// [{"id":"3", "name":"Treasure Island"}, {"id":"4", "name":"Presidio of San Francisco"}]
}
}
在此项目中, 我们的路线是通过yml文件(routing.yml)定义的, 路线如下所示:
# AppBundle/Resources/config/routing/person.yml
person_list_neighborhoods:
path: /get-neighborhoods-from-city
defaults: { _controller: "AppBundle:Person:listNeighborhoodsOfCity" }
methods: GET
端点可用后, 你可以通过访问路由来手动对其进行测试。重要的是, 控制器需要返回一个JSON响应, 其中包含包含属于所需城市的社区的数组。
3.编写JavaScript来处理城市变化
最后一步, 我们需要确保当用户更改城市时, 将使用先前创建的控制器的数据来更新社区。为此, 你将需要编写自己的JavaScript并向先前创建的端点发出AJAX请求。这部分完全取决于你使用的JS框架或你喜欢使用JavaScript的方式。为了使我们的示例具有通用性, 我们将使用jQuery。
逻辑需要放置在两个表单视图中(新建和编辑), 例如在我们的new.html.twig中, 代码将为:
{# views/new.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<h1>Person creation</h1>
{{ form_start(form) }}
{{ form_widget(form) }}
<input type="submit" value="Create" />
{{ form_end(form) }}
<ul>
<li>
<a href="{{ path('person_index') }}">Back to the list</a>
</li>
</ul>
{% endblock %}
{% block javascripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script>
$('#appbundle_person_city').change(function () {
var citySelector = $(this);
// Request the neighborhoods of the selected city.
$.ajax({
url: "{{ path('person_list_neighborhoods') }}", type: "GET", dataType: "JSON", data: {
cityid: citySelector.val()
}, success: function (neighborhoods) {
var neighborhoodSelect = $("#appbundle_person_neighborhood");
// Remove current options
neighborhoodSelect.html('');
// Empty value ...
neighborhoodSelect.append('<option value> Select a neighborhood of ' + citySelector.find("option:selected").text() + ' ...</option>');
$.each(neighborhoods, function (key, neighborhood) {
neighborhoodSelect.append('<option value="' + neighborhood.id + '">' + neighborhood.name + '</option>');
});
}, error: function (err) {
alert("An error ocurred while loading data ...");
}
});
});
</script>
{% endblock %}
如果一切都正确实现, 则当用户尝试使用该表单创建新的注册表时, 选择更改时将加载所选城市的社区。同样, 由于Symfony的表单事件, 当用户编辑表单时, 社区将自动加载到服务器端的字段上:
编码愉快!
评论前必须登录!
注册