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

如何在Symfony中结合Doctrine与User、Restaurant、Table实体创建新预订

在Symfony中结合Doctrine ORM实现预订创建功能

以下是基于User、Restaurant、Table(命名为RestaurantTable避免SQL关键字冲突)实体的完整实现步骤:

1. 定义实体及关联关系

明确核心实体的关联逻辑:

  • User 与 Reservation:一对多(单个用户可创建多个预订)
  • Restaurant 与 RestaurantTable:一对多(单个餐厅包含多张桌子)
  • Reservation 关联 User、Restaurant、RestaurantTable:多对一(每个预订归属一个用户、对应一个餐厅和一张桌子)

User实体

// src/Entity/User.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

#[ORM\Entity]
class User
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private $id;

    // 补充name、email等业务字段

    #[ORM\OneToMany(mappedBy: 'user', targetEntity: Reservation::class, cascade: ['persist', 'remove'])]
    private $reservations;

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

    // 生成id、reservations的getter/setter,以及addReservation、removeReservation方法
}

Restaurant实体

// src/Entity/Restaurant.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

#[ORM\Entity]
class Restaurant
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private $id;

    #[ORM\Column(type: 'string', length: 255)]
    private $name;

    // 补充address、phone等字段

    #[ORM\OneToMany(mappedBy: 'restaurant', targetEntity: RestaurantTable::class, cascade: ['persist', 'remove'])]
    private $tables;

    #[ORM\OneToMany(mappedBy: 'restaurant', targetEntity: Reservation::class, cascade: ['persist', 'remove'])]
    private $reservations;

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

    // 生成getter/setter及关联操作方法
}

RestaurantTable实体

// src/Entity/RestaurantTable.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

#[ORM\Entity]
#[ORM\Table(name: 'restaurant_table')] // 规避SQL关键字冲突
class RestaurantTable
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private $id;

    #[ORM\Column(type: 'integer')]
    private $number; // 桌号

    #[ORM\Column(type: 'integer')]
    private $capacity; // 容纳人数

    #[ORM\ManyToOne(targetEntity: Restaurant::class, inversedBy: 'tables')]
    #[ORM\JoinColumn(nullable: false)]
    private $restaurant;

    #[ORM\OneToMany(mappedBy: 'table', targetEntity: Reservation::class)]
    private $reservations;

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

    // 生成getter/setter及关联操作方法
}

Reservation实体

// src/Entity/Reservation.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Reservation
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private $id;

    #[ORM\Column(type: 'datetime')]
    private $reservationTime;

    #[ORM\Column(type: 'integer')]
    private $guestsCount; // 用餐人数

    #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'reservations')]
    #[ORM\JoinColumn(nullable: false)]
    private $user;

    #[ORM\ManyToOne(targetEntity: Restaurant::class, inversedBy: 'reservations')]
    #[ORM\JoinColumn(nullable: false)]
    private $restaurant;

    #[ORM\ManyToOne(targetEntity: RestaurantTable::class, inversedBy: 'reservations')]
    #[ORM\JoinColumn(nullable: false)]
    private $table;

    // 生成所有字段的getter/setter方法
}

2. 生成数据库表

执行以下命令创建迁移文件并同步到数据库:

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

3. 创建预订表单

创建表单类型处理前端提交的预订数据:

// src/Form/ReservationType.php
namespace App\Form;

use App\Entity\Reservation;
use App\Entity\Restaurant;
use App\Entity\RestaurantTable;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;

class ReservationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('reservationTime', DateTimeType::class, [
                'widget' => 'single_text',
                'label' => '预订时间'
            ])
            ->add('guestsCount', IntegerType::class, [
                'label' => '用餐人数'
            ])
            ->add('restaurant', EntityType::class, [
                'class' => Restaurant::class,
                'choice_label' => 'name',
                'label' => '选择餐厅',
                'placeholder' => '请选择餐厅'
            ])
            ->add('table', EntityType::class, [
                'class' => RestaurantTable::class,
                'choice_label' => 'number',
                'label' => '选择桌号',
                'placeholder' => '请先选择餐厅',
                'disabled' => true
            ]);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Reservation::class,
        ]);
    }
}

4. 编写控制器处理预订逻辑

创建控制器处理表单提交、验证预订冲突并保存数据:

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

use App\Entity\Reservation;
use App\Form\ReservationType;
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 ReservationController extends AbstractController
{
    #[Route('/reservation/new', name: 'app_reservation_new')]
    public function new(Request $request, EntityManagerInterface $em): Response
    {
        $reservation = new Reservation();
        $reservation->setUser($this->getUser()); // 关联当前登录用户

        $form = $this->createForm(ReservationType::class, $reservation);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // 检查桌号在指定时间是否已被预订
            $existingReservation = $em->getRepository(Reservation::class)->findOneBy([
                'table' => $reservation->getTable(),
                'reservationTime' => $reservation->getReservationTime()
            ]);

            if ($existingReservation) {
                $this->addFlash('error', '该桌号在所选时间已被占用');
                return $this->redirectToRoute('app_reservation_new');
            }

            $em->persist($reservation);
            $em->flush();

            $this->addFlash('success', '预订创建成功');
            return $this->redirectToRoute('app_reservation_list'); // 替换为你的预订列表路由
        }

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

    // 动态加载餐厅桌号的API接口
    #[Route('/api/restaurants/{id}/tables', name: 'api_restaurant_tables', methods: ['GET'])]
    public function getRestaurantTables(Restaurant $restaurant): Response
    {
        $data = [];
        foreach ($restaurant->getTables() as $table) {
            $data[] = [
                'id' => $table->getId(),
                'number' => $table->getNumber(),
                'capacity' => $table->getCapacity()
            ];
        }
        return $this->json($data);
    }
}

5. 编写前端模板

创建Twig模板渲染表单,并添加JS实现餐厅桌号的动态加载:

{# templates/reservation/new.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
    <h1>创建新预订</h1>

    {% for flashError in app.flashes('error') %}
        <div class="alert alert-danger">{{ flashError }}</div>
    {% endfor %}
    {% for flashSuccess in app.flashes('success') %}
        <div class="alert alert-success">{{ flashSuccess }}</div>
    {% endfor %}

    {{ form_start(form) }}
        {{ form_widget(form) }}
        <button type="submit" class="btn btn-primary mt-3">提交预订</button>
    {{ form_end(form) }}

    <script>
        const restaurantSelect = document.querySelector('select[name="reservation[restaurant]"]');
        const tableSelect = document.querySelector('select[name="reservation[table]"]');

        restaurantSelect.addEventListener('change', () => {
            const restaurantId = restaurantSelect.value;
            tableSelect.innerHTML = '<option value="">请先选择餐厅</option>';
            tableSelect.disabled = true;

            if (!restaurantId) return;

            fetch(`{{ path('api_restaurant_tables', {id: 0}) }}`.replace('0', restaurantId))
                .then(res => res.json())
                .then(tables => {
                    tableSelect.innerHTML = '<option value="">选择桌号</option>';
                    tables.forEach(table => {
                        const option = document.createElement('option');
                        option.value = table.id;
                        option.textContent = `桌号 ${table.number} (可容纳 ${table.capacity} 人)`;
                        tableSelect.appendChild(option);
                    });
                    tableSelect.disabled = false;
                });
        });
    </script>
{% endblock %}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 11:40:12