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

Symfony表单实现固定星期的多时段营业时间批量提交需求

实现固定星期的多时段营业时间表单

嘿,这个需求很常见,我帮你搞定这个固定星期+多时段的Symfony表单,完全符合你想要的UI和交互逻辑!我们分四步来实现:

步骤1:创建单个时段的子表单类型

首先把单个时段的open/close时间抽成独立的子表单,因为星期是固定展示的,不需要用户选择,所以子表单里去掉weekday字段:

// src/Form/Type/BusinessHourType.php
namespace App\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TimeType;
use Symfony\Component\Form\FormBuilderInterface;

class BusinessHourType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('open', TimeType::class, [
                'label' => 'site.hours.open',
                'input' => 'datetime_immutable',
                'widget' => 'single_text', // 可选,让时间输入更简洁
            ])
            ->add('close', TimeType::class, [
                'label' => 'site.hours.close',
                'input' => 'datetime_immutable',
                'widget' => 'single_text',
            ]);
    }
}

步骤2:构建主表单(按星期分组的集合)

接下来创建主表单,注入WeekdayRepository获取所有星期,为每个星期生成一个CollectionType,直接绑定对应星期的时段数据,不用让用户手动选择星期:

// src/Form/Type/BusinessHoursWeekType.php
namespace App\Form\Type;

use App\Entity\Weekday;
use App\Repository\WeekdayRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class BusinessHoursWeekType extends AbstractType
{
    public function __construct(private WeekdayRepository $weekdayRepository)
    {}

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        // 按id排序获取所有星期(1=周一,2=周二...)
        $weekdays = $this->weekdayRepository->findBy([], ['id' => 'ASC']);

        foreach ($weekdays as $weekday) {
            // 为每个星期添加时段集合
            $builder->add('weekday_' . $weekday->getId(), CollectionType::class, [
                'label' => $weekday->getName(), // 直接显示星期名称
                'entry_type' => BusinessHourType::class,
                'allow_add' => true,
                'allow_delete' => true,
                'by_reference' => false,
                'prototype' => true, // 开启原型用于前端动态添加
                'prototype_name' => '__business_hour_prototype__',
                'required' => false, // 允许某个星期没有时段
                'attr' => [
                    'class' => 'business-hour-collection',
                    'data-weekday-id' => $weekday->getId(),
                ],
            ]);
        }

        $builder->add('submit', SubmitType::class, ['label' => 'app.save']);
    }

    // 如果用DTO接收数据,可以在这里配置data_class
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            // 'data_class' => BusinessHoursWeekDTO::class,
        ]);
    }
}

步骤3:渲染表单模板(带动态添加/移除功能)

在Twig模板里渲染每个星期的标题和时段集合,同时添加JavaScript实现动态添加/移除时段的交互:

{# templates/business_hours/form.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
    <h1>{{ 'site.hours.edit'|trans }}</h1>

    {{ form_start(form) }}

    {% for weekdayForm in form %}
        {% if weekdayForm.vars.name starts with 'weekday_' %}
            <div class="mb-4">
                <h3>{{ weekdayForm.vars.label }}</h3>

                {# 渲染已有的时段 #}
                <div class="business-hour-items mb-2" data-prototype="{{ form_widget(weekdayForm.vars.prototype)|e('html_attr') }}">
                    {% for hourForm in weekdayForm %}
                        <div class="business-hour-item d-flex gap-2 mb-2">
                            {{ form_widget(hourForm.open) }}
                            {{ form_widget(hourForm.close) }}
                            <button type="button" class="btn btn-danger btn-sm remove-hour">X</button>
                        </div>
                    {% endfor %}
                </div>

                {# 添加时段按钮 #}
                <button type="button" class="btn btn-primary btn-sm add-hour" data-target="{{ weekdayForm.vars.id }}">
                    {{ 'site.hours.add_slot'|trans }}
                </button>
            </div>
        {% endif %}
    {% endfor %}

    {{ form_row(form.submit) }}
    {{ form_end(form) }}

    <script>
        // 绑定添加时段按钮事件
        document.querySelectorAll('.add-hour').forEach(button => {
            button.addEventListener('click', function() {
                const targetId = this.getAttribute('data-target');
                const collectionHolder = document.querySelector(`#${targetId} .business-hour-items`);
                const prototype = collectionHolder.getAttribute('data-prototype');
                // 替换原型中的占位符为当前集合长度
                const newForm = prototype.replace(/__business_hour_prototype__/g, collectionHolder.children.length);

                const div = document.createElement('div');
                div.className = 'business-hour-item d-flex gap-2 mb-2';
                div.innerHTML = newForm;

                // 给新表单添加移除按钮
                const removeBtn = document.createElement('button');
                removeBtn.type = 'button';
                removeBtn.className = 'btn btn-danger btn-sm remove-hour';
                removeBtn.textContent = 'X';
                div.appendChild(removeBtn);

                collectionHolder.appendChild(div);
                bindRemoveEvent(removeBtn);
            });
        });

        // 绑定移除时段按钮事件
        function bindRemoveEvent(button) {
            button.addEventListener('click', function() {
                this.parentElement.remove();
            });
        }

        // 初始化已有的移除按钮
        document.querySelectorAll('.remove-hour').forEach(bindRemoveEvent);
    </script>
{% endblock %}

步骤4:处理表单提交与数据持久化

在控制器里接收表单数据,遍历每个星期的时段,关联对应的Weekday实体并持久化:

// src/Controller/BusinessHoursController.php
namespace App\Controller;

use App\Entity\BusinessHour;
use App\Form\Type\BusinessHoursWeekType;
use App\Repository\WeekdayRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BusinessHoursController extends AbstractController
{
    #[Route('/business-hours/edit', name: 'business_hours_edit')]
    public function edit(Request $request, EntityManagerInterface $em, WeekdayRepository $weekdayRepository): Response
    {
        // 加载现有营业时间作为初始数据
        $initialData = [];
        $weekdays = $weekdayRepository->findBy([], ['id' => 'ASC']);
        foreach ($weekdays as $weekday) {
            $initialData['weekday_' . $weekday->getId()] = $weekday->getBusinessHours();
        }

        $form = $this->createForm(BusinessHoursWeekType::class, $initialData);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $data = $form->getData();

            // 先删除所有旧的营业时间(可选,也可以对比更新)
            $existingHours = $em->getRepository(BusinessHour::class)->findAll();
            foreach ($existingHours as $hour) {
                $em->remove($hour);
            }

            // 遍历提交的数据,创建新的营业时间实体
            foreach ($data as $key => $hours) {
                if (str_starts_with($key, 'weekday_')) {
                    $weekdayId = str_replace('weekday_', '', $key);
                    $weekday = $weekdayRepository->find($weekdayId);

                    foreach ($hours as $hourData) {
                        $businessHour = new BusinessHour();
                        $businessHour->setWeekday($weekday);
                        $businessHour->setOpen($hourData['open']);
                        $businessHour->setClose($hourData['close']);

                        $em->persist($businessHour);
                    }
                }
            }

            $em->flush();
            $this->addFlash('success', 'app.hours_saved_successfully');
            return $this->redirectToRoute('business_hours_edit');
        }

        return $this->render('business_hours/form.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}

额外提示

  • 可以在BusinessHourType里添加表单验证,比如用Callback约束确保关门时间晚于开门时间。
  • 样式可以根据你的项目UI框架调整(比如Bootstrap类),让布局更美观。
  • 如果不想删除旧数据,可以对比现有数据和提交数据,只新增/修改/删除差异部分。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 09:07:43