andanteproject/page-filter-form-bundle

一个简化管理面板中列表/表格页面过滤器处理的 Symfony Bundle。

1.0.6 2024-07-29 10:51 UTC

This package is auto-updated.

Last update: 2024-08-29 11:10:55 UTC


README

Andante Project Logo

页面过滤器表单Bundle

Symfony Bundle - AndanteProject

Latest Version Github actions Framework Php7 PhpStan

一个简化管理面板中列表/表格页面过滤器处理的 Symfony Bundle。🧪

需求

Symfony 4.x-7.x 和 PHP 7.4-8.0。

特性

  • 使用 Symfony Form
  • 默认保持 URL 参数清洁,如 ?search=value&otherFilterName=anotherValue
  • 即使你在网页中 表单标签外部 渲染表单元素,也可以使表单工作,正好满足你的需求,避免嵌套表单冲突
  • 易于实现和维护;
  • 效果如同魔法 ✨。

如何安装

安装后,确保你的 Bundle 已在 symfony bundles 列表中注册(config/bundles.php

return [
    /// bundles...
    Andante\PageFilterFormBundle\AndantePageFilterFormBundle::class => ['all' => true],
    /// bundles...
];

如果你使用的是 Symfony Flex,这将自动完成。否则,请自行注册。

问题

假设你有一个常见的行政面板控制器,其中有一个页面列出了一些 Employee 实体。

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use App\Repository\EmployeeRepository;
use Knp\Component\Pager\PaginatorInterface;

class EmployeeController extends AbstractController{
    
    public function index(Request $request, EmployeeRepository $employeeRepository, PaginatorInterface $paginator){
        /** @var Doctrine\ORM\QueryBuilder $qb */
        $qb = $employeeRepository->getFancyQueryBuilderLogic('employee');
        
        $employees = $paginator->paginate($qb, $request);
        return $this->render('admin/employee/index.html.twig', [
            'employees' => $employees,
        ]);
    }
}

要给这个页面添加过滤器,让我们创建一个 Symfony 表单。

<?php

namespace App\Form\Admin;

use Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class EmployeeFilterType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('search', Type\SearchType::class);
        $builder->add('senior', Type\CheckboxType::class);
        $builder->add('orderBy', Type\ChoiceType::class, [
            'choices' => [
                'name' => 'name',
                'age' => 'birthday'     
            ],
        ]);
    }
}

让我们将这个表单添加到我们的控制器页面

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use App\Repository\EmployeeRepository;
use Knp\Component\Pager\PaginatorInterface;
use App\Form\Admin\EmployeeFilterType;
 
class EmployeeController extends AbstractController{
    
    public function index(Request $request, EmployeeRepository $employeeRepository, PaginatorInterface $paginator){
        /** @var Doctrine\ORM\QueryBuilder $qb */
        $qb = $employeeRepository->getFancyQueryBuilderLogic('employee');
        
        $form = $this->createForm(EmployeeFilterType::class);
        $form->handleRequest($request);
        
        if($form->isSubmitted() && $form->isValid()){
            $qb->expr()->like('employee.name',':name');
            $qb->setParameter('name', $form->get('search')->getData());
            
            $qb->expr()->like('employee.senior',':senior');
            $qb->setParameter('senior', $form->get('senior')->getData());
            
            $qb->orderBy('employee.'. $form->get('orderBy')->getData(), 'asc');
            
            // Don't you see the problem here?
        }
        
        $employees = $paginator->paginate($qb, $request);
        return $this->render('admin/employee/index.html.twig', [
            'employees' => $employees,
            'form' => $form->createView()
        ]);
    }
}

上面的代码有一些严重问题

  • 👎 在控制器中处理所有这些过滤逻辑不是一个好主意。当然,你可以将其移动到一个专用服务中,但这意味着我们需要创建一个与 EmployeeFilterType 一起的另一个文件类来处理过滤,但这甚至没有解决这个列表的第二个问题;
  • 👎 你需要携带和匹配表单元素名称。如 searchseniororderBy 这样的键可以存储在某个常量中以避免重复,但随着过滤逻辑的增长,这会让你发疯。

使用页面过滤器表的解决方案

Andante\PageFilterFormBundle\Form\PageFilterType 作为你的过滤表单的父类(为什么?)并在你的表单元素上实现 target_callback 选项,如下所示

<?php

namespace App\Form\Admin;

use Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Andante\PageFilterFormBundle\Form\PageFilterType;
use Doctrine\ORM\QueryBuilder;

class EmployeeFilterType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('search', Type\SearchType::class, [
            'target_callback' => function(QueryBuilder $qb, ?string $searchValue):void {
                $qb->expr()->like('employee.name',':name'); // Don't want to guess for entity alias "employee"?
                $qb->setParameter('name', $searchValue);    // Check andanteproject/shared-query-builder
            }
        ]);
        $builder->add('senior', Type\CheckboxType::class, [
            'target_callback' => function(QueryBuilder $qb, bool $seniorValue):void {
                $qb->expr()->like('employee.senior',':senior');
                $qb->setParameter('senior', $seniorValue);
            }
        ]);
        $builder->add('orderBy', Type\ChoiceType::class, [
            'choices' => [
                'name' => 'name',
                'age' => 'birthday'     
            ],
            'target_callback' => function(QueryBuilder $qb, string $orderByValue):void {
                $qb->orderBy('employee.'. $orderByValue, 'asc');
            }
        ]);
    }
    public function getParent() : string
    {
        return PageFilterType::class;
    }
}

在你的控制器中实现 Andante\PageFilterFormBundle\PageFilterFormTrait(或注入一个 Andante\PageFilterFormBundle\PageFilterManagerInterface 参数)并像这样使用表单

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use App\Repository\EmployeeRepository;
use Knp\Component\Pager\PaginatorInterface;
use App\Form\Admin\EmployeeFilterType;
use Andante\PageFilterFormBundle\PageFilterFormTrait;

class EmployeeController extends AbstractController{

    use PageFilterFormTrait;
    
    public function index(Request $request, EmployeeRepository $employeeRepository, PaginatorInterface $paginator){
        /** @var Doctrine\ORM\QueryBuilder $qb */
        $qb = $employeeRepository->getFancyQueryBuilderLogic('employee');
        
        $form = $this->createAndHandleFilter(EmployeeFilterType::class, $qb, $request);
        
        $employees = $paginator->paginate($qb, $request);
        return $this->render('admin/employee/index.html.twig', [
            'employees' => $employees,
            'form' => $form->createView()
        ]);
    }
}

✅ 完成!

  • 👍 控制器干净且易于阅读;
  • 👍 只有一个类负责处理过滤;
  • 👍 选项 target_callback 允许你避免携带表单元素名称;
  • 👍 你可以对可调用参数进行类型提示 🥰(检查回调参数);
  • 👍 我们解决了可能的嵌套表单问题(如何?);

"target_callback" 选项

target_callback

类型: nullcallable 默认值: null

callable 将有 3 个参数(第三个是可选的)

为什么使用 PageFilterType 作为表单的父类

您可以选择不使用 Andante\PageFilterFormBundle\Form\PageFilterType 作为表单的父类,但请注意它设置了一些您可能希望复制的有用默认值。

在 twig 中渲染表单

只要 andante_smart_form_attr 设置为 true,您就可以这样渲染表单

<div class="header-filters">
    {{ form_start(form) }} {# id="list_filter" #}
        {{ form_errors(form) }}
        {{ form_row(form.search) }}
        {{ form_row(form.orderBy) }}
    {{ form_end(form, {'render_rest': false}) }}
</div>

<!-- -->
<!-- Some other HTML content, like a table or even another Symfony form -->
<!-- -->

<div class="footer-filters">
    {{ form_row(form.orderBy) }} {# has attribute form="list_filter" #}
</div>

form.perPage 元素即使在表单标签外部也能正常工作。(这是怎么回事?!

给我们一颗 ⭐!

AndanteProject 团队用爱 ❤️ 构建而成。