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
相关产品推荐
相关产品推荐

