You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Symfony 6.4子请求渲染表单提交后刷新重复提交问题求助

解决Symfony 6.4中Bootstrap模态框表单刷新重复提交问题

核心问题分析

你的实现存在两个关键问题:

  1. 违背了HTTP规范:主页面是GET请求,却通过render(controller())调用仅支持POST的导入接口,导致子请求上下文混乱
  2. 未遵循POST/REDIRECT/GET标准模式:表单提交后直接返回页面内容,浏览器会保留POST请求状态,刷新时重复提交

下面提供两种成熟的解决方案:


方案一:POST/REDIRECT/GET标准模式(推荐)

这是防止表单重复提交的经典方案,处理完成后重定向回主页面,彻底清除POST请求状态。

1. 重构ExportImportController

拆分表单展示与处理逻辑,分别对应GET/POST路由:

class ExportImportController extends AbstractController
{
    // GET路由:仅负责渲染导入模态框的空表单
    #[Route('/equipment/import', name: 'route_equipment_import_form', methods: ['GET'])]
    public function importForm(): Response
    {
        $form = $this->createForm(DeviceImportType::class);
        return $this->render('Equipment/import.html.twig', [
            'form' => $form->createView()
        ]);
    }

    // POST路由:处理导入逻辑,完成后重定向
    #[Route('/equipment/import/submit', name: 'route_equipment_import_submit', methods: ['POST'])]
    public function importSubmit(Request $request): Response
    {
        $form = $this->createForm(DeviceImportType::class);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            if ($form->get('save')->isClicked()) {
                $this->importer->import($request);
                // 添加导入成功的Flash提示
                $this->addFlash('success', '设备导入成功!');
            }
        }

        // 重定向回主页面,彻底清除POST请求状态
        return $this->redirectToRoute('route_equipment_show');
    }
}

2. 修改import.html.twig表单

指定表单提交到POST路由:

<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            {{ form_start(form, {action: path('route_equipment_import_submit')}) }}
            <div class="modal-header">
                <h1 class="modal-title fs-5" id="exampleModalLabel">导入设备</h1>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                {{ form_label(form.file) }}
                {{ form_widget(form.file) }}
                {{ form_errors(form.file) }}
            </div>
            <div class="modal-footer">
                {{ form_widget(form.save, {label: '导入'}) }}
            </div>
            {{ form_end(form) }}
        </div>
    </div>
</div>

3. 更新show.html.twig

调用GET路由渲染模态框,并添加Flash消息展示:

{% extends 'base.html.twig' %}

{% block body %}
    {# 调用GET路由渲染导入模态框,不再传递主请求 #}
    {{ render(controller('App\\Controller\\Equipment\\ExportImportController::importForm')) }}

    {# 显示导入成功提示 #}
    {% for message in app.flashes('success') %}
        <div class="alert alert-success">{{ message }}</div>
    {% endfor %}

    <table>
        {# 设备列表渲染代码 #}
    </table>
{% endblock %}

方案二:AJAX异步提交(无刷新体验)

如果需要更流畅的用户体验,可通过AJAX提交表单,避免页面跳转:

1. 修改import.html.twig添加JS逻辑

<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            {{ form_start(form, {action: path('route_equipment_import_submit'), id: 'import-form'}) }}
            <div class="modal-header">
                <h1 class="modal-title fs-5" id="exampleModalLabel">导入设备</h1>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                {{ form_label(form.file) }}
                {{ form_widget(form.file) }}
                <div id="import-error" class="text-danger mt-2"></div>
            </div>
            <div class="modal-footer">
                {{ form_widget(form.save, {label: '导入'}) }}
            </div>
            {{ form_end(form) }}
        </div>
    </div>
</div>

{% block javascripts %}
    {{ parent() }}
    <script>
        document.getElementById('import-form').addEventListener('submit', function(e) {
            e.preventDefault();
            const formData = new FormData(this);
            const errorEl = document.getElementById('import-error');
            
            fetch(this.action, {
                method: 'POST',
                body: formData,
                headers: {'X-Requested-With': 'XMLHttpRequest'}
            })
            .then(res => {
                if (res.ok) {
                    // 导入成功:关闭模态框并刷新列表
                    bootstrap.Modal.getInstance(document.getElementById('exampleModal')).hide();
                    window.location.reload();
                } else {
                    res.text().then(msg => errorEl.textContent = msg || '导入失败,请检查文件格式');
                }
            })
            .catch(() => errorEl.textContent = '服务器错误,请稍后重试');
        });
    </script>
{% endblock %}

2. 调整控制器支持AJAX响应

#[Route('/equipment/import/submit', name: 'route_equipment_import_submit', methods: ['POST'])]
public function importSubmit(Request $request): Response
{
    $form = $this->createForm(DeviceImportType::class);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        if ($form->get('save')->isClicked()) {
            $this->importer->import($request);
            
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => true]);
            }
            $this->addFlash('success', '设备导入成功!');
        }
    }

    if ($request->isXmlHttpRequest()) {
        return new JsonResponse(['success' => false, 'message' => '表单验证失败'], 400);
    }

    return $this->redirectToRoute('route_equipment_show');
}

内容的提问来源于stack exchange,提问作者tcoch

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 16:39:56