├── .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 | 
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 |
17 |
18 |
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 |
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 |
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 |
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 |
104 | {% endif %}
105 |
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 |
104 | {% endif %}
105 |
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 |
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 |
92 | {% endif %}
93 |
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 |
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 |
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 |
8 |
11 |
14 |
17 | {% endif %}
18 |
--------------------------------------------------------------------------------
/src/Resources/views/_paginationNumbers.html.twig:
--------------------------------------------------------------------------------
1 | {# @KilikTable/_paginationNumbers.html.twig #}
2 |
3 | {% if tableRenderPagination is defined %}
4 | {# previous page #}
5 |
8 |
9 | {# first page #}
10 | {% if table.page > 2 %}
11 |
14 | {% endif %}
15 |
16 | {# page - 2 ? #}
17 | {% if table.page > 3 %}
18 | {% if table.page == 4 %}
19 |
22 | {% else %}
23 |
24 | ...
25 |
26 | {% endif %}
27 | {% endif %}
28 |
29 | {# page - 1 ? (previous) #}
30 | {% if table.page > 1 %}
31 |
34 | {% endif %}
35 |
36 | {# page active #}
37 |
40 |
41 | {# page + 1 ? (next) #}
42 | {% if table.lastPage-table.page > 0 %}
43 |
46 | {% endif %}
47 |
48 | {# page + 2 ? #}
49 | {% if table.lastPage-table.page > 2 %}
50 | {% if table.lastPage-table.page == 3 %}
51 |
54 | {% else %}
55 |
56 | ...
57 |
58 | {% endif %}
59 | {% endif %}
60 |
61 | {# last page #}
62 | {% if table.lastPage-table.page > 1 %}
63 |
66 | {% endif %}
67 |
68 | {# next page #}
69 |
72 | {% endif %}
73 |
--------------------------------------------------------------------------------
/src/Resources/views/_paginationNumbersIcons.html.twig:
--------------------------------------------------------------------------------
1 | {# @KilikTable/_paginationNumbersIcons.html.twig #}
2 |
3 | {% if tableRenderPagination is defined %}
4 |
7 |
10 |
13 |
16 |
19 | {% endif %}
20 |
--------------------------------------------------------------------------------
/src/Resources/views/_rowsPerPage.html.twig:
--------------------------------------------------------------------------------
1 | {# @KilikTable/_rowsPerPage.html.twig #}
2 |
3 |
4 | {% for rowsPerPage in table.rowsPerPageOptions %}
5 | {{ rowsPerPage }}
6 | {% endfor %}
7 |
8 | {{ "kiliktable.rows_per_page" |trans }}
9 |
10 |
--------------------------------------------------------------------------------
/src/Resources/views/_setup.html.twig:
--------------------------------------------------------------------------------
1 | {# @KilikTable/_setup.html.twig #}
2 |
3 |
4 |
5 | {{ "kiliktable.setup" |trans }}
6 |
23 |
24 |
25 |
26 |
27 |
28 |
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 | 
8 |
9 | tables/alternative.html.twig:
10 |
11 | 
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 |
8 |
11 |
14 |
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 |
8 |
9 | {# first page #}
10 | {% if table.page > 2 %}
11 |
14 | {% endif %}
15 |
16 | {# page - 2 ? #}
17 | {% if table.page > 3 %}
18 | {% if table.page == 4 %}
19 |
22 | {% else %}
23 |
24 | ...
25 |
26 | {% endif %}
27 | {% endif %}
28 |
29 | {# page - 1 ? (previous) #}
30 | {% if table.page > 1 %}
31 |
34 | {% endif %}
35 |
36 | {# page active #}
37 |
40 |
41 | {# page + 1 ? (next) #}
42 | {% if table.lastPage-table.page > 0 %}
43 |
46 | {% endif %}
47 |
48 | {# page + 2 ? #}
49 | {% if table.lastPage-table.page > 2 %}
50 | {% if table.lastPage-table.page == 3 %}
51 |
54 | {% else %}
55 |
56 | ...
57 |
58 | {% endif %}
59 | {% endif %}
60 |
61 | {# last page #}
62 | {% if table.lastPage-table.page > 1 %}
63 |
66 | {% endif %}
67 |
68 | {# next page #}
69 |
72 | {% endif %}
73 |
--------------------------------------------------------------------------------
/src/Resources/views/theme/dark4/components/paginationNumbersIcons.html.twig:
--------------------------------------------------------------------------------
1 | {# @KilikTable/_paginationNumbersIcons.html.twig #}
2 |
3 | {% if tableRenderPagination is defined %}
4 |
7 |
10 |
13 |
16 |
19 | {% endif %}
20 |
--------------------------------------------------------------------------------
/src/Resources/views/theme/dark4/components/setup.html.twig:
--------------------------------------------------------------------------------
1 | {# @KilikTable/_setup.html.twig #}
2 |
3 |
4 | {{ "kiliktable.setup" |trans }}
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 |
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 |
103 | {% endif %}
104 |
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 |
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 |
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 |
104 | {% endif %}
105 |
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 |
--------------------------------------------------------------------------------