PHP项目Symfony form与ajax提交

wen PHP项目 3

本文目录导读:

PHP项目Symfony form与ajax提交

  1. 创建表单类型
  2. 控制器处理 AJAX 请求
  3. 前端模板和 JavaScript
  4. 使用 Fetch API 发送 JSON 格式
  5. 使用 jQuery 的替代方案
  6. 配置 CSRF 保护
  7. 可选:创建独立的 AJAX 端点
  8. 注意事项

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

注意事项

  1. CSRF Token: 确保在 AJAX 请求中包含 CSRF token
  2. 错误处理: 提供友好的错误提示
  3. 加载状态: 显示提交过程中的加载状态
  4. 数据验证: 前端和后端都要进行数据验证
  5. 安全性: 验证请求来源,防止 CSRF 攻击

这个实现方案提供了完整的 AJAX 表单提交功能,你可以根据具体需求进行调整。

抱歉,评论功能暂时关闭!