GithubHelp home page GithubHelp logo

andanteproject / page-filter-form-bundle Goto Github PK

View Code? Open in Web Editor NEW
7.0 2.0 1.0 33 KB

A Symfony Bundle to simplify the handling of page filters for lists/tables in admin panels.

License: MIT License

PHP 100.00%
symfony symfony-bundle form symfony-form admin-panel filters php php7 php74 page-filters

page-filter-form-bundle's Introduction

Andante Project Logo

Page Filter Form Bundle

Symfony Bundle - AndanteProject

Latest Version Github actions Framework Php7 PhpStan

A Symfony Bundle to simplify the handling of page filters for lists/tables in admin panels. ๐Ÿงช

Requirements

Symfony 4.x-6.x and PHP 7.4-8.0.

Features

  • Use Symfony Form;
  • Keep you URL parameters clean as ?search=value&otherFilterName=anotherValue by default;
  • Form will work even if you render form elements outside the form tag, around the web page, exactly where you need, avoiding nested form conflicts.
  • Super easy to implement and maintains;
  • Works like magic โœจ.

How to install

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.

The problem

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:

  • ๐Ÿ‘Ž Handling all this filter logic inside the controller is not a good idea. Sure, you can move it inside a dedicated service, but this means we are creating another file class alongside EmployeeFilterType to handle filters and this is not even solving this list's the second point;
  • ๐Ÿ‘Ž You need to carry around and match form elements names. search, senior and orderBy are keys you could store in some constants to don't repeat yourself, but this will drive you crazy as the filter logic grows.

The solution with Page Filter Form

Use 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!

  • ๐Ÿ‘ Controller is clean and easy to read;
  • ๐Ÿ‘ We have just one class taking care of filters;
  • ๐Ÿ‘ The option target_callback allows you to not repeat yourself carrying around form elements names;
  • ๐Ÿ‘ You can type-hint you callable ๐Ÿฅฐ (check callback arguments);
  • ๐Ÿ‘ We got you covered solving possible nested form problems (how?);

"target_callback" option

target_callback

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.

Why use PageFilterType as from Parent

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).

Render the form in twig

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.

page-filter-form-bundle's People

Contributors

cristoforocervino avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

mustanggb

page-filter-form-bundle's Issues

Symfony 7

Any chance Symfony 7 can be enabled, I've tested and it all seems to work fine.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.