本文目录导读:

我来为您详细介绍Symfony表单与重建搜索的实现方案。
创建搜索表单
实体定义
// src/Entity/ArticleSearch.php
namespace App\Entity;
class ArticleSearch
{
private ?string $title = null;
private ?string $category = null;
private ?string $authorName = null;
private ?\DateTime $startDate = null;
private ?\DateTime $endDate = null;
private ?bool $isPublished = null;
private ?int $status = null;
// Getters/Setters...
}
表单类型
// src/Form/ArticleSearchType.php
namespace App\Form;
use App\Entity\ArticleSearch;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class ArticleSearchType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title', TextType::class, [
'required' => false,
'label' => '标题',
'attr' => [
'placeholder' => '输入文章标题',
'class' => 'form-control'
]
])
->add('category', ChoiceType::class, [
'required' => false,
'label' => '分类',
'choices' => [
'技术' => 'tech',
'生活' => 'life',
'教育' => 'education',
],
'placeholder' => '选择分类',
'attr' => ['class' => 'form-select']
])
->add('authorName', TextType::class, [
'required' => false,
'label' => '作者',
'attr' => [
'placeholder' => '输入作者名称',
'class' => 'form-control'
]
])
->add('startDate', DateType::class, [
'required' => false,
'label' => '开始日期',
'widget' => 'single_text',
'attr' => ['class' => 'form-control']
])
->add('endDate', DateType::class, [
'required' => false,
'label' => '结束日期',
'widget' => 'single_text',
'attr' => ['class' => 'form-control']
])
->add('isPublished', CheckboxType::class, [
'required' => false,
'label' => '已发布',
'attr' => ['class' => 'form-check-input']
])
->add('status', ChoiceType::class, [
'required' => false,
'label' => '状态',
'choices' => [
'草稿' => 0,
'审核中' => 1,
'已发布' => 2,
'已下线' => 3,
],
'placeholder' => '选择状态',
'attr' => ['class' => 'form-select']
])
->add('search', SubmitType::class, [
'label' => '搜索',
'attr' => ['class' => 'btn btn-primary']
])
->add('reset', SubmitType::class, [
'label' => '重置',
'attr' => ['class' => 'btn btn-secondary']
]);
// 添加日期验证
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
$search = $event->getData();
$form = $event->getForm();
if ($search->getStartDate() && $search->getEndDate()) {
if ($search->getStartDate() > $search->getEndDate()) {
$form->get('endDate')->addError(new FormError('结束日期不能早于开始日期'));
}
}
});
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => ArticleSearch::class,
'method' => 'GET',
'csrf_protection' => false, // GET请求通常不需要CSRF
]);
}
public function getBlockPrefix(): string
{
return ''; // 移除前缀,使URL更简洁
}
}
创建Searchable Repository Trait
// src/Repository/Traits/SearchableTrait.php
namespace App\Repository\Traits;
use Doctrine\ORM\QueryBuilder;
use App\Entity\ArticleSearch;
trait SearchableTrait
{
protected function addSearchCriteria(QueryBuilder $qb, ArticleSearch $search, string $alias = 'e'): void
{
// 标题模糊搜索
if ($title = $search->getTitle()) {
$qb->andWhere("$alias.title LIKE :title")
->setParameter('title', '%' . $title . '%');
}
// 分类精确匹配
if ($category = $search->getCategory()) {
$qb->andWhere("$alias.category = :category")
->setParameter('category', $category);
}
// 作者搜索
if ($authorName = $search->getAuthorName()) {
$qb->andWhere("$alias.authorName LIKE :authorName")
->setParameter('authorName', '%' . $authorName . '%');
}
// 日期范围
if ($startDate = $search->getStartDate()) {
$qb->andWhere("$alias.createdAt >= :startDate")
->setParameter('startDate', $startDate->format('Y-m-d'));
}
if ($endDate = $search->getEndDate()) {
$qb->andWhere("$alias.createdAt <= :endDate")
->setParameter('endDate', $endDate->format('Y-m-d 23:59:59'));
}
// 发布状态
if ($isPublished = $search->getIsPublished()) {
$qb->andWhere("$alias.isPublished = :isPublished")
->setParameter('isPublished', true);
}
// 状态
if ($status = $search->getStatus()) {
$qb->andWhere("$alias.status = :status")
->setParameter('status', $status);
}
}
}
Repository实现
// src/Repository/ArticleRepository.php
namespace App\Repository;
use App\Entity\Article;
use App\Entity\ArticleSearch;
use App\Repository\Traits\SearchableTrait;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\ORM\QueryBuilder;
class ArticleRepository extends ServiceEntityRepository
{
use SearchableTrait;
private const MAX_PER_PAGE = 10;
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Article::class);
}
public function findBySearchCriteria(ArticleSearch $search, int $page = 1): array
{
$qb = $this->createQueryBuilder('a')
->orderBy('a.createdAt', 'DESC');
$this->addSearchCriteria($qb, $search);
// 计算总记录数
$countQb = clone $qb;
$total = $countQb->select('COUNT(a.id)')
->getQuery()
->getSingleScalarResult();
// 分页
$offset = ($page - 1) * self::MAX_PER_PAGE;
$qb->setFirstResult($offset)
->setMaxResults(self::MAX_PER_PAGE);
$items = $qb->getQuery()->getResult();
return [
'items' => $items,
'total' => $total,
'page' => $page,
'maxPerPage' => self::MAX_PER_PAGE,
'totalPages' => ceil($total / self::MAX_PER_PAGE)
];
}
}
控制器实现
// src/Controller/ArticleController.php
namespace App\Controller;
use App\Entity\ArticleSearch;
use App\Form\ArticleSearchType;
use App\Repository\ArticleRepository;
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('/articles', name: 'article_index')]
public function index(Request $request, ArticleRepository $repository): Response
{
$search = new ArticleSearch();
$form = $this->createForm(ArticleSearchType::class, $search);
$form->handleRequest($request);
$page = $request->query->getInt('page', 1);
$result = [];
if ($form->isSubmitted() && $form->isValid()) {
// 处理重置按钮
if ($form->get('reset')->isClicked()) {
return $this->redirectToRoute('article_index');
}
$result = $repository->findBySearchCriteria($search, $page);
} else {
// 默认显示所有文章
$result = $repository->findBySearchCriteria(new ArticleSearch(), $page);
}
return $this->render('article/index.html.twig', [
'form' => $form->createView(),
'articles' => $result['items'] ?? [],
'pagination' => [
'currentPage' => $result['page'] ?? 1,
'totalPages' => $result['totalPages'] ?? 0,
'total' => $result['total'] ?? 0,
],
'searchActive' => $form->isSubmitted()
]);
}
}
前端模板
{# templates/article/index.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<div class="container mt-4">
<h1>文章列表</h1>
<!-- 搜索表单 -->
<div class="card mb-4">
<div class="card-header">
<button class="btn btn-link" data-bs-toggle="collapse" data-bs-target="#searchForm">
搜索条件
</button>
</div>
<div class="collapse {{ searchActive ? 'show' : '' }}" id="searchForm">
<div class="card-body">
{{ form_start(form, {'attr': {'class': 'row g-3'}}) }}
<div class="col-md-4">
{{ form_row(form.title) }}
</div>
<div class="col-md-4">
{{ form_row(form.category) }}
</div>
<div class="col-md-4">
{{ form_row(form.authorName) }}
</div>
<div class="col-md-4">
{{ form_row(form.startDate) }}
</div>
<div class="col-md-4">
{{ form_row(form.endDate) }}
</div>
<div class="col-md-4">
<div class="form-check mt-4">
{{ form_row(form.isPublished) }}
</div>
</div>
<div class="col-md-4">
{{ form_row(form.status) }}
</div>
<div class="col-12">
{{ form_row(form.search) }}
{{ form_row(form.reset) }}
</div>
{{ form_end(form) }}
</div>
</div>
</div>
<!-- 搜索结果统计 -->
{% if searchActive %}
<div class="alert alert-info">
共找到 {{ pagination.total }} 篇文章
</div>
{% endif %}
<!-- 文章列表 -->
<div class="row">
{% for article in articles %}
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ article.title }}</h5>
<p class="card-text">
<small class="text-muted">
作者: {{ article.authorName }} |
分类: {{ article.category }} |
创建时间: {{ article.createdAt|date('Y-m-d') }}
</small>
</p>
<span class="badge bg-{{ article.isPublished ? 'success' : 'warning' }}">
{{ article.isPublished ? '已发布' : '草稿' }}
</span>
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="alert alert-warning">
暂无文章
</div>
</div>
{% endfor %}
</div>
<!-- 分页 -->
{% if pagination.totalPages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% for i in 1..pagination.totalPages %}
<li class="page-item {{ pagination.currentPage == i ? 'active' : '' }}">
<a class="page-link" href="{{ path('article_index', app.request.query.all|merge({'page': i})) }}">
{{ i }}
</a>
</li>
{% endfor %}
</ul>
</nav>
{% endif %}
</div>
{% endblock %}
AJAX搜索(可选)
// assets/js/search.js
document.addEventListener('DOMContentLoaded', function() {
const searchForm = document.querySelector('form[name="article_search"]');
if (!searchForm) return;
let searchTimeout;
searchForm.addEventListener('change', function(e) {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
submitForm(1);
}, 500);
});
searchForm.addEventListener('submit', function(e) {
e.preventDefault();
submitForm(1);
});
function submitForm(page) {
const formData = new FormData(searchForm);
const params = new URLSearchParams(formData);
if (page > 1) params.set('page', page);
fetch(searchForm.action + '?' + params.toString(), {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.text())
.then(html => {
document.querySelector('.articles-container').innerHTML = html;
history.pushState({}, '', searchForm.action + '?' + params.toString());
});
}
// 分页点击事件
document.addEventListener('click', function(e) {
if (e.target.closest('.pagination a')) {
e.preventDefault();
const link = e.target.closest('.pagination a');
const url = new URL(link.href);
const page = url.searchParams.get('page') || 1;
submitForm(page);
}
});
});
性能优化建议
缓存查询结果
// src/Repository/ArticleRepository.php
use Symfony\Contracts\Cache\CacheInterface;
class ArticleRepository extends ServiceEntityRepository
{
private $cache;
public function findBySearchCriteria(ArticleSearch $search, int $page = 1): array
{
$cacheKey = $this->generateCacheKey($search, $page);
return $this->cache->get($cacheKey, function() use ($search, $page) {
// 原有的查询逻辑
// ...
});
}
private function generateCacheKey(ArticleSearch $search, int $page): string
{
return 'article_search_' . md5(serialize($search)) . '_page_' . $page;
}
}
添加索引
-- 在数据库中创建索引 CREATE INDEX idx_article_title ON article (title); CREATE INDEX idx_article_category ON article (category); CREATE INDEX idx_article_created_at ON article (created_at); CREATE INDEX idx_article_status ON article (status);
这样的实现提供了完整的搜索功能,包括表单验证、分页、重置、可选AJAX和缓存优化。