PHP项目Symfony form与重建搜索

wen PHP项目 2

本文目录导读:

PHP项目Symfony form与重建搜索

  1. 创建搜索表单
  2. 创建Searchable Repository Trait
  3. Repository实现
  4. 控制器实现
  5. 前端模板
  6. AJAX搜索(可选)
  7. 性能优化建议

我来为您详细介绍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和缓存优化。

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