A Symfony Bundle to simplify the handling of page filters for lists/tables in admin panels.
MIT License
A Symfony Bundle to simplify the handling of page filters for lists/tables in admin panels. ๐งช
Symfony 4.x-7.x and PHP 7.4-8.0.
?search=value&otherFilterName=anotherValue
by default;After install, make sure you have the bundle registered in your symfony bundles list (config/bundles.php
):
return [
/// bundles...
Andante\PageFilterFormBundle\AndantePageFilterFormBundle::class => ['all' => true],
/// bundles...
];
This should have been done automagically if you are using Symfony Flex. Otherwise, just register it by yourself.
Let's suppose you have this common admin panel controller with a page listing some Employee
entities.
<?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,
]);
}
}
To add filters to this page, let's create a Symfony form.
<?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'
],
]);
}
}
Let's add this Form to our controller page:
<?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()
]);
}
}
The code above has some huge problems:
EmployeeFilterType
to handle filters and thissearch
, senior
and orderBy
are keys you could storeUse Andante\PageFilterFormBundle\Form\PageFilterType
as parent of your filter
form (why?) and implement target_callback
option on your form elements like
this:
<?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;
}
}
Implement Andante\PageFilterFormBundle\PageFilterFormTrait
in you controller (or inject an
argument Andante\PageFilterFormBundle\PageFilterManagerInterface
as argument) and use form like this:
<?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()
]);
}
}
โ Done!
target_callback
allows you to not repeat yourself carrying around form elements names;type: null
or callable
default: null
The callable
is going to have 3 parameters (third is optional):
Parameter | What | Mandatory | Description |
---|---|---|---|
1 | Filter $target
|
yes |
It's the second argument of createAndHandleFilter . It can be whatever you want: a query builder, an array, a collection, a object. It doesn't matter as long you match it's type with this argument sign. |
2 | form data | yes |
Equivalent to call $form->getData() on the current context. It is going to be a ?string on a TextType or a ?\DateTime on a DateTimeType
|
3 | form itself | no |
It's the current $form itself. |
You could avoid to use Andante\PageFilterFormBundle\Form\PageFilterType
as parent for your form, but be aware it sets
some useful default you may want to replicate:
Option | Value | Description |
---|---|---|
method |
GET |
You probably want filters to be part of the URL of the page, don't you? |
csrf_protection |
false |
You want the user to be able to share the URL of the page to another user without facing problems |
allow_extra_fields |
true |
Allow other URL parameters outside your form values |
andante_smart_form_attr |
true |
Enable form elements rendering wherever you want inside you page, even outside form tag while keeping them working properly (discover more). |
As long as andante_smart_form_attr
is true
, you can render your form like this:
<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
element work properly even outside form tag (how?!).
Give us a โญ!
Built with love โค๏ธ by AndanteProject team.