├── .composer └── .gitkeep ├── .gitignore ├── .php-cs-fixer.dist.php ├── .travis.yml ├── LICENSE ├── README.md ├── UPGRADE-2.0.md ├── composer.json ├── composer.lock ├── doc └── custom_cell_template.md ├── phpunit.xml.dist ├── prepare-tests.sh ├── run-tests.sh ├── scripts └── composer.sh ├── src ├── Api │ ├── AbstractResult.php │ ├── ApiInterface.php │ ├── ResultInterface.php │ └── StandardResult.php ├── Components │ ├── AbstractTable.php │ ├── ApiTable.php │ ├── Column.php │ ├── Filter.php │ ├── FilterCheckbox.php │ ├── FilterDate.php │ ├── FilterSelect.php │ ├── MassAction.php │ ├── Table.php │ └── TableInterface.php ├── DependencyInjection │ ├── Configuration.php │ └── KilikTableExtension.php ├── KilikTableBundle.php ├── Resources │ ├── config │ │ └── services.yml │ ├── public │ │ ├── css │ │ │ └── KilikTable.css │ │ └── js │ │ │ └── KilikTable.js │ ├── translations │ │ ├── messages.de.yml │ │ ├── messages.en.yml │ │ ├── messages.es.yml │ │ ├── messages.fr.yml │ │ └── messages.nl.yml │ └── views │ │ ├── _blocks.html.twig │ │ ├── _columnCell.html.twig │ │ ├── _columnCellNoTable.html.twig │ │ ├── _columnFilter.html.twig │ │ ├── _columnFilterNoTable.html.twig │ │ ├── _columnName.html.twig │ │ ├── _columnNameNoTable.html.twig │ │ ├── _condensedTable.html.twig │ │ ├── _defaultTable.html.twig │ │ ├── _defaultTableAlt.html.twig │ │ ├── _defaultTableSimple.html.twig │ │ ├── _formLeftNoTable.html.twig │ │ ├── _pagination.html.twig │ │ ├── _paginationNumbers.html.twig │ │ ├── _paginationNumbersIcons.html.twig │ │ ├── _rowsPerPage.html.twig │ │ ├── _setup.html.twig │ │ ├── _stats.html.twig │ │ ├── layout.html.twig │ │ └── theme │ │ └── dark4 │ │ ├── README.md │ │ ├── components │ │ ├── columnName.html.twig │ │ ├── pagination.html.twig │ │ ├── paginationNumbers.html.twig │ │ ├── paginationNumbersIcons.html.twig │ │ └── setup.html.twig │ │ ├── doc │ │ ├── alternative.png │ │ └── default.png │ │ └── tables │ │ ├── alternative.html.twig │ │ └── default.html.twig └── Services │ ├── AbstractTableService.php │ ├── TableApiService.php │ ├── TableService.php │ └── TableServiceInterface.php └── tests ├── Components ├── ColumnTest.php ├── FilterDateTest.php └── TableTest.php └── Services └── TableServiceTest.php /.composer/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilikFr/TableBundle/7e01d148c9fe3d2be1183042adc46c622609eeeb/.composer/.gitkeep -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /.composer/* 4 | !/.composer/.gitkeep 5 | /.phpunit.result.cache 6 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in(['src/', 'tests/']) 5 | ; 6 | 7 | return (new PhpCsFixer\Config()) 8 | ->setRules([ 9 | '@Symfony' => true, 10 | '@Symfony:risky' => true, 11 | 'combine_consecutive_unsets' => true, 12 | 'no_superfluous_phpdoc_tags' => true, 13 | 'phpdoc_separation' => false, 14 | 'phpdoc_types_order' => false, 15 | 'native_function_invocation' => false, 16 | 'single_line_throw' => false, 17 | 'heredoc_to_nowdoc' => true, 18 | 'no_extra_blank_lines' => ['tokens' => [ 19 | 'break', 'continue', 'extra', 'return', 'throw', 'use', 20 | 'parenthesis_brace_block', 'square_brace_block', 'curly_brace_block', 21 | ]], 22 | 'no_unreachable_default_argument_value' => true, 23 | 'no_useless_else' => true, 24 | 'no_useless_return' => true, 25 | 'ordered_class_elements' => true, 26 | 'ordered_imports' => true, 27 | 'phpdoc_order' => true, 28 | 'psr_autoloading' => true, 29 | ]) 30 | ->setUsingCache(false) 31 | ->setRiskyAllowed(true) 32 | ->setFinder($finder) 33 | ; 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: bash 4 | 5 | services: 6 | - docker 7 | 8 | script: 9 | - ./prepare-tests.sh 10 | - ./scripts/composer.sh validate 11 | - ./run-tests.sh 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kilik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | README 2 | ====== 3 | 4 | What's KilikTableBundle ? 5 | -------------------------- 6 | KilikTableBundle is a fast, modern, and easy-to-use way to manipulate paginated 7 | information, with filtering and ordering features, with ajax queries. 8 | 9 | This bundle is a work in progress. 10 | 11 | Links: 12 | ------ 13 | - [Live demo](http://tabledemo.kilik.fr/) 14 | - [KilikTableDemoBundle](https://github.com/KilikFr/TableDemoBundle) 15 | 16 | Working features: 17 | ----------------- 18 | - pagination 19 | - basic filtering (like %...%) 20 | - advanced filtering (<,>,<=,>=,=,!,!=) 21 | - ordering by column (and reverse) 22 | - basic table template extendable 23 | - keep filters and orders in browser local storage (api REST) 24 | - filtering on queries with group by 25 | - show ordered column (normal and reverse) 26 | - max items per page selector (customizable) 27 | - delay on keyup events (to prevent multiple reloads) 28 | - checkbox and select filter 29 | - CSV export of filtered rows 30 | - customization of visible columns (hide/show checkboxes) 31 | - column display colum cells with callback 32 | - [custom display colum cells with template](doc/custom_cell_template.md) 33 | - multiple lists on one page 34 | - pre-load default filters and reset local storage filters 35 | - smart filtering on many words (Filter::TYPE_LIKE_WORDS_AND) 36 | - (beta) support api calls to load resources via web services 37 | 38 | Planned features: 39 | ------------------ 40 | - more translations 41 | - add advanced templates 42 | 43 | Installation 44 | ------------ 45 | ```sh 46 | composer require kilik/table 47 | ``` 48 | 49 | Patch your AppKernel.php (symfony <4): 50 | ```php 51 | class AppKernel extends Kernel 52 | { 53 | public function registerBundles() 54 | { 55 | $bundles = [ 56 | // ... 57 | new \Kilik\TableBundle\KilikTableBundle(), 58 | ]; 59 | 60 | // ... 61 | } 62 | } 63 | ``` 64 | 65 | Patch your AppKernel.php (symfony >=4): 66 | 67 | ```php 68 | ['all' => true], 72 | ]; 73 | ``` 74 | 75 | 76 | Install assets 77 | ```sh 78 | ./bin/console assets:install --symlink 79 | ``` 80 | 81 | And create your first list: 82 | 83 | Feature disabled on 1.0 branch (symfony 4 compatibility WIP) 84 | 85 | ```sh 86 | ./bin/console kilik:table:generate 87 | ``` 88 | 89 | (With default parameters, your list is available here http://localhost/yourcontroller/list) 90 | 91 | Usage 92 | ----- 93 | 94 | This documentation need to be completed. 95 | 96 | Here, some examples to show latest features. 97 | 98 | Optimized version to load entities, from Repository Name: 99 | 100 | ```php 101 | $table = (new Table()) 102 | // ... 103 | ->setEntityLoaderRepository("KilikDemoBundle:Product") 104 | // ... 105 | ``` 106 | 107 | Optimized version to load entities, from Callback method (Eager loading): 108 | 109 | ```php 110 | $table = (new Table()) 111 | // ... 112 | ->setEntityLoaderCallback(function($ids) { 113 | return $this->manager()->getRepository('KilikDemoBundle:Product')->findById($ids); 114 | }) 115 | // ... 116 | ``` 117 | 118 | ### Mass actions 119 | 120 | Define a mass action for list 121 | 122 | ```php 123 | 124 | $massAction = new MassAction('delete', 'Delete selected items'); 125 | // First parameter 'delete' must not contain space or special characters (identifier) 126 | $massAction->setAction('path/to/my-form-action.php'); 127 | 128 | $table = (new Table()) 129 | // ... 130 | ->addMassAction($massAction) 131 | // ... 132 | 133 | // Then your form action, you can grab selected rows as entities 134 | $selectedEntities = $this->get('kilik_table') 135 | ->getSelectedRows($request, $this->getTable()); 136 | 137 | foreach ($selectedEntities as $entity) { 138 | // ... 139 | $entity->doSomething(); 140 | // ... 141 | } 142 | ``` 143 | 144 | If mass action does not have a specified action, a javascript event is fired. 145 | You can get all rows checked as following : 146 | 147 | ```javascript 148 | $("#table_id").on('kilik:massAction', function (e, detail) { 149 | if (detail.checked.length === 0) return false; 150 | if (detail.action === 'delete') { 151 | //... 152 | } 153 | }); 154 | ``` 155 | 156 | ### Events / Listeners 157 | 158 | * `kilik:init:start` jQuery event when table init process starts 159 | 160 | ```javascript 161 | $(document).on('kilik:init:start', function(event, table) { 162 | // Do something with event or table object 163 | }); 164 | ``` 165 | 166 | * `kilik:init:end` jQuery event when table init process ends 167 | 168 | ```javascript 169 | $(document).on('kilik:init:start', function(event, table) { 170 | // Do something with event or table object 171 | }); 172 | ``` 173 | 174 | ### Autoload Kilik Tables 175 | 176 | A new twig block provide metadata information about table so you can autoload it if necessary without any javascript in your twig template. 177 | 178 | ```html 179 | {% block tableMetadata %} 180 |
{{ table.options | json_encode | raw }}
181 | {% endblock tableMetadata %} 182 | ``` 183 | 184 | You can access table configurations from HTML attributes with jQuery, see the example : 185 | 186 | ```javascript 187 | var loadKiliktables = function() { 188 | var $kilikTables = $("[data-kiliktable-id]"); 189 | if ($kilikTables && $kilikTables.length > 0) { 190 | $kilikTables.each(function(index, currentTable){ 191 | var $currentTable = $(currentTable); 192 | var id = $currentTable.data("kiliktable-id"); 193 | if (id.length > 0) { 194 | var path = $currentTable.data("kiliktable-path"); 195 | var options = $currentTable.html(); 196 | new KilikTableFA(id, path, JSON.parse(options)).init(); 197 | } 198 | }); 199 | } 200 | } 201 | ``` 202 | 203 | ### Bootstrap 4 204 | 205 | Note: WIP on Bootstrap 4 (with Font Awesome) integration, use new JS function: 206 | 207 | ```javascript 208 | $(document).ready(function () { 209 | var table = new KilikTableFA("{{ table.id }}", "{{ table.path }}", JSON.parse('{{ table.options | json_encode |raw }}')); 210 | table.init(); 211 | }); 212 | ``` 213 | 214 | ### Use other storage for table filters 215 | 216 | If you want to use a custom storage for table filters (Eg. Session). 217 | 218 | ```php 219 | // Disable using javascript local storage form filters 220 | public function getTable() 221 | { 222 | return (new Table())->setSkipLoadFilterFromLocalStorage(true); 223 | } 224 | 225 | // On ajax action : store filters data 226 | public function _list(Request $request) 227 | { 228 | $table = $this->getTable(); 229 | $response = $this->get('kilik_table')->handleRequest($table, $request); 230 | 231 | // Handle request for table form 232 | $this->kilik->createFormView($table); 233 | $table->getForm()->handleRequest($request); 234 | $data = $table->getForm()->getData(); 235 | 236 | $this->filterStorage->store($data); // Use your custom storage 237 | 238 | return $response; 239 | } 240 | 241 | 242 | // On default action 243 | public function list() 244 | { 245 | $table = $this->getTable(); 246 | $data = $this->filterStorage->get(); 247 | 248 | return $this->render('list.html.twig', array( 249 | 'table' => $this->kilik->createFormView($table, $data), 250 | )); 251 | } 252 | 253 | ``` 254 | 255 | ### Customize filled filters 256 | 257 | When a filter is filled, class table-filter-filled is added on field. By default, no style is applied, but you can override it to fit your needs : 258 | 259 | ```css 260 | .table-filter-filled { 261 | ... 262 | } 263 | ``` 264 | 265 | ### Filter date columns 266 | 267 | ```php 268 | $table 269 | ->addColumn( 270 | (new Column()) 271 | ->setSort(['u.createdAt' => 'asc']) 272 | ->setDisplayFormat(Column::FORMAT_DATE) 273 | ->setDisplayFormatParams('d/m/Y H:i:s') // or for example FilterDate::INPUT_FORMAT_LITTLE_ENDIAN 274 | ->setFilter((new FilterDate()) 275 | ->setName('u_createdAt') 276 | ->setField('u.createdAt') 277 | ->setInputFormat(FilterDate::INPUT_FORMAT_LITTLE_ENDIAN) 278 | ) 279 | ) 280 | ; 281 | ``` 282 | 283 | Users can filter this data using various operators, for example : 284 | - `26/02/1802` or `=26/02/1802` : expects a specific day 285 | - `!=21/11/1694` : expects any day except 21 November 1694 286 | - `>26/02/1802 18:00` : expects specific day after 18:00 and without end limit 287 | - `>=02/1802` : expects in february 1802 and after 288 | - `<2024` : expects in 2023 and before 289 | - `<=26/02/1802 15` : expects 26 February 1802 at 3pm or earlier 290 | - `=` : expects date is NULL 291 | - `!=` : expects date is not NULL 292 | 293 | 294 | For bundle developpers 295 | ====================== 296 | 297 | ```shell 298 | # prepare tests 299 | ./prepare-tests.sh 300 | 301 | # run tests 302 | ./run-tests.sh 303 | 304 | # launch composer 305 | ./scripts/composer.sh 306 | ``` 307 | -------------------------------------------------------------------------------- /UPGRADE-2.0.md: -------------------------------------------------------------------------------- 1 | # Upgrade from 1.x to 2.x 2 | 3 | * Php >= 7.2 4 | * Symfony >= 4.x required 5 | * Twig_Environment now replaced by Twig\Environment in TableService dependencies injection 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kilik/table", 3 | "description": "Symfony Ajax Datagrid Bundle for doctrine entities", 4 | "keywords": ["symfony","jquery","table"], 5 | "homepage": "https://github.com/KilikFr/TableBundle", 6 | "license": "MIT", 7 | "type": "symfony-bundle", 8 | "authors": [ 9 | { 10 | "name": "Michel Naud", 11 | "email": "mitch@kilik.fr", 12 | "role": "Author" 13 | } 14 | ], 15 | "require": { 16 | "php": "^7.4||^8.0", 17 | "ext-json": "*", 18 | "twig/twig": "^1.0||^2.0||^3.0", 19 | "doctrine/orm": "^2.5|^3.2", 20 | "doctrine/doctrine-bundle": "~1.0||~2.0", 21 | "symfony/form": "^4.0||^5.0||^6.0||^7.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Kilik\\TableBundle\\": "src" 26 | } 27 | }, 28 | "require-dev": { 29 | "symfony/phpunit-bridge": "^5.0||^6.0||^7.0" 30 | }, 31 | "config": { 32 | "allow-plugins": { 33 | "composer/package-versions-deprecated": true 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /doc/custom_cell_template.md: -------------------------------------------------------------------------------- 1 | # Custom Cell Template 2 | 3 | ## How to use custom cell template on a column ? 4 | 5 | This column is rendered with a custom template: 6 | 7 | ![image](https://user-images.githubusercontent.com/10455897/127518818-59b57ac5-db70-445d-9623-a1862819cb81.png) 8 | 9 | In this example, we want to display a formatted date and the time difference (with a twig filter named "ago"). 10 | 11 | ## How to ? 12 | 13 | - use setCellTemplate method (on Column object) 14 | - define a custom template (wich extends or replace @KilikTable/_columnCell.html.twig) 15 | 16 | **Controller** 17 | 18 | ```php 19 | $table->addColumn( 20 | (new Column()) 21 | ->setLabel('Création') 22 | // setup custom template for cell (body) rendering 23 | ->setCellTemplate('application/_column_creation.html.twig') 24 | ->setSort(['a.creationDateTime' => 'asc']) 25 | ->setFilter( 26 | (new Filter()) 27 | ->setField('a.creationDateTime') 28 | ->setName('a_creationDateTime') 29 | ->setDataFormat(Filter::FORMAT_DATE) 30 | ) 31 | ); 32 | ``` 33 | 34 | **View** 35 | 36 | ```twig 37 | {% extends "@KilikTable/_columnCell.html.twig" %} 38 | 39 | {# @KilikTable/_columnCell.html.twig #} 40 | {# @param table: Kilik\Table #} 41 | {# @param column: Kilik\Column #} 42 | {# @param row: array (from line result) #} 43 | 44 | {% block tableBodyCellInner %} 45 | {{ table.value(column,row) | date('d/m/Y H:i') }} - {{ table.value(column,row) | ago }} 46 | {% endblock tableBodyCellInner %} 47 | ``` 48 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ./tests/ 18 | 19 | 20 | 21 | 22 | 23 | ./src/ 24 | 25 | ./Resources 26 | ./vendor 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Kilik\Table 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /prepare-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker run -it --rm -u ${UID} -v `pwd`:/app -v `pwd`/.composer:/.composer -w /app kilik/php:8.3-dev composer install 4 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker run -it --rm -u ${UID} -v `pwd`:/app -v `pwd`/.composer:/.composer -w /app kilik/php:8.3-dev vendor/bin/simple-phpunit 4 | -------------------------------------------------------------------------------- /scripts/composer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #PHP_IMAGE=kilik/php:8.0-bullseye-dev 4 | PHP_IMAGE=kilik/php:7.4-buster-dev 5 | 6 | if [ -t 0 ] 7 | then 8 | TTY_DOCKER=-it 9 | else 10 | TTY_DOCKER= 11 | fi 12 | 13 | docker run ${TTY_DOCKER} --rm -v ${PWD}:/var/www/html -v ${PWD}/.composer:/.composer -w /var/www/html ${PHP_IMAGE} \ 14 | composer "$@" 15 | -------------------------------------------------------------------------------- /src/Api/AbstractResult.php: -------------------------------------------------------------------------------- 1 | nbTotalRows = $nbTotalRows; 38 | 39 | return $this; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function getNbTotalRows() 46 | { 47 | return $this->nbTotalRows; 48 | } 49 | 50 | /** 51 | * Set Nb Filtered Rows. 52 | * 53 | * @param int 54 | * 55 | * @return static 56 | */ 57 | public function setNbFilteredRows($nbFilteredRows) 58 | { 59 | $this->nbFilteredRows = $nbFilteredRows; 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function getNbFilteredRows() 68 | { 69 | return $this->nbFilteredRows; 70 | } 71 | 72 | /** 73 | * Add a row. 74 | * 75 | * @param mixed $row 76 | */ 77 | public function addRow($row) 78 | { 79 | $this->rows[] = $row; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function getRows() 86 | { 87 | return $this->rows; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Api/ApiInterface.php: -------------------------------------------------------------------------------- 1 | value) 14 | * @param array $orderBy associative aray (ex: name=>ASC,email=>DESC) 15 | * @param int $page 16 | * @param int $limit 17 | * 18 | * @return ResultInterface 19 | */ 20 | public function load(TableInterface $table, $filters, $orderBy = [], $page = null, $limit = null); 21 | } 22 | -------------------------------------------------------------------------------- /src/Api/ResultInterface.php: -------------------------------------------------------------------------------- 1 | filters = []; 130 | $this->columns = []; 131 | } 132 | 133 | /** 134 | * Set table identifiant. 135 | * 136 | * @param string $id 137 | * 138 | * @return static 139 | */ 140 | public function setId($id) 141 | { 142 | $this->id = $id; 143 | 144 | return $this; 145 | } 146 | 147 | /** 148 | * Set table title. 149 | * 150 | * @param string $id 151 | * 152 | * @return static 153 | */ 154 | public function setTitle($title) 155 | { 156 | $this->title = $title; 157 | 158 | return $this; 159 | } 160 | 161 | /** 162 | * Set URL for ajax call. 163 | * 164 | * @param string $path 165 | * 166 | * @return static 167 | */ 168 | public function setPath($path) 169 | { 170 | $this->path = $path; 171 | 172 | return $this; 173 | } 174 | 175 | /** 176 | * @param string $template 177 | * 178 | * @return static 179 | */ 180 | public function setTemplate($template) 181 | { 182 | $this->template = $template; 183 | 184 | return $this; 185 | } 186 | 187 | /** 188 | * @return string 189 | */ 190 | public function getTemplate() 191 | { 192 | return $this->template; 193 | } 194 | 195 | /** 196 | * Set template params. 197 | * 198 | * @param array $templateParams 199 | * 200 | * @return static 201 | */ 202 | public function setTemplateParams($templateParams) 203 | { 204 | $this->templateParams = $templateParams; 205 | 206 | return $this; 207 | } 208 | 209 | /** 210 | * Get template params. 211 | * 212 | * @return array 213 | */ 214 | public function getTemplateParams() 215 | { 216 | return $this->templateParams; 217 | } 218 | 219 | /** 220 | * Get Table ID. 221 | * 222 | * @return string 223 | */ 224 | public function getId() 225 | { 226 | return $this->id; 227 | } 228 | 229 | /** 230 | * Get Table Title. 231 | * 232 | * @return string 233 | */ 234 | public function getTitle() 235 | { 236 | return $this->title; 237 | } 238 | 239 | /** 240 | * Get Table path. 241 | * 242 | * @return string 243 | */ 244 | public function getPath() 245 | { 246 | return $this->path; 247 | } 248 | 249 | /** 250 | * Set Rows per page. 251 | * 252 | * @param int $rowsPerPage 253 | * 254 | * @return static 255 | */ 256 | public function setRowsPerPage($rowsPerPage) 257 | { 258 | $this->rowsPerPage = $rowsPerPage; 259 | 260 | return $this; 261 | } 262 | 263 | /** 264 | * Get rows per page. 265 | * 266 | * @return int 267 | */ 268 | public function getRowsPerPage() 269 | { 270 | return $this->rowsPerPage; 271 | } 272 | 273 | /** 274 | * Set rows per page options (selectable). 275 | * 276 | * @param array|int $rowsPerPageOptions 277 | * 278 | * @return static 279 | */ 280 | public function setRowsPerPageOptions($rowsPerPageOptions) 281 | { 282 | $this->rowsPerPageOptions = $rowsPerPageOptions; 283 | 284 | return $this; 285 | } 286 | 287 | /** 288 | * {@inheritdoc} 289 | */ 290 | public function getRowsPerPageOptions() 291 | { 292 | return $this->rowsPerPageOptions; 293 | } 294 | 295 | /** 296 | * {@inheritdoc} 297 | */ 298 | public function setPage($page) 299 | { 300 | $this->page = max(1, $page); 301 | 302 | return $this; 303 | } 304 | 305 | /** 306 | * {@inheritdoc} 307 | */ 308 | public function getPage() 309 | { 310 | return $this->page; 311 | } 312 | 313 | /** 314 | * {@inheritdoc} 315 | */ 316 | public function getPreviousPage() 317 | { 318 | return $this->page - 1; 319 | } 320 | 321 | /** 322 | * {@inheritdoc} 323 | */ 324 | public function getNextPage() 325 | { 326 | return min($this->lastPage, $this->page + 1); 327 | } 328 | 329 | /** 330 | * {@inheritdoc} 331 | */ 332 | public function setLastPage($page) 333 | { 334 | $this->lastPage = $page; 335 | 336 | return $this; 337 | } 338 | 339 | /** 340 | * {@inheritdoc} 341 | */ 342 | public function getLastPage() 343 | { 344 | return $this->lastPage; 345 | } 346 | 347 | /** 348 | * {@inheritdoc} 349 | */ 350 | public function setTotalRows($totalRows) 351 | { 352 | $this->totalRows = $totalRows; 353 | 354 | return $this; 355 | } 356 | 357 | /** 358 | * {@inheritdoc} 359 | */ 360 | public function getTotalRows() 361 | { 362 | return $this->totalRows; 363 | } 364 | 365 | /** 366 | * {@inheritdoc} 367 | */ 368 | public function setFilteredRows($filteredRows) 369 | { 370 | $this->filteredRows = $filteredRows; 371 | 372 | return $this; 373 | } 374 | 375 | /** 376 | * {@inheritdoc} 377 | */ 378 | public function getFilteredRows() 379 | { 380 | return $this->filteredRows; 381 | } 382 | 383 | /** 384 | * {@inheritdoc} 385 | */ 386 | public function addFilter(Filter $filter) 387 | { 388 | $this->filters[] = $filter; 389 | 390 | return $this; 391 | } 392 | 393 | /** 394 | * {@inheritdoc} 395 | */ 396 | public function getFilters() 397 | { 398 | return $this->filters; 399 | } 400 | 401 | /** 402 | * {@inheritdoc} 403 | */ 404 | public function getAllFilters() 405 | { 406 | $filters = $this->getFilters(); 407 | foreach ($this->getColumns() as $column) { 408 | if (!is_null($column->getFilter())) { 409 | $filters[] = $column->getFilter(); 410 | } 411 | } 412 | 413 | return $filters; 414 | } 415 | 416 | /** 417 | * {@inheritdoc} 418 | */ 419 | public function getForm() 420 | { 421 | return $this->form; 422 | } 423 | 424 | /** 425 | * {@inheritdoc} 426 | */ 427 | public function setForm(FormInterface $form) 428 | { 429 | $this->form = $form; 430 | 431 | return $this; 432 | } 433 | 434 | /** 435 | * {@inheritdoc} 436 | */ 437 | public function setFormView($formView) 438 | { 439 | $this->formView = $formView; 440 | 441 | return $this; 442 | } 443 | 444 | /** 445 | * {@inheritdoc} 446 | */ 447 | public function getFormView() 448 | { 449 | return $this->formView; 450 | } 451 | 452 | /** 453 | * {@inheritdoc} 454 | */ 455 | public function addColumn(Column $column) 456 | { 457 | $this->columns[] = $column; 458 | 459 | return $this; 460 | } 461 | 462 | /** 463 | * {@inheritdoc} 464 | */ 465 | public function getColumns() 466 | { 467 | return $this->columns; 468 | } 469 | 470 | /** 471 | * {@inheritdoc} 472 | */ 473 | public function getColumnByName($name) 474 | { 475 | foreach ($this->columns as $column) { 476 | // if name match 477 | if ($column->getName() == $name) { 478 | return $column; 479 | } 480 | } 481 | 482 | // if not found 483 | return; 484 | } 485 | 486 | /** 487 | * {@inheritdoc} 488 | */ 489 | public function getBodyId() 490 | { 491 | return $this->id.'_body'; 492 | } 493 | 494 | /** 495 | * {@inheritdoc} 496 | */ 497 | public function getFootId() 498 | { 499 | return $this->id.'_foot'; 500 | } 501 | 502 | /** 503 | * {@inheritdoc} 504 | */ 505 | public function getFormId() 506 | { 507 | return $this->id.'_form'; 508 | } 509 | 510 | /** 511 | * {@inheritdoc} 512 | */ 513 | public function getFirstRow() 514 | { 515 | return ($this->page - 1) * $this->rowsPerPage + 1; 516 | } 517 | 518 | /** 519 | * {@inheritdoc} 520 | */ 521 | public function getLastRow() 522 | { 523 | return min($this->filteredRows, ($this->page) * $this->rowsPerPage); 524 | } 525 | 526 | /** 527 | * {@inheritdoc} 528 | */ 529 | public function getValue(Column $column, array $row, array $rows = []) 530 | { 531 | if (!is_null($column->getName())) { 532 | return $column->getValue($row, $rows); 533 | } 534 | 535 | return; 536 | } 537 | 538 | /** 539 | * {@inheritdoc} 540 | */ 541 | public function addCustomOption($option, $value) 542 | { 543 | $this->customOptions[$option] = $value; 544 | 545 | return $this; 546 | } 547 | 548 | /** 549 | * {@inheritdoc} 550 | */ 551 | public function getCustomOptions() 552 | { 553 | return $this->customOptions; 554 | } 555 | 556 | /** 557 | * {@inheritdoc} 558 | */ 559 | public function getHiddenColumnsNames() 560 | { 561 | $hiddenColumns = []; 562 | 563 | foreach ($this->columns as $column) { 564 | if ($column->getHiddenByDefault()) { 565 | $hiddenColumns[] = $column->getName(); 566 | } 567 | } 568 | 569 | return $hiddenColumns; 570 | } 571 | 572 | /** 573 | * {@inheritdoc} 574 | */ 575 | public function setSkipLoadFromLocalStorage($skipLoadFromLocalStorage) 576 | { 577 | $this->skipLoadFromLocalStorage = $skipLoadFromLocalStorage; 578 | 579 | return $this; 580 | } 581 | 582 | /** 583 | * {@inheritdoc} 584 | */ 585 | public function isSkipLoadFromLocalStorage() 586 | { 587 | return $this->skipLoadFromLocalStorage; 588 | } 589 | 590 | /** 591 | * {@inheritdoc} 592 | */ 593 | public function setSkipLoadFilterFromLocalStorage($skip) 594 | { 595 | $this->skipLoadFilterFromLocalStorage = $skip; 596 | 597 | return $this; 598 | } 599 | 600 | /** 601 | * {@inheritdoc} 602 | */ 603 | public function isSkipLoadFilterFromLocalStorage() 604 | { 605 | return $this->skipLoadFilterFromLocalStorage; 606 | } 607 | 608 | /** 609 | * {@inheritdoc} 610 | */ 611 | public function getOptions() 612 | { 613 | return array_merge( 614 | $this->customOptions, 615 | [ 616 | 'rowsPerPage' => $this->rowsPerPage, 617 | 'defaultHiddenColumns' => $this->getHiddenColumnsNames(), 618 | 'skipLoadFromLocalStorage' => $this->skipLoadFromLocalStorage, 619 | 'skipLoadFilterFromLocalStorage' => $this->skipLoadFilterFromLocalStorage, 620 | ] 621 | ); 622 | } 623 | 624 | /** 625 | * {@inheritdoc} 626 | */ 627 | public function getFilterByName($filterName) 628 | { 629 | foreach ($this->getAllFilters() as $filter) { 630 | if ($filter->getName() == $filterName) { 631 | return $filter; 632 | } 633 | } 634 | 635 | return; 636 | } 637 | 638 | /** 639 | * @param MassAction $massAction 640 | * 641 | * @return static 642 | */ 643 | public function addMassAction(MassAction $massAction) 644 | { 645 | $this->massActions[] = $massAction; 646 | 647 | return $this; 648 | } 649 | 650 | /** 651 | * @return MassAction[] 652 | */ 653 | public function getMassActions() 654 | { 655 | return $this->massActions; 656 | } 657 | 658 | /** 659 | * @return string 660 | */ 661 | public function getSelectionFormKey() 662 | { 663 | return 'kilik_' . $this->getId() . '_selected'; 664 | } 665 | } 666 | -------------------------------------------------------------------------------- /src/Components/ApiTable.php: -------------------------------------------------------------------------------- 1 | api = $api; 22 | 23 | return $this; 24 | } 25 | 26 | /** 27 | * Get API. 28 | * 29 | * @return ApiInterface 30 | */ 31 | public function getApi() 32 | { 33 | return $this->api; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Components/Column.php: -------------------------------------------------------------------------------- 1 | name) && null !== $filter) { 173 | $this->name = $filter->getName(); 174 | } 175 | $this->filter = $filter; 176 | 177 | return $this; 178 | } 179 | 180 | /** 181 | * @return Filter|null 182 | */ 183 | public function getFilter() 184 | { 185 | return $this->filter; 186 | } 187 | 188 | /** 189 | * Set column label. 190 | * 191 | * @param string $label 192 | * 193 | * @return static 194 | */ 195 | public function setLabel($label) 196 | { 197 | $this->label = $label; 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * Get column label. 204 | * 205 | * @return string 206 | */ 207 | public function getLabel() 208 | { 209 | return $this->label; 210 | } 211 | 212 | /** 213 | * Set name (scalar field). 214 | * 215 | * @param string $name 216 | * 217 | * @return static 218 | */ 219 | public function setName($name) 220 | { 221 | $this->name = $name; 222 | 223 | return $this; 224 | } 225 | 226 | /** 227 | * Get name (scalar field). 228 | * 229 | * @return string 230 | */ 231 | public function getName() 232 | { 233 | return $this->name; 234 | } 235 | 236 | /** 237 | * Set export name 238 | * 239 | * @param string $exportName 240 | * 241 | * @return static 242 | */ 243 | public function setExportName(string $exportName) 244 | { 245 | $this->exportName = $exportName; 246 | 247 | return $this; 248 | } 249 | 250 | /** 251 | * Get export name 252 | * 253 | * @return string 254 | */ 255 | public function getExportName(): ?string 256 | { 257 | return $this->exportName; 258 | } 259 | 260 | /** 261 | * Set sort fields. 262 | * 263 | * @param array $sort 264 | * 265 | * @return static 266 | */ 267 | public function setSort($sort) 268 | { 269 | $this->sort = $sort; 270 | 271 | return $this; 272 | } 273 | 274 | /** 275 | * Get sort reversed, or not (if sortReverse is empty, auto revert all sort orders). 276 | * 277 | * @param bool $reverse 278 | * 279 | * @return array 280 | */ 281 | public function getAutoSort($reverse) 282 | { 283 | if ($reverse) { 284 | if (count($this->sortReverse) == 0) { 285 | return $this->getAutoInvertedSort(); 286 | } else { 287 | return $this->sortReverse; 288 | } 289 | } 290 | 291 | return $this->sort; 292 | } 293 | 294 | /** 295 | * Get sort, with auto inverted orders. 296 | * 297 | * @return array 298 | */ 299 | public function getAutoInvertedSort() 300 | { 301 | $result = []; 302 | foreach ($this->getSort() as $sort => $order) { 303 | $order = strtolower($order); 304 | $result[$sort] = ($order == 'asc' ? 'desc' : 'asc'); 305 | } 306 | 307 | return $result; 308 | } 309 | 310 | /** 311 | * Get sort fields. 312 | * 313 | * @return array 314 | */ 315 | public function getSort() 316 | { 317 | return $this->sort; 318 | } 319 | 320 | /** 321 | * Set reversed sort fields. 322 | * 323 | * @param array $sortReverse 324 | * 325 | * @return static 326 | */ 327 | public function setSortReverse($sortReverse) 328 | { 329 | $this->sortReverse = $sortReverse; 330 | 331 | return $this; 332 | } 333 | 334 | /** 335 | * Get reversed sort fields. 336 | * 337 | * @return array 338 | */ 339 | public function getSortReverse() 340 | { 341 | return $this->sortReverse; 342 | } 343 | 344 | /** 345 | * Column is sortable ? 346 | * 347 | * @return bool 348 | */ 349 | public function sortable() 350 | { 351 | return !empty($this->sort) || !empty($this->sortReverse); 352 | } 353 | 354 | /** 355 | * Set label to translation. 356 | * 357 | * @param bool $translate 358 | * 359 | * @return static 360 | */ 361 | public function setTranslateLabel(bool $translate) 362 | { 363 | if ($translate) { 364 | $this->translateDomain = 'messages'; 365 | } else { 366 | $this->translateDomain = null; 367 | } 368 | 369 | return $this; 370 | } 371 | 372 | /** 373 | * Column label should be translated ? 374 | * 375 | * @return bool 376 | */ 377 | public function getTranslateLabel(): bool 378 | { 379 | return !is_null($this->translateDomain); 380 | } 381 | 382 | /** 383 | * Set label domain translation. 384 | * 385 | * @param string $domain 386 | * 387 | * @return static 388 | */ 389 | public function setTranslateDomain($domain) 390 | { 391 | $this->translateDomain = $domain; 392 | 393 | return $this; 394 | } 395 | 396 | /** 397 | * Column label translation domain. 398 | * 399 | * @return bool 400 | */ 401 | public function getTranslateDomain() 402 | { 403 | return $this->translateDomain; 404 | } 405 | 406 | /** 407 | * Set the display format. 408 | * 409 | * @param string $displayFormat 410 | * 411 | * @return static 412 | */ 413 | public function setDisplayFormat($displayFormat) 414 | { 415 | if (!in_array($displayFormat, static::FORMATS)) { 416 | throw new \InvalidArgumentException("bad format '{$displayFormat}'"); 417 | } 418 | $this->displayFormat = $displayFormat; 419 | 420 | return $this; 421 | } 422 | 423 | /** 424 | * Get the display format. 425 | * 426 | * @return string 427 | */ 428 | public function getDisplayFormat() 429 | { 430 | return $this->displayFormat; 431 | } 432 | 433 | /** 434 | * Set the raw option (for raw twig rendering). 435 | * 436 | * @param bool $raw 437 | * 438 | * @return static 439 | */ 440 | public function setRaw($raw) 441 | { 442 | $this->raw = $raw; 443 | 444 | return $this; 445 | } 446 | 447 | /** 448 | * Get the raw option. 449 | * 450 | * @return bool 451 | */ 452 | public function getRaw() 453 | { 454 | return $this->raw; 455 | } 456 | 457 | /** 458 | * Set display format parameters. 459 | * 460 | * @param mixed $displayFormatParams 461 | * 462 | * @return static 463 | */ 464 | public function setDisplayFormatParams($displayFormatParams) 465 | { 466 | $this->displayFormatParams = $displayFormatParams; 467 | 468 | return $this; 469 | } 470 | 471 | public function setTotal($total) 472 | { 473 | $this->total = $total; 474 | 475 | return $this; 476 | } 477 | 478 | public function useTotal() 479 | { 480 | $this->useTotal = true; 481 | 482 | return $this; 483 | } 484 | 485 | public function isUseTotal() 486 | { 487 | return $this->useTotal; 488 | } 489 | 490 | public function getTotal() 491 | { 492 | return $this->total; 493 | } 494 | 495 | /** 496 | * Get display format parameters. 497 | * 498 | * @return string 499 | */ 500 | public function getDisplayFormatParams() 501 | { 502 | return $this->displayFormatParams; 503 | } 504 | 505 | /** 506 | * Set display callback method. 507 | * 508 | * @param mixed $callback : the function or [object,method], that accepts 3 parameters (cell value, row values, 509 | * rows) 510 | * 511 | * @return static 512 | */ 513 | public function setDisplayCallback($callback) 514 | { 515 | $this->displayCallback = $callback; 516 | 517 | return $this; 518 | } 519 | 520 | /** 521 | * Get display callback method. 522 | * 523 | * @return mixed 524 | */ 525 | public function getDisplayCallback() 526 | { 527 | return $this->displayCallback; 528 | } 529 | 530 | /** 531 | * Set export callback method. 532 | * 533 | * @param mixed $callback : the function or [object,method], that accepts 3 parameters (cell value, row values, 534 | * rows) 535 | * 536 | * @return static 537 | */ 538 | public function setExportCallback($callback) 539 | { 540 | $this->exportCallback = $callback; 541 | 542 | return $this; 543 | } 544 | 545 | /** 546 | * Get export callback method. 547 | * 548 | * @return mixed 549 | */ 550 | public function getExportCallback() 551 | { 552 | return $this->exportCallback; 553 | } 554 | 555 | /** 556 | * Callback sample. 557 | * 558 | * @param mixed $value : the column value (the object or a field) 559 | * @param array $row : the row values 560 | * @param array $rows : the rows values (of the page) 561 | * 562 | * @return string 563 | */ 564 | public function sampleCallback($value, $row, $rows) 565 | { 566 | // this sample just return the value, but could do many more 567 | return (string) $value; 568 | } 569 | 570 | /** 571 | * Set hidden by default. 572 | * 573 | * @param bool $hidden 574 | * 575 | * @return static 576 | */ 577 | public function setHiddenByDefault($hidden) 578 | { 579 | $this->hiddenByDefault = $hidden; 580 | 581 | return $this; 582 | } 583 | 584 | /** 585 | * Get hidden by default. 586 | * 587 | * @return bool 588 | */ 589 | public function getHiddenByDefault() 590 | { 591 | return $this->hiddenByDefault; 592 | } 593 | 594 | /** 595 | * Set hidden. 596 | * 597 | * @param bool $hidden 598 | * 599 | * @return static 600 | */ 601 | public function setHidden($hidden) 602 | { 603 | $this->hidden = $hidden; 604 | 605 | return $this; 606 | } 607 | 608 | /** 609 | * Get hidden. 610 | * 611 | * @return bool 612 | */ 613 | public function getHidden() 614 | { 615 | return $this->hidden; 616 | } 617 | 618 | /** 619 | * Get the formatted value to display. 620 | * 621 | * priority formatter methods: 622 | * - callback 623 | * - known formats 624 | * - default (raw text) 625 | * 626 | * @param $row 627 | * @param array $rows 628 | * 629 | * @return string 630 | * 631 | * @throws \Exception 632 | */ 633 | public function getValue(array $row, array $rows = []) 634 | { 635 | if (isset($row[$this->getName()])) { 636 | $rawValue = $row[$this->getName()]; 637 | } else { 638 | $rawValue = null; 639 | } 640 | // if a callback is set 641 | $callback = $this->getDisplayCallback(); 642 | if (!is_null($callback)) { 643 | if (!is_callable($callback)) { 644 | throw new \Exception('displayCallback is not callable'); 645 | } 646 | 647 | return $callback($rawValue, $row, $rows); 648 | } else { 649 | switch ($this->getDisplayFormat()) { 650 | case static::FORMAT_DATE: 651 | $formatParams = $this->getDisplayFormatParams(); 652 | if (is_null($formatParams)) { 653 | $formatParams = 'Y-m-d H:i:s'; 654 | } 655 | if (!is_null($rawValue) && is_object($rawValue) && $rawValue instanceof \DateTimeInterface) { 656 | return $rawValue->format($formatParams); 657 | } else { 658 | return ''; 659 | } 660 | break; 661 | case static::FORMAT_TEXT: 662 | default: 663 | if (is_array($rawValue)) { 664 | return implode(',', $rawValue); 665 | } else { 666 | return $rawValue; 667 | } 668 | break; 669 | } 670 | } 671 | } 672 | 673 | /** 674 | * Get the formatted value to export (used by CSV export). 675 | * 676 | * priority formatter methods: 677 | * - callback 678 | * - known formats 679 | * - default (raw text) 680 | * 681 | * @param array $row 682 | * @param array $rows 683 | * 684 | * @return string 685 | * 686 | * @throws \Exception 687 | */ 688 | public function getExportValue(array $row, array $rows = []) 689 | { 690 | if (isset($row[$this->getName()])) { 691 | $rawValue = $row[$this->getName()]; 692 | // if a callback is set 693 | $callback = $this->getExportCallback(); 694 | if (!is_null($callback)) { 695 | if (!is_callable($callback)) { 696 | throw new \Exception('exportCallback is not callable'); 697 | } 698 | 699 | return $callback($rawValue, $row, $rows); 700 | } else { 701 | switch ($this->getDisplayFormat()) { 702 | case static::FORMAT_DATE: 703 | $formatParams = $this->getDisplayFormatParams(); 704 | if (is_null($formatParams)) { 705 | $formatParams = 'Y-m-d H:i:s'; 706 | } 707 | if (!is_null($rawValue) && is_object($rawValue) && $rawValue instanceof \DateTimeInterface) { 708 | return $rawValue->format($formatParams); 709 | } else { 710 | return ''; 711 | } 712 | break; 713 | case static::FORMAT_TEXT: 714 | default: 715 | if (is_array($rawValue)) { 716 | return implode(',', $rawValue); 717 | } else { 718 | return $rawValue; 719 | } 720 | break; 721 | } 722 | } 723 | } else { 724 | return ''; 725 | } 726 | } 727 | 728 | /** 729 | * Enable/Disable the capitalize filter. 730 | * 731 | * @param bool $capitalize 732 | * 733 | * @return static 734 | */ 735 | public function setCapitalize($capitalize = true) 736 | { 737 | $this->capitalize = $capitalize; 738 | 739 | return $this; 740 | } 741 | 742 | /** 743 | * Get the capitalize filter status. 744 | * 745 | * @return bool 746 | */ 747 | public function getCapitalize() 748 | { 749 | return $this->capitalize; 750 | } 751 | 752 | /** 753 | * @param string $displayClass 754 | * 755 | * @return static 756 | */ 757 | public function setDisplayClass($displayClass) 758 | { 759 | $this->displayClass = $displayClass; 760 | 761 | return $this; 762 | } 763 | 764 | /** 765 | * @return string 766 | */ 767 | public function getDisplayClass() 768 | { 769 | return $this->displayClass; 770 | } 771 | 772 | /** 773 | * @param string $headerClass 774 | * 775 | * @return static 776 | */ 777 | public function setHeaderClass($headerClass) 778 | { 779 | $this->headerClass = $headerClass; 780 | 781 | return $this; 782 | } 783 | 784 | /** 785 | * @return string 786 | */ 787 | public function getHeaderClass() 788 | { 789 | return $this->headerClass; 790 | } 791 | 792 | /** 793 | * @param string $filterClass 794 | * 795 | * @return static 796 | */ 797 | public function setFilterClass($filterClass) 798 | { 799 | $this->filterClass = $filterClass; 800 | 801 | return $this; 802 | } 803 | 804 | /** 805 | * @return string 806 | */ 807 | public function getFilterClass() 808 | { 809 | return $this->filterClass; 810 | } 811 | 812 | /** 813 | * @param string $cellTemplate 814 | */ 815 | public function setCellTemplate(string $cellTemplate) 816 | { 817 | $this->cellTemplate = $cellTemplate; 818 | 819 | return $this; 820 | } 821 | 822 | /** 823 | * @return string 824 | */ 825 | public function getCellTemplate(): ?string 826 | { 827 | return $this->cellTemplate; 828 | } 829 | } 830 | -------------------------------------------------------------------------------- /src/Components/Filter.php: -------------------------------------------------------------------------------- 1 | 'value' 27 | const TYPE_GREATER = '>'; 28 | // WHERE field >= 'value' 29 | const TYPE_GREATER_OR_EQUAL = '>='; 30 | // WHERE field < 'value' 31 | const TYPE_LESS = '<'; 32 | // WHERE field <= 'value' 33 | const TYPE_LESS_OR_EQUAL = '<='; 34 | // use input to apply arithmetic comparators, then filter the results 35 | const TYPE_AUTO = 'auto'; 36 | const TYPES 37 | = array( 38 | self::TYPE_LIKE, 39 | self::TYPE_NOT_LIKE, 40 | self::TYPE_EQUAL, 41 | self::TYPE_NOT_EQUAL, 42 | self::TYPE_EQUAL_STRICT, 43 | self::TYPE_GREATER, 44 | self::TYPE_GREATER_OR_EQUAL, 45 | self::TYPE_LESS, 46 | self::TYPE_LESS_OR_EQUAL, 47 | self::TYPE_LIKE_WORDS_AND, 48 | self::TYPE_LIKE_WORDS_OR, 49 | self::TYPE_AUTO, 50 | ); 51 | const TYPE_DEFAULT = self::TYPE_AUTO; 52 | // specials types: 53 | const TYPE_NULL = 'null'; 54 | const TYPE_NOT_NULL = 'not_null'; 55 | const TYPE_IN = 'in'; 56 | const TYPE_NOT_IN = 'not_in'; 57 | 58 | /** 59 | * data formats. 60 | */ 61 | const FORMAT_INTEGER = 'integer'; 62 | const FORMAT_TEXT = 'text'; 63 | const FORMAT_DEFAULT = self::FORMAT_TEXT; 64 | /** @deprecated prefer new \Kilik\TableBundle\Components\FilterDate */ 65 | const FORMAT_DATE = 'date'; 66 | 67 | const FORMATS = array(self::FORMAT_DATE, self::FORMAT_INTEGER, self::FORMAT_TEXT); 68 | 69 | /** 70 | * Input type. 71 | * 72 | * @var string 73 | */ 74 | protected $input = TextType::class; 75 | 76 | /** 77 | * Options for input. 78 | * 79 | * This are the options for the symfony FormType 80 | * 81 | * @var array 82 | */ 83 | protected $options = array('required' => false); 84 | 85 | /** 86 | * Filter name. 87 | * 88 | * @var string 89 | */ 90 | private $name; 91 | 92 | /** 93 | * Filter field. 94 | * 95 | * @var string 96 | */ 97 | private $field; 98 | 99 | /** 100 | * This filter is a HAVING constraint ? 101 | * 102 | * @var bool 103 | */ 104 | private $having = false; 105 | 106 | /** 107 | * Filter type. 108 | * 109 | * @var string 110 | */ 111 | protected $type = self::TYPE_DEFAULT; 112 | 113 | /** 114 | * Data format. 115 | * 116 | * @var string 117 | */ 118 | private $dataFormat = self::FORMAT_DEFAULT; 119 | 120 | /** 121 | * Custom inputFormatter. 122 | * 123 | * @var callable 124 | * 125 | * prototype (Filter,$defaultOperator,$value) 126 | */ 127 | private $inputFormatter = null; 128 | 129 | /** 130 | * Default filter value (forced from GET VARS for example). 131 | * 132 | * @var string 133 | */ 134 | private $defaultValue = null; 135 | 136 | /** 137 | * Custom query part builder handler. 138 | * 139 | * @var callable 140 | */ 141 | private $queryPartBuilder = null; 142 | 143 | /** 144 | * Set the filter name. 145 | * 146 | * @param string $name 147 | * 148 | * @return static 149 | */ 150 | public function setName($name) 151 | { 152 | $this->name = $name; 153 | 154 | return $this; 155 | } 156 | 157 | /** 158 | * Get the filter name. 159 | * 160 | * @return string 161 | */ 162 | public function getName() 163 | { 164 | return $this->name; 165 | } 166 | 167 | /** 168 | * Set the filter field (used in a query). 169 | * 170 | * @param string $field 171 | * 172 | * @return static 173 | */ 174 | public function setField($field) 175 | { 176 | $this->field = $field; 177 | 178 | return $this; 179 | } 180 | 181 | /** 182 | * Get the filter field (user in a query). 183 | * 184 | * @return string 185 | */ 186 | public function getField() 187 | { 188 | return $this->field; 189 | } 190 | 191 | /** 192 | * Set the filter type. 193 | * 194 | * @param string $type 195 | * 196 | * @return static 197 | */ 198 | public function setType($type) 199 | { 200 | if (!in_array($type, static::TYPES)) { 201 | throw new \InvalidArgumentException("bad type {$type}"); 202 | } 203 | $this->type = $type; 204 | 205 | return $this; 206 | } 207 | 208 | /** 209 | * Get the filter type. 210 | * 211 | * @return string 212 | */ 213 | public function getType() 214 | { 215 | return $this->type; 216 | } 217 | 218 | /** 219 | * Set if this filter is working on a HAVING clause, or not. 220 | * 221 | * @param bool $having 222 | * 223 | * @return static 224 | */ 225 | public function setHaving($having) 226 | { 227 | $this->having = $having; 228 | 229 | return $this; 230 | } 231 | 232 | /** 233 | * Get if this filter is working on a HAVING clause, or not. 234 | * 235 | * @return string 236 | */ 237 | public function getHaving() 238 | { 239 | return $this->having; 240 | } 241 | 242 | /** 243 | * Set the data format converter (from user input to sql value). 244 | * 245 | * @param string $dataFormat 246 | * 247 | * @return static 248 | */ 249 | public function setDataFormat($dataFormat) 250 | { 251 | if (!in_array($dataFormat, static::FORMATS)) { 252 | throw new \InvalidArgumentException("bad format '{$dataFormat}'"); 253 | } 254 | $this->dataFormat = $dataFormat; 255 | 256 | return $this; 257 | } 258 | 259 | /** 260 | * Get the data format. 261 | * 262 | * @return string 263 | */ 264 | public function getDataFormat() 265 | { 266 | return $this->dataFormat; 267 | } 268 | 269 | /** 270 | * Get the operator and the value of an input string. 271 | * 272 | * @param string $input 273 | * 274 | * @return array [string operator,string value] 275 | */ 276 | public function getOperatorAndValue($input): array 277 | { 278 | switch ($this->getType()) { 279 | case self::TYPE_GREATER: 280 | case self::TYPE_GREATER_OR_EQUAL: 281 | case self::TYPE_LESS: 282 | case self::TYPE_LESS_OR_EQUAL: 283 | case self::TYPE_NOT_LIKE: 284 | case self::TYPE_LIKE: 285 | case self::TYPE_NOT_EQUAL: 286 | case self::TYPE_EQUAL: 287 | case self::TYPE_EQUAL_STRICT: 288 | case self::TYPE_LIKE_WORDS_AND: 289 | case self::TYPE_LIKE_WORDS_OR: 290 | return [$this->getType(), $input]; 291 | case self::TYPE_AUTO: 292 | default: 293 | if ((string) $input == '') { 294 | return [self::TYPE_LIKE, false]; 295 | } 296 | 297 | $simpleOperator = substr($input, 0, 1); 298 | $doubleOperator = substr($input, 0, 2); 299 | // if start with operators 300 | switch ($doubleOperator) { 301 | case self::TYPE_GREATER_OR_EQUAL: 302 | case self::TYPE_LESS_OR_EQUAL: 303 | case self::TYPE_NOT_EQUAL: 304 | case self::TYPE_EQUAL_STRICT: 305 | return [$doubleOperator, substr($input, 2)]; 306 | default: 307 | switch ($simpleOperator) { 308 | case self::TYPE_GREATER: 309 | case self::TYPE_LESS: 310 | case self::TYPE_EQUAL: 311 | case self::TYPE_NOT_LIKE: 312 | return [$simpleOperator, substr($input, 1)]; 313 | } 314 | } 315 | 316 | return [self::TYPE_LIKE, $input]; 317 | } 318 | } 319 | 320 | /** 321 | * Set the custom formatter input. 322 | * 323 | * @param callable $formatter 324 | * 325 | * @return static 326 | */ 327 | public function setInputFormatter($formatter) 328 | { 329 | $this->inputFormatter = $formatter; 330 | 331 | return $this; 332 | } 333 | 334 | /** 335 | * Get formatted input. 336 | * 337 | * @param string $operator 338 | * @param string $input 339 | * 340 | * @return array searchOperator, formatted input 341 | */ 342 | public function getFormattedInput($operator, $input) 343 | { 344 | // if we use custom formatter 345 | if (is_callable($this->inputFormatter)) { 346 | $function = $this->inputFormatter; 347 | 348 | return $function($this, $operator, $input); 349 | } 350 | 351 | switch ($this->getDataFormat()) { 352 | // date/time format dd/mm/YYYY HH:ii:ss 353 | case self::FORMAT_DATE: 354 | $params = explode('/', str_replace(array('-', ' ',':'), '/', $input)); 355 | // only year ? 356 | if (count($params) == 1) { 357 | $fInput = $params[0]; 358 | } // month/year ? 359 | elseif (count($params) == 2) { 360 | $fInput = sprintf('%04d-%02d', $params[1], $params[0]); 361 | } // day/month/year ? 362 | elseif (count($params) == 3) { 363 | $fInput = sprintf('%04d-%02d-%02d', $params[2], $params[1], $params[0]); 364 | } // day/month/year hour ? 365 | elseif (count($params) == 4) { 366 | $fInput = sprintf('%04d-%02d-%02d %02d', $params[2], $params[1], $params[0], $params[3]); 367 | } // day/month/year hour:minute ? 368 | elseif (count($params) == 5) { 369 | $fInput = sprintf( 370 | '%04d-%02d-%02d %02d:%02d', 371 | $params[2], 372 | $params[1], 373 | $params[0], 374 | $params[3], 375 | $params[4] 376 | ); 377 | } // day/month/year hour:minute:second ? 378 | elseif (count($params) == 6) { 379 | $fInput = sprintf( 380 | '%04d-%02d-%02d %02d:%02d:%02d', 381 | $params[2], 382 | $params[1], 383 | $params[0], 384 | $params[3], 385 | $params[4], 386 | $params[5] 387 | ); 388 | } // default, same has raw value 389 | else { 390 | $fInput = $input; 391 | } 392 | break; 393 | case self::FORMAT_INTEGER: 394 | $fInput = (int) $input; 395 | switch ($operator) { 396 | case self::TYPE_NOT_LIKE: 397 | $operator = self::TYPE_NOT_EQUAL; 398 | break; 399 | case self::TYPE_LIKE: 400 | case self::TYPE_LIKE_WORDS_AND: 401 | case self::TYPE_LIKE_WORDS_OR: 402 | case self::TYPE_AUTO: 403 | $operator = self::TYPE_EQUAL_STRICT; 404 | break; 405 | } 406 | break; 407 | case self::FORMAT_TEXT: 408 | default: 409 | $fInput = $input; 410 | break; 411 | } 412 | 413 | return array($operator, $fInput); 414 | } 415 | 416 | /** 417 | * Set Default value. 418 | * 419 | * @param string $defaultValue 420 | * 421 | * @return static 422 | */ 423 | public function setDefaultValue($defaultValue) 424 | { 425 | $this->defaultValue = $defaultValue; 426 | 427 | return $this; 428 | } 429 | 430 | /** 431 | * Get default value. 432 | * 433 | * @return string 434 | */ 435 | public function getDefaultValue() 436 | { 437 | return $this->defaultValue; 438 | } 439 | 440 | /** 441 | * @param callable $queryPartBuilder (Filter $filter, Table $table, \Doctrine\ORM\QueryBuilder $queryBuilder, mixed $value) 442 | * 443 | * @return static 444 | */ 445 | public function setQueryPartBuilder($queryPartBuilder) 446 | { 447 | $this->queryPartBuilder = $queryPartBuilder; 448 | 449 | return $this; 450 | } 451 | 452 | /** 453 | * @return callable 454 | */ 455 | public function getQueryPartBuilder() 456 | { 457 | return $this->queryPartBuilder; 458 | } 459 | 460 | /** 461 | * @param string $input 462 | * 463 | * @return static 464 | */ 465 | public function setInput($input) 466 | { 467 | $this->input = $input; 468 | 469 | return $this; 470 | } 471 | 472 | /** 473 | * @return string 474 | */ 475 | public function getInput() 476 | { 477 | return $this->input; 478 | } 479 | 480 | /** 481 | * @return array 482 | */ 483 | public function getOptions() 484 | { 485 | return $this->options; 486 | } 487 | 488 | /** 489 | * @param array $options 490 | * 491 | * @return static 492 | */ 493 | public function setOptions(array $options) 494 | { 495 | // We do an array_merge to keep the possibility to overwrite the required option 496 | $this->options = array_merge($this->options,$options); 497 | 498 | return $this; 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /src/Components/FilterCheckbox.php: -------------------------------------------------------------------------------- 1 | 2014-07-27` expects > 2014-07-27 23:59:59 21 | self::TYPE_GREATER_OR_EQUAL, // `>=2014-07-27` expects >= 2014-07-27 00:00:00 22 | self::TYPE_LESS, // `<2014-07-27` expects < 2014-07-27 00:00:00 23 | self::TYPE_LESS_OR_EQUAL, // `<=2014-07-27` expects < 2014-07-27 23:59:59 24 | ]; 25 | 26 | protected string $inputFormat = self::INPUT_FORMAT_BIG_ENDIAN; 27 | 28 | public function __construct() 29 | { 30 | $this->setQueryPartBuilder(function (Filter $filter, Table $table, QueryBuilder $qb, $rawInput) { 31 | $rawInput = trim((string) $rawInput); 32 | if ('' === $rawInput) { 33 | return; 34 | } 35 | 36 | list($operator, $input) = $this->getOperatorAndValue($rawInput); 37 | if ('' === (string) $input) { 38 | switch ($operator) { 39 | case static::TYPE_EQUAL: 40 | $qb->andWhere($this->getField().' IS NULL'); 41 | 42 | return; 43 | case static::TYPE_NOT_EQUAL: 44 | $qb->andWhere($this->getField().' IS NOT NULL'); 45 | 46 | return; 47 | } 48 | } 49 | if (!in_array($operator, static::TYPES)) { 50 | $operator = static::TYPE_EQUAL; 51 | } 52 | 53 | if (null === $period = $this->getPeriodFromInput($input)) { 54 | $qb->andWhere('0=1'); 55 | 56 | return; 57 | } 58 | 59 | $query = $this->buildWhereQuery($operator); 60 | if (false !== strpos($query, $this->buildPeriodStartParameterName())) { 61 | $qb->setParameter($this->buildPeriodStartParameterName(), $period[0]); 62 | } 63 | if (false !== strpos($query, $this->buildPeriodEndParameterName())) { 64 | $qb->setParameter($this->buildPeriodEndParameterName(), $period[1]); 65 | } 66 | 67 | $qb->andWhere($query); 68 | }); 69 | } 70 | 71 | public function setDataFormat($dataFormat) 72 | { 73 | throw new \LogicException('FilterDate data format cannot be modified.'); 74 | } 75 | 76 | public function setInputFormatter($formatter) 77 | { 78 | throw new \LogicException('FilterDate input formatter cannot be modified.'); 79 | } 80 | 81 | public function getInputFormat(): string 82 | { 83 | return $this->inputFormat; 84 | } 85 | 86 | public function setInputFormat(string $inputFormat): self 87 | { 88 | if (!in_array($inputFormat, static::INPUT_FORMATS)) { 89 | throw new \InvalidArgumentException('Unexpected input format'); 90 | } 91 | $this->inputFormat = $inputFormat; 92 | 93 | return $this; 94 | } 95 | 96 | protected function getPeriodFromInput(string $input): ?array 97 | { 98 | $input = trim(str_replace('/', '-', $input)); 99 | $format = $this->inputFormat; 100 | 101 | // Complete datetime 102 | if (false !== $date = date_create_immutable_from_format($format, $input)) { 103 | return $date->getLastErrors() ? null : [$date, $date]; 104 | } 105 | 106 | // Without second 107 | $format = trim(str_replace('s', '', $format), '-: '); 108 | if (false !== $date = date_create_immutable_from_format('!'.$format, $input)) { 109 | return $date->getLastErrors() ? null : [$date, $date->modify('+59 seconds')]; 110 | } 111 | 112 | // Without minute 113 | $format = trim(str_replace('i', '', $format), '-: '); 114 | if (false !== $date = date_create_immutable_from_format('!'.$format, $input)) { 115 | return $date->getLastErrors() ? null : [$date, $date->modify('+1 hour -1 second')]; 116 | } 117 | 118 | // Only date (without time) 119 | $format = trim(str_replace('H', '', $format), '-: '); 120 | if (false !== $date = date_create_immutable_from_format('!'.$format, $input)) { 121 | return $date->getLastErrors() ? null : [$date, $date->modify('+1 day -1 second')]; 122 | } 123 | 124 | // Only month and year 125 | $format = trim(str_replace('d', '', $format), '-: '); 126 | if (false !== $date = date_create_immutable_from_format('!'.$format, $input)) { 127 | return $date->getLastErrors() ? null : [$date, $date->modify('+1 month -1 second')]; 128 | } 129 | 130 | // Only year 131 | $format = trim(str_replace('m', '', $format), '-: '); 132 | if (false !== $date = date_create_immutable_from_format('!'.$format, $input)) { 133 | return $date->getLastErrors() ? null : [$date, $date->modify('+1 year -1 second')]; 134 | } 135 | 136 | return null; 137 | } 138 | 139 | protected function buildWhereQuery(string $operator): string 140 | { 141 | switch ($operator) { 142 | case static::TYPE_NOT_EQUAL: 143 | return $this->getField().' NOT BETWEEN :'.$this->buildPeriodStartParameterName().' AND :'.$this->buildPeriodEndParameterName(); 144 | case static::TYPE_GREATER: 145 | return $this->getField().' > :'.$this->buildPeriodEndParameterName(); 146 | case static::TYPE_GREATER_OR_EQUAL: 147 | return $this->getField().' >= :'.$this->buildPeriodStartParameterName(); 148 | case static::TYPE_LESS: 149 | return $this->getField().' < :'.$this->buildPeriodStartParameterName(); 150 | case static::TYPE_LESS_OR_EQUAL: 151 | return $this->getField().' <= :'.$this->buildPeriodEndParameterName(); 152 | default: 153 | case static::TYPE_EQUAL: 154 | return $this->getField().' BETWEEN :'.$this->buildPeriodStartParameterName().' AND :'.$this->buildPeriodEndParameterName(); 155 | } 156 | } 157 | 158 | protected function buildPeriodStartParameterName(): string 159 | { 160 | return 'filter_'.$this->getName().'_start'; 161 | } 162 | 163 | protected function buildPeriodEndParameterName(): string 164 | { 165 | return 'filter_'.$this->getName().'_end'; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Components/FilterSelect.php: -------------------------------------------------------------------------------- 1 | choices = $choices; 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Get the choices. 81 | * 82 | * @return array 83 | */ 84 | public function getChoices() 85 | { 86 | return $this->choices; 87 | } 88 | 89 | /** 90 | * Set the placeholder. 91 | * 92 | * @param string $placeholder 93 | * 94 | * @return static 95 | */ 96 | public function setPlaceholder($placeholder) 97 | { 98 | $this->placeholder = $placeholder; 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * Get the placeholder. 105 | * 106 | * @return string 107 | */ 108 | public function getPlaceholder() 109 | { 110 | return $this->placeholder; 111 | } 112 | 113 | /** 114 | * @param callable $choiceLabel 115 | * 116 | * @return static 117 | */ 118 | public function setChoiceLabel($choiceLabel) 119 | { 120 | $this->choiceLabel = $choiceLabel; 121 | 122 | return $this; 123 | } 124 | 125 | /** 126 | * @return callable 127 | */ 128 | public function getChoiceLabel() 129 | { 130 | return $this->choiceLabel; 131 | } 132 | 133 | /** 134 | * @param callable $choiceValue 135 | * 136 | * @return static 137 | */ 138 | public function setChoiceValue($choiceValue) 139 | { 140 | $this->choiceValue = $choiceValue; 141 | 142 | return $this; 143 | } 144 | 145 | /** 146 | * @return callable 147 | */ 148 | public function getChoiceValue() 149 | { 150 | return $this->choiceValue; 151 | } 152 | 153 | /** 154 | * @param callable|null $choicesGroupBy 155 | * 156 | * @return static 157 | */ 158 | public function setChoicesGroupBy($choicesGroupBy) 159 | { 160 | $this->choicesGroupBy = $choicesGroupBy; 161 | 162 | return $this; 163 | } 164 | 165 | /** 166 | * @return callable|null 167 | */ 168 | public function getChoicesGroupBy() 169 | { 170 | return $this->choicesGroupBy; 171 | } 172 | 173 | /** 174 | * @param string|bool $translationDomain 175 | * 176 | * @return static 177 | */ 178 | public function setTranslationDomain($translationDomain) 179 | { 180 | $this->translationDomain = $translationDomain; 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * @return string|bool 187 | */ 188 | public function getTranslationDomain() 189 | { 190 | return $this->translationDomain; 191 | } 192 | 193 | /** 194 | * @param string|bool $choiceTranslationDomain 195 | * 196 | * @return static 197 | */ 198 | public function setChoiceTranslationDomain($choiceTranslationDomain) 199 | { 200 | $this->choiceTranslationDomain = $choiceTranslationDomain; 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * @return string|bool 207 | */ 208 | public function getChoiceTranslationDomain() 209 | { 210 | return $this->choiceTranslationDomain; 211 | } 212 | 213 | /** 214 | * Disable translation domains. 215 | * 216 | * @return static 217 | */ 218 | public function disableTranslation() 219 | { 220 | $this->setTranslationDomain(false); 221 | $this->setChoiceTranslationDomain(false); 222 | 223 | return $this; 224 | } 225 | 226 | /** 227 | * @return array 228 | */ 229 | public function getOptions() 230 | { 231 | $options = array_merge( 232 | [ 233 | 'required' => false, 234 | 'choices' => $this->getChoices(), 235 | 'placeholder' => $this->getPlaceholder(), 236 | 'group_by' => $this->getChoicesGroupBy(), 237 | 'choice_label' => $this->getChoiceLabel(), 238 | 'choice_value' => $this->getChoiceValue(), 239 | 'translation_domain' => $this->getTranslationDomain(), 240 | 'choice_translation_domain' => $this->getChoiceTranslationDomain(), 241 | ], 242 | $this->options 243 | ); 244 | 245 | return $options; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/Components/MassAction.php: -------------------------------------------------------------------------------- 1 | name = $name; 41 | $this->label = $label; 42 | $this->class = $class; 43 | } 44 | 45 | /** 46 | * @return string 47 | */ 48 | public function getName() 49 | { 50 | return $this->name; 51 | } 52 | 53 | /** 54 | * @param string $name 55 | * 56 | * @return static 57 | */ 58 | public function setName($name) 59 | { 60 | $this->name = $name; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * @return string 67 | */ 68 | public function getLabel() 69 | { 70 | return $this->label; 71 | } 72 | 73 | /** 74 | * @param string $label 75 | * 76 | * @return static 77 | */ 78 | public function setLabel($label) 79 | { 80 | $this->label = $label; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * @return string 87 | */ 88 | public function getClass() 89 | { 90 | return $this->class; 91 | } 92 | 93 | /** 94 | * @param string $class 95 | * 96 | * @return static 97 | */ 98 | public function setClass($class) 99 | { 100 | $this->class = $class; 101 | return $this; 102 | } 103 | 104 | /** 105 | * @return string 106 | */ 107 | public function getAction() 108 | { 109 | return $this->action; 110 | } 111 | 112 | /** 113 | * @param string $action 114 | */ 115 | public function setAction($action) 116 | { 117 | $this->action = $action; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Components/Table.php: -------------------------------------------------------------------------------- 1 | queryBuilder = $queryBuilder; 68 | $this->alias = $alias; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * Defines default identifiers from query builder in order to optimize count queries. 75 | * 76 | * @return $this 77 | * 78 | * @throws \Doctrine\Common\Persistence\Mapping\MappingException 79 | */ 80 | public function setDefaultIdentifierFieldNames() 81 | { 82 | //Default identifier for table rows 83 | $rootEntity = $this->queryBuilder->getRootEntities()[0]; 84 | $metadata = $this->queryBuilder->getEntityManager()->getMetadataFactory()->getMetadataFor($rootEntity); 85 | $identifiers = array(); 86 | foreach ($metadata->getIdentifierFieldNames() as $identifierFieldName) { 87 | $identifiers[] = $this->getAlias().'.'.$identifierFieldName; 88 | } 89 | $rootEntityIdentifier = implode(',', $identifiers); 90 | $this->setIdentifierFieldNames($rootEntityIdentifier ?: null); 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * @return QueryBuilder 97 | */ 98 | public function getQueryBuilder() 99 | { 100 | return $this->queryBuilder; 101 | } 102 | 103 | /** 104 | * @return string 105 | */ 106 | public function getAlias() 107 | { 108 | return $this->alias; 109 | } 110 | 111 | /** 112 | * @param string|null $identifierFieldNames 113 | * 114 | * @return static 115 | */ 116 | public function setIdentifierFieldNames($identifierFieldNames = null) 117 | { 118 | $this->identifierFieldNames = $identifierFieldNames; 119 | 120 | return $this; 121 | } 122 | 123 | /** 124 | * @return string|null 125 | */ 126 | public function getIdentifierFieldNames() 127 | { 128 | return $this->identifierFieldNames; 129 | } 130 | 131 | /** 132 | * @param int $entityLoaderMode 133 | * 134 | * @return static 135 | */ 136 | public function setEntityLoaderMode($entityLoaderMode) 137 | { 138 | $this->entityLoaderMode = $entityLoaderMode; 139 | 140 | return $this; 141 | } 142 | 143 | /** 144 | * @return int 145 | */ 146 | public function getEntityLoaderMode() 147 | { 148 | return $this->entityLoaderMode; 149 | } 150 | 151 | /** 152 | * @param string $entityLoaderRepository 153 | * 154 | * @return static 155 | */ 156 | public function setEntityLoaderRepository($entityLoaderRepository) 157 | { 158 | // force mode 159 | $this->setEntityLoaderMode(self::ENTITY_LOADER_REPOSITORY); 160 | 161 | $this->entityLoaderRepository = $entityLoaderRepository; 162 | 163 | return $this; 164 | } 165 | 166 | /** 167 | * @return string 168 | */ 169 | public function getEntityLoaderRepository() 170 | { 171 | return $this->entityLoaderRepository; 172 | } 173 | 174 | /** 175 | * @param callable $entityLoaderCallback 176 | * 177 | * @return static 178 | */ 179 | public function setEntityLoaderCallback($entityLoaderCallback) 180 | { 181 | // force mode 182 | $this->setEntityLoaderMode(self::ENTITY_LOADER_CALLBACK); 183 | 184 | $this->entityLoaderCallback = $entityLoaderCallback; 185 | 186 | return $this; 187 | } 188 | 189 | /** 190 | * @return callable 191 | */ 192 | public function getEntityLoaderCallback() 193 | { 194 | return $this->entityLoaderCallback; 195 | } 196 | 197 | public function haveTotalColumns(): bool 198 | { 199 | foreach ($this->getColumns() as $column) { 200 | if ($column->isUseTotal()) { 201 | return true; 202 | } 203 | } 204 | 205 | return false; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/Components/TableInterface.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 22 | 23 | // Here you should define the parameters that are allowed to 24 | // configure your bundle. See the documentation linked above for 25 | // more information on that topic. 26 | 27 | return $treeBuilder; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/DependencyInjection/KilikTableExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 24 | 25 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 26 | $loader->load('services.yml'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/KilikTableBundle.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | {% endif %} 7 | {% endblock tableHeadMassActionsColumn %} 8 | 9 | {% block tableFilterMassActionsColumn %} 10 | {% if table.massActions %} 11 | 12 |
13 | 14 | 15 | 16 | 19 | 35 |
36 | 37 | {% endif %} 38 | {% endblock tableFilterMassActionsColumn %} 39 | 40 | {% block tableBodyMassActionsColumn %} 41 | {% if table.massActions %} 42 | 43 |
44 | 45 | 46 | 47 | 48 |
49 | 50 | {% endif %} 51 | {% endblock tableBodyMassActionsColumn %} 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/Resources/views/_columnCell.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_columnCell.html.twig #} 2 | {# @param table: Kilik\Table #} 3 | {# @param column: Kilik\Column #} 4 | {# @param row: array (from line result) #} 5 | 6 | {% if not column.hidden %} 7 | {% block tableBodyCellOuter %} 8 | 9 | {% block tableBodyCellInner %} 10 | {% set cellHtml=table.value(column,row) %} 11 | {% if column.raw %} 12 | {{ cellHtml | raw }} 13 | {% else %} 14 | {{ cellHtml }} 15 | {% endif %} 16 | {% endblock tableBodyCellInner %} 17 | 18 | {% endblock tableBodyCellOuter %} 19 | {% endif %} 20 | -------------------------------------------------------------------------------- /src/Resources/views/_columnCellNoTable.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_columnCell.html.twig #} 2 | {% if not column.hidden %} 3 | {% block tableBodyCellOuter %} 4 |
5 | {% block tableBodyCellInner %} 6 | {% set cellHtml=table.value(column,row) %} 7 | {% if column.raw %} 8 | {{ cellHtml | raw }} 9 | {% else %} 10 | {{ cellHtml }} 11 | {% endif %} 12 | {% endblock tableBodyCellInner %} 13 |
14 | {% endblock tableBodyCellOuter %} 15 | {% endif %} 16 | -------------------------------------------------------------------------------- /src/Resources/views/_columnFilter.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_columnFilter.html.twig #} 2 | 3 | {% if column.filter is not null %} 4 | {{ form_widget(attribute(table.formView,column.filter.name ),{"attr": {"class": "form-control refreshOnKeyup refreshOnChange","data-column": column.name} }) }} 5 | {% endif %} 6 | 7 | -------------------------------------------------------------------------------- /src/Resources/views/_columnFilterNoTable.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_columnFilterNoTable.html.twig #} 2 |
3 | {% if column.filter is not null %} 4 | {{ form_widget(attribute(table.formView,column.filter.name ),{"attr": {"class": "form-control refreshOnKeyup refreshOnChange","data-column": column.name,"data-label": column.label} }) }} 5 | {% endif %} 6 |
7 | -------------------------------------------------------------------------------- /src/Resources/views/_columnName.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_columnName.html.twig #} 2 | {% set label = column.label %} 3 | {% if column.translateDomain is not null %} 4 | {% set label = (column.label | trans({}, column.translateDomain)) %} 5 | {% if column.capitalize %} 6 | {% set label = label | capitalize %} 7 | {% endif %} 8 | {% endif %} 9 | 10 | {% if column.sortable %} 11 | 12 | 13 | {{ label }} 14 | 15 | 16 | 17 | {% else %} 18 | 19 | {{ label }} 20 | 21 | {% endif %} 22 | 23 | -------------------------------------------------------------------------------- /src/Resources/views/_columnNameNoTable.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_columnNameNoTable.html.twig #} 2 | {% set label = column.label %} 3 | {% if column.translateDomain is not null %} 4 | {% set label = (column.label | trans({}, column.translateDomain)) %} 5 | {% if column.capitalize %} 6 | {% set label = label | capitalize %} 7 | {% endif %} 8 | {% endif %} 9 | 10 | {% if column.sortable %} 11 |
12 | 13 | {{ label }} 14 | 15 | 16 |
17 | {% else %} 18 |
19 | {{ label }} 20 |
21 | {% endif %} 22 | 23 | -------------------------------------------------------------------------------- /src/Resources/views/_condensedTable.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_condensedTable.html.twig #} 2 | 3 | {# @param Kilik\Components\Table table #} 4 | {{ form_start(table.formView) }} 5 | {% block tableBeforePanel %} 6 | {% endblock tableBeforePanel %} 7 | {% block tableMetadata %} 8 |
{{ table.options | json_encode | raw }}
9 | {% endblock tableMetadata %} 10 | 11 | 12 | {% block tableHead %} 13 | 14 | {% block tableHeadStdColumns %} 15 | {% for column in table.columns %} 16 | {% include "@KilikTable/_columnName.html.twig" %} 17 | {% endfor %} 18 | {% endblock tableHeadStdColumns %} 19 | 20 | {% if table.columns|length > 0 %} 21 | 22 | {% block tableHeadStdFilters %} 23 | {% for column in table.columns %} 24 | {% include "@KilikTable/_columnFilter.html.twig" %} 25 | {% endfor %} 26 | {% endblock tableHeadStdFilters %} 27 | 28 | {% endif %} 29 | {% endblock tableHead %} 30 | 31 | 32 | {% block tableBody %} 33 | {% if tableRenderBody is defined %} 34 | {% for row in rows %} 35 | 36 | {% block tableBodyStdColumns %} 37 | {% for column in table.columns %} 38 | {% if column.cellTemplate is not null %} 39 | {# custom cell template is defined ? #} 40 | {% include column.cellTemplate %} 41 | {% else %} 42 | {# cell template fallback #} 43 | {% include "@KilikTable/_columnCell.html.twig" %} 44 | {% endif %} 45 | {% endfor %} 46 | {% endblock tableBodyStdColumns %} 47 | 48 | {% endfor %} 49 | {% endif %} 50 | {% endblock tableBody %} 51 | 52 |
53 | 54 |
55 |
56 | {% block tableStats %} 57 |
58 | {% block tableStatsAjax %} 59 | {% include "@KilikTable/_stats.html.twig" %} 60 | {% endblock tableStatsAjax %} 61 |
62 | {% endblock tableStats %} 63 |
64 | 65 |
66 | {% block tablePagination %} 67 |
68 | {% block tablePaginationAjax %} 69 | {% include "@KilikTable/_paginationNumbersIcons.html.twig" %} 70 | {% endblock tablePaginationAjax %} 71 |
72 | {% endblock tablePagination %} 73 |
74 |
75 | 76 | {% block tableAfterPanel %} 77 | {% endblock tableAfterPanel %} 78 | {{ form_end(table.formView) }} 79 | -------------------------------------------------------------------------------- /src/Resources/views/_defaultTable.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_defaultTable.html.twig #} 2 | {% use "@KilikTable/_blocks.html.twig" with 3 | tableHeadMassActionsColumn as parent_tableHeadMassActionsColumn, 4 | tableFilterMassActionsColumn as parent_tableFilterMassActionsColumn, 5 | tableBodyMassActionsColumn as parent_tableBodyMassActionsColumn 6 | %} 7 | {# @param Kilik\Components\Table table #} 8 | {{ form_start(table.formView) }} 9 | {% block tableBeforePanel %} 10 | {% endblock tableBeforePanel %} 11 |
12 |
13 |
14 | {% block tablePagination %} 15 |
16 | {% block tablePaginationAjax %} 17 | {% include "@KilikTable/_paginationNumbersIcons.html.twig" %} 18 | {% endblock tablePaginationAjax %} 19 |
20 | {% endblock tablePagination %} 21 | {% block tableTitle %} 22 | Default title 23 | {% endblock tableTitle %} 24 |
25 |
26 |
27 | {% block tableLoader %}
{% endblock tableLoader %} 28 | {% block tableMetadata %} 29 |
{{ table.options | json_encode | raw }}
30 | {% endblock tableMetadata %} 31 | 32 | 33 | {% block tableHead %} 34 | 35 | {% block tableHeadMassActionsColumn %} 36 | {{ block('parent_tableHeadMassActionsColumn') }} 37 | {% endblock %} 38 | {% block tableHeadStdColumns %} 39 | {% for column in table.columns %} 40 | {% include "@KilikTable/_columnName.html.twig" %} 41 | {% endfor %} 42 | {% endblock tableHeadStdColumns %} 43 | 44 | {% if table.columns|length > 0 %} 45 | 46 | {% block tableFilterMassActionsColumn %} 47 | {{ block('parent_tableFilterMassActionsColumn') }} 48 | {% endblock %} 49 | {% block tableHeadStdFilters %} 50 | {% for column in table.columns %} 51 | {% include "@KilikTable/_columnFilter.html.twig" %} 52 | {% endfor %} 53 | {% endblock tableHeadStdFilters %} 54 | 55 | {% endif %} 56 | {% endblock tableHead %} 57 | 58 | 59 | {% block tableBody %} 60 | {% if tableRenderBody is defined %} 61 | {% for row in rows %} 62 | 63 | {% block tableBodyMassActionsColumn %} 64 | {{ block('parent_tableBodyMassActionsColumn') }} 65 | {% endblock %} 66 | {% block tableBodyStdColumns %} 67 | {% for column in table.columns %} 68 | {% if column.cellTemplate is not null %} 69 | {# custom cell template is defined ? #} 70 | {% include column.cellTemplate %} 71 | {% else %} 72 | {# cell template fallback #} 73 | {% include "@KilikTable/_columnCell.html.twig" %} 74 | {% endif %} 75 | {% endfor %} 76 | {% endblock tableBodyStdColumns %} 77 | 78 | {% endfor %} 79 | {% endif %} 80 | {% endblock tableBody %} 81 | 82 | {% if table.haveTotalColumns %} 83 | 84 | {% block tableFoot %} 85 | {% if tableRenderFoot is defined %} 86 | 87 | {% block tableFootStdColumns %} 88 | {% for key, column in table.columns %} 89 | {% if not column.hidden %} 90 | 97 | {% endif %} 98 | {% endfor %} 99 | {% endblock tableFootStdColumns %} 100 | 101 | {% endif %} 102 | {% endblock tableFoot %} 103 | 104 | {% endif %} 105 |
91 | {% if key == 0 and not column.isUseTotal %} 92 | {{ 'kiliktable.total' | trans | upper }} 93 | {% elseif column.isUseTotal %} 94 | {{ column.total }} 95 | {% endif %} 96 |
106 |
107 | 116 |
117 | {% block tableAfterPanel %} 118 | {% endblock tableAfterPanel %} 119 | {{ form_end(table.formView) }} 120 | -------------------------------------------------------------------------------- /src/Resources/views/_defaultTableAlt.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_defaultTableAlt.html.twig #} 2 | {% use "@KilikTable/_blocks.html.twig" with 3 | tableHeadMassActionsColumn as parent_tableHeadMassActionsColumn, 4 | tableFilterMassActionsColumn as parent_tableFilterMassActionsColumn, 5 | tableBodyMassActionsColumn as parent_tableBodyMassActionsColumn 6 | %} 7 | {# @param Kilik\Components\Table table #} 8 | {{ form_start(table.formView) }} 9 | {% block tableBeforePanel %} 10 | {% endblock tableBeforePanel %} 11 |
12 |
13 |
14 | {% block tableTitle %} 15 | Default title 16 | {% endblock tableTitle %} 17 |
18 | 19 | {# setup of the list: hidden columns, rows per page ... #} 20 | {% include "@KilikTable/_setup.html.twig" %} 21 | {% include "@KilikTable/_rowsPerPage.html.twig" %} 22 | 23 |
24 |
25 | {% block tableLoader %}
{% endblock tableLoader %} 26 | {% block tableMetadata %} 27 |
{{ table.options | json_encode | raw }}
28 | {% endblock tableMetadata %} 29 | 30 | 31 | {% block tableHead %} 32 | 33 | {# columns names #} 34 | {% block tableHeadMassActionsColumn %} 35 | {{ block('parent_tableHeadMassActionsColumn') }} 36 | {% endblock %} 37 | {% block tableHeadStdColumns %} 38 | {% for column in table.columns %} 39 | {% include "@KilikTable/_columnName.html.twig" %} 40 | {% endfor %} 41 | {% endblock tableHeadStdColumns %} 42 | 43 | {# columns filters #} 44 | {% if table.columns|length > 0 %} 45 | 46 | {% block tableFilterMassActionsColumn %} 47 | {{ block('parent_tableFilterMassActionsColumn') }} 48 | {% endblock %} 49 | {% block tableHeadStdFilters %} 50 | {% for column in table.columns %} 51 | {% include "@KilikTable/_columnFilter.html.twig" %} 52 | {% endfor %} 53 | {% endblock tableHeadStdFilters %} 54 | 55 | {% endif %} 56 | {% endblock tableHead %} 57 | 58 | 59 | {% block tableBody %} 60 | {% if tableRenderBody is defined %} 61 | {% for row in rows %} 62 | 63 | {% block tableBodyMassActionsColumn %} 64 | {{ block('parent_tableBodyMassActionsColumn') }} 65 | {% endblock %} 66 | {% block tableBodyStdColumns %} 67 | {% for column in table.columns %} 68 | {% if column.cellTemplate is not null %} 69 | {# custom cell template is defined ? #} 70 | {% include column.cellTemplate %} 71 | {% else %} 72 | {# cell template fallback #} 73 | {% include "@KilikTable/_columnCell.html.twig" %} 74 | {% endif %} 75 | {% endfor %} 76 | {% endblock tableBodyStdColumns %} 77 | 78 | {% endfor %} 79 | {% endif %} 80 | {% endblock tableBody %} 81 | 82 | {% if table.haveTotalColumns %} 83 | 84 | {% block tableFoot %} 85 | {% if tableRenderFoot is defined %} 86 | 87 | {% block tableFootStdColumns %} 88 | {% for key, column in table.columns %} 89 | {% if not column.hidden %} 90 | 97 | {% endif %} 98 | {% endfor %} 99 | {% endblock tableFootStdColumns %} 100 | 101 | {% endif %} 102 | {% endblock tableFoot %} 103 | 104 | {% endif %} 105 |
91 | {% if key == 0 and not column.isUseTotal %} 92 | {{ 'kiliktable.total' | trans | upper }} 93 | {% elseif column.isUseTotal %} 94 | {{ column.total }} 95 | {% endif %} 96 |
106 |
107 |
108 |
109 | {% block tableStats %} 110 |
111 | {% block tableStatsAjax %} 112 | {% include "@KilikTable/_stats.html.twig" %} 113 | {% endblock tableStatsAjax %} 114 |
115 | {% endblock tableStats %} 116 |
117 |
118 | 119 | {% block tablePagination %} 120 |
121 | {% block tablePaginationAjax %} 122 | {% include "@KilikTable/_paginationNumbers.html.twig" %} 123 | {% endblock tablePaginationAjax %} 124 |
125 | {% endblock tablePagination %} 126 |
127 |
128 | {% block tableAfterPanel %} 129 | {% endblock tableAfterPanel %} 130 | {{ form_end(table.formView) }} 131 | -------------------------------------------------------------------------------- /src/Resources/views/_defaultTableSimple.html.twig: -------------------------------------------------------------------------------- 1 | {# @param Kilik\Components\Table table #} 2 | {{ form_start(table.formView) }} 3 | {% block tableBeforePanel %} 4 | {% endblock tableBeforePanel %} 5 |
6 | {% block panelHeading %} 7 |
8 | {% block panelHeadingTools %} 9 |
10 | {% block panelHeadingToolsInner %} 11 | {% endblock panelHeadingToolsInner %} 12 |
13 | {% endblock %} 14 | {% block tableTitle %} 15 | Default title 16 | {% endblock tableTitle %} 17 | {% include "@KilikTable/_setup.html.twig" %} 18 | {% include "@KilikTable/_rowsPerPage.html.twig" %} 19 |
20 | {% endblock %} 21 |
22 | {% block tableLoader %}
{% endblock tableLoader %} 23 | {% block tableMetadata %} 24 |
{{ table.options | json_encode | raw }}
25 | {% endblock tableMetadata %} 26 | 27 | 28 | {% block tableHead %} 29 | 30 | {# columns names #} 31 | {% block tableHeadStdColumns %} 32 | {% for column in table.columns %} 33 | {% include "@KilikTable/_columnName.html.twig" %} 34 | {% endfor %} 35 | {% endblock tableHeadStdColumns %} 36 | 37 | {# columns filters #} 38 | {% if table.columns|length > 0 %} 39 | 40 | {% block tableHeadStdFilters %} 41 | {% for column in table.columns %} 42 | {% include "@KilikTable/_columnFilter.html.twig" %} 43 | {% endfor %} 44 | {% endblock tableHeadStdFilters %} 45 | 46 | {% endif %} 47 | {% endblock tableHead %} 48 | 49 | 50 | {% block tableBody %} 51 | {% if tableRenderBody is defined %} 52 | {% for row in rows %} 53 | 54 | {% block tableBodyStdColumns %} 55 | {% for column in table.columns %} 56 | {% if column.cellTemplate is not null %} 57 | {# custom cell template is defined ? #} 58 | {% include column.cellTemplate %} 59 | {% else %} 60 | {# cell template fallback #} 61 | {% include "@KilikTable/_columnCell.html.twig" %} 62 | {% endif %} 63 | {% endfor %} 64 | {% endblock tableBodyStdColumns %} 65 | 66 | {% endfor %} 67 | {% endif %} 68 | {% endblock tableBody %} 69 | 70 | {% if table.haveTotalColumns %} 71 | 72 | {% block tableFoot %} 73 | {% if tableRenderFoot is defined %} 74 | 75 | {% block tableFootStdColumns %} 76 | {% for key, column in table.columns %} 77 | {% if not column.hidden %} 78 | 85 | {% endif %} 86 | {% endfor %} 87 | {% endblock tableFootStdColumns %} 88 | 89 | {% endif %} 90 | {% endblock tableFoot %} 91 | 92 | {% endif %} 93 |
79 | {% if key == 0 and not column.isUseTotal %} 80 | {{ 'kiliktable.total' | trans | upper }} 81 | {% elseif column.isUseTotal %} 82 | {{ column.total }} 83 | {% endif %} 84 |
94 | 95 | 96 |
97 | {% block tableStats %} 98 |
99 | {% block tableStatsAjax %} 100 | {% include "@KilikTable/_stats.html.twig" %} 101 | {% endblock tableStatsAjax %} 102 |
103 | {% endblock tableStats %} 104 |
105 |
106 | 107 | {% block tablePagination %} 108 |
109 | {% block tablePaginationAjax %} 110 | {% include "@KilikTable/_paginationNumbers.html.twig" %} 111 | {% endblock tablePaginationAjax %} 112 |
113 | {% endblock tablePagination %} 114 |
115 |
116 |
117 |
118 | {% block tableAfterPanel %} 119 | {% endblock tableAfterPanel %} 120 | {{ form_end(table.formView) }} 121 | -------------------------------------------------------------------------------- /src/Resources/views/_formLeftNoTable.html.twig: -------------------------------------------------------------------------------- 1 | {# Alternative display (without table) #} 2 | {# @param Kilik\Components\Table table #} 3 | {{ form_start(table.formView) }} 4 | {% block tableBeforePanel %} 5 | {% endblock tableBeforePanel %} 6 |
7 |
8 | {% block tableHead %} 9 | {% for column in table.columns %} 10 | {% include "@KilikTable/_columnNameNoTable.html.twig" %} 11 | {% include "@KilikTable/_columnFilterNoTable.html.twig" %} 12 | {% endfor %} 13 | {% endblock tableHead %} 14 |
15 |
16 | {% block tableTitle %} 17 | Default title 18 | {% endblock tableTitle %} 19 | 20 |
21 | {% block tableBody %} 22 | {# table body should always be overridden in this template #} 23 | {% if tableRenderBody is defined %} 24 | {% for row in rows %} 25 |
26 | {% block tableBodyStdColumns %} 27 | {% for column in table.columns %} 28 |
29 | {% include "@KilikTable/_columnCellNoTable.html.twig" %} 30 |
31 | {% endfor %} 32 | {% endblock tableBodyStdColumns %} 33 |
34 | {% endfor %} 35 | {% endif %} 36 | {% endblock tableBody %} 37 |
38 | 39 | {% block tableStats %} 40 |
41 | {% block tableStatsAjax %} 42 | {% include "@KilikTable/_stats.html.twig" %} 43 | {% endblock tableStatsAjax %} 44 |
45 | {% endblock tableStats %} 46 | 47 | {% block tablePagination %} 48 |
49 | {% block tablePaginationAjax %} 50 | {% include "@KilikTable/_paginationNumbersIcons.html.twig" %} 51 | {% endblock tablePaginationAjax %} 52 |
53 | {% endblock tablePagination %} 54 |
55 |
56 | 57 | {% block tableAfterPanel %} 58 | {% endblock tableAfterPanel %} 59 | {{ form_end(table.formView) }} 60 | -------------------------------------------------------------------------------- /src/Resources/views/_pagination.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_pagination.html.twig #} 2 | 3 | {# basic pagination #} 4 | {% if tableRenderPagination is defined %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% endif %} 18 | -------------------------------------------------------------------------------- /src/Resources/views/_paginationNumbers.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_paginationNumbers.html.twig #} 2 | 3 | {% if tableRenderPagination is defined %} 4 | {# previous page #} 5 | 6 | {{ "kiliktable.previous" |trans }} 7 | 8 | 9 | {# first page #} 10 | {% if table.page > 2 %} 11 | 12 | 1 13 | 14 | {% endif %} 15 | 16 | {# page - 2 ? #} 17 | {% if table.page > 3 %} 18 | {% if table.page == 4 %} 19 | 20 | {{ table.page-2 }} 21 | 22 | {% else %} 23 | 24 | ... 25 | 26 | {% endif %} 27 | {% endif %} 28 | 29 | {# page - 1 ? (previous) #} 30 | {% if table.page > 1 %} 31 | 32 | {{ table.page-1 }} 33 | 34 | {% endif %} 35 | 36 | {# page active #} 37 | 38 | {{ table.page }} 39 | 40 | 41 | {# page + 1 ? (next) #} 42 | {% if table.lastPage-table.page > 0 %} 43 | 44 | {{ table.page+1 }} 45 | 46 | {% endif %} 47 | 48 | {# page + 2 ? #} 49 | {% if table.lastPage-table.page > 2 %} 50 | {% if table.lastPage-table.page == 3 %} 51 | 52 | {{ table.page+2 }} 53 | 54 | {% else %} 55 | 56 | ... 57 | 58 | {% endif %} 59 | {% endif %} 60 | 61 | {# last page #} 62 | {% if table.lastPage-table.page > 1 %} 63 | 64 | {{ table.lastPage }} 65 | 66 | {% endif %} 67 | 68 | {# next page #} 69 | 70 | {{ "kiliktable.next"|trans }} 71 | 72 | {% endif %} 73 | -------------------------------------------------------------------------------- /src/Resources/views/_paginationNumbersIcons.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_paginationNumbersIcons.html.twig #} 2 | 3 | {% if tableRenderPagination is defined %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{ table.page }} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% endif %} 20 | -------------------------------------------------------------------------------- /src/Resources/views/_rowsPerPage.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_rowsPerPage.html.twig #} 2 | 3 | 8 | {{ "kiliktable.rows_per_page" |trans }} 9 | 10 | -------------------------------------------------------------------------------- /src/Resources/views/_setup.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_setup.html.twig #} 2 | 3 | 29 | -------------------------------------------------------------------------------- /src/Resources/views/_stats.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_stats.html.twig #} 2 | 3 | {% if tableRenderStats is defined %} 4 | {{ "kiliktable.showing_entries"|trans({"%count%" : table.filteredRows, "%firstRow%": table.firstRow,"%lastRow%": table.lastRow,"%filteredRows%":table.filteredRows}) }} 5 | {% if table.filteredRows != table.totalRows and table.totalRows > 0 %} 6 | ({{ "kiliktable.filtered_from"|trans({"%count%": table.totalRows, "%totalRows%":table.totalRows}) }}) 7 | {% endif %} 8 | {% endif %} 9 | -------------------------------------------------------------------------------- /src/Resources/views/layout.html.twig: -------------------------------------------------------------------------------- 1 | {# Minimal layout to present a working version of KilikTable #} 2 | 3 | 4 | 5 | 6 | 7 | Kilik/TableBundle 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% block body %} 20 | {% endblock body %} 21 | {% block javascript %} 22 | {% endblock javascript %} 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Resources/views/theme/dark4/README.md: -------------------------------------------------------------------------------- 1 | # Dark 4 2 | 3 | This is the new Kilik Theme (1.1+) based on Bootstrap 4 + Font Awesome. 4 | 5 | tables/default.html.twig: 6 | 7 | ![](doc/default.png) 8 | 9 | tables/alternative.html.twig: 10 | 11 | ![](doc/alternative.png) 12 | -------------------------------------------------------------------------------- /src/Resources/views/theme/dark4/components/columnName.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_columnName.html.twig #} 2 | {% set label = column.label %} 3 | {% if column.translateDomain is not null %} 4 | {% set label = (column.label | trans({}, column.translateDomain)) %} 5 | {% if column.capitalize %} 6 | {% set label = label | capitalize %} 7 | {% endif %} 8 | {% endif %} 9 | 10 | {% if column.sortable %} 11 | 12 | 13 | {{ label }} 14 | 15 | 16 | 17 | {% else %} 18 | 19 | {{ label }} 20 | 21 | {% endif %} 22 | 23 | -------------------------------------------------------------------------------- /src/Resources/views/theme/dark4/components/pagination.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/theme/bootstrap-4-fa/_pagination.html.twig #} 2 | 3 | {# basic pagination #} 4 | {% if tableRenderPagination is defined %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% endif %} 18 | -------------------------------------------------------------------------------- /src/Resources/views/theme/dark4/components/paginationNumbers.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_paginationNumbers.html.twig #} 2 | 3 | {% if tableRenderPagination is defined %} 4 | {# previous page #} 5 | 6 | {{ "kiliktable.previous" |trans }} 7 | 8 | 9 | {# first page #} 10 | {% if table.page > 2 %} 11 | 12 | 1 13 | 14 | {% endif %} 15 | 16 | {# page - 2 ? #} 17 | {% if table.page > 3 %} 18 | {% if table.page == 4 %} 19 | 20 | {{ table.page-2 }} 21 | 22 | {% else %} 23 | 24 | ... 25 | 26 | {% endif %} 27 | {% endif %} 28 | 29 | {# page - 1 ? (previous) #} 30 | {% if table.page > 1 %} 31 | 32 | {{ table.page-1 }} 33 | 34 | {% endif %} 35 | 36 | {# page active #} 37 | 38 | {{ table.page }} 39 | 40 | 41 | {# page + 1 ? (next) #} 42 | {% if table.lastPage-table.page > 0 %} 43 | 44 | {{ table.page+1 }} 45 | 46 | {% endif %} 47 | 48 | {# page + 2 ? #} 49 | {% if table.lastPage-table.page > 2 %} 50 | {% if table.lastPage-table.page == 3 %} 51 | 52 | {{ table.page+2 }} 53 | 54 | {% else %} 55 | 56 | ... 57 | 58 | {% endif %} 59 | {% endif %} 60 | 61 | {# last page #} 62 | {% if table.lastPage-table.page > 1 %} 63 | 64 | {{ table.lastPage }} 65 | 66 | {% endif %} 67 | 68 | {# next page #} 69 | 70 | {{ "kiliktable.next"|trans }} 71 | 72 | {% endif %} 73 | -------------------------------------------------------------------------------- /src/Resources/views/theme/dark4/components/paginationNumbersIcons.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_paginationNumbersIcons.html.twig #} 2 | 3 | {% if tableRenderPagination is defined %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{ table.page }} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% endif %} 20 | -------------------------------------------------------------------------------- /src/Resources/views/theme/dark4/components/setup.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_setup.html.twig #} 2 | 3 | 4 | 5 | 22 | 23 | -------------------------------------------------------------------------------- /src/Resources/views/theme/dark4/doc/alternative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilikFr/TableBundle/7e01d148c9fe3d2be1183042adc46c622609eeeb/src/Resources/views/theme/dark4/doc/alternative.png -------------------------------------------------------------------------------- /src/Resources/views/theme/dark4/doc/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilikFr/TableBundle/7e01d148c9fe3d2be1183042adc46c622609eeeb/src/Resources/views/theme/dark4/doc/default.png -------------------------------------------------------------------------------- /src/Resources/views/theme/dark4/tables/alternative.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/theme/bootstrap-4-fa/_defaultTableAlt.html.twig #} 2 | {% use "@KilikTable/_blocks.html.twig" with 3 | tableHeadMassActionsColumn as parent_tableHeadMassActionsColumn, 4 | tableFilterMassActionsColumn as parent_tableFilterMassActionsColumn, 5 | tableBodyMassActionsColumn as parent_tableBodyMassActionsColumn %} 6 | {# @param Kilik\Components\Table table #} 7 | {{ form_start(table.formView) }} 8 | {% block tableBeforePanel %} 9 | {% endblock tableBeforePanel %} 10 |
11 |
12 |
13 |
14 | {% block tableTitle %} 15 | Default title 16 | {% endblock tableTitle %} 17 | 18 | {# setup of the list: hidden columns, rows per page ... #} 19 | {% include "@KilikTable/theme/dark4/components/setup.html.twig" %} 20 | {% include "@KilikTable/_rowsPerPage.html.twig" %} 21 | 22 |
23 |
24 | {% block tableLoader %}
{% endblock tableLoader %} 25 | {% block tableMetadata %} 26 |
{{ table.options | json_encode | raw }}
27 | {% endblock tableMetadata %} 28 | 29 | 30 | {% block tableHead %} 31 | 32 | {# columns names #} 33 | {% block tableHeadMassActionsColumn %} 34 | {{ block('parent_tableHeadMassActionsColumn') }} 35 | {% endblock %} 36 | {% block tableHeadStdColumns %} 37 | {% for column in table.columns %} 38 | {% include "@KilikTable/theme/dark4/components/columnName.html.twig" %} 39 | {% endfor %} 40 | {% endblock tableHeadStdColumns %} 41 | 42 | {# columns filters #} 43 | {% if table.columns|length > 0 %} 44 | 45 | {% block tableFilterMassActionsColumn %} 46 | {{ block('parent_tableFilterMassActionsColumn') }} 47 | {% endblock %} 48 | {% block tableHeadStdFilters %} 49 | {% for column in table.columns %} 50 | {% include "@KilikTable/_columnFilter.html.twig" %} 51 | {% endfor %} 52 | {% endblock tableHeadStdFilters %} 53 | 54 | {% endif %} 55 | {% endblock tableHead %} 56 | 57 | 58 | {% block tableBody %} 59 | {% if tableRenderBody is defined %} 60 | {% for row in rows %} 61 | 62 | {% block tableBodyMassActionsColumn %} 63 | {{ block('parent_tableBodyMassActionsColumn') }} 64 | {% endblock %} 65 | {% block tableBodyStdColumns %} 66 | {% for column in table.columns %} 67 | {% if column.cellTemplate is not null %} 68 | {# custom cell template is defined ? #} 69 | {% include column.cellTemplate %} 70 | {% else %} 71 | {# cell template fallback #} 72 | {% include "@KilikTable/_columnCell.html.twig" %} 73 | {% endif %} 74 | {% endfor %} 75 | {% endblock tableBodyStdColumns %} 76 | 77 | {% endfor %} 78 | {% endif %} 79 | {% endblock tableBody %} 80 | 81 | {% if table.haveTotalColumns %} 82 | 83 | {% block tableFoot %} 84 | {% if tableRenderFoot is defined %} 85 | 86 | {% block tableFootStdColumns %} 87 | {% for key, column in table.columns %} 88 | {% if not column.hidden %} 89 | 96 | {% endif %} 97 | {% endfor %} 98 | {% endblock tableFootStdColumns %} 99 | 100 | {% endif %} 101 | {% endblock tableFoot %} 102 | 103 | {% endif %} 104 |
90 | {% if key == 0 and not column.isUseTotal %} 91 | {{ 'kiliktable.total' | trans | upper }} 92 | {% elseif column.isUseTotal %} 93 | {{ column.total }} 94 | {% endif %} 95 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | {% block tableStats %} 112 |
113 | {% block tableStatsAjax %} 114 | {% include "@KilikTable/_stats.html.twig" %} 115 | {% endblock tableStatsAjax %} 116 |
117 | {% endblock tableStats %} 118 |
119 |
120 |
121 | {% block tablePagination %} 122 |
123 | {% block tablePaginationAjax %} 124 | {% include "@KilikTable/theme/dark4/components/paginationNumbers.html.twig" %} 125 | {% endblock tablePaginationAjax %} 126 |
127 | {% endblock tablePagination %} 128 |
129 |
130 |
131 | {% block tableAfterPanel %} 132 | {% endblock tableAfterPanel %} 133 | {{ form_end(table.formView) }} 134 | -------------------------------------------------------------------------------- /src/Resources/views/theme/dark4/tables/default.html.twig: -------------------------------------------------------------------------------- 1 | {# @KilikTable/_defaultTable.html.twig #} 2 | {% use "@KilikTable/_blocks.html.twig" with 3 | tableHeadMassActionsColumn as parent_tableHeadMassActionsColumn, 4 | tableFilterMassActionsColumn as parent_tableFilterMassActionsColumn, 5 | tableBodyMassActionsColumn as parent_tableBodyMassActionsColumn %} 6 | {# @param Kilik\Components\Table table #} 7 | {{ form_start(table.formView) }} 8 | {% block tableBeforePanel %} 9 | {% endblock tableBeforePanel %} 10 |
11 |
12 |
13 |
14 | {% block tablePagination %} 15 |
16 | {% block tablePaginationAjax %} 17 | {% include "@KilikTable/theme/dark4/components/paginationNumbersIcons.html.twig" %} 18 | {% endblock tablePaginationAjax %} 19 |
20 | {% endblock tablePagination %} 21 | {% block tableTitle %} 22 | Default title 23 | {% endblock tableTitle %} 24 |
25 |
26 |
27 | {% block tableLoader %}
{% endblock tableLoader %} 28 | {% block tableMetadata %} 29 |
{{ table.options | json_encode | raw }}
30 | {% endblock tableMetadata %} 31 | 32 | 33 | {% block tableHead %} 34 | 35 | {% block tableHeadMassActionsColumn %} 36 | {{ block('parent_tableHeadMassActionsColumn') }} 37 | {% endblock %} 38 | {% block tableHeadStdColumns %} 39 | {% for column in table.columns %} 40 | {% include "@KilikTable/theme/dark4/components/columnName.html.twig" %} 41 | {% endfor %} 42 | {% endblock tableHeadStdColumns %} 43 | 44 | {% if table.columns|length > 0 %} 45 | 46 | {% block tableFilterMassActionsColumn %} 47 | {{ block('parent_tableFilterMassActionsColumn') }} 48 | {% endblock %} 49 | {% block tableHeadStdFilters %} 50 | {% for column in table.columns %} 51 | {% include "@KilikTable/_columnFilter.html.twig" %} 52 | {% endfor %} 53 | {% endblock tableHeadStdFilters %} 54 | 55 | {% endif %} 56 | {% endblock tableHead %} 57 | 58 | 59 | {% block tableBody %} 60 | {% if tableRenderBody is defined %} 61 | {% for row in rows %} 62 | 63 | {% block tableBodyMassActionsColumn %} 64 | {{ block('parent_tableBodyMassActionsColumn') }} 65 | {% endblock %} 66 | {% block tableBodyStdColumns %} 67 | {% for column in table.columns %} 68 | {% if column.cellTemplate is not null %} 69 | {# custom cell template is defined ? #} 70 | {% include column.cellTemplate %} 71 | {% else %} 72 | {# cell template fallback #} 73 | {% include "@KilikTable/_columnCell.html.twig" %} 74 | {% endif %} 75 | {% endfor %} 76 | {% endblock tableBodyStdColumns %} 77 | 78 | {% endfor %} 79 | {% endif %} 80 | {% endblock tableBody %} 81 | 82 | {% if table.haveTotalColumns %} 83 | 84 | {% block tableFoot %} 85 | {% if tableRenderFoot is defined %} 86 | 87 | {% block tableFootStdColumns %} 88 | {% for key, column in table.columns %} 89 | {% if not column.hidden %} 90 | 97 | {% endif %} 98 | {% endfor %} 99 | {% endblock tableFootStdColumns %} 100 | 101 | {% endif %} 102 | {% endblock tableFoot %} 103 | 104 | {% endif %} 105 |
91 | {% if key == 0 and not column.isUseTotal %} 92 | {{ 'kiliktable.total' | trans | upper }} 93 | {% elseif column.isUseTotal %} 94 | {{ column.total }} 95 | {% endif %} 96 |
106 |
107 | 116 |
117 |
118 | {% block tableAfterPanel %} 119 | {% endblock tableAfterPanel %} 120 | {{ form_end(table.formView) }} 121 | -------------------------------------------------------------------------------- /src/Services/AbstractTableService.php: -------------------------------------------------------------------------------- 1 | twig = $twig; 37 | $this->formFactory = $formFactory; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function form(TableInterface $table, array $data = array()) 44 | { 45 | // prepare defaults values 46 | $defaultValues = array(); 47 | foreach ($table->getAllFilters() as $filter) { 48 | if (null !== $filter->getDefaultValue()) { 49 | $defaultValues[$filter->getName()] = $filter->getDefaultValue(); 50 | } 51 | } 52 | 53 | $data = array_merge($defaultValues, $data); 54 | $form = $this->formFactory->createNamedBuilder($table->getId().'_form', FormType::class, $data); 55 | //$this->formBuilder->set 56 | foreach ($table->getAllFilters() as $filter) { 57 | $form->add( 58 | $filter->getName(), 59 | $filter->getInput(), 60 | $filter->getOptions() 61 | ); 62 | } 63 | 64 | // append special inputs (used for export csv for exemple) 65 | $form->add('sortColumn', \Symfony\Component\Form\Extension\Core\Type\HiddenType::class, array('required' => false)); 66 | $form->add('sortReverse', \Symfony\Component\Form\Extension\Core\Type\HiddenType::class, array('required' => false)); 67 | $table->setForm($form->getForm()); 68 | 69 | return $table->getForm()->createView(); 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function createFormView(TableInterface $table, array $data = array()) 76 | { 77 | return $table->setFormView($this->form($table, $data)); 78 | } 79 | 80 | /** 81 | * Export (selection by filters) as a CSV buffer. 82 | * 83 | * @param TableInterface $table 84 | * @param Request $request 85 | * 86 | * @return string 87 | */ 88 | public function exportAsCsv(TableInterface $table, Request $request) 89 | { 90 | $stream = fopen('php://memory', 'w+'); 91 | // execute query with filters, without pagination, only scalar results 92 | $rows = $this->getRows($table, $request, false, false); 93 | // first line: keys 94 | if (count($rows) > 0) { 95 | $headers = array_map(function($column){ 96 | return $column->getExportName() ?? $column->getName(); 97 | }, $table->getColumns()); 98 | 99 | if ($table->haveTotalColumns()) { 100 | $headers = array_merge([""], $headers); 101 | } 102 | 103 | fputcsv($stream, $headers, ';'); 104 | } 105 | 106 | foreach ($rows as $row) { 107 | $line = array_map(function($column) use ($row, $rows){ 108 | return $column->getExportValue($row, $rows); 109 | }, $table->getColumns()); 110 | 111 | if ($table->haveTotalColumns()){ 112 | $line = array_merge([""], $line); 113 | } 114 | 115 | fputcsv($stream, $line, ';'); 116 | } 117 | 118 | 119 | if ($table->haveTotalColumns()) { 120 | $total = array_map(function($column){ 121 | return $column->getTotal(); 122 | }, $table->getColumns()); 123 | 124 | fputcsv($stream, array_merge(['Total'], $total), ';'); 125 | } 126 | 127 | rewind($stream); 128 | $buffer = stream_get_contents($stream); 129 | fclose($stream); 130 | return $buffer; 131 | } 132 | 133 | /** 134 | * Handle the user request and return the JSON response (with pagination). 135 | * 136 | * @param TableInterface $table 137 | * @param Request $request 138 | * 139 | * @return Response 140 | * @throws \Exception|\Throwable 141 | */ 142 | public function handleRequest(TableInterface $table, Request $request) 143 | { 144 | // execute query with filters 145 | $rows = $this->getRows($table, $request); 146 | 147 | // params for twig parts 148 | $twigParams = array( 149 | 'table' => $table, 150 | 'rows' => $rows, 151 | ); 152 | 153 | $template = $this->twig->load($table->getTemplate()); 154 | 155 | $responseParams = array( 156 | 'page' => $table->getPage(), 157 | 'rowsPerPage' => $table->getRowsPerPage(), 158 | 'totalRows' => $table->getTotalRows(), 159 | 'filteredRows' => $table->getFilteredRows(), 160 | 'lastPage' => $table->getLastPage(), 161 | 'tableBody' => $template->renderBlock( 162 | 'tableBody', 163 | array_merge($twigParams, array('tableRenderBody' => true), $table->getTemplateParams()) 164 | ), 165 | 'tableFoot' => $template->renderBlock( 166 | 'tableFoot', 167 | array_merge($twigParams, array('tableRenderFoot' => true)) 168 | ), 169 | 'tableStats' => $template->renderBlock( 170 | 'tableStatsAjax', 171 | array_merge($twigParams, array('tableRenderStats' => true)) 172 | ), 173 | 'tablePagination' => $template->renderBlock( 174 | 'tablePaginationAjax', 175 | array_merge($twigParams, array('tableRenderPagination' => true)) 176 | ), 177 | ); 178 | 179 | // encode response 180 | $response = new Response(json_encode($responseParams)); 181 | 182 | return $response; 183 | } 184 | 185 | /** 186 | * @param Request $request 187 | * @param TableInterface $table 188 | * 189 | * @return mixed 190 | */ 191 | public function getSelectedRows(Request $request, TableInterface $table) 192 | { 193 | $identifiers = $request->request->all($table->getSelectionFormKey()); 194 | $entities = $this->loadRowsById($table, $identifiers); 195 | 196 | return $entities; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Services/TableApiService.php: -------------------------------------------------------------------------------- 1 | get($table->getFormId()); 27 | 28 | foreach ($table->getAllFilters() as $filter) { 29 | if (isset($queryParams[$filter->getName()])) { 30 | $searchParamRaw = trim($queryParams[$filter->getName()]); 31 | if ($searchParamRaw != '') { 32 | $filters[$filter->getName()] = $searchParamRaw; 33 | } 34 | } 35 | } 36 | 37 | return $filters; 38 | } 39 | 40 | /** 41 | * Parse OrderBy. 42 | * 43 | * @param TableInterface $table 44 | * @param Request $request 45 | * 46 | * @return array 47 | */ 48 | private function parseOrderBy(TableInterface $table, Request $request) 49 | { 50 | $orderBy = []; 51 | 52 | $queryParams = $request->get($table->getFormId()); 53 | 54 | if (isset($queryParams['sortColumn']) && $queryParams['sortColumn'] != '') { 55 | $column = $table->getColumnByName($queryParams['sortColumn']); 56 | // if column exists 57 | if (!is_null($column)) { 58 | if (!is_null($column->getSort())) { 59 | if (isset($queryParams['sortReverse'])) { 60 | $sortReverse = $queryParams['sortReverse']; 61 | } else { 62 | $sortReverse = false; 63 | } 64 | foreach ($column->getAutoSort($sortReverse) as $sortField => $sortOrder) { 65 | $orderBy[$sortField] = $sortOrder; 66 | } 67 | } 68 | } 69 | } 70 | 71 | return $orderBy; 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function getRows(TableInterface $table, Request $request, $paginate = true, $getObjects = true) 78 | { 79 | /* @var ApiTable $table */ 80 | $table->setRowsPerPage($request->get('rowsPerPage', 10)); 81 | $table->setPage($request->get('page', 1)); 82 | 83 | foreach ($request->get('hiddenColumns', []) as $hiddenColumnName => $notUsed) { 84 | $column = $table->getColumnByName($hiddenColumnName); 85 | if (!is_null($column)) { 86 | $column->setHidden(true); 87 | } 88 | } 89 | 90 | // get results with api 91 | $apiResult = $table->getApi()->load( 92 | $table, 93 | $this->parseFilters($table, $request), 94 | $this->parseOrderBy($table, $request), 95 | $paginate ? $table->getPage() : null, 96 | $paginate ? $table->getRowsPerPage() : null 97 | ); 98 | 99 | if ($paginate) { 100 | $table->setTotalRows($apiResult->getNbTotalRows()); 101 | $table->setFilteredRows($apiResult->getNbFilteredRows()); 102 | 103 | $table->setLastPage(ceil($table->getFilteredRows() / $table->getRowsPerPage())); 104 | 105 | if ($table->getPage() > $table->getLastPage()) { 106 | $table->setPage($table->getLastPage()); 107 | } 108 | } 109 | 110 | return $apiResult->getRows(); 111 | } 112 | 113 | /** 114 | * @inheritdoc 115 | */ 116 | public function loadRowsById(TableInterface $table, $identifiers) 117 | { 118 | throw new \Exception('this method should be overridden'); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Services/TableService.php: -------------------------------------------------------------------------------- 1 | get($table->getFormId()); 26 | 27 | foreach ($table->getAllFilters() as $filter) { 28 | if (!isset($queryParams[$filter->getName()])) { 29 | continue; 30 | } 31 | // Multiple inputs 32 | if (is_array($queryParams[$filter->getName()])) { 33 | $searchParamRaw = array_map('trim', $queryParams[$filter->getName()]); 34 | if (is_callable($filter->getQueryPartBuilder())) { 35 | $callback = $filter->getQueryPartBuilder(); 36 | $callback($filter, $table, $queryBuilder, $searchParamRaw); 37 | } 38 | continue; 39 | } 40 | 41 | $searchParamRaw = trim($queryParams[$filter->getName()]); 42 | 43 | // query build callback 44 | if (is_callable($filter->getQueryPartBuilder())) { 45 | $callback = $filter->getQueryPartBuilder(); 46 | $callback($filter, $table, $queryBuilder, $searchParamRaw); 47 | continue; 48 | } 49 | 50 | list($operator, $searchParam) = $filter->getOperatorAndValue($searchParamRaw); 51 | if ('' === (string)$searchParam) { 52 | continue; 53 | } 54 | 55 | list($searchOperator, $formattedSearch) = $filter->getFormattedInput($operator, $searchParam); 56 | 57 | // depending on operator 58 | switch ($searchOperator) { 59 | case Filter::TYPE_GREATER: 60 | case Filter::TYPE_GREATER_OR_EQUAL: 61 | case Filter::TYPE_LESS: 62 | case Filter::TYPE_LESS_OR_EQUAL: 63 | case Filter::TYPE_NOT_EQUAL: 64 | $sql = $filter->getField()." {$searchOperator} :filter_".$filter->getName(); 65 | $queryBuilder->setParameter('filter_'.$filter->getName(), $formattedSearch); 66 | break; 67 | case Filter::TYPE_EQUAL_STRICT: 68 | $sql = $filter->getField().' = :filter_'.$filter->getName(); 69 | $queryBuilder->setParameter('filter_'.$filter->getName(), $formattedSearch); 70 | break; 71 | case Filter::TYPE_EQUAL: 72 | $sql = $filter->getField().' like :filter_'.$filter->getName(); 73 | $queryBuilder->setParameter('filter_'.$filter->getName(), $formattedSearch); 74 | break; 75 | case Filter::TYPE_NOT_LIKE: 76 | $sql = $filter->getField().' not like :filter_'.$filter->getName(); 77 | $queryBuilder->setParameter('filter_'.$filter->getName(), '%'.$formattedSearch.'%'); 78 | break; 79 | case Filter::TYPE_NULL: 80 | $sql = $filter->getField().' IS NULL'; 81 | break; 82 | case Filter::TYPE_NOT_NULL: 83 | $sql = $filter->getField().' IS NOT NULL'; 84 | break; 85 | case Filter::TYPE_IN: 86 | $sql = $filter->getField().' IN (:filter_'.$filter->getName().')'; 87 | // $formattedSearch is like 'new,cancelled' 88 | $values = is_array($formattedSearch) ? $formattedSearch : explode(',', $formattedSearch); 89 | $queryBuilder->setParameter('filter_'.$filter->getName(), $values); 90 | break; 91 | case Filter::TYPE_NOT_IN: 92 | $sql = $filter->getField().' NOT IN (:filter_'.$filter->getName().')'; 93 | // $formattedSearch is like 'new,cancelled' 94 | $values = is_array($formattedSearch) ? $formattedSearch : explode(',', $formattedSearch); 95 | $queryBuilder->setParameter('filter_'.$filter->getName(), $values); 96 | break; 97 | // when filtering on 'description LIKE WORDS "house red blue"' 98 | // results are: description LIKE '%house%' AND 99 | case Filter::TYPE_LIKE_WORDS_AND: 100 | case Filter::TYPE_LIKE_WORDS_OR: 101 | $binaryOperator = Filter::TYPE_LIKE_WORDS_OR == $searchOperator ? 'OR' : 'AND'; 102 | $words = array_filter(array_map('trim', explode(' ', trim($formattedSearch)))); 103 | if (empty($words)) { 104 | break; 105 | } 106 | $sql = '('; 107 | foreach ($words as $i => $word) { 108 | if ($i > 0) { 109 | $sql .= ' '.$binaryOperator.' '; // AND / OR 110 | } 111 | $termKey = 'filter_'.$filter->getName().'_t'.$i; 112 | $sql .= $filter->getField().' like :'.$termKey; 113 | $queryBuilder->setParameter($termKey, '%'.$word.'%'); 114 | } 115 | $sql .= ')'; 116 | break; 117 | default: 118 | case Filter::TYPE_LIKE: 119 | $sql = $filter->getField().' like :filter_'.$filter->getName(); 120 | $queryBuilder->setParameter('filter_'.$filter->getName(), '%'.$formattedSearch.'%'); 121 | break; 122 | } 123 | 124 | if (!$sql) { 125 | continue; 126 | } 127 | 128 | $filter->getHaving() ? $queryBuilder->andHaving($sql) : $queryBuilder->andWhere($sql); 129 | } 130 | } 131 | 132 | /** 133 | * Set total rows count without filters. 134 | * 135 | * @param Table $table 136 | */ 137 | protected function setTotalRows(Table $table) 138 | { 139 | $qb = $table->getQueryBuilder(); 140 | $qbtr = clone $qb; 141 | 142 | $identifiers = $table->getIdentifierFieldNames(); 143 | $count = $this->countRows($qbtr, $identifiers); 144 | 145 | $table->setTotalRows($count); 146 | } 147 | 148 | /** 149 | * Set total rows count with filters. 150 | * 151 | * @param Table $table 152 | * @param Request $request 153 | */ 154 | private function setFilteredRows(Table $table, Request $request) 155 | { 156 | $qb = $table->getQueryBuilder(); 157 | $qbfr = clone $qb; 158 | $this->addSearch($table, $request, $qbfr); 159 | 160 | $identifiers = $table->getIdentifierFieldNames(); 161 | $count = $this->countRows($qbfr, $identifiers); 162 | 163 | $table->setFilteredRows($count); 164 | } 165 | 166 | /** 167 | * {@inheritdoc} 168 | */ 169 | public function getRows(TableInterface $table, Request $request, $paginate = true, $getObjects = true) 170 | { 171 | $table->setRowsPerPage($request->get('rowsPerPage', 10)); 172 | $table->setPage($request->get('page', 1)); 173 | 174 | foreach ($request->get('hiddenColumns', []) as $hiddenColumnName => $notUsed) { 175 | $column = $table->getColumnByName($hiddenColumnName); 176 | if (!is_null($column)) { 177 | $column->setHidden(true); 178 | } 179 | } 180 | 181 | $qb = $table->getQueryBuilder(); 182 | 183 | if ($paginate) { 184 | // @todo: had possibility to define custom count queries 185 | $this->setTotalRows($table); 186 | $this->setFilteredRows($table, $request); 187 | 188 | // compute last page and floor curent page 189 | $table->setLastPage(ceil($table->getFilteredRows() / $table->getRowsPerPage())); 190 | 191 | if ($table->getPage() > $table->getLastPage()) { 192 | $table->setPage($table->getLastPage()); 193 | } 194 | 195 | $qb->setMaxResults($table->getRowsPerPage()); 196 | $qb->setFirstResult(($table->getPage() - 1) * $table->getRowsPerPage()); 197 | } 198 | 199 | // add filters 200 | $this->addSearch($table, $request, $qb); 201 | 202 | // handle ordering 203 | $queryParams = $request->get($table->getFormId()); 204 | 205 | if (isset($queryParams['sortColumn']) && $queryParams['sortColumn'] != '') { 206 | $column = $table->getColumnByName($queryParams['sortColumn']); 207 | // if column exists 208 | if (!is_null($column)) { 209 | if (!is_null($column->getSort())) { 210 | $qb->resetDQLPart('orderBy'); 211 | if (isset($queryParams['sortReverse'])) { 212 | $sortReverse = $queryParams['sortReverse']; 213 | } else { 214 | $sortReverse = false; 215 | } 216 | foreach ($column->getAutoSort($sortReverse) as $sortField => $sortOrder) { 217 | $qb->addOrderBy($sortField, $sortOrder); 218 | } 219 | } 220 | } 221 | } 222 | 223 | // force a final ordering by id 224 | $qb->addOrderBy($table->getAlias().'.id', 'asc'); 225 | 226 | if ($table->haveTotalColumns()) { 227 | $totalQueryBuilder = clone ($qb); 228 | $totalQueryBuilder->setMaxResults(null)->setFirstResult(null); 229 | foreach ($table->getColumns() as $column) { 230 | if ($column->isUseTotal()) { 231 | $totalQueryBuilder->addSelect('SUM('.$column->getFilter()->getField(). ') AS ' . self::TOTAL_PREFIX.$column->getName()); 232 | } 233 | } 234 | $totalResults = $totalQueryBuilder->getQuery()->getResult()[0] ?? []; 235 | foreach ($table->getColumns() as $column) { 236 | $totalColumnResultName = self::TOTAL_PREFIX.$column->getName(); 237 | if ($column->isUseTotal()) { 238 | $callback = $column->getDisplayCallback(); 239 | if (!is_null($callback)) { 240 | if (!is_callable($callback)) { 241 | throw new \Exception('displayCallback is not callable'); 242 | } 243 | $column->setTotal($callback($totalResults[$totalColumnResultName]) ?? 0); 244 | } else { 245 | $column->setTotal($totalResults[$totalColumnResultName] ?? 0); 246 | } 247 | } 248 | } 249 | } 250 | 251 | $query = $qb->getQuery(); 252 | 253 | // if we need to get objects, LEGACY mode 254 | if ($getObjects && $table->getEntityLoaderMode() == $table::ENTITY_LOADER_LEGACY) { 255 | if (!is_null($qb->getDQLPart('groupBy'))) { 256 | // results as objects 257 | $objects = []; 258 | foreach ($query->getResult(Query::HYDRATE_OBJECT) as $object) { 259 | if (is_object($object)) { 260 | $index = is_int($object->getId()) ? $object->getId() : (string) $object->getId(); 261 | $objects[$index] = $object; 262 | } // when results are mixed with objects and scalar 263 | elseif (isset($object[0]) && is_object($object[0])) { 264 | $index = is_int($object[0]->getId()) ? $object[0]->getId() : (string) $object[0]->getId(); 265 | $objects[$index] = $object[0]; 266 | } 267 | } 268 | } 269 | } 270 | 271 | $rows = $query->getResult(Query::HYDRATE_SCALAR); 272 | 273 | // if we need to get objects 274 | if ($getObjects && in_array($table->getEntityLoaderMode(), [$table::ENTITY_LOADER_REPOSITORY, $table::ENTITY_LOADER_CALLBACK])) { 275 | // create entities identifiers list from scalar rows 276 | $identifiers = []; 277 | // results as scalar 278 | foreach ($rows as $row) { 279 | // add row identifier to array 280 | $identifiers[] = $row[$table->getAlias().'_id']; 281 | } 282 | 283 | // if at least one identifier should be used to load entities 284 | if (count($identifiers) > 0) { 285 | 286 | // loaded entities 287 | $entities = $this->loadRowsById($table, $identifiers); 288 | 289 | // associate objects to rows 290 | if (count($entities) > 0) { 291 | foreach ($rows as &$row) { 292 | $row['object'] = null; 293 | foreach ($entities as $entity) { 294 | if ($row[$table->getAlias().'_id'] == $entity->getId()) { 295 | $row['object'] = $entity; 296 | break; 297 | } 298 | } 299 | } 300 | } 301 | } 302 | } 303 | 304 | // if we need to get objects (legacy mode) 305 | if ($getObjects && $table->getEntityLoaderMode() == $table::ENTITY_LOADER_LEGACY) { 306 | // results as scalar 307 | foreach ($rows as &$row) { 308 | $index = is_int($row[$table->getAlias().'_id']) 309 | ? $row[$table->getAlias().'_id'] 310 | : (string) $row[$table->getAlias().'_id']; 311 | 312 | if (isset($objects[$index])) { 313 | $row['object'] = $objects[$index]; 314 | } 315 | } 316 | } 317 | 318 | return $rows; 319 | } 320 | 321 | /** 322 | * @inheritdoc 323 | */ 324 | public function loadRowsById(TableInterface $table, $identifiers) 325 | { 326 | $entities = []; 327 | // if we need to use repository name 328 | if ($table->getEntityLoaderMode() == $table::ENTITY_LOADER_REPOSITORY) { 329 | // if repository name is missing 330 | if (!$table->getEntityLoaderRepository()) { 331 | throw new \InvalidArgumentException('entity loader repository name is missing for ENTITY_LOADER_REPOSITORY mode'); 332 | } 333 | 334 | // load entities from identifiers 335 | $loaderQueryBuilder = $table->getQueryBuilder() 336 | ->getEntityManager() 337 | ->getRepository($table->getEntityLoaderRepository()) 338 | ->createQueryBuilder('e') 339 | ->select('e') 340 | ->where('e.id IN (:identifiers)') 341 | ->setParameter('identifiers', $identifiers); 342 | 343 | $entities = $loaderQueryBuilder->getQuery()->getResult(); 344 | } elseif ($table->getEntityLoaderMode() == $table::ENTITY_LOADER_CALLBACK) { 345 | // if repository callback is missing 346 | if (!is_callable($table->getEntityLoaderCallback())) { 347 | throw new \InvalidArgumentException('entity loader callback is missing or not callable for ENTITY_LOADER_CALLBACK mode'); 348 | } 349 | // else, load entities from callback method 350 | $callback = $table->getEntityLoaderCallback(); 351 | $entities = $callback($identifiers); 352 | } else { 353 | throw new \InvalidArgumentException('unsupported entity loader mode'); 354 | } 355 | 356 | return $entities; 357 | } 358 | 359 | /** 360 | * @param QueryBuilder $qb 361 | * @param string|null $identifiers 362 | * 363 | * @return float|int 364 | * @throws \Doctrine\ORM\NoResultException 365 | * @throws \Doctrine\ORM\NonUniqueResultException 366 | */ 367 | protected function countRows(QueryBuilder $qb, string $identifiers = null) 368 | { 369 | switch (true) { 370 | case $qb->getQuery()->hasHint(Query::HINT_CUSTOM_OUTPUT_WALKER) && is_null($identifiers): 371 | $em = $qb->getEntityManager(); 372 | $sql = $qb->getQuery()->getSQL(); 373 | $rsm = new Query\ResultSetMapping(); 374 | $rsm->addScalarResult('dctrn_count', 'count'); 375 | $nativeQuery = $em->createNativeQuery(sprintf('SELECT COUNT(*) AS dctrn_count FROM (%s) AS dctrn_table', $sql), $rsm); 376 | foreach ($qb->getParameters() as $key => $item) { 377 | $nativeQuery->setParameter($key + 1, $item->getValue()); 378 | } 379 | 380 | return (int)$nativeQuery->getSingleScalarResult(); 381 | case is_null($identifiers): 382 | $paginatorFiltered = new Paginator($qb->getQuery()); 383 | 384 | return $paginatorFiltered->count(); 385 | default: 386 | $qb->select($qb->expr()->count($identifiers)); 387 | 388 | return (int)$qb->getQuery()->getSingleScalarResult(); 389 | } 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/Services/TableServiceInterface.php: -------------------------------------------------------------------------------- 1 | setLabel('mylabel'); 18 | $this->assertEquals('mylabel', $column->getLabel()); 19 | 20 | $column->setName('myname'); 21 | $this->assertEquals('myname', $column->getName()); 22 | 23 | $column->setSort(['field1' => 'ASC', 'field2' => 'ASC']); 24 | $this->assertEquals(['field1' => 'ASC', 'field2' => 'ASC'], $column->getSort()); 25 | 26 | $column->setSortReverse(['field2' => 'DESC', 'field1' => 'DESC']); 27 | $this->assertEquals(['field2' => 'DESC', 'field1' => 'DESC'], $column->getSortReverse()); 28 | 29 | $this->assertEquals(false, $column->getTranslateLabel()); 30 | $this->assertNull($column->getTranslateDomain()); 31 | $column->setTranslateLabel(true); 32 | $this->assertEquals(true, $column->getTranslateLabel()); 33 | $this->assertEquals('messages', $column->getTranslateDomain()); 34 | $column->setTranslateDomain('other_domain'); 35 | $this->assertEquals('other_domain', $column->getTranslateDomain()); 36 | 37 | $this->assertEquals(false, $column->getRaw()); 38 | $column->setRaw(true); 39 | $this->assertEquals(true, $column->getRaw()); 40 | 41 | $this->assertEquals('text', $column->getDisplayFormat()); 42 | $column->setDisplayFormat('date'); 43 | $this->assertEquals('date', $column->getDisplayFormat()); 44 | } 45 | 46 | /** 47 | * @dataProvider getValueProvider 48 | */ 49 | public function testGetValue($wanted, Column $column) 50 | { 51 | $row = [ 52 | 'field1' => 'value1', 53 | 'field2' => 'value2', 54 | 'field3' => '03/08/2020', 55 | 'field4' => null, 56 | 'field5' => '

test

', 57 | 'field6' => new \DateTime('2020-08-03 11:34:00'), 58 | ]; 59 | 60 | $rows = [ 61 | [ 62 | 'field1' => 'value1', 63 | 'field2' => 'value2', 64 | 'field3' => '03/08/2020', 65 | 'field4' => null, 66 | 'field5' => '

test

', 67 | 'field6' => new \DateTime('2020-08-03 11:34:00'), 68 | ], 69 | [ 70 | 'field1' => 'value11', 71 | 'field2' => 'value12', 72 | 'field3' => '04/08/2020', 73 | 'field4' => null, 74 | 'field5' => '

test2

', 75 | 'field6' => new \DateTime('2020-09-04 20:15:45'), 76 | ], 77 | ]; 78 | 79 | $this->assertEquals($wanted, $column->getValue($row, $rows)); 80 | } 81 | 82 | public function getValueProvider() 83 | { 84 | return [ 85 | [ 86 | 'value1', 87 | (new Column())->setName('field1'), 88 | ], 89 | [ 90 | 'value2', 91 | (new Column())->setName('field2'), 92 | ], 93 | [ 94 | '03/08/2020', 95 | (new Column())->setName('field3'), 96 | ], 97 | [ 98 | null, 99 | (new Column())->setName('field4'), 100 | ], 101 | [ 102 | '

test

', 103 | (new Column())->setName('field5'), 104 | ], 105 | [ 106 | 'VALUE1', 107 | (new Column())->setName('field1')->setDisplayCallback(function ($value, $row, $rows) { return strtoupper($value); }), 108 | ], 109 | [ 110 | '2020-08-03 11:34:00', 111 | (new Column())->setName('field6')->setDisplayFormat('date'), 112 | ], 113 | [ 114 | '03/08/2020', 115 | (new Column())->setName('field6')->setDisplayFormat('date')->setDisplayFormatParams('d/m/Y'), 116 | ], 117 | ]; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/Components/FilterDateTest.php: -------------------------------------------------------------------------------- 1 | getMethod('getPeriodFromInput'); 20 | $method->setAccessible(true); 21 | } 22 | 23 | $result = $method->invoke($filter, $input); 24 | $this->assertEquals($result[0], date_create_immutable($expectedStart)); 25 | $this->assertEquals($result[1], date_create_immutable($expectedEnd)); 26 | }; 27 | 28 | $filter = (new FilterDate())->setInputFormat(FilterDate::INPUT_FORMAT_BIG_ENDIAN); 29 | $assertPeriodFromInput($filter, '1802-02-26 10:25:26', '1802-02-26 10:25:26', '1802-02-26 10:25:26'); 30 | $assertPeriodFromInput($filter, '1802-02-26 10:25 ', '1802-02-26 10:25:00', '1802-02-26 10:25:59'); 31 | $assertPeriodFromInput($filter, ' 1802-02-26 10', '1802-02-26 10:00:00', '1802-02-26 10:59:59'); 32 | $assertPeriodFromInput($filter, ' 1802-02-26 ', '1802-02-26 00:00:00', '1802-02-26 23:59:59'); 33 | $assertPeriodFromInput($filter, ' 1804-02', '1804-02-01 00:00:00', '1804-02-29 23:59:59'); 34 | $assertPeriodFromInput($filter, ' 1989', '1989-01-01 00:00:00', '1989-12-31 23:59:59'); 35 | $assertPeriodFromInput($filter, '2089-07-21 21:00:22', '2089-07-21 21:00:22', '2089-07-21 21:00:22'); 36 | $assertPeriodFromInput($filter, '2089-07-21 15:00', '2089-07-21 15:00:00', '2089-07-21 15:00:59'); 37 | $assertPeriodFromInput($filter, '2089-07-21 13', '2089-07-21 13:00:00', '2089-07-21 13:59:59'); 38 | $assertPeriodFromInput($filter, '2089-07-21', '2089-07-21 00:00:00', '2089-07-21 23:59:59'); 39 | $assertPeriodFromInput($filter, '2089-07', '2089-07-01 00:00:00', '2089-07-31 23:59:59'); 40 | $assertPeriodFromInput($filter, '2089', '2089-01-01 00:00:00', '2089-12-31 23:59:59'); 41 | 42 | $filter = (new FilterDate())->setInputFormat(FilterDate::INPUT_FORMAT_LITTLE_ENDIAN); 43 | $assertPeriodFromInput($filter, '21/11/1694 10:25:26', '1694-11-21 10:25:26', '1694-11-21 10:25:26'); 44 | $assertPeriodFromInput($filter, '21/11/1694 10:25 ', '1694-11-21 10:25:00', '1694-11-21 10:25:59'); 45 | $assertPeriodFromInput($filter, ' 21-11-1694 10', '1694-11-21 10:00:00', '1694-11-21 10:59:59'); 46 | $assertPeriodFromInput($filter, ' 21/11/1694 ', '1694-11-21 00:00:00', '1694-11-21 23:59:59'); 47 | $assertPeriodFromInput($filter, ' 11-1694', '1694-11-01 00:00:00', '1694-11-30 23:59:59'); 48 | $assertPeriodFromInput($filter, ' 1517', '1517-01-01 00:00:00', '1517-12-31 23:59:59'); 49 | $assertPeriodFromInput($filter, '23/01/2091 09:43:22', '2091-01-23 09:43:22', '2091-01-23 09:43:22'); 50 | $assertPeriodFromInput($filter, '23-01-2091 09:43', '2091-01-23 09:43:00', '2091-01-23 09:43:59'); 51 | $assertPeriodFromInput($filter, '23/01/2091 09', '2091-01-23 09:00:00', '2091-01-23 09:59:59'); 52 | $assertPeriodFromInput($filter, '23/01/2091', '2091-01-23 00:00:00', '2091-01-23 23:59:59'); 53 | $assertPeriodFromInput($filter, '01-2091', '2091-01-01 00:00:00', '2091-01-31 23:59:59'); 54 | $assertPeriodFromInput($filter, '2091', '2091-01-01 00:00:00', '2091-12-31 23:59:59'); 55 | } 56 | 57 | public function testGetPeriodFromInvalidInput() 58 | { 59 | $class = new \ReflectionClass(FilterDate::class); 60 | $method = $class->getMethod('getPeriodFromInput'); 61 | $method->setAccessible(true); 62 | 63 | $filter = (new FilterDate())->setInputFormat(FilterDate::INPUT_FORMAT_BIG_ENDIAN); 64 | $this->assertNull($method->invoke($filter, '2024ZZ')); 65 | $this->assertNull($method->invoke($filter, '2023-02-29')); 66 | $this->assertNull($method->invoke($filter, '2024-02-30')); 67 | $this->assertNull($method->invoke($filter, '2024/28/02')); 68 | $this->assertNull($method->invoke($filter, '2024-02-28 26:00:00')); 69 | $this->assertNull($method->invoke($filter, '2024-02-28 12:65:00')); 70 | $this->assertNull($method->invoke($filter, '23/01/1991 09:00:00')); 71 | $this->assertNull($method->invoke($filter, 'Murs, ville, Et port. Asile De mort,')); 72 | 73 | $filter = (new FilterDate())->setInputFormat(FilterDate::INPUT_FORMAT_LITTLE_ENDIAN); 74 | $this->assertNull($method->invoke($filter, '202A')); 75 | $this->assertNull($method->invoke($filter, '29/02/2023')); 76 | $this->assertNull($method->invoke($filter, '30/02/2024')); 77 | $this->assertNull($method->invoke($filter, '02-28-2024')); 78 | $this->assertNull($method->invoke($filter, '28/02/2024 24:29:59')); 79 | $this->assertNull($method->invoke($filter, '28/02/2024 09:00:68')); 80 | $this->assertNull($method->invoke($filter, '1991-01-23')); 81 | $this->assertNull($method->invoke($filter, 'Mer grise Où brise La brise, Tout dort.')); 82 | } 83 | 84 | public function testBuildWhereQuery() 85 | { 86 | $assertQuery = function (FilterDate $filter, string $operator, string $expected) { 87 | static $class, $method; 88 | if (null === $class) { 89 | $class = new \ReflectionClass(FilterDate::class); 90 | $method = $class->getMethod('buildWhereQuery'); 91 | $method->setAccessible(true); 92 | } 93 | 94 | $result = $method->invoke($filter, $operator); 95 | $this->assertEquals($result, $expected); 96 | }; 97 | 98 | $filter = (new FilterDate())->setName('zz')->setField('f.createdAt'); 99 | $assertQuery($filter, '', 'f.createdAt BETWEEN :filter_zz_start AND :filter_zz_end'); 100 | $assertQuery($filter, FilterDate::TYPE_EQUAL, 'f.createdAt BETWEEN :filter_zz_start AND :filter_zz_end'); 101 | $assertQuery($filter, FilterDate::TYPE_NOT_EQUAL, 'f.createdAt NOT BETWEEN :filter_zz_start AND :filter_zz_end'); 102 | $assertQuery($filter, FilterDate::TYPE_GREATER, 'f.createdAt > :filter_zz_end'); 103 | $assertQuery($filter, FilterDate::TYPE_GREATER_OR_EQUAL, 'f.createdAt >= :filter_zz_start'); 104 | $assertQuery($filter, FilterDate::TYPE_LESS, 'f.createdAt < :filter_zz_start'); 105 | $assertQuery($filter, FilterDate::TYPE_LESS_OR_EQUAL, 'f.createdAt <= :filter_zz_end'); 106 | } 107 | 108 | public function testQueryPartBuilder() 109 | { 110 | $assert = function (FilterDate $filter, string $input, string $expectedQuery, array $expectedParameters) { 111 | $qb = $this->getMockBuilder(QueryBuilder::class)->disableOriginalConstructor()->getMock(); 112 | $qb->expects($this->once())->method('andWhere')->with($this->equalTo($expectedQuery)); 113 | 114 | $consecutiveParameters = []; 115 | foreach ($expectedParameters as $key => $rawDate) { 116 | $consecutiveParameters[] = [$key, date_create_immutable($rawDate)]; 117 | } 118 | 119 | $qb->expects(new InvokedCount(count($expectedParameters)))->method('setParameter')->withConsecutive(...$consecutiveParameters); 120 | $filter->getQueryPartBuilder()($filter, new Table(), $qb, $input); 121 | }; 122 | 123 | $filter = (new FilterDate())->setInputFormat(FilterDate::INPUT_FORMAT_BIG_ENDIAN)->setName('zz')->setField('yy'); 124 | $assert($filter, '2024-02-28', 'yy BETWEEN :filter_zz_start AND :filter_zz_end', ['filter_zz_start' => '2024-02-28 00:00:00', 'filter_zz_end' => '2024-02-28 23:59:59']); 125 | $assert($filter, '=2024-01-31', 'yy BETWEEN :filter_zz_start AND :filter_zz_end', ['filter_zz_start' => '2024-01-31 00:00:00', 'filter_zz_end' => '2024-01-31 23:59:59']); 126 | $assert($filter, '!=2024-01-31', 'yy NOT BETWEEN :filter_zz_start AND :filter_zz_end', ['filter_zz_start' => '2024-01-31 00:00:00', 'filter_zz_end' => '2024-01-31 23:59:59']); 127 | $assert($filter, '>2024-01-31', 'yy > :filter_zz_end', ['filter_zz_end' => '2024-01-31 23:59:59']); 128 | $assert($filter, '>=2024-01-31', 'yy >= :filter_zz_start', ['filter_zz_start' => '2024-01-31 00:00:00']); 129 | $assert($filter, '<2024-01-31', 'yy < :filter_zz_start', ['filter_zz_start' => '2024-01-31 00:00:00']); 130 | $assert($filter, '<=2024-01-31', 'yy <= :filter_zz_end', ['filter_zz_end' => '2024-01-31 23:59:59']); 131 | $assert($filter, '<=20240131', '0=1', []); 132 | $assert($filter, '=djobi', '0=1', []); 133 | $assert($filter, '=', 'yy IS NULL', []); 134 | $assert($filter, '!=', 'yy IS NOT NULL', []); 135 | 136 | $filter = (new FilterDate())->setInputFormat(FilterDate::INPUT_FORMAT_LITTLE_ENDIAN)->setName('zz')->setField('yy'); 137 | $assert($filter, '28-02-2024', 'yy BETWEEN :filter_zz_start AND :filter_zz_end', ['filter_zz_start' => '2024-02-28 00:00:00', 'filter_zz_end' => '2024-02-28 23:59:59']); 138 | $assert($filter, '=31-01-2024', 'yy BETWEEN :filter_zz_start AND :filter_zz_end', ['filter_zz_start' => '2024-01-31 00:00:00', 'filter_zz_end' => '2024-01-31 23:59:59']); 139 | $assert($filter, '!=31-01-2024', 'yy NOT BETWEEN :filter_zz_start AND :filter_zz_end', ['filter_zz_start' => '2024-01-31 00:00:00', 'filter_zz_end' => '2024-01-31 23:59:59']); 140 | $assert($filter, '>31-01-2024', 'yy > :filter_zz_end', ['filter_zz_end' => '2024-01-31 23:59:59']); 141 | $assert($filter, '>=31-01-2024', 'yy >= :filter_zz_start', ['filter_zz_start' => '2024-01-31 00:00:00']); 142 | $assert($filter, '<31-01-2024', 'yy < :filter_zz_start', ['filter_zz_start' => '2024-01-31 00:00:00']); 143 | $assert($filter, '<=31-01-2024', 'yy <= :filter_zz_end', ['filter_zz_end' => '2024-01-31 23:59:59']); 144 | $assert($filter, '>31012024', '0=1', []); 145 | $assert($filter, '!=djoba', '0=1', []); 146 | $assert($filter, '=', 'yy IS NULL', []); 147 | $assert($filter, '!=', 'yy IS NOT NULL', []); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/Components/TableTest.php: -------------------------------------------------------------------------------- 1 | setId('myid'); 17 | $this->assertEquals('myid',$table->getId()); 18 | $this->assertEquals('kilik_myid_selected',$table->getSelectionFormKey()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Services/TableServiceTest.php: -------------------------------------------------------------------------------- 1 | setId('test'); 47 | 48 | $column1 = new Column(); 49 | $column1->setFilter((new Filter())->setName('column1')); 50 | $table->addColumn($column1); 51 | 52 | $form = $service->form($table); 53 | $this->assertEquals(3, $form->count(), 'should have 3 items: sortColumn,sortReverse, column1'); 54 | } 55 | } 56 | --------------------------------------------------------------------------------