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

Symfony中如何根据预订日期范围动态生成对应数量的早餐子表单

Symfony中如何根据预订日期范围动态生成对应数量的早餐子表单

我来帮你搞定这个动态生成早餐子表单的问题!咱们一步步来,先把基础逻辑理清楚,再落地到代码上~

第一步:先搞定实体关联关系

要保存多个早餐记录,首先得确保RoomBookings和Breakfasts实体是一对多的关联关系,这是动态表单的基础:

1. 修改RoomBookings实体

// src/Entity/RoomBookings.php
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

class RoomBookings
{
    // ... 其他已有的属性和方法

    /**
     * @ORM\OneToMany(targetEntity=Breakfasts::class, mappedBy="roomBooking", cascade={"persist", "remove"}, orphanRemoval=true)
     */
    private $breakfasts;

    public function __construct()
    {
        $this->breakfasts = new ArrayCollection();
    }

    public function getBreakfasts(): Collection
    {
        return $this->breakfasts;
    }

    public function addBreakfast(Breakfasts $breakfast): self
    {
        if (!$this->breakfasts->contains($breakfast)) {
            $this->breakfasts->add($breakfast);
            $breakfast->setRoomBooking($this);
        }
        return $this;
    }

    public function removeBreakfast(Breakfasts $breakfast): self
    {
        if ($this->breakfasts->removeElement($breakfast)) {
            if ($breakfast->getRoomBooking() === $this) {
                $breakfast->setRoomBooking(null);
            }
        }
        return $this;
    }
}

2. 修改Breakfasts实体

// src/Entity/Breakfasts.php
use Doctrine\ORM\Mapping as ORM;

class Breakfasts
{
    // ... 其他已有的属性和方法

    /**
     * @ORM\ManyToOne(targetEntity=RoomBookings::class, inversedBy="breakfasts")
     * @ORM\JoinColumn(nullable=false)
     */
    private $roomBooking;

    public function getRoomBooking(): ?RoomBookings
    {
        return $this->roomBooking;
    }

    public function setRoomBooking(?RoomBookings $roomBooking): self
    {
        $this->roomBooking = $roomBooking;
        return $this;
    }
}

记得执行数据库迁移,让关联生效:

php bin/console make:migration
php bin/console doctrine:migrations:migrate

第二步:改造主表单,支持多子表单

在RoomBookingsType里添加CollectionType,用来管理多个早餐子表单:

// src/Form/RoomBookingsType.php
public function buildForm(FormBuilderInterface $builder, array $options): void
{
    $builder
        // ... 你原来的所有表单字段
        ->add('breakfasts', CollectionType::class, [
            'entry_type' => BreakfastsType::class,
            'allow_add' => true,
            'allow_delete' => true,
            'by_reference' => false,
            'prototype' => true,
            'label' => false,
            'required' => false,
        ]);

    // 可选:编辑表单时自动同步早餐数量
    $builder->addEventListener(
        FormEvents::PRE_SET_DATA,
        function (FormEvent $event): void {
            $form = $event->getForm();
            $data = $event->getData();

            if (!$data instanceof RoomBookings || !$data->getStartDate() || !$data->getEndDate()) {
                return;
            }

            // 计算入住天数
            $interval = $data->getStartDate()->diff($data->getEndDate());
            $nights = $interval->days;
            $currentBreakfastsCount = $data->getBreakfasts()->count();

            // 补充缺失的早餐记录
            for ($i = $currentBreakfastsCount; $i < $nights; $i++) {
                $breakfastDate = (clone $data->getStartDate())->modify("+$i day");
                $breakfast = new Breakfasts();
                $breakfast->setDate($breakfastDate);
                $data->addBreakfast($breakfast);
            }

            // 删除多余的早餐记录(可选)
            while ($data->getBreakfasts()->count() > $nights) {
                $data->removeBreakfast($data->getBreakfasts()->last());
            }
        }
    );
}

第三步:写一个接口生成动态早餐表单

用户在前端选完日期后,需要请求后端生成对应数量的早餐子表单,所以新增一个控制器方法:

// src/Controller/RoomBookingsController.php
#[Route('/breakfasts-form', name: 'app_breakfasts_form', methods: ['POST'])]
public function getBreakfastsForm(Request $request): Response
{
    try {
        $startDate = new \DateTime($request->request->get('start_date'));
        $endDate = new \DateTime($request->request->get('end_date'));
    } catch (\Exception $e) {
        return new Response('', 400);
    }

    // 计算入住晚数(比如3晚就生成3个早餐表单)
    $interval = $startDate->diff($endDate);
    $nights = $interval->days;

    $roomBooking = new RoomBookings();
    // 给每个早餐设置对应日期
    for ($i = 0; $i < $nights; $i++) {
        $breakfastDate = (clone $startDate)->modify("+$i day");
        $breakfast = new Breakfasts();
        $breakfast->setDate($breakfastDate);
        $roomBooking->addBreakfast($breakfast);
    }

    // 只生成早餐部分的表单
    $form = $this->createForm(RoomBookingsType::class, $roomBooking)->get('breakfasts');

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

然后创建一个渲染早餐子表单的小模板templates/room_bookings/_breakfasts_form.html.twig:

{% for breakfast in form %}
    <div class="bg-gray-50 p-4 mb-3 rounded-lg">
        <h4 class="font-medium mb-2">早餐:{{ breakfast.date.vars.value|date('Y-m-d') }}</h4>
        {{ form_row(breakfast.isTaken, {'label': '是否需要早餐?'}) }}
        {{ form_row(breakfast.nbGuest, {'label': '用餐人数'}) }}
        {{ form_row(breakfast.comment, {'label': '特殊要求'}) }}
    </div>
{% endfor %}

第四步:前端监听日期变化,动态加载表单

修改你的主表单模板templates/room_bookings/new.html.twig,添加前端逻辑:

{{ form_start(form, {'attr': {'class': 'w-full max-w-3xl mx-auto'}}) }}
    <!-- 原有的表单字段渲染 -->
    <div class="form-input-container">
        <h3 class="col-span-2 text-lg font-bold border-b-[1px] border-b-orange-500">早餐设置</h3>
        <div id="breakfasts-container" class="mt-2">
            {# 动态表单会插入到这里 #}
        </div>
    </div>

    <div class="mt-6">
        {{ form_row(form.save, {'attr': {'class': 'bg-orange-500 text-white px-4 py-2 rounded'}}) }}
    </div>
{{ form_end(form) }}

<script>
document.addEventListener('DOMContentLoaded', function() {
    const startDateInput = document.getElementById('start_date');
    const endDateInput = document.getElementById('end_date');
    const breakfastsContainer = document.getElementById('breakfasts-container');
    const csrfToken = '{{ csrf_token('authenticate') }}';

    // 监听日期变化,更新早餐表单
    function updateBreakfasts() {
        const start = startDateInput.value;
        const end = endDateInput.value;

        if (!start || !end) {
            breakfastsContainer.innerHTML = '';
            return;
        }

        fetch('{{ path('app_breakfasts_form') }}', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'X-CSRF-Token': csrfToken
            },
            body: new URLSearchParams({
                start_date: start,
                end_date: end
            })
        })
        .then(res => res.text())
        .then(html => breakfastsContainer.innerHTML = html)
        .catch(err => console.error('加载早餐表单失败:', err));
    }

    // 初始加载+监听变化
    updateBreakfasts();
    startDateInput.addEventListener('change', updateBreakfasts);
    endDateInput.addEventListener('change', updateBreakfasts);
});
</script>

最后注意事项

  1. 日期逻辑:如果你的早餐是对应入住后的第二天(比如1号入住,2号吃早餐),可以把控制器里的日期计算改成(clone $startDate)->modify("+".($i+1)." day")
  2. 表单提交:因为我们设置了cascade={"persist"},提交主表单时,关联的早餐记录会自动被持久化到数据库
  3. 样式:可以根据你的项目需求调整子表单的CSS样式

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 09:03:01