PHP项目Symfony form与草稿恢复

wen PHP项目 2

本文目录导读:

PHP项目Symfony form与草稿恢复

  1. 数据库设计
  2. 草稿服务
  3. 控制器实现
  4. 表单类型
  5. 前端实现
  6. 自动保存API端点
  7. 配置服务

在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' }

这个完整的解决方案包括:

  1. 数据库存储 - 存储表单数据和元数据
  2. 草稿服务 - 处理草稿的保存、加载和删除
  3. 控制器 - 处理草稿的恢复和自动保存
  4. 前端 - 显示草稿列表和自动保存功能
  5. 安全考虑 - 根据用户或会话隔离草稿

你可以根据具体需求调整自动保存间隔、数据序列化方式和其他功能细节。

抱歉,评论功能暂时关闭!