本文目录导读:

在Symfony项目中,实现表单的草稿恢复功能是一个常见需求,我来为你提供一个完整的解决方案。
数据库设计
// src/Entity/Draft.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity(repositoryClass: DraftRepository::class)]
class Draft
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['draft:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['draft:read'])]
private ?string $formType = null;
#[ORM\Column(type: 'text')]
private ?string $data = null;
#[ORM\Column(type: 'datetime')]
#[Groups(['draft:read'])]
private ?\DateTimeInterface $createdAt = null;
#[ORM\Column(type: 'datetime', nullable: true)]
#[Groups(['draft:read'])]
private ?\DateTimeInterface $updatedAt = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $userId = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $sessionId = null;
// Getters and setters...
}
草稿服务
// src/Service/DraftService.php
namespace App\Service;
use App\Entity\Draft;
use App\Form\Type\YourFormType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\SerializerInterface;
class DraftService
{
private EntityManagerInterface $entityManager;
private SerializerInterface $serializer;
private FormFactoryInterface $formFactory;
private RequestStack $requestStack;
private ?Security $security;
public function __construct(
EntityManagerInterface $entityManager,
SerializerInterface $serializer,
FormFactoryInterface $formFactory,
RequestStack $requestStack,
?Security $security = null
) {
$this->entityManager = $entityManager;
$this->serializer = $serializer;
$this->formFactory = $formFactory;
$this->requestStack = $requestStack;
$this->security = $security;
}
public function saveDraft(string $formType, array $data, ?int $draftId = null): Draft
{
$draft = $draftId ?
$this->entityManager->getRepository(Draft::class)->find($draftId) :
new Draft();
if (!$draft) {
throw new \InvalidArgumentException('Draft not found');
}
$draft->setFormType($formType);
$draft->setData($this->serializer->serialize($data, 'json'));
$draft->setUpdatedAt(new \DateTime());
if (!$draftId) {
$draft->setCreatedAt(new \DateTime());
// 绑定用户或会话
if ($this->security && $this->security->getUser()) {
$draft->setUserId($this->security->getUser()->getId());
} else {
$session = $this->requestStack->getSession();
$draft->setSessionId($session->getId());
}
}
$this->entityManager->persist($draft);
$this->entityManager->flush();
return $draft;
}
public function loadDraft(int $draftId): ?array
{
$draft = $this->entityManager->getRepository(Draft::class)->find($draftId);
if (!$draft) {
return null;
}
return $this->serializer->deserialize(
$draft->getData(),
'array',
'json'
);
}
public function getFormDataFromDraft(FormInterface $form, int $draftId): FormInterface
{
$draftData = $this->loadDraft($draftId);
if ($draftData) {
$form->submit($draftData, false); // false = 不验证
}
return $form;
}
public function getUserDrafts(?string $formType = null): array
{
$criteria = [];
if ($this->security && $this->security->getUser()) {
$criteria['userId'] = $this->security->getUser()->getId();
} else {
$session = $this->requestStack->getSession();
$criteria['sessionId'] = $session->getId();
}
if ($formType) {
$criteria['formType'] = $formType;
}
return $this->entityManager
->getRepository(Draft::class)
->findBy($criteria, ['updatedAt' => 'DESC']);
}
public function deleteDraft(int $draftId): void
{
$draft = $this->entityManager->getRepository(Draft::class)->find($draftId);
if ($draft) {
$this->entityManager->remove($draft);
$this->entityManager->flush();
}
}
}
控制器实现
// src/Controller/FormController.php
namespace App\Controller;
use App\Entity\Article;
use App\Form\Type\ArticleType;
use App\Service\DraftService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class FormController extends AbstractController
{
#[Route('/article/new', name: 'article_new')]
#[Route('/article/{id}/edit', name: 'article_edit')]
public function new(Request $request, DraftService $draftService, ?int $id = null): Response
{
$article = $id ? $this->getArticle($id) : new Article();
$form = $this->createForm(ArticleType::class, $article);
// 自动保存草稿
$draftId = $request->query->get('draft');
if ($draftId) {
// 恢复草稿
$form = $draftService->getFormDataFromDraft($form, $draftId);
}
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// 如果是保存草稿
if ($form->get('save_draft')->isClicked()) {
$draft = $draftService->saveDraft(
ArticleType::class,
$form->getData(),
$request->getSession()->get('current_draft_id')
);
$request->getSession()->set('current_draft_id', $draft->getId());
$this->addFlash('success', '草稿已保存');
return $this->redirectToRoute('article_new', ['draft' => $draft->getId()]);
}
// 实际提交
// ... 保存到数据库
// 删除草稿
if ($draftId = $request->getSession()->remove('current_draft_id')) {
$draftService->deleteDraft($draftId);
}
return $this->redirectToRoute('article_list');
}
// 获取用户的所有草稿
$userDrafts = $draftService->getUserDrafts(ArticleType::class);
return $this->render('article/form.html.twig', [
'form' => $form->createView(),
'userDrafts' => $userDrafts,
'currentDraftId' => $request->getSession()->get('current_draft_id'),
]);
}
#[Route('/draft/load/{id}', name: 'draft_load')]
public function loadDraft(int $id, DraftService $draftService): Response
{
$draft = $draftService->loadDraft($id);
if (!$draft) {
throw $this->createNotFoundException('草稿不存在');
}
return $this->redirectToRoute('article_new', ['draft' => $id]);
}
#[Route('/draft/delete/{id}', name: 'draft_delete')]
public function deleteDraft(int $id, DraftService $draftService): Response
{
$draftService->deleteDraft($id);
$this->addFlash('success', '草稿已删除');
return $this->redirectToRoute('article_new');
}
}
表单类型
// src/Form/Type/ArticleType.php
namespace App\Form\Type;
use App\Entity\Article;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title', TextType::class, [
'label' => '标题',
])
->add('content', TextareaType::class, [
'label' => '内容',
])
->add('save_draft', SubmitType::class, [
'label' => '保存草稿',
'attr' => ['class' => 'btn-secondary'],
])
->add('submit', SubmitType::class, [
'label' => '发布',
'attr' => ['class' => 'btn-primary'],
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Article::class,
]);
}
}
前端实现
{# templates/article/form.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<div class="container mt-4">
<h1>{{ form.vars.value.id ? '编辑文章' : '新建文章' }}</h1>
{% if userDrafts|length > 0 %}
<div class="alert alert-info">
<h5>草稿列表</h5>
<div class="list-group">
{% for draft in userDrafts %}
<div class="list-group-item d-flex justify-content-between align-items-center">
<a href="{{ path('draft_load', {'id': draft.id}) }}" class="text-decoration-none">
草稿 #{{ draft.id }}
({{ draft.updatedAt|date('Y-m-d H:i:s') }})
</a>
<span>
<a href="{{ path('draft_delete', {'id': draft.id}) }}"
class="btn btn-sm btn-danger"
onclick="return confirm('确定删除此草稿?')">
删除
</a>
</span>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{{ form_start(form, {'attr': {'class': 'needs-validation', 'novalidate': 'novalidate'}}) }}
{{ form_widget(form) }}
{{ form_end(form) }}
</div>
<!-- 自动保存功能 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
let autoSaveTimer;
// 监听表单输入变化
form.addEventListener('input', function() {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(autoSaveDraft, 30000); // 30秒自动保存
});
function autoSaveDraft() {
const formData = new FormData(form);
const data = {};
formData.forEach((value, key) => {
// 排除提交按钮
if (!key.includes('submit') && !key.includes('save_draft')) {
data[key] = value;
}
});
// 发送AJAX请求保存草稿
fetch('{{ path('draft_autosave') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('草稿已自动保存');
}
})
.catch(error => console.error('自动保存失败:', error));
}
// 显示通知的函数
function showNotification(message) {
const notification = document.createElement('div');
notification.className = 'alert alert-success alert-dismissible fade show position-fixed top-0 end-0 m-3';
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
});
</script>
{% endblock %}
自动保存API端点
// src/Controller/ApiController.php
namespace App\Controller;
use App\Service\DraftService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class ApiController extends AbstractController
{
#[Route('/draft/autosave', name: 'draft_autosave', methods: ['POST'])]
public function autoSave(Request $request, DraftService $draftService): JsonResponse
{
try {
$data = json_decode($request->getContent(), true);
if (!$data) {
return $this->json(['success' => false, 'error' => '无效的数据'], 400);
}
// 获取当前草稿ID(如果存在)
$currentDraftId = $request->getSession()->get('current_draft_id');
$draft = $draftService->saveDraft(
'App\\Form\\Type\\ArticleType',
$data,
$currentDraftId
);
$request->getSession()->set('current_draft_id', $draft->getId());
return $this->json([
'success' => true,
'draftId' => $draft->getId(),
'message' => '草稿已自动保存'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'error' => $e->getMessage()
], 500);
}
}
}
配置服务
# config/services.yaml
services:
App\Service\DraftService:
arguments:
$security: '@security.helper'
tags:
- { name: 'kernel.event_listener', event: 'kernel.request', method: 'onKernelRequest' }
这个完整的解决方案包括:
- 数据库存储 - 存储表单数据和元数据
- 草稿服务 - 处理草稿的保存、加载和删除
- 控制器 - 处理草稿的恢复和自动保存
- 前端 - 显示草稿列表和自动保存功能
- 安全考虑 - 根据用户或会话隔离草稿
你可以根据具体需求调整自动保存间隔、数据序列化方式和其他功能细节。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播