{"id":15026723,"url":"https://github.com/andanteproject/page-filter-form-bundle","last_synced_at":"2025-04-09T20:21:43.273Z","repository":{"id":56947633,"uuid":"347158952","full_name":"andanteproject/page-filter-form-bundle","owner":"andanteproject","description":"A Symfony Bundle to simplify the handling of page filters for lists/tables in admin panels.","archived":false,"fork":false,"pushed_at":"2024-07-29T10:54:26.000Z","size":52,"stargazers_count":10,"open_issues_count":1,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-23T22:13:32.563Z","etag":null,"topics":["admin-panel","admin-panels","filter-form","filters","form","page-filters","php","php7","php74","symfony","symfony-bundle","symfony-form"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andanteproject.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-03-12T18:22:51.000Z","updated_at":"2025-02-01T09:17:37.000Z","dependencies_parsed_at":"2025-02-15T20:33:02.212Z","dependency_job_id":"db644681-7e68-473b-b712-d20e9b993d42","html_url":"https://github.com/andanteproject/page-filter-form-bundle","commit_stats":{"total_commits":6,"total_committers":3,"mean_commits":2.0,"dds":"0.33333333333333337","last_synced_commit":"692ca354a6cc4f6f9d30f6210bcd8aa8e74bca8e"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andanteproject%2Fpage-filter-form-bundle","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andanteproject%2Fpage-filter-form-bundle/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andanteproject%2Fpage-filter-form-bundle/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andanteproject%2Fpage-filter-form-bundle/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andanteproject","download_url":"https://codeload.github.com/andanteproject/page-filter-form-bundle/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247809962,"owners_count":20999816,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["admin-panel","admin-panels","filter-form","filters","form","page-filters","php","php7","php74","symfony","symfony-bundle","symfony-form"],"created_at":"2024-09-24T20:04:57.797Z","updated_at":"2025-04-09T20:21:43.222Z","avatar_url":"https://github.com/andanteproject.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Andante Project Logo](https://github.com/andanteproject/page-filter-form-bundle/blob/main/andanteproject-logo.png?raw=true)\n\n# Page Filter Form Bundle\n\n#### Symfony Bundle - [AndanteProject](https://github.com/andanteproject)\n\n[![Latest Version](https://img.shields.io/github/release/andanteproject/page-filter-form-bundle.svg)](https://github.com/andanteproject/page-filter-form-bundle/releases)\n![Github actions](https://github.com/andanteproject/page-filter-form-bundle/actions/workflows/workflow.yml/badge.svg?branch=main)\n![Framework](https://img.shields.io/badge/Symfony-4.x|5.x|6.x|7.x-informational?Style=flat\u0026logo=symfony)\n![Php7](https://img.shields.io/badge/PHP-%207.4|8.x-informational?style=flat\u0026logo=php)\n![PhpStan](https://img.shields.io/badge/PHPStan-Level%208-syccess?style=flat\u0026logo=php)\n\nA Symfony Bundle to simplify the handling of page filters for lists/tables in admin panels. 🧪\n\n## Requirements\n\nSymfony 4.x-7.x and PHP 7.4-8.0.\n\n## Features\n\n- Use [Symfony Form](https://symfony.com/doc/current/forms.html);\n- Keep you URL parameters clean as `?search=value\u0026otherFilterName=anotherValue` by default;\n- Form will work even if you render form elements **outside the form tag**, around the web page, exactly where you need,\n  **avoiding nested form conflicts**.\n- Super easy to implement and maintains;\n- Works like magic ✨.\n\n## How to install\n\nAfter [install](#install), make sure you have the bundle registered in your symfony bundles list (`config/bundles.php`):\n\n```php\nreturn [\n    /// bundles...\n    Andante\\PageFilterFormBundle\\AndantePageFilterFormBundle::class =\u003e ['all' =\u003e true],\n    /// bundles...\n];\n```\n\nThis should have been done automagically if you are using [Symfony Flex](https://symfony.com/components/Symfony%20Flex). Otherwise, just\nregister it by yourself.\n\n## The problem\n\nLet's suppose you have this common admin panel controller with a page listing some `Employee` entities.\n\n```php\n\u003c?php\n\nnamespace App\\Controller;\n\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse App\\Repository\\EmployeeRepository;\nuse Knp\\Component\\Pager\\PaginatorInterface;\n\nclass EmployeeController extends AbstractController{\n    \n    public function index(Request $request, EmployeeRepository $employeeRepository, PaginatorInterface $paginator){\n        /** @var Doctrine\\ORM\\QueryBuilder $qb */\n        $qb = $employeeRepository-\u003egetFancyQueryBuilderLogic('employee');\n        \n        $employees = $paginator-\u003epaginate($qb, $request);\n        return $this-\u003erender('admin/employee/index.html.twig', [\n            'employees' =\u003e $employees,\n        ]);\n    }\n}\n```\n\nTo add filters to this page, let's create a Symfony form.\n\n```php\n\u003c?php\n\nnamespace App\\Form\\Admin;\n\nuse Symfony\\Component\\Form\\Extension\\Core\\Type;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\n\nclass EmployeeFilterType extends AbstractType\n{\n    public function buildForm(FormBuilderInterface $builder, array $options): void\n    {\n        $builder-\u003eadd('search', Type\\SearchType::class);\n        $builder-\u003eadd('senior', Type\\CheckboxType::class);\n        $builder-\u003eadd('orderBy', Type\\ChoiceType::class, [\n            'choices' =\u003e [\n                'name' =\u003e 'name',\n                'age' =\u003e 'birthday'     \n            ],\n        ]);\n    }\n}\n```\n\nLet's add this Form to our controller page:\n\n```php\n\u003c?php\n\nnamespace App\\Controller;\n\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse App\\Repository\\EmployeeRepository;\nuse Knp\\Component\\Pager\\PaginatorInterface;\nuse App\\Form\\Admin\\EmployeeFilterType;\n \nclass EmployeeController extends AbstractController{\n    \n    public function index(Request $request, EmployeeRepository $employeeRepository, PaginatorInterface $paginator){\n        /** @var Doctrine\\ORM\\QueryBuilder $qb */\n        $qb = $employeeRepository-\u003egetFancyQueryBuilderLogic('employee');\n        \n        $form = $this-\u003ecreateForm(EmployeeFilterType::class);\n        $form-\u003ehandleRequest($request);\n        \n        if($form-\u003eisSubmitted() \u0026\u0026 $form-\u003eisValid()){\n            $qb-\u003eexpr()-\u003elike('employee.name',':name');\n            $qb-\u003esetParameter('name', $form-\u003eget('search')-\u003egetData());\n            \n            $qb-\u003eexpr()-\u003elike('employee.senior',':senior');\n            $qb-\u003esetParameter('senior', $form-\u003eget('senior')-\u003egetData());\n            \n            $qb-\u003eorderBy('employee.'. $form-\u003eget('orderBy')-\u003egetData(), 'asc');\n            \n            // Don't you see the problem here?\n        }\n        \n        $employees = $paginator-\u003epaginate($qb, $request);\n        return $this-\u003erender('admin/employee/index.html.twig', [\n            'employees' =\u003e $employees,\n            'form' =\u003e $form-\u003ecreateView()\n        ]);\n    }\n}\n```\n\nThe code above has some huge problems:\n\n- 👎 Handling all this filter logic inside the controller is not a good idea. Sure, you can move it inside a dedicated\n  service, but this means we are creating another file class alongside `EmployeeFilterType` to handle filters and this\n  is not even solving this list's the second point;\n- 👎 You need to carry around and match form elements names. `search`, `senior` and `orderBy` are keys you could store\n  in some constants to don't repeat yourself, but this will drive you crazy as the filter logic grows.\n\n## The solution with Page Filter Form\n\nUse `Andante\\PageFilterFormBundle\\Form\\PageFilterType` as parent of your filter\nform ([why?](#why-use-pagefiltertype-as-from-parent)) and implement `target_callback` option on your form elements like\nthis:\n\n```php\n\u003c?php\n\nnamespace App\\Form\\Admin;\n\nuse Symfony\\Component\\Form\\Extension\\Core\\Type;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\nuse Andante\\PageFilterFormBundle\\Form\\PageFilterType;\nuse Doctrine\\ORM\\QueryBuilder;\n\nclass EmployeeFilterType extends AbstractType\n{\n    public function buildForm(FormBuilderInterface $builder, array $options): void\n    {\n        $builder-\u003eadd('search', Type\\SearchType::class, [\n            'target_callback' =\u003e function(QueryBuilder $qb, ?string $searchValue):void {\n                $qb-\u003eexpr()-\u003elike('employee.name',':name'); // Don't want to guess for entity alias \"employee\"?\n                $qb-\u003esetParameter('name', $searchValue);    // Check andanteproject/shared-query-builder\n            }\n        ]);\n        $builder-\u003eadd('senior', Type\\CheckboxType::class, [\n            'target_callback' =\u003e function(QueryBuilder $qb, bool $seniorValue):void {\n                $qb-\u003eexpr()-\u003elike('employee.senior',':senior');\n                $qb-\u003esetParameter('senior', $seniorValue);\n            }\n        ]);\n        $builder-\u003eadd('orderBy', Type\\ChoiceType::class, [\n            'choices' =\u003e [\n                'name' =\u003e 'name',\n                'age' =\u003e 'birthday'     \n            ],\n            'target_callback' =\u003e function(QueryBuilder $qb, string $orderByValue):void {\n                $qb-\u003eorderBy('employee.'. $orderByValue, 'asc');\n            }\n        ]);\n    }\n    public function getParent() : string\n    {\n        return PageFilterType::class;\n    }\n}\n```\n\nImplement `Andante\\PageFilterFormBundle\\PageFilterFormTrait` in you controller (or inject an\nargument `Andante\\PageFilterFormBundle\\PageFilterManagerInterface` as argument) and use form like this:\n\n```php\n\u003c?php\n\nnamespace App\\Controller;\n\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse App\\Repository\\EmployeeRepository;\nuse Knp\\Component\\Pager\\PaginatorInterface;\nuse App\\Form\\Admin\\EmployeeFilterType;\nuse Andante\\PageFilterFormBundle\\PageFilterFormTrait;\n\nclass EmployeeController extends AbstractController{\n\n    use PageFilterFormTrait;\n    \n    public function index(Request $request, EmployeeRepository $employeeRepository, PaginatorInterface $paginator){\n        /** @var Doctrine\\ORM\\QueryBuilder $qb */\n        $qb = $employeeRepository-\u003egetFancyQueryBuilderLogic('employee');\n        \n        $form = $this-\u003ecreateAndHandleFilter(EmployeeFilterType::class, $qb, $request);\n        \n        $employees = $paginator-\u003epaginate($qb, $request);\n        return $this-\u003erender('admin/employee/index.html.twig', [\n            'employees' =\u003e $employees,\n            'form' =\u003e $form-\u003ecreateView()\n        ]);\n    }\n}\n```\n\n✅ Done!\n\n- 👍 Controller is clean and easy to read;\n- 👍 We have just one class taking care of filters;\n- 👍 The option `target_callback` allows you to not repeat yourself carrying around form elements names;\n- 👍 You can type-hint you callable 🥰 ([check callback arguments](#target_callback-option));\n- 👍 We got you covered solving possible nested form problems ([how?](#render-the-form-in-twig));\n\n### \"target_callback\" option\n\n#### target_callback\n\n**type**: `null` or `callable` **default**: `null`\n\nThe `callable` is going to have 3 parameters (third is optional):\n\n| Parameter | What | Mandatory | Description |\n| --------- | ---- | --------- | ----------- |\n| 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. |\n| 2 | form data | `yes` | Equivalent to call `$form-\u003egetData()` on the current context. It is going to be a `?string` on a `TextType` or a `?\\DateTime` on a `DateTimeType` |\n| 3 | form itself | `no` | It's the current `$form` itself. | \n\n### Why use PageFilterType as from Parent\n\nYou could avoid to use `Andante\\PageFilterFormBundle\\Form\\PageFilterType` as parent for your form, but be aware it sets\nsome useful default you may want to replicate:\n\n| Option | Value | Description |\n| --- | --- | --- |\n| `method` | `GET` | You probably want filters to be part of the URL of the page, don't you? |\n| `csrf_protection` | `false` | You want the user to be able to share the URL of the page to another user without facing problems |\n| `allow_extra_fields` | `true` | Allow other URL parameters outside your form values |\n| `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](https://www.w3schools.com/html/html_form_attributes_form.asp)). |\n\n### Render the form in twig\n\nAs long as `andante_smart_form_attr` is `true`, you can render your form like this:\n\n```twig\n\u003cdiv class=\"header-filters\"\u003e\n    {{ form_start(form) }} {# id=\"list_filter\" #}\n        {{ form_errors(form) }}\n        {{ form_row(form.search) }}\n        {{ form_row(form.orderBy) }}\n    {{ form_end(form, {'render_rest': false}) }}\n\u003c/div\u003e\n\n\u003c!-- --\u003e\n\u003c!-- Some other HTML content, like a table or even another Symfony form --\u003e\n\u003c!-- --\u003e\n\n\u003cdiv class=\"footer-filters\"\u003e\n    {{ form_row(form.orderBy) }} {# has attribute form=\"list_filter\" #}\n\u003c/div\u003e\n```\n✅ `form.perPage` element work properly even outside form tag ([how?!](https://www.w3schools.com/html/html_form_attributes_form.asp)).\n\nGive us a ⭐!\n\nBuilt with love ❤️ by [AndanteProject](https://github.com/andanteproject) team.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandanteproject%2Fpage-filter-form-bundle","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandanteproject%2Fpage-filter-form-bundle","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandanteproject%2Fpage-filter-form-bundle/lists"}