全书目录

第三十七篇:Symfony 完全指南 —— 组件化政务城区宇宙

第十四章:完整示例,把一张营业许可申请跑完整

1 分钟 251 字 第 711 / 962 个阅读单元

下面把前面的概念串成一条完整的政务流。

场景:

  • 市民访问 /permits/new
  • 系统渲染申请表
  • 提交后自动映射到 BusinessPermit
  • 表单校验通过
  • PermitService 负责落库并投递异步消息
  • 后台专办员继续做通知与后续流程
  • 最后重定向到详情页

先看总流转:

text
GET /permits/new
    |
    v
Controller 创建 Form
    |
    v
Twig 渲染申请页
    |
    v
POST /permits/new
    |
    v
handleRequest() 回填对象
    |
    v
Validator 校验
    |
    +--> 不通过:原页回显错误
    |
    +--> 通过
           |
           v
      PermitService
           |
           +--> Doctrine flush()
           |
           +--> Messenger dispatch()
           |
           v
      redirectToRoute('permit_show')

核心代码如下。

#1. 实体:档案对象
php
<?php

namespace App\Entity;

use App\Repository\BusinessPermitRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity(repositoryClass: BusinessPermitRepository::class)]
class BusinessPermit
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 120)]
    #[Assert\NotBlank]
    private string $applicantName = '';

    #[ORM\Column(length: 160)]
    #[Assert\NotBlank]
    private string $businessName = '';

    #[ORM\Column(type: 'text', nullable: true)]
    private ?string $description = null;

    #[ORM\Column(length: 20)]
    private string $status = 'draft';

    #[ORM\Column(type: 'datetime_immutable')]
    private \DateTimeImmutable $createdAt;

    public function __construct()
    {
        $this->createdAt = new \DateTimeImmutable();
    }

    public function markSubmitted(): void
    {
        $this->status = 'submitted';
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getBusinessName(): string
    {
        return $this->businessName;
    }

    public function getStatus(): string
    {
        return $this->status;
    }
}
#2. 表单:申报表定义
php
<?php

namespace App\Form;

use App\Entity\BusinessPermit;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class BusinessPermitType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('applicantName', TextType::class, ['label' => '申请人'])
            ->add('businessName', TextType::class, ['label' => '商户名称'])
            ->add('description', TextareaType::class, [
                'label' => '经营说明',
                'required' => false,
            ]);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => BusinessPermit::class,
        ]);
    }
}
#3. 服务:真正的办事处室
php
<?php

namespace App\Service;

use App\Entity\BusinessPermit;
use App\Message\PermitSubmittedMessage;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\MessageBusInterface;

final class PermitService
{
    public function __construct(
        private EntityManagerInterface $entityManager,
        private MessageBusInterface $bus,
    ) {
    }

    public function submit(BusinessPermit $permit): void
    {
        $permit->markSubmitted();

        $this->entityManager->persist($permit);
        $this->entityManager->flush();

        $this->bus->dispatch(new PermitSubmittedMessage($permit->getId()));
    }
}
#4. 控制器:窗口只做协调
php
<?php

namespace App\Controller;

use App\Entity\BusinessPermit;
use App\Form\BusinessPermitType;
use App\Service\PermitService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

final class PermitController extends AbstractController
{
    #[Route('/permits/new', name: 'permit_new', methods: ['GET', 'POST'])]
    public function new(Request $request, PermitService $permitService): Response
    {
        $permit = new BusinessPermit();
        $form = $this->createForm(BusinessPermitType::class, $permit);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $permitService->submit($permit);

            return $this->redirectToRoute('permit_show', [
                'id' => $permit->getId(),
            ]);
        }

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

    #[Route('/permits/{id}', name: 'permit_show', methods: ['GET'])]
    public function show(BusinessPermit $permit): Response
    {
        return $this->render('permit/show.html.twig', [
            'permit' => $permit,
        ]);
    }
}
#5. 消息与处理器:后台专办员继续干活
php
<?php

namespace App\Message;

final readonly class PermitSubmittedMessage
{
    public function __construct(public int $permitId)
    {
    }
}
php
<?php

namespace App\MessageHandler;

use App\Message\PermitSubmittedMessage;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final class PermitSubmittedMessageHandler
{
    public function __construct(
        private LoggerInterface $logger,
    ) {
    }

    public function __invoke(PermitSubmittedMessage $message): void
    {
        $this->logger->info('营业许可申请已进入后台处理', [
            'permit_id' => $message->permitId,
        ]);
    }
}
#6. 模板:给人看的公告页
twig
{# templates/permit/new.html.twig #}
<h1>营业许可申请</h1>

{{ form_start(form) }}
    {{ form_row(form.applicantName) }}
    {{ form_row(form.businessName) }}
    {{ form_row(form.description) }}
    <button type="submit">提交申请</button>
{{ form_end(form) }}
twig
{# templates/permit/show.html.twig #}
<h1>{{ permit.businessName }}</h1>
<p>当前状态:{{ permit.status }}</p>

这套例子最重要的不是语法,而是结构:

text
表单
-> 控制器
-> 服务
-> Doctrine
-> Messenger
-> Twig

每一层都像政务城区里职责清晰的楼
而不是把所有事堆在一个“超级窗口”里