本文目录导读:

我来为您详细介绍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和缓存优化。
版权声明: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直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播