PHP项目Symfony form与子表单

wen PHP项目 2

本文目录导读:

PHP项目Symfony form与子表单

  1. 基础概念
  2. 实体关联设置
  3. 表单类型定义
  4. 控制器处理
  5. Twig 模板
  6. 高级技巧
  7. 常见问题解决
  8. 完整示例

在 Symfony 中处理表单与子表单(嵌入表单)是构建复杂数据的常见需求,以下是完整的实现指南:

基础概念

子表单通常用于处理 OneToManyManyToOne 关联关系,

  • 文章(Article) → 评论(Comment)
  • 订单(Order) → 订单项(OrderItem)

实体关联设置

// src/Entity/Article.php
class Article
{
    private $id;
    private $title;
    #[ORM\OneToMany(mappedBy: 'article', targetEntity: Comment::class, cascade: ['persist', 'remove'])]
    private Collection $comments;
    public function __construct()
    {
        $this->comments = new ArrayCollection();
    }
    public function getComments(): Collection
    {
        return $this->comments;
    }
    public function addComment(Comment $comment): self
    {
        if (!$this->comments->contains($comment)) {
            $this->comments[] = $comment;
            $comment->setArticle($this);
        }
        return $this;
    }
    public function removeComment(Comment $comment): self
    {
        if ($this->comments->removeElement($comment)) {
            if ($comment->getArticle() === $this) {
                $comment->setArticle(null);
            }
        }
        return $this;
    }
}

表单类型定义

子表单类型

// src/Form/CommentType.php
namespace App\Form;
use App\Entity\Comment;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
class CommentType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('author', TextType::class, [
                'label' => '作者',
            ])
            ->add('email', EmailType::class, [
                'label' => '邮箱',
            ])
            ->add('content', TextareaType::class, [
                'label' => '评论内容',
            ]);
        // 如果子表单不需要提交,可以禁用CSRF
        // $builder->setAttribute('csrf_protection', false);
    }
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Comment::class,
        ]);
    }
}

主表单类型

// src/Form/ArticleType.php
namespace App\Form;
use App\Entity\Article;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title', TextType::class, [
                'label' => '文章标题',
            ])
            ->add('content', TextareaType::class, [
                'label' => '内容',
            ])
            ->add('comments', CollectionType::class, [
                'label' => '评论',
                'entry_type' => CommentType::class,
                'entry_options' => ['label' => false],
                'allow_add' => true,          // 允许添加新项目
                'allow_delete' => true,       // 允许删除项目
                'prototype' => true,           // 生成原型用于JavaScript
                'by_reference' => false,       // 强制使用setter方法
                'delete_empty' => true,        // 自动删除空内容
            ])
            ->add('save', SubmitType::class, [
                'label' => '保存',
            ]);
    }
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Article::class,
        ]);
    }
}

控制器处理

// src/Controller/ArticleController.php
namespace App\Controller;
use App\Entity\Article;
use App\Form\ArticleType;
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 ArticleController extends AbstractController
{
    #[Route('/article/new', name: 'article_new')]
    public function new(Request $request, EntityManagerInterface $entityManager): Response
    {
        $article = new Article();
        // 添加一个默认的空评论(可选)
        // $comment = new Comment();
        // $article->addComment($comment);
        $form = $this->createForm(ArticleType::class, $article);
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $entityManager->persist($article);
            $entityManager->flush();
            $this->addFlash('success', '文章已保存');
            return $this->redirectToRoute('article_show', ['id' => $article->getId()]);
        }
        return $this->render('article/new.html.twig', [
            'form' => $form->createView(),
        ]);
    }
    #[Route('/article/{id}/edit', name: 'article_edit')]
    public function edit(Request $request, Article $article, EntityManagerInterface $entityManager): Response
    {
        $form = $this->createForm(ArticleType::class, $article);
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $entityManager->flush();
            $this->addFlash('success', '文章已更新');
            return $this->redirectToRoute('article_show', ['id' => $article->getId()]);
        }
        return $this->render('article/edit.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}

Twig 模板

基本模板

{# templates/article/new.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
    <h1>新建文章</h1>
    {{ form_start(form) }}
        {{ form_row(form.title) }}
        {{ form_row(form.content) }}
        <h3>评论</h3>
        <ul class="comments" 
            data-index="{{ form.comments|length > 0 ? form.comments|length : 0 }}"
            data-prototype="{{ form_widget(form.comments.vars.prototype)|e('html_attr') }}">
            {% for comment in form.comments %}
                <li class="comment-item">
                    {{ form_errors(comment) }}
                    {{ form_widget(comment) }}
                    <button type="button" class="remove-comment btn btn-danger btn-sm">删除</button>
                </li>
            {% endfor %}
        </ul>
        <button type="button" class="add-comment btn btn-success">添加评论</button>
        {{ form_row(form.save) }}
    {{ form_end(form) }}
{% endblock %}
{% block javascripts %}
    {{ parent() }}
    <script>
        // 添加评论的动态JavaScript
        document.querySelector('.add-comment').addEventListener('click', function() {
            const container = document.querySelector('.comments');
            const index = parseInt(container.dataset.index);
            const prototype = container.dataset.prototype;
            const newForm = prototype.replace(/__name__/g, index);
            const item = document.createElement('li');
            item.className = 'comment-item';
            item.innerHTML = newForm;
            item.innerHTML += '<button type="button" class="remove-comment btn btn-danger btn-sm">删除</button>';
            container.appendChild(item);
            container.dataset.index = index + 1;
        });
        // 删除评论
        document.querySelector('.comments').addEventListener('click', function(e) {
            if (e.target.classList.contains('remove-comment')) {
                e.target.closest('.comment-item').remove();
            }
        });
    </script>
{% endblock %}

显示文章和评论

{# templates/article/show.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
    <h1>{{ article.title }}</h1>
    <p>{{ article.content }}</p>
    <h3>评论 ({{ article.comments|length }})</h3>
    {% for comment in article.comments %}
        <div class="comment">
            <strong>{{ comment.author }}</strong> ({{ comment.email }})
            <p>{{ comment.content }}</p>
        </div>
    {% else %}
        <p>暂无评论</p>
    {% endfor %}
{% endblock %}

高级技巧

1 动态添加子表单集合

使用 JavaScript 动态添加:

// public/js/form-collection.js
class CollectionManager {
    constructor(containerSelector, addButtonSelector) {
        this.container = document.querySelector(containerSelector);
        this.addButton = document.querySelector(addButtonSelector);
        this.prototype = this.container.dataset.prototype;
        this.index = parseInt(this.container.dataset.index) || 0;
        this.addButton.addEventListener('click', () => this.addItem());
        this.container.addEventListener('click', (e) => {
            if (e.target.classList.contains('remove-item')) {
                this.removeItem(e.target);
            }
        });
    }
    addItem() {
        const newForm = this.prototype.replace(/__name__/g, this.index);
        const item = document.createElement('div');
        item.className = 'collection-item';
        item.innerHTML = newForm;
        item.innerHTML += '<button type="button" class="remove-item btn btn-danger">删除</button>';
        this.container.appendChild(item);
        this.index++;
    }
    removeItem(button) {
        button.closest('.collection-item').remove();
    }
}

2 使用 LiveComponent (Symfony UX)

// src/Twig/Components/CommentComponent.php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
#[AsLiveComponent('comment_form')]
class CommentComponent
{
    use DefaultActionTrait;
    #[LiveProp]
    public array $comments = [];
    public function addComment(): void
    {
        $this->comments[] = ['author' => '', 'content' => ''];
    }
    public function removeComment(int $index): void
    {
        unset($this->comments[$index]);
    }
}

常见问题解决

1 表单验证错误

// 自定义验证
use Symfony\Component\Validator\Constraints as Assert;
class Comment
{
    #[Assert\NotBlank(message: '评论内容不能为空')]
    private $content;
}

2 处理空集合

// 在表单类型中设置
'delete_empty' => true, // 自动删除空数据

3 排序子表单

// 使用排序字段
use Doctrine\Common\Collections\Criteria;
// 在实体中
public function getCommentsSorted(): Collection
{
    $criteria = Criteria::create()
        ->orderBy(['createdAt' => 'ASC']);
    return $this->comments->matching($criteria);
}

完整示例

查看完整项目示例

这种结构非常适合处理:

  • 带多个子项的订单系统
  • 问题和选项的问卷系统
  • 包含多个联系人的通讯录
  • 带标签和分类的文章系统

通过合理使用 Symfony Form 组件和 CollectionType,可以构建灵活且功能完整的表单系统。

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