本文目录导读:

- 创建表单类型
- 控制器处理 AJAX 请求
- 前端模板和 JavaScript
- 使用 Fetch API 发送 JSON 格式
- 使用 jQuery 的替代方案
- 配置 CSRF 保护
- 可选:创建独立的 AJAX 端点
- 注意事项
在 Symfony 项目中使用 AJAX 提交表单是一个常见的需求,以下是完整的实现方案:
创建表单类型
// src/Form/ContactType.php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
class ContactType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class, [
'label' => '姓名',
'attr' => ['class' => 'form-control']
])
->add('email', EmailType::class, [
'label' => '邮箱',
'attr' => ['class' => 'form-control']
])
->add('message', TextareaType::class, [
'label' => '消息',
'attr' => ['class' => 'form-control', 'rows' => 5]
])
->add('submit', SubmitType::class, [
'label' => '提交',
'attr' => ['class' => 'btn btn-primary']
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_token_id' => 'contact_item',
]);
}
}
控制器处理 AJAX 请求
// src/Controller/ContactController.php
namespace App\Controller;
use App\Form\ContactType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Form\FormInterface;
class ContactController extends AbstractController
{
#[Route('/contact', name: 'contact', methods: ['GET', 'POST'])]
public function contact(Request $request): JsonResponse
{
$form = $this->createForm(ContactType::class);
$form->handleRequest($request);
if ($request->isXmlHttpRequest() && $request->isMethod('POST')) {
// 处理 JSON 格式的提交
if ($request->getContentType() === 'json') {
$data = json_decode($request->getContent(), true);
$form->submit($data);
} else {
// 处理表单数据格式
$form->submit($request->request->all());
}
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
// 这里处理业务逻辑,比如发送邮件
// $this->sendContactEmail($data);
return $this->json([
'success' => true,
'message' => '表单提交成功!',
'data' => $data
]);
}
// 表单验证失败
$errors = $this->getFormErrors($form);
return $this->json([
'success' => false,
'message' => '表单验证失败',
'errors' => $errors
], 422);
}
// 非 AJAX 请求直接返回表单视图
return $this->render('contact/index.html.twig', [
'form' => $form->createView(),
]);
}
private function getFormErrors(FormInterface $form): array
{
$errors = [];
foreach ($form->getErrors(true) as $error) {
$fieldName = $error->getOrigin()->getName();
$errors[$fieldName][] = $error->getMessage();
}
return $errors;
}
}
前端模板和 JavaScript
{# templates/contact/index.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<div class="container mt-5">
<h1>联系我们</h1>
<div id="form-messages" class="mb-3"></div>
{{ form_start(form, {'attr': {'id': 'contact-form', 'novalidate': 'novalidate'}}) }}
<div class="form-group mb-3">
{{ form_label(form.name) }}
{{ form_widget(form.name) }}
<div class="invalid-feedback" data-error-for="name"></div>
</div>
<div class="form-group mb-3">
{{ form_label(form.email) }}
{{ form_widget(form.email) }}
<div class="invalid-feedback" data-error-for="email"></div>
</div>
<div class="form-group mb-3">
{{ form_label(form.message) }}
{{ form_widget(form.message) }}
<div class="invalid-feedback" data-error-for="message"></div>
</div>
{{ form_rest(form) }}
<button type="submit" id="submit-btn" class="btn btn-primary">
<span class="spinner-border spinner-border-sm d-none" role="status"></span>
提交
</button>
{{ form_end(form) }}
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('contact-form');
const submitBtn = document.getElementById('submit-btn');
const spinner = submitBtn.querySelector('.spinner-border');
const messagesDiv = document.getElementById('form-messages');
form.addEventListener('submit', function(e) {
e.preventDefault();
// 清除之前的错误信息
clearErrors();
clearMessages();
// 显示加载状态
setLoading(true);
// 收集表单数据
const formData = new FormData(form);
// 发送 AJAX 请求
fetch(form.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw data;
});
}
return response.json();
})
.then(data => {
if (data.success) {
showMessage('success', data.message);
form.reset();
// 可选:提交成功后重定向
// window.location.href = '/thank-you';
} else {
showMessage('error', data.message);
if (data.errors) {
displayErrors(data.errors);
}
}
})
.catch(error => {
if (error.errors) {
showMessage('error', error.message);
displayErrors(error.errors);
} else {
showMessage('error', '网络错误,请稍后重试');
}
})
.finally(() => {
setLoading(false);
});
});
function setLoading(loading) {
if (loading) {
submitBtn.disabled = true;
spinner.classList.remove('d-none');
submitBtn.textContent = ' 提交中...';
submitBtn.prepend(spinner);
} else {
submitBtn.disabled = false;
spinner.classList.add('d-none');
submitBtn.textContent = '提交';
}
}
function showMessage(type, message) {
messagesDiv.innerHTML = `
<div class="alert alert-${type === 'success' ? 'success' : 'danger'} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
}
function clearMessages() {
messagesDiv.innerHTML = '';
}
function clearErrors() {
document.querySelectorAll('.is-invalid').forEach(el => {
el.classList.remove('is-invalid');
});
document.querySelectorAll('.invalid-feedback').forEach(el => {
el.textContent = '';
});
}
function displayErrors(errors) {
Object.keys(errors).forEach(fieldName => {
const input = document.querySelector(`[name="contact[${fieldName}]"]`);
const errorDiv = document.querySelector(`[data-error-for="${fieldName}"]`);
if (input && errorDiv) {
input.classList.add('is-invalid');
errorDiv.textContent = errors[fieldName].join(', ');
}
});
}
});
</script>
{% endblock %}
使用 Fetch API 发送 JSON 格式
// 替代方案:使用 JSON 格式提交
form.addEventListener('submit', function(e) {
e.preventDefault();
setLoading(true);
clearErrors();
clearMessages();
// 构建 JSON 数据
const formData = new FormData(form);
const jsonData = {};
formData.forEach((value, key) => {
// 处理 Symfony 表单的字段名格式:contact[name]
const match = key.match(/contact\[(.+)\]/);
if (match) {
jsonData[match[1]] = value;
} else {
jsonData[key] = value;
}
});
fetch(form.action, {
method: 'POST',
body: JSON.stringify(jsonData),
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showMessage('success', data.message);
form.reset();
} else {
showMessage('error', data.message);
if (data.errors) {
displayErrors(data.errors);
}
}
})
.catch(error => {
showMessage('error', '提交失败,请重试');
})
.finally(() => {
setLoading(false);
});
});
使用 jQuery 的替代方案
// 如果项目中使用了 jQuery
$('#contact-form').on('submit', function(e) {
e.preventDefault();
var $form = $(this);
var $submitBtn = $('#submit-btn');
var $spinner = $submitBtn.find('.spinner-border');
var $messages = $('#form-messages');
// 清理错误
$('.is-invalid').removeClass('is-invalid');
$('.invalid-feedback').text('');
$messages.empty();
// 禁用按钮
$submitBtn.prop('disabled', true);
$spinner.removeClass('d-none');
$.ajax({
url: $form.attr('action'),
method: 'POST',
data: $form.serialize(),
dataType: 'json',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
success: function(response) {
if (response.success) {
$messages.html(
'<div class="alert alert-success">' + response.message + '</div>'
);
$form[0].reset();
} else {
$messages.html(
'<div class="alert alert-danger">' + response.message + '</div>'
);
if (response.errors) {
$.each(response.errors, function(field, errors) {
var $input = $('[name="contact[' + field + ']"]');
var $errorDiv = $('[data-error-for="' + field + '"]');
$input.addClass('is-invalid');
$errorDiv.text(errors.join(', '));
});
}
}
},
error: function(xhr) {
var response = xhr.responseJSON;
$messages.html(
'<div class="alert alert-danger">' + (response.message || '请求失败') + '</div>'
);
},
complete: function() {
$submitBtn.prop('disabled', false);
$spinner.addClass('d-none');
}
});
});
配置 CSRF 保护
# config/packages/framework.yaml
framework:
csrf_protection: ~
session:
handler_id: null
可选:创建独立的 AJAX 端点
// 专门用于 AJAX 提交的独立端点
#[Route('/contact/ajax', name: 'contact_ajax', methods: ['POST'])]
public function contactAjax(Request $request): JsonResponse
{
if (!$request->isXmlHttpRequest()) {
throw $this->createNotFoundException('Only AJAX requests allowed');
}
$form = $this->createForm(ContactType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
// 保存或处理数据
// ...
return $this->json([
'success' => true,
'message' => '提交成功'
]);
}
$errors = [];
foreach ($form->getErrors(true) as $error) {
$errors[] = $error->getMessage();
}
return $this->json([
'success' => false,
'errors' => $errors
], 400);
}
注意事项
- CSRF Token: 确保在 AJAX 请求中包含 CSRF token
- 错误处理: 提供友好的错误提示
- 加载状态: 显示提交过程中的加载状态
- 数据验证: 前端和后端都要进行数据验证
- 安全性: 验证请求来源,防止 CSRF 攻击
这个实现方案提供了完整的 AJAX 表单提交功能,你可以根据具体需求进行调整。