├── .github └── workflows │ └── symfony.yml ├── LICENSE.md ├── README.md ├── assets ├── .gitignore ├── controllers │ ├── controller.js │ └── styling │ │ ├── bootstrap3_controller.js │ │ ├── bootstrap4_controller.js │ │ ├── bootstrap5_controller.js │ │ ├── bulma_controller.js │ │ ├── foundation_controller.js │ │ └── jqueryui_controller.js └── package.json ├── composer.json ├── docs ├── columns.md ├── configuration.md ├── datatables_from_array.md ├── datatables_from_entity.md ├── event.md ├── global_controller_example.md ├── images │ └── datatable-example.png ├── index.md ├── installation.md ├── languages_and_translation.md └── themes.md └── src ├── Builder ├── DatatableBuilder.php └── DatatableBuilderInterface.php ├── Column ├── AbstractColumn.php ├── BadgeColumn.php ├── BooleanColumn.php ├── DateColumn.php ├── EntityColumn.php ├── InlineTwigColumn.php ├── TextColumn.php └── TwigColumn.php ├── DatatableBundle.php ├── DependencyInjection ├── Configuration.php └── DatatableExtension.php ├── Event ├── Events.php ├── RenderDataEvent.php ├── RenderQueryEvent.php └── RenderSearchQueryEvent.php ├── EventListener └── RenderSubscriber.php ├── Exception └── GetterNotFoundException.php ├── Helper ├── DatatableQueriesHelper.php └── DatatableTemplatingHelper.php ├── Model ├── AbstractDatatable.php ├── ArrayDatatable.php └── EntityDatatable.php ├── Service └── DataService.php └── Twig └── DatatableExtension.php /.github/workflows/symfony.yml: -------------------------------------------------------------------------------- 1 | name: Symfony 2 | 3 | on: 4 | push: 5 | branches: [ "1.x" ] 6 | pull_request: 7 | branches: [ "1.x" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | symfony-tests: 14 | runs-on: ubuntu-latest 15 | steps: 16 | # To automatically get bug fixes and new Php versions for shivammathur/setup-php, 17 | # change this to (see https://github.com/shivammathur/setup-php#bookmark-versioning): 18 | # uses: shivammathur/setup-php@v2 19 | - uses: shivammathur/setup-php@2cb9b829437ee246e9b3cac53555a39208ca6d28 20 | with: 21 | php-version: '8.0' 22 | - uses: actions/checkout@v3 23 | - name: Copy .env.test.local 24 | run: php -r "file_exists('.env.test.local') || copy('.env.test', '.env.test.local');" 25 | - name: Cache Composer packages 26 | id: composer-cache 27 | uses: actions/cache@v3 28 | with: 29 | path: vendor 30 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 31 | restore-keys: | 32 | ${{ runner.os }}-php- 33 | - name: Install Dependencies 34 | run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist 35 | - name: Create Database 36 | run: | 37 | mkdir -p data 38 | touch data/database.sqlite 39 | - name: Execute tests (Unit and Feature tests) via PHPUnit 40 | env: 41 | DATABASE_URL: sqlite:///%kernel.project_dir%/data/database.sqlite 42 | run: vendor/bin/simple-phpunit 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Aziz BenMallouk 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 | UX Datatables.net 2 | =================== 3 | 4 | Symfony UX Datatables.net is a Symfony bundle integrating the 5 | `Datatables.net` library in Symfony applications. 6 | 7 | ![Datatable Example](/docs/images/datatable-example.png?raw=true) 8 | 9 | Installation 10 | ------------ 11 | 12 | Before you start, make sure you have `Symfony UX configured in your app`. 13 | 14 | Then, install this bundle using Composer and Symfony Flex: 15 | 16 | $ composer require aziz403/ux-datatable 17 | 18 | # Don't forget to install the JavaScript dependencies as well and compile 19 | $ npm install --force 20 | $ npm run watch 21 | 22 | # or use yarn 23 | $ yarn install --force 24 | $ yarn watch 25 | 26 | Also make sure you have at least version 3.0 of `@symfony/stimulus-bridge` 27 | in your ``package.json`` file. 28 | 29 | Documentation 30 | ----- 31 | Read [UxDatatable Docs](/docs/index.md) on ``docs/index.md`` 32 | -------------------------------------------------------------------------------- /assets/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | package-lock.json -------------------------------------------------------------------------------- /assets/controllers/controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '@hotwired/stimulus'; 2 | import DataTable from "datatables.net"; 3 | 4 | /* stimulusFetch: 'lazy' */ 5 | export default class extends Controller { 6 | static values = { 7 | view: Object 8 | } 9 | 10 | connect() { 11 | if (!(this.element instanceof HTMLTableElement)) { 12 | throw new Error('Invalid element'); 13 | } 14 | 15 | //consult EntityDatatable::createView() result 16 | const { path, options } = this.viewValue; 17 | let datatableId = '#'+this.element.id; 18 | 19 | this._dispatchEvent('datatable:before-connect', { path, options , datatableId }); 20 | 21 | //check if datatable already exists 22 | if(DataTable.isDataTable(datatableId)){ 23 | this.table = new DataTable(datatableId); 24 | } 25 | //configuration the table 26 | else{ 27 | this.table = new DataTable(datatableId, options); 28 | } 29 | 30 | // destroy table when return by cache (in ux-turbo) 31 | document.addEventListener("turbo:before-cache", ()=> { 32 | 33 | this._dispatchEvent('datatable:before-cache', { table: this.table , datatableId }); 34 | 35 | if(document.querySelectorAll(datatableId+'_wrapper').length === 1){ 36 | this.table.destroy() 37 | } 38 | }) 39 | 40 | this._dispatchEvent('datatable:connect', { table: this.table, options, path }); 41 | } 42 | 43 | _dispatchEvent(name, payload) { 44 | this.element.dispatchEvent(new CustomEvent(name, { detail: payload })); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /assets/controllers/styling/bootstrap3_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '@hotwired/stimulus'; 2 | 3 | import "datatables.net-bs/js/dataTables.bootstrap"; 4 | import "datatables.net-bs/css/dataTables.bootstrap.css"; 5 | 6 | /* stimulusFetch: 'lazy' */ 7 | export default class extends Controller { 8 | connect() { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/controllers/styling/bootstrap4_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '@hotwired/stimulus'; 2 | 3 | import "datatables.net-bs4/js/dataTables.bootstrap4"; 4 | import "datatables.net-bs4/css/dataTables.bootstrap4.css"; 5 | 6 | /* stimulusFetch: 'lazy' */ 7 | export default class extends Controller { 8 | connect() { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/controllers/styling/bootstrap5_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '@hotwired/stimulus'; 2 | 3 | import "datatables.net-bs5/js/dataTables.bootstrap5"; 4 | import "datatables.net-bs5/css/dataTables.bootstrap5.css"; 5 | 6 | /* stimulusFetch: 'lazy' */ 7 | export default class extends Controller { 8 | connect() { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/controllers/styling/bulma_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '@hotwired/stimulus'; 2 | 3 | import "datatables.net-bm/js/dataTables.bulma"; 4 | import "datatables.net-bm/css/dataTables.bulma.css"; 5 | 6 | /* stimulusFetch: 'lazy' */ 7 | export default class extends Controller { 8 | connect() { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/controllers/styling/foundation_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '@hotwired/stimulus'; 2 | 3 | import "datatables.net-zf/js/dataTables.foundation"; 4 | import "datatables.net-zf/css/dataTables.foundation.css"; 5 | 6 | /* stimulusFetch: 'lazy' */ 7 | export default class extends Controller { 8 | connect() { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/controllers/styling/jqueryui_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '@hotwired/stimulus'; 2 | 3 | import "datatables.net-jqui/js/dataTables.jqueryui"; 4 | import "datatables.net-jqui/css/dataTables.jqueryui.css"; 5 | 6 | /* stimulusFetch: 'lazy' */ 7 | export default class extends Controller { 8 | connect() { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aziz403/ux-datatable", 3 | "description": "Datatable.net integration for Symfony", 4 | "license": "MIT", 5 | "version": "1.0.5", 6 | "symfony": { 7 | "controllers": { 8 | "datatable": { 9 | "main": "controllers/controller.js", 10 | "webpackMode": "lazy", 11 | "fetch": "lazy", 12 | "enabled": true 13 | }, 14 | "styling_bootstrap3": { 15 | "main": "controllers/styling/bootstrap3_controller.js", 16 | "webpackMode": "lazy", 17 | "fetch": "lazy", 18 | "enabled": true 19 | }, 20 | "styling_bootstrap4": { 21 | "main": "controllers/styling/bootstrap4_controller.js", 22 | "webpackMode": "lazy", 23 | "fetch": "lazy", 24 | "enabled": true 25 | }, 26 | "styling_bootstrap5": { 27 | "main": "controllers/styling/bootstrap5_controller.js", 28 | "webpackMode": "lazy", 29 | "fetch": "lazy", 30 | "enabled": true 31 | }, 32 | "styling_bulma": { 33 | "main": "controllers/styling/bulma_controller.js", 34 | "webpackMode": "lazy", 35 | "fetch": "lazy", 36 | "enabled": true 37 | }, 38 | "styling_foundation": { 39 | "main": "controllers/styling/foundation_controller.js", 40 | "webpackMode": "lazy", 41 | "fetch": "lazy", 42 | "enabled": true 43 | }, 44 | "styling_jqueryui": { 45 | "main": "controllers/styling/jqueryui_controller.js", 46 | "webpackMode": "lazy", 47 | "fetch": "lazy", 48 | "enabled": true 49 | } 50 | } 51 | }, 52 | "peerDependencies": { 53 | "@hotwired/stimulus": "^3.0.0", 54 | "datatables.net": "^1.12.1", 55 | "datatables.net-dt": "^1.12.1", 56 | "datatables.net-bm": "^1.13.1", 57 | "datatables.net-bs": "^1.13.1", 58 | "datatables.net-bs4": "^1.13.1", 59 | "datatables.net-bs5": "^1.13.1", 60 | "datatables.net-jqui": "^1.13.1", 61 | "datatables.net-zf": "^1.13.1", 62 | "jquery": "^3.6.1" 63 | }, 64 | "devDependencies": { 65 | "@hotwired/stimulus": "^3.0.0", 66 | "datatables.net": "^1.12.1", 67 | "datatables.net-dt": "^1.12.1", 68 | "jquery": "^3.6.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aziz403/ux-datatable", 3 | "type": "symfony-bundle", 4 | "description": "Datatable.net integration for Symfony", 5 | "version": "1.1.1", 6 | "keywords": [ 7 | "symfony", 8 | "symfony-ux", 9 | "bundle", 10 | "datatables" 11 | ], 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Aziz Benmallouk", 16 | "email": "azizbenmallouk4@gmail.com" 17 | } 18 | ], 19 | "autoload": { 20 | "psr-4": { 21 | "Aziz403\\UX\\Datatable\\": "src/" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "Aziz403\\UX\\Datatable\\Tests\\": "tests/" 27 | } 28 | }, 29 | "require": { 30 | "php": ">=7.4", 31 | "symfony/config": "^4.4.17|^5.0|^6.0", 32 | "symfony/dependency-injection": "^4.4.17|^5.0|^6.0", 33 | "symfony/http-kernel": "^4.4.17|^5.0|^6.0", 34 | "symfony/framework-bundle": "^4.4.17|^5.0|^6.0", 35 | "doctrine/annotations": "^1.0|^2.0", 36 | "doctrine/doctrine-bundle": "^2.0|^2.2|^2.6", 37 | "doctrine/orm": "~2.13", 38 | "symfony/twig-bundle": "^4.4.17|^5.0|^6.0", 39 | "symfony/webpack-encore-bundle": "^1.11", 40 | "symfony/translation": "^5.4|^6.0", 41 | "symfony/event-dispatcher": "^5.4|^6.0", 42 | "symfony/property-access": "^5.4|^6.0" 43 | }, 44 | "require-dev": { 45 | "symfony/phpunit-bridge": "^5.4|^6.0", 46 | "zenstruck/foundry": "^1.19", 47 | "symfony/browser-kit": "^5.4|^6.0", 48 | "symfony/css-selector": "^5.4|^6.0" 49 | }, 50 | "conflict": { 51 | "symfony/flex": "<1.13", 52 | "symfony/webpack-encore-bundle": "<1.11" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/columns.md: -------------------------------------------------------------------------------- 1 | ## Columns: 2 | When you're trying to render ``EntityDatatable`` the most important thing is specified the columns, to represent the entity fields and data as datatable columns. 3 | 4 | In this is why we have many ``Columns`` types with different jobs: 5 | 6 | ### TextColumn 7 | The simple column type, helps you to render the text of your entity. 8 | ### BadgeColumn 9 | Another simple column type, but its writes your entity field inside a html badge. 10 | ### BooleanColumn 11 | In the main it's for the boolean fields, to display for example 'Yes' & 'No' keywords place of 'true' & 'false', 12 | but can you use it with another field types with the by added ``render`` in your BooleanColumn. 13 | ### DateColumn 14 | To render Date & DateTime objects as fields in your entity. 15 | ### EntityColumn 16 | by EntityColumn can you display a related entity field inside your datatable. 17 | ### TwigColumn 18 | To render templates as a datatable column. 19 | ### InlineTwigColumn 20 | To render string templates. 21 | 22 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | Configuration 2 | ========= 3 | 4 | If you're using ``symfony/flex`` can you found this is your default config: 5 | 6 | ```yaml 7 | //config/packages/datatable.yaml 8 | datatable: 9 | # Load i18n data from DataTables CDN or locally 10 | language_from_cdn: true 11 | # Language of the Datatables (if language_from_cdn true) 12 | language: 'en' 13 | # Default options to load into DataTables 14 | # options: 15 | # stateSave: true 16 | # Default parameters to be passed to the template 17 | template_parameters: 18 | # Default Datatables style (one from none,bootstrap3,bootstrap4,bootstrap5,foundation,bulma,jqueryui) 19 | style: 'bootstrap5' 20 | # Default class attribute to apply to the root table elements (change it to be compatible with the style) 21 | className: 'table table-striped' 22 | ``` 23 | 24 | Can you customize the config to be compatible with what you're wants. 25 | But first remember! This should be global configuration and can you change it in your `Datatable`. 26 | 27 | This the Datatable options: 28 | 29 | 30 | | Option | Description | Value | 31 | | ---- | ---- | ---- | 32 | | language_from_cdn | Load Datatable language from ``dataTables.net`` CDN or locally. [See More](/docs/languages_and_translation.md#locally-datatable-translation) | ``boolean`` | 33 | | language | Language will used in Datatable, Also supports locale language base on request. [See More](/docs/languages_and_translation.md#available-language-options) | ``string`` | 34 | | options | To set default options to load into ``dataTables.net``, [See More](https://datatables.net/reference/option) | ``array`` | 35 | | template_parameters | Style parameters has two options: ``style``: One of ``dataTables.net`` themes. ``className`` class attribute of your table. [See More](/docs/themes.md) | ``array`` | 36 | | global_controller | Create and use a custom controller to use custom datatable style,functions,plugins,extensions... [Example](/docs/global_controller_example.md) | ``boolean`` | 37 | -------------------------------------------------------------------------------- /docs/datatables_from_array.md: -------------------------------------------------------------------------------- 1 | Array Datatable: 2 | ========= 3 | 4 | Array Datatable is awesome tool, it helps you by create datatable by passing columns & data as array. 5 | 6 | To use UX Datatable Bundle with ``ArrayDatatable`` : 7 | 8 | ``` 9 | // ... 10 | use Symfony\UX\Datatable\Model\ArrayDatatable; 11 | use Symfony\Component\HttpFoundation\Request; 12 | use Aziz403\UX\Datatable\Builder\DatatableBuilderInterface; 13 | use Aziz403\UX\Datatable\Column\BadgeColumn; 14 | use Aziz403\UX\Datatable\Column\BooleanColumn; 15 | use Aziz403\UX\Datatable\Column\DateColumn; 16 | use Aziz403\UX\Datatable\Column\TextColumn; 17 | use Aziz403\UX\Datatable\Column\TwigColumn; 18 | 19 | class HomeController extends AbstractController 20 | { 21 | #[Route('/', name: 'app_homepage')] 22 | public function index(Request $request,DatatableBuilderInterface $builder): Response 23 | { 24 | //create ArrayDatatable instance with the builder 25 | $datatable = $builder->createDatatableFromArray( 26 | [ 27 | new TextColumn('id'), 28 | new BadgeColumn('category'), 29 | new TextColumn('slug'), 30 | new DateColumn('publishAt'), 31 | new BooleanColumn('isActive'), 32 | new TwigColumn("actions","post/_actions.html.twig",displayName: ""), 33 | ], 34 | [ 35 | { 36 | "name": "Tiger Nixon", 37 | "position": "System Architect", 38 | "salary": "$3,120", 39 | "start_date": "2011/04/25", 40 | "office": "Edinburgh", 41 | "extn": "5421" 42 | }, 43 | { 44 | "name": "Garrett Winters", 45 | "position": "Director", 46 | "salary": "$5,300", 47 | "start_date": "2011/07/25", 48 | "office": "Edinburgh", 49 | "extn": "8422" 50 | } 51 | ] 52 | ); 53 | 54 | return $this->render('home/index.html.twig', [ 55 | 'datatable' => $datatable 56 | ]); 57 | } 58 | } 59 | ``` 60 | 61 | And now can you easy use ``{{ render_datatable(datatable) }}`` to render datatable! 62 | 63 | -------------------------------------------------------------------------------- /docs/datatables_from_entity.md: -------------------------------------------------------------------------------- 1 | Entity Datatable: 2 | ========= 3 | 4 | Entity Datatable is awesome tool, it helps you by create ready queries to render entity data as datatable. 5 | 6 | To use UX Datatable Bundle with ``EntityDatatable`` and ready backend api by : 7 | 8 | // ... 9 | use Symfony\UX\Datatable\Model\EntityDatatable; 10 | use Symfony\Component\HttpFoundation\Request; 11 | use Aziz403\UX\Datatable\Builder\DatatableBuilderInterface; 12 | use Aziz403\UX\Datatable\Column\BadgeColumn; 13 | use Aziz403\UX\Datatable\Column\BooleanColumn; 14 | use Aziz403\UX\Datatable\Column\DateColumn; 15 | use Aziz403\UX\Datatable\Column\TextColumn; 16 | use Aziz403\UX\Datatable\Column\TwigColumn; 17 | 18 | class HomeController extends AbstractController 19 | { 20 | #[Route('/', name: 'app_homepage')] 21 | public function index(Request $request,DatatableBuilderInterface $builder): Response 22 | { 23 | //create EntityDatatable instance with the builder 24 | $datatable = $builder->createDatatableFromEntity(Post::class); 25 | 26 | //add the columns(proprities) of your ``Post`` entity 27 | /** 28 | * you can change the column type class base on your proprity type 29 | * and how to want its will 30 | */ 31 | $datatable 32 | ->addColumn(new TextColumn('id')) 33 | ->addColumn(new BadgeColumn('category')) 34 | ->addColumn(new TextColumn('slug')) 35 | ->addColumn(new DateColumn('publishAt')) 36 | ->addColumn(new BooleanColumn('isActive')) 37 | ->addColumn(new TwigColumn("actions","post/_actions.html.twig",displayName: "")) 38 | 39 | /** 40 | * by handling request you can send response when $datatable->isSubmitted() 41 | * the response its will be the datatable result 42 | */ 43 | $datatable->handleRequest($request); 44 | 45 | if($datatable->isSubmitted()) { 46 | return $datatable->getResponse(); 47 | } 48 | 49 | return $this->render('home/index.html.twig', [ 50 | 'datatable' => $datatable 51 | ]); 52 | } 53 | } 54 | 55 | And now can you easy use ``{{ render_datatable(datatable) }}`` to render datatable for ``Post`` entity ! 56 | 57 | -------------------------------------------------------------------------------- /docs/event.md: -------------------------------------------------------------------------------- 1 | Events 2 | ========= 3 | 4 | UxDatatable Supports Events! 5 | This image explain how events runs: 6 | 7 | 8 | The following is a list of events you can listen to: 9 | 10 | | Event name | Event constant | Event argument | Trigger point | 11 | |------------|----------------|----------------|--------------| 12 | | `datatable.pre_query` | `Events::PRE_QUERY` | `RenderQueryEvent::class` | before create query to get records | 13 | | `datatable.search_query` | `Events::SEARCH_QUERY` | `RenderSearchQueryEvent::class` | after create query builder to get records | 14 | | `datatable.pre_data` | `Events::PRE_DATA` | `RenderDataEvent::class` | before create convert query result to view data | 15 | 16 | ### Example 1 17 | Create a listener class: 18 | 19 | ```php 20 | getDatatable(); 31 | $query = $event->getQuery(); 32 | 33 | // do your stuff with $datatable and/or $query... 34 | } 35 | 36 | } 37 | ``` 38 | 39 | Configure it in your configuration: 40 | 41 | ```yaml 42 | # config/services.yaml or app/config/services.yml 43 | services: 44 | App\EventListener\CustomListener: 45 | tags: 46 | - { name: kernel.event_listener, event: datatable.pre_query } 47 | ``` 48 | 49 | ### Example 2 50 | 51 | We also have another way to set event easy, but this is technique works just with `datatable.search_query` event: 52 | 53 | ```php 54 | $datatable = $builder->createDatatableFromEntity(Product::class); 55 | $datatable 56 | ->addColumn(new TextColumn('name')) 57 | ->addColumn(new TextColumn('description','description',false)) 58 | ->addColumn(new TextColumn('price')) 59 | ->addColumn(new BooleanColumn('isEnabled')) 60 | ->addColumn(new EntityColumn('category','name')) 61 | ->addFilter(function(RenderSearchQueryEvent $event){ 62 | $q = $event->getQuery(); 63 | $q->andWhere('entity.name = :name') 64 | ->setParameter('name',"Product 3"); 65 | }) 66 | ; 67 | 68 | $datatable->handleRequest($request); 69 | 70 | if($datatable->isSubmitted()){ 71 | return $datatable->getResponse(); 72 | } 73 | 74 | return $this->render('simple_datatable.html.twig', [ 75 | 'datatable' => $datatable 76 | ]); 77 | ``` 78 | 79 | ### Different Between ``addCriteria`` and ``addFilter`` 80 | 81 | To explain the different, lets see examples: 82 | 83 | ### Without ``addCriteria`` or ``addFilter`` 84 | 85 | The Result looks like: 86 | ``` 87 | [ 88 | 'data' => [...], 89 | 'recordsTotal', => 50, 90 | 'recordsFiltered' => 50 91 | ] 92 | ``` 93 | 94 | #### Using ``addCriteria`` 95 | 96 | The Criteria applied for all records, filtered or not! 97 | This customized data will be viewed in datatable 98 | 99 | ``` 100 | [ 101 | 'data' => [...], 102 | 'recordsTotal', => 40, 103 | 'recordsFiltered' => 40 104 | ] 105 | ``` 106 | 107 | #### Using ``addFilter`` 108 | 109 | The Filter applied just to the filtered records. 110 | And can you use it to add a custom search or filter. 111 | 112 | ``` 113 | [ 114 | 'data' => [...], 115 | 'recordsTotal', => 50, 116 | 'recordsFiltered' => 40 117 | ] 118 | ``` 119 | -------------------------------------------------------------------------------- /docs/global_controller_example.md: -------------------------------------------------------------------------------- 1 | Example of using ``global_controller`` 2 | ========= 3 | 4 | In this is example we will use ``global_controller`` to render a custom theme. 5 | 6 | ### Step 1 : Create Stimulus Controller 7 | Of course the first step is create the controller, and also we need to relate it with our main datatable controller, how ? its easy like that: 8 | 9 | ``` 10 | // myglobal-datatable_controller.js 11 | 12 | import { Controller } from '@hotwired/stimulus'; 13 | import $ from 'jquery'; 14 | import "myglobal_datatable.css"; // Use your datatable custom style 15 | 16 | export default class extends Controller { 17 | connect() { 18 | this.element.addEventListener('datatable:before-connect', this._onPreConnect); 19 | this.element.addEventListener('datatable:connect', this._onConnect); 20 | } 21 | 22 | disconnect() { 23 | // You should always remove listeners when the controller is disconnected to avoid side effects 24 | this.element.removeEventListener('datatable:before-connect', this._onPreConnect); 25 | this.element.removeEventListener('datatable:connect', this._onConnect); 26 | } 27 | 28 | _onPreConnect(event) { 29 | // The datatable is not yet created 30 | // You can access the datatable options using the event details 31 | event.detail.options['dom'] = "<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'<'float-right destination-datatable-buttons-container'>>><'row'<'col-sm-12'tr>><'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>"; 32 | } 33 | 34 | _onConnect(event) { 35 | // The datatable was just created 36 | // use global search input 37 | let globalSearchBar = $('input.global-search-bar'); 38 | globalSearchBar.keyup(function(){ 39 | // You can access the datatable instance using the event details 40 | event.detail.table.search($(this).val()).draw(); 41 | }) 42 | } 43 | } 44 | ``` 45 | 46 | ### Step 2 : Use Stimulus Controller In Config 47 | Set controller in config 48 | 49 | ```yaml 50 | //config/packages/datatable.yaml 51 | datatable: 52 | global_controller: 'myglobal-datatable' 53 | ``` -------------------------------------------------------------------------------- /docs/images/datatable-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aziz403/ux-datatable/7e56ee0c32fbfd6830165f503ac4ed5fee275b75/docs/images/datatable-example.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | UX Datatable 2 | ========= 3 | 4 | `UX Datatable` creates beautiful tables for your Symfony 5 | applications. It's fast and fully documented. 6 | 7 | Table of Contents 8 | ----------------- 9 | * [Installation](/docs/installation.md) 10 | * [Step 1: Installation](/docs/installation.md#step-1-installation) 11 | * [Step 2: Enable the bundle](/docs/installation.md#step-2-enable-the-bundle-optional) 12 | * [Configuration](/docs/configuration.md) 13 | * [Datatables.net Styling Themes](/docs/themes.md) 14 | * [Languages & Translation](/docs/languages_and_translation.md) 15 | * [Global Customization](/docs/global_controller_example.md) 16 | * [Datatable From Array](/docs/datatables_from_array.md) 17 | * [Datatable From Entity](/docs/datatables_from_entity.md) 18 | * [Columns](/docs/columns.md) 19 | * [Events](/docs/event.md) 20 | 21 | Technical Requirements 22 | ---------------------- 23 | 24 | UX Datatable requires the following: 25 | 26 | * PHP 7.4 or higher 27 | * Symfony 5.4 or higher -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | Installation 2 | ========= 3 | ### Step 1: Installation 4 | 5 | For Symfony Flex installation you need to enable community recipes: 6 | 7 | ```sh 8 | composer config extra.symfony.allow-contrib true 9 | ``` 10 | 11 | Install 12 | 13 | ```sh 14 | composer require aziz403/ux-datatable 15 | ``` 16 | 17 | ### Step 2: Enable the bundle (Optional) 18 | 19 | Enable the bundle in the kernel (not needed with symfony flex): 20 | 21 | ```php 22 | ['all' => true], 28 | ]; 29 | ``` 30 | 31 | And add your global datatable configuration in 32 | 33 | ```yaml 34 | datatable: 35 | # Load i18n data from DataTables CDN or locally 36 | language_from_cdn: true 37 | # Language of the Datatables (if language_from_cdn true) 38 | language: 'en' 39 | # Default options to load into DataTables 40 | # options: 41 | # stateSave: true 42 | # Default parameters to be passed to the template 43 | template_parameters: 44 | # Default Datatables style (one from none,bootstrap3,bootstrap4,bootstrap5,foundation,bulma,jqueryui) 45 | style: 'bootstrap5' 46 | # Default class attribute to apply to the root table elements (change it to be compatible with the style) 47 | className: 'table table-striped' 48 | ``` 49 | 50 | See [Docs](/docs/configuration.md) for more about configuration. -------------------------------------------------------------------------------- /docs/languages_and_translation.md: -------------------------------------------------------------------------------- 1 | Languages and Translation 2 | ========= 3 | 4 | ### Locally Datatable Translation 5 | ``language_from_cdn``: Can you specify if you want use the language from your translation files (using **symfony/translation**), by set it to ``false``. 6 | And in this is situation should you translation missing datatable words: 7 | 8 | | Words | 9 | | ---- | 10 | | datatable.datatable.processing | 11 | | datatable.datatable.search | 12 | | datatable.datatable.lengthMenu | 13 | | datatable.datatable.info | 14 | | datatable.datatable.infoEmpty | 15 | | datatable.datatable.infoFiltered | 16 | | datatable.datatable.infoPostFix | 17 | | datatable.datatable.loadingRecords | 18 | | datatable.datatable.zeroRecords | 19 | | datatable.datatable.emptyTable | 20 | | datatable.datatable.searchPlaceholder | 21 | | datatable.datatable.paginate.first | 22 | | datatable.datatable.paginate.previous | 23 | | datatable.datatable.paginate.next | 24 | | datatable.datatable.paginate.last | 25 | | datatable.datatable.aria.sortAscending | 26 | | datatable.datatable.aria.sortDescending | 27 | 28 | ### Available Language Options 29 | ``language``: Set the language you want in your datatable, the available options is : 30 | 31 | | Language | FullName | 32 | | ---- | ---- | 33 | | en | English | 34 | | fr | French | 35 | | de | German | 36 | | es | Spanish | 37 | | it | Italian | 38 | | pt | Portuguese | 39 | | ru | Russian | 40 | | zh | Chinese | 41 | | ja | Japanese | 42 | | ar | Arabic | 43 | | hi | Hindi | 44 | | bn | Bengali | 45 | | sw | Swahili | 46 | | mr | Marathi | 47 | | ta | Tamil | 48 | | tr | Turkish | 49 | | pl | Polish | 50 | | uk | Ukrainian | 51 | | fa | Persian | 52 | | ur | Urdu | 53 | | he | Hebrew | 54 | | th | Thai | 55 | | request | The language base of the Request | 56 | 57 | 58 | ### Usage 59 | We have two why to config language in datatable : 60 | 61 | #### 1 - Global Configuration 62 | When you want to configure the global language: 63 | 64 | ```yaml 65 | datatable: 66 | # Load i18n data from DataTables CDN or locally 67 | language_from_cdn: true 68 | # Language of the Datatables (if language_from_cdn true) 69 | language: 'en' 70 | ``` 71 | 72 | #### 2 - Local Configuration 73 | In this is situation the config will be applied just in the current ``$datatable`` instance: 74 | 75 | ```php 76 | //.... 77 | $datatable->setLangFromCDN(true); 78 | $datatable->setLanguage('en'); 79 | ``` -------------------------------------------------------------------------------- /docs/themes.md: -------------------------------------------------------------------------------- 1 | Themes Configuration 2 | ========= 3 | 4 | Change the global datatable theme by changing the ``style`` key in configuration: 5 | 6 | ```yaml 7 | datatable: 8 | template_parameters: 9 | style: 'none' 10 | ``` 11 | 12 | The available style themes is the same of datatables.net [styling themes](https://datatables.net/manual/styling/): 13 | | Theme | Reference | 14 | | ---- | ---- | 15 | | none | without using any theme | 16 | | bootstrap3 | [Example](https://datatables.net/examples/styling/bootstrap.html) | 17 | | bootstrap4 | [Example](https://datatables.net/examples/styling/bootstrap4.html) | 18 | | bootstrap5 | [Example](https://datatables.net/examples/styling/bootstrap5.html) | 19 | | foundation | [Example](https://datatables.net/manual/styling/foundation) | 20 | | bulma | [Example](https://datatables.net/examples/styling/bulma.html) | 21 | | jqueryui | [Example](https://datatables.net/manual/styling/jqueryui) | 22 | 23 | or can you create your custom theme and use it in ``global_controller`` from [theme creator](https://datatables.net/manual/styling/theme-creator).[Example](/docs/global_controller_example.md) 24 | -------------------------------------------------------------------------------- /src/Builder/DatatableBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Builder; 13 | 14 | use Aziz403\UX\Datatable\Model\ArrayDatatable; 15 | use Aziz403\UX\Datatable\Model\EntityDatatable; 16 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 17 | use Symfony\Component\HttpFoundation\RequestStack; 18 | use Symfony\Contracts\Translation\TranslatorInterface; 19 | 20 | /** 21 | * @author Aziz Benmallouk 22 | */ 23 | class DatatableBuilder implements DatatableBuilderInterface 24 | { 25 | private string $locale; 26 | 27 | private EventDispatcherInterface $dispatcher; 28 | private TranslatorInterface $translator; 29 | 30 | private array $config; 31 | 32 | public function __construct( 33 | RequestStack $requestStack, 34 | EventDispatcherInterface $dispatcher, 35 | TranslatorInterface $translator, 36 | array $config 37 | ) 38 | { 39 | $this->locale = $requestStack->getCurrentRequest()?->getLocale() ?? 'en'; 40 | 41 | $this->dispatcher = $dispatcher; 42 | $this->translator = $translator; 43 | 44 | $this->config = $config; 45 | } 46 | 47 | public function createDatatableFromEntity(string $className): EntityDatatable 48 | { 49 | return new EntityDatatable( 50 | $className, 51 | $this->dispatcher, 52 | $this->translator, 53 | $this->config, 54 | $this->locale 55 | ); 56 | } 57 | 58 | public function createDatatableFromArray(array $columns, array $data): ArrayDatatable 59 | { 60 | return (new ArrayDatatable( 61 | $this->dispatcher, 62 | $this->translator, 63 | $this->config, 64 | $this->locale, 65 | $data 66 | )) 67 | ->setColumns($columns); 68 | } 69 | } -------------------------------------------------------------------------------- /src/Builder/DatatableBuilderInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Builder; 13 | 14 | use Aziz403\UX\Datatable\Model\ArrayDatatable; 15 | use Aziz403\UX\Datatable\Model\EntityDatatable; 16 | 17 | /** 18 | * @author Aziz Benmallouk 19 | */ 20 | interface DatatableBuilderInterface 21 | { 22 | public function createDatatableFromEntity(string $className): EntityDatatable; 23 | 24 | public function createDatatableFromArray(array $columns,array $data): ArrayDatatable; 25 | } -------------------------------------------------------------------------------- /src/Column/AbstractColumn.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Column; 13 | 14 | use Doctrine\ORM\Query\Expr\Comparison; 15 | use Doctrine\ORM\QueryBuilder; 16 | use Twig\Environment; 17 | 18 | /** 19 | * @author Aziz Benmallouk 20 | */ 21 | abstract class AbstractColumn 22 | { 23 | protected string $data; 24 | protected string $text; 25 | protected bool $visible; 26 | protected bool $orderable; 27 | protected bool $searchable; 28 | protected bool $mapped; 29 | 30 | protected Environment $environment; 31 | 32 | public function __construct(string $data,?string $text,bool $visible,bool $orderable,bool $searchable,bool $mapped) 33 | { 34 | $this->data = $data; 35 | $this->text = $text ?? $data; 36 | $this->visible = $visible; 37 | $this->orderable = $orderable; 38 | $this->searchable = $searchable; 39 | $this->mapped = $mapped; 40 | } 41 | 42 | abstract public function render($entity,$value); 43 | 44 | abstract public function search(QueryBuilder $builder,string $query) :?Comparison; 45 | 46 | public function order(QueryBuilder $builder,string $dir) :QueryBuilder 47 | { 48 | if($this->orderable){ 49 | $alias = $builder->getRootAliases()[0]; 50 | $builder->addOrderBy("$alias.$this",$dir); 51 | } 52 | return $builder; 53 | } 54 | 55 | /** 56 | * @return string 57 | */ 58 | public function getText(): string 59 | { 60 | return $this->text; 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | public function getData(): string 67 | { 68 | return $this->data; 69 | } 70 | 71 | /** 72 | * @return bool 73 | */ 74 | public function isSearchable(): bool 75 | { 76 | return $this->searchable; 77 | } 78 | 79 | /** 80 | * @return bool 81 | */ 82 | public function isVisible(): bool 83 | { 84 | return $this->visible; 85 | } 86 | 87 | /** 88 | * @return bool 89 | */ 90 | public function isOrderable(): bool 91 | { 92 | return $this->orderable; 93 | } 94 | 95 | /** 96 | * @return bool 97 | */ 98 | public function isMapped(): bool 99 | { 100 | return $this->mapped; 101 | } 102 | 103 | /** 104 | * Used inside render method in TwigColumn to render view 105 | * @param Environment $environment 106 | */ 107 | public function setEnvironment(Environment $environment): void 108 | { 109 | $this->environment = $environment; 110 | } 111 | 112 | public function __toString() 113 | { 114 | return $this->data; 115 | } 116 | } -------------------------------------------------------------------------------- /src/Column/BadgeColumn.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Column; 13 | 14 | use Doctrine\ORM\Query\Expr\Comparison; 15 | use Doctrine\ORM\QueryBuilder; 16 | 17 | /** 18 | * @author Aziz Benmallouk 19 | */ 20 | class BadgeColumn extends AbstractColumn 21 | { 22 | const COLOR_DEFAULT = 'default'; 23 | const COLOR_PRIMARY = 'primary'; 24 | const COLOR_SECONDARY = 'secondary'; 25 | const COLOR_SUCCESS = 'success'; 26 | const COLOR_INFO = 'info'; 27 | const COLOR_DANGER = 'danger'; 28 | 29 | private $render; 30 | private string $trueColor; 31 | private string $falseColor; 32 | 33 | public function __construct(string $field,string $trueColor = self::COLOR_PRIMARY,string $falseColor = self::COLOR_DEFAULT,?callable $render = null,?string $displayName = null,bool $visible = true,bool $orderable = true,bool $searchable = true) 34 | { 35 | parent::__construct($field,$displayName,$visible,$orderable,$searchable,true); 36 | $this->trueColor = $trueColor; 37 | $this->falseColor = $falseColor; 38 | $this->render = $render; 39 | } 40 | 41 | public function render($entity,$value) :string 42 | { 43 | if($this->render && is_callable($this->render)){ 44 | //get badge color base on condition 45 | $color = call_user_func($this->render,$entity,$value); 46 | } 47 | else{ 48 | //get badge color from true&false colors 49 | $color = $value ? $this->trueColor : $this->falseColor; 50 | } 51 | return "$value"; 52 | } 53 | 54 | public function search(QueryBuilder $builder, string $query): Comparison 55 | { 56 | //get root alias 57 | $alias = $builder->getRootAliases()[0]; 58 | 59 | //where in badge text 60 | return $builder->expr()->like("$alias.$this->data","'%$query%'"); 61 | } 62 | } -------------------------------------------------------------------------------- /src/Column/BooleanColumn.php: -------------------------------------------------------------------------------- 1 | trueResult = $trueResult; 18 | $this->falseResult = $falseResult; 19 | $this->render = $render; 20 | } 21 | 22 | public function render($entity,$value) :string 23 | { 24 | //check if has custom render condition 25 | if($this->render && is_callable($this->render)){ 26 | return call_user_func($this->render,$entity,$value); 27 | } 28 | //return the same result 29 | return $value ? $this->trueResult : $this->falseResult; 30 | } 31 | 32 | public function search(QueryBuilder $builder, string $query): ?Comparison 33 | { 34 | //get root alias 35 | $alias = $builder->getRootAliases()[0]; 36 | 37 | //generate unique key for search 38 | $key = "search_$this".rand(9,999); 39 | 40 | //where in bool 41 | $expr = $builder->expr()->eq("$alias.$this->data",":$key"); 42 | if($query=='true'||$query==$this->trueResult){ 43 | $param = true; 44 | } 45 | elseif($query=='false'||$query==$this->falseResult){ 46 | $param = false; 47 | } 48 | else{ 49 | return null; 50 | } 51 | $builder->setParameter($key,$param); 52 | return $expr; 53 | } 54 | } -------------------------------------------------------------------------------- /src/Column/DateColumn.php: -------------------------------------------------------------------------------- 1 | format = $format; 17 | $this->render = $render; 18 | } 19 | 20 | public function render($entity,$value) :string 21 | { 22 | //check if has custom render condition 23 | if($this->render && is_callable($this->render)){ 24 | return call_user_func($this->render,$entity,$value); 25 | } 26 | 27 | if(!$value){ 28 | return ''; 29 | } 30 | 31 | //else render using format 32 | return $value->format($this->format); 33 | } 34 | 35 | public function search(QueryBuilder $builder, string $query): Comparison 36 | { 37 | //get root alias 38 | $alias = $builder->getRootAliases()[0]; 39 | 40 | //where in date 41 | return $builder->expr()->like("$alias.$this->data","'%$query%'"); 42 | } 43 | } -------------------------------------------------------------------------------- /src/Column/EntityColumn.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Column; 13 | 14 | use Aziz403\UX\Datatable\Service\DataService; 15 | use Doctrine\ORM\Query\Expr\Comparison; 16 | use Doctrine\ORM\Query\Expr\Join; 17 | use Doctrine\ORM\QueryBuilder; 18 | 19 | /** 20 | * @author Aziz Benmallouk 21 | */ 22 | class EntityColumn extends AbstractColumn 23 | { 24 | const ENTITY_INNER_JOIN = "ENTITY_INNER_JOIN"; 25 | const ENTITY_LEFT_JOIN = "ENTITY_LEFT_JOIN"; 26 | 27 | private string $entity; 28 | private ?string $field; 29 | private ?string $nullValue; 30 | private $render; 31 | private string $joinType; 32 | 33 | public function __construct(string $entity,?string $field = null,?string $displayName = null,$render = null,?string $nullValue = null,string $joinType = self::ENTITY_LEFT_JOIN,bool $visible = true,bool $orderable = true,bool $searchable = true) 34 | { 35 | $data = "$entity $field"; 36 | parent::__construct($data,$displayName,$visible,$orderable,$searchable,true); 37 | $this->entity = $entity; 38 | $this->field = $field; 39 | $this->nullValue = $nullValue; 40 | $this->render = $render; 41 | $this->joinType = $joinType; 42 | } 43 | 44 | public function render($entity,$value) :?string 45 | { 46 | if(!$value){ 47 | return "$this->nullValue"; 48 | } 49 | //check if has custom render condition 50 | if($this->render && is_callable($this->render)){ 51 | return call_user_func($this->render,$entity,$value); 52 | } 53 | //else check if has specific field to display 54 | if($this->field){ 55 | return DataService::getPropValue($value,$this->field); 56 | } 57 | //return __toString result 58 | return "$value"; 59 | } 60 | 61 | public function search(QueryBuilder $builder, string $query): Comparison 62 | { 63 | //where in entity 64 | if($this->field){ 65 | return $builder->expr()->like("$this.$this->field","'%$query%'"); 66 | } 67 | return $builder->expr()->eq("$this.id","'$query'"); 68 | } 69 | 70 | public function join(QueryBuilder $builder) :QueryBuilder 71 | { 72 | //get root alias 73 | $alias = $builder->getRootAliases()[0]; 74 | //check if join exists before 75 | if(array_key_exists($alias,$builder->getDQLPart("join"))){ 76 | $join = array_filter($builder->getDQLPart("join")[$alias],function (Join $item){ 77 | return $item->getAlias()===$this->entity; 78 | }); 79 | //if join exists don't add it again 80 | if($join){ 81 | return $builder; 82 | } 83 | } 84 | if($alias){ 85 | switch ($this->joinType){ 86 | case EntityColumn::ENTITY_LEFT_JOIN: 87 | $builder->leftJoin($alias.".".$this->entity,$this->entity); 88 | break; 89 | case EntityColumn::ENTITY_INNER_JOIN: 90 | $builder->innerJoin($alias.".".$this->entity,$this->entity); 91 | break; 92 | } 93 | } 94 | return $builder; 95 | } 96 | 97 | public function order(QueryBuilder $builder,string $dir) :QueryBuilder 98 | { 99 | //get root alias 100 | $alias = $builder->getRootAliases()[0]; 101 | 102 | //order by field or id if entity field not exists 103 | $field = $this->field; 104 | $field ??= "id"; 105 | $builder->addOrderBy($this->entity.".".$field,$dir); 106 | 107 | return $builder; 108 | } 109 | 110 | /** 111 | * @return string 112 | */ 113 | public function getEntity(): string 114 | { 115 | return $this->entity; 116 | } 117 | 118 | /** 119 | * @return string|null 120 | */ 121 | public function getField(): ?string 122 | { 123 | return $this->field; 124 | } 125 | 126 | /** 127 | * @return string 128 | */ 129 | public function getJoinType(): string 130 | { 131 | return $this->joinType; 132 | } 133 | 134 | public function __toString() 135 | { 136 | return $this->entity; 137 | } 138 | } -------------------------------------------------------------------------------- /src/Column/InlineTwigColumn.php: -------------------------------------------------------------------------------- 1 | template = $template; 17 | $this->params = $params; 18 | } 19 | 20 | public function render($entity, $value) 21 | { 22 | return $this->environment 23 | ->createTemplate($this->template) 24 | ->render(array_merge(['entity'=>$entity],$this->params)) 25 | ; 26 | } 27 | 28 | public function search(QueryBuilder $builder, string $query): ?Comparison 29 | { 30 | return null; 31 | } 32 | } -------------------------------------------------------------------------------- /src/Column/TextColumn.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Column; 13 | 14 | use Doctrine\Common\Collections\Criteria; 15 | use Doctrine\ORM\Query\Expr\Comparison; 16 | use Doctrine\ORM\QueryBuilder; 17 | 18 | /** 19 | * @author Aziz Benmallouk 20 | */ 21 | class TextColumn extends AbstractColumn 22 | { 23 | private $render; 24 | 25 | public function __construct(string $field,?string $displayName = null,bool $visible = true,bool $orderable = true,$render = null,bool $searchable = true) 26 | { 27 | parent::__construct($field,$displayName,$visible,$orderable,$searchable,true); 28 | $this->render = $render; 29 | } 30 | 31 | public function render($entity,$value) :string 32 | { 33 | //check if has custom render condition 34 | if($this->render && is_callable($this->render)){ 35 | return call_user_func($this->render,$entity,$value); 36 | } 37 | //return the same result 38 | return "$value"; 39 | } 40 | 41 | public function search(QueryBuilder $builder, string $query): Comparison 42 | { 43 | //get root alias 44 | $alias = $builder->getRootAliases()[0]; 45 | 46 | //where in text 47 | return $builder->expr()->like("$alias.$this->data","'%$query%'"); 48 | } 49 | } -------------------------------------------------------------------------------- /src/Column/TwigColumn.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Column; 13 | 14 | use Doctrine\ORM\Query\Expr\Comparison; 15 | use Doctrine\ORM\QueryBuilder; 16 | 17 | /** 18 | * @author Aziz Benmallouk 19 | */ 20 | class TwigColumn extends AbstractColumn 21 | { 22 | private string $template; 23 | private array $params; 24 | 25 | public function __construct(string $field,string $template,array $params = [],?string $displayName = null, $visible = true) 26 | { 27 | parent::__construct($field, $displayName, $visible, false, false,false); 28 | $this->template = $template; 29 | $this->params = $params; 30 | } 31 | 32 | public function render($entity,$value) :string 33 | { 34 | return $this->environment->render($this->template,array_merge(['entity'=>$entity],$this->params)); 35 | } 36 | 37 | public function search(QueryBuilder $builder, string $query): ?Comparison 38 | { 39 | return null; 40 | } 41 | } -------------------------------------------------------------------------------- /src/DatatableBundle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable; 13 | 14 | use Aziz403\UX\Datatable\Event\Events; 15 | use Aziz403\UX\Datatable\Event\RenderDataEvent; 16 | use Aziz403\UX\Datatable\Event\RenderQueryEvent; 17 | use Aziz403\UX\Datatable\Event\RenderSearchQueryEvent; 18 | use Symfony\Component\DependencyInjection\Compiler\PassConfig; 19 | use Symfony\Component\DependencyInjection\ContainerBuilder; 20 | use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass; 21 | use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; 22 | use Symfony\Component\EventDispatcher\EventDispatcher; 23 | use Symfony\Component\HttpKernel\Bundle\Bundle; 24 | 25 | /** 26 | * @author Aziz Benmallouk 27 | */ 28 | class DatatableBundle extends Bundle 29 | { 30 | public function getPath(): string 31 | { 32 | return \dirname(__DIR__); 33 | } 34 | 35 | public function build(ContainerBuilder $container) 36 | { 37 | parent::build($container); 38 | 39 | $container->addCompilerPass(new AddEventAliasesPass([ 40 | RenderQueryEvent::class => Events::PRE_QUERY, 41 | RenderSearchQueryEvent::class => Events::SEARCH_QUERY, 42 | RenderDataEvent::class => Events::PRE_DATA, 43 | ])); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\DependencyInjection; 13 | 14 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 15 | use Symfony\Component\Config\Definition\ConfigurationInterface; 16 | 17 | /** 18 | * @author Aziz Benmallouk 19 | */ 20 | class Configuration implements ConfigurationInterface 21 | { 22 | 23 | public function getConfigTreeBuilder(): TreeBuilder 24 | { 25 | $treeBuilder = new TreeBuilder('datatable'); 26 | 27 | $rootNode = $treeBuilder->getRootNode(); 28 | 29 | $rootNode 30 | ->children() 31 | ->booleanNode('language_from_cdn') 32 | ->info('Load i18n data from DataTables CDN or locally') 33 | ->defaultTrue() 34 | ->isRequired() 35 | ->end() 36 | ->scalarNode('language') 37 | ->info('Language of the Datatables (if language_from_cdn true)') 38 | ->defaultValue('en') 39 | ->end() 40 | ->scalarNode('global_controller') 41 | ->info('Integrate a stimulus controller will be added to datatable_controller') 42 | ->end() 43 | ->arrayNode('options') 44 | ->info('Default options to load into DataTables') 45 | ->useAttributeAsKey('option') 46 | ->prototype('variable')->end() 47 | ->end() 48 | ->arrayNode('template_parameters') 49 | ->info('Default parameters to be passed to the template') 50 | ->addDefaultsIfNotSet() 51 | ->ignoreExtraKeys() 52 | ->children() 53 | ->enumNode('style') 54 | ->values(['none','bootstrap3','bootstrap4','bootstrap5','foundation','bulma','jqueryui']) 55 | ->info('Default Datatables style') 56 | ->defaultValue('bootstrap5') 57 | ->end() 58 | ->scalarNode('className') 59 | ->info('Default class attribute to apply to the root table elements (change it to be compatible with the style)') 60 | ->defaultValue('table table-bordered') 61 | ->end() 62 | ->end() 63 | ->end() 64 | ->end() 65 | ; 66 | 67 | return $treeBuilder; 68 | } 69 | } -------------------------------------------------------------------------------- /src/DependencyInjection/DatatableExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\DependencyInjection; 13 | 14 | use Aziz403\UX\Datatable\Twig\DatatableExtension as TwigDatatableExtension; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\Definition; 17 | use Symfony\Component\DependencyInjection\Reference; 18 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 19 | use Aziz403\UX\Datatable\Builder\DatatableBuilder; 20 | use Aziz403\UX\Datatable\Builder\DatatableBuilderInterface; 21 | use Aziz403\UX\Datatable\EventListener\RenderSubscriber; 22 | use Symfony\WebpackEncoreBundle\Twig\StimulusTwigExtension; 23 | use Twig\Environment; 24 | 25 | /** 26 | * @author Aziz Benmallouk 27 | */ 28 | class DatatableExtension extends Extension 29 | { 30 | public function load(array $configs, ContainerBuilder $container) 31 | { 32 | $configuration = new Configuration(); 33 | $config = $this->processConfiguration($configuration, $configs); 34 | 35 | $container->setParameter('datatable.config', $config); 36 | 37 | if (!isset($container->getParameter('kernel.bundles')['TwigBundle'])) { 38 | throw new \LogicException('The TwigBundle is not registered in your application. Try running "composer require symfony/twig-bundle".'); 39 | } 40 | 41 | if(class_exists(Environment::class)) { 42 | $container 43 | ->setDefinition('datatable.builder', new Definition(DatatableBuilder::class)) 44 | ->addArgument(new Reference('request_stack')) 45 | ->addArgument(new Reference('event_dispatcher')) 46 | ->addArgument(new Reference('translator.default')) 47 | ->addArgument($config); 48 | $container 49 | ->setAlias(DatatableBuilderInterface::class, 'datatable.builder'); 50 | } 51 | 52 | if (class_exists(Environment::class) && class_exists(StimulusTwigExtension::class)) { 53 | $container 54 | ->setDefinition('datatable.twig_extension', new Definition(TwigDatatableExtension::class)) 55 | ->addArgument(new Reference('webpack_encore.twig_stimulus_extension')) 56 | ->addTag('twig.extension'); 57 | } 58 | 59 | $container->register('datatable.event_listener.render_subscriber', RenderSubscriber::class) 60 | ->addTag('kernel.event_subscriber') 61 | ->addArgument(new Reference('doctrine.orm.default_entity_manager')) 62 | ->addArgument(new Reference('twig')) 63 | ->addArgument(new Reference('property_accessor')) 64 | ->addArgument(new Reference('event_dispatcher')) 65 | ; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Event/Events.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Event; 13 | 14 | /** 15 | * @author Aziz Benmallouk 16 | */ 17 | final class Events 18 | { 19 | public const PRE_QUERY = 'datatable.pre_query'; 20 | 21 | public const SEARCH_QUERY = 'datatable.search_query'; 22 | 23 | public const PRE_DATA = 'datatable.pre_data'; 24 | } -------------------------------------------------------------------------------- /src/Event/RenderDataEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Event; 13 | 14 | use Aziz403\UX\Datatable\Model\AbstractDatatable; 15 | use Symfony\Contracts\EventDispatcher\Event; 16 | 17 | /** 18 | * @author Aziz Benmallouk 19 | */ 20 | final class RenderDataEvent extends Event 21 | { 22 | private AbstractDatatable $datatable; 23 | private iterable $records = []; 24 | 25 | /** 26 | * @internal 27 | */ 28 | public function __construct(AbstractDatatable $datatable,iterable $records) { 29 | $this->datatable = $datatable; 30 | $this->records = $records; 31 | } 32 | 33 | /** 34 | * Get the value of records 35 | */ 36 | public function getRecords() 37 | { 38 | return $this->records; 39 | } 40 | 41 | /** 42 | * Set the value of records 43 | * 44 | * @return self 45 | */ 46 | public function setRecords($records) 47 | { 48 | $this->records = $records; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Get the value of datatable 55 | */ 56 | public function getDatatable() 57 | { 58 | return $this->datatable; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Event/RenderQueryEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Event; 13 | 14 | use Aziz403\UX\Datatable\Model\EntityDatatable; 15 | use Symfony\Contracts\EventDispatcher\Event; 16 | 17 | /** 18 | * @author Aziz Benmallouk 19 | */ 20 | final class RenderQueryEvent extends Event 21 | { 22 | private EntityDatatable $datatable; 23 | 24 | private array $query = []; 25 | 26 | private int $recordsTotal = 0; 27 | private int $recordsFiltered = 0; 28 | private iterable $records = []; 29 | 30 | /** 31 | * @internal 32 | */ 33 | public function __construct(EntityDatatable $datatable,array $query) { 34 | $this->datatable = $datatable; 35 | $this->query = $query; 36 | } 37 | 38 | /** 39 | * Get the value of recordsTotal 40 | */ 41 | public function getRecordsTotal() 42 | { 43 | return $this->recordsTotal; 44 | } 45 | 46 | /** 47 | * Set the value of recordsTotal 48 | * 49 | * @return self 50 | */ 51 | public function setRecordsTotal($recordsTotal) 52 | { 53 | $this->recordsTotal = $recordsTotal; 54 | 55 | return $this; 56 | } 57 | 58 | /** 59 | * Get the value of recordsFiltered 60 | */ 61 | public function getRecordsFiltered() 62 | { 63 | return $this->recordsFiltered; 64 | } 65 | 66 | /** 67 | * Set the value of recordsFiltered 68 | * 69 | * @return self 70 | */ 71 | public function setRecordsFiltered($recordsFiltered) 72 | { 73 | $this->recordsFiltered = $recordsFiltered; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Get the value of records 80 | */ 81 | public function getRecords() 82 | { 83 | return $this->records; 84 | } 85 | 86 | /** 87 | * Set the value of records 88 | * 89 | * @return self 90 | */ 91 | public function setRecords($records) 92 | { 93 | $this->records = $records; 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * Get the value of query 100 | */ 101 | public function getQuery() 102 | { 103 | return $this->query; 104 | } 105 | 106 | /** 107 | * Get the value of datatable 108 | */ 109 | public function getDatatable() 110 | { 111 | return $this->datatable; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Event/RenderSearchQueryEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Event; 13 | 14 | use Aziz403\UX\Datatable\Model\EntityDatatable; 15 | use Doctrine\ORM\QueryBuilder; 16 | use Symfony\Contracts\EventDispatcher\Event; 17 | 18 | /** 19 | * @author Aziz Benmallouk 20 | */ 21 | final class RenderSearchQueryEvent extends Event 22 | { 23 | private EntityDatatable $datatable; 24 | 25 | private QueryBuilder $query; 26 | 27 | /** 28 | * @internal 29 | */ 30 | public function __construct(EntityDatatable $datatable,QueryBuilder $query) { 31 | $this->datatable = $datatable; 32 | $this->query = $query; 33 | } 34 | 35 | /** 36 | * Get the value of query 37 | */ 38 | public function getQuery() 39 | { 40 | return $this->query; 41 | } 42 | 43 | /** 44 | * Get the value of datatable 45 | */ 46 | public function getDatatable() 47 | { 48 | return $this->datatable; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/EventListener/RenderSubscriber.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\EventListener; 13 | 14 | use Aziz403\UX\Datatable\Event\Events; 15 | use Aziz403\UX\Datatable\Event\RenderDataEvent; 16 | use Aziz403\UX\Datatable\Event\RenderQueryEvent; 17 | use Aziz403\UX\Datatable\Helper\DatatableQueriesHelper; 18 | use Aziz403\UX\Datatable\Helper\DatatableTemplatingHelper; 19 | use Doctrine\ORM\EntityManagerInterface; 20 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 21 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 22 | use Symfony\Component\PropertyAccess\PropertyAccessorInterface; 23 | use Twig\Environment; 24 | 25 | /** 26 | * 27 | * @author Aziz Benmallouk 28 | * 29 | * @internal 30 | */ 31 | final class RenderSubscriber implements EventSubscriberInterface 32 | { 33 | private EntityManagerInterface $manager; 34 | private Environment $templating; 35 | private PropertyAccessorInterface $propertyAccessor; 36 | private EventDispatcherInterface $dispatcher; 37 | 38 | public function __construct(EntityManagerInterface $manager, 39 | Environment $templating, 40 | PropertyAccessorInterface $propertyAccessor, 41 | EventDispatcherInterface $dispatcher) 42 | { 43 | $this->manager = $manager; 44 | $this->templating = $templating; 45 | $this->propertyAccessor = $propertyAccessor; 46 | $this->dispatcher = $dispatcher; 47 | } 48 | 49 | public function onRenderQuery(RenderQueryEvent $event): void 50 | { 51 | $datatable = $event->getDatatable(); 52 | $query = $event->getQuery(); 53 | 54 | $queryKeys = ['columns','orders','search','start','length']; 55 | 56 | foreach($queryKeys as $key){ 57 | if (!\array_key_exists($key, $query)) { 58 | throw new \Exception(sprintf("Your query don't have %s key, If you have a %s please don't unset query keys.",$key,"RenderQueryEvent")); 59 | } 60 | } 61 | 62 | $helper = new DatatableQueriesHelper( 63 | $this->manager, 64 | $this->dispatcher, 65 | $datatable 66 | ); 67 | 68 | $event->setRecordsTotal($helper->countRecords()); 69 | $event->setRecordsFiltered($helper->countRecords($query)); 70 | $event->setRecords($helper->findRecords($query)); 71 | } 72 | 73 | public function onRenderData(RenderDataEvent $event): void 74 | { 75 | $datatable = $event->getDatatable(); 76 | $records = $event->getRecords(); 77 | 78 | $helper = new DatatableTemplatingHelper( 79 | $this->templating, 80 | $this->propertyAccessor, 81 | $datatable, 82 | $records 83 | ); 84 | 85 | $event->setRecords($helper->renderData()); 86 | } 87 | 88 | public static function getSubscribedEvents(): array 89 | { 90 | return [ 91 | Events::PRE_QUERY => 'onRenderQuery', 92 | Events::PRE_DATA => 'onRenderData', 93 | ]; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Exception/GetterNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Helper; 13 | 14 | use Aziz403\UX\Datatable\Column\EntityColumn; 15 | use Aziz403\UX\Datatable\Event\Events; 16 | use Aziz403\UX\Datatable\Event\RenderSearchQueryEvent; 17 | use Aziz403\UX\Datatable\Model\EntityDatatable; 18 | use Doctrine\ORM\EntityManagerInterface; 19 | use Doctrine\ORM\EntityRepository; 20 | use Doctrine\ORM\QueryBuilder; 21 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 22 | 23 | /** 24 | * @author Aziz Benmallouk 25 | */ 26 | class DatatableQueriesHelper 27 | { 28 | private EntityDatatable $datatable; 29 | private EntityRepository $repository; 30 | private EventDispatcherInterface $dispatcher; 31 | private string $alias; 32 | 33 | public function __construct(EntityManagerInterface $manager,EventDispatcherInterface $dispatcher,EntityDatatable $datatable) 34 | { 35 | $this->datatable = $datatable; 36 | $this->repository = $manager->getRepository($datatable->getClassName()); 37 | if(!$this->repository){ 38 | throw new \LogicException(sprintf("%s Repository Not Found ",$datatable->getClassName())); 39 | } 40 | $this->dispatcher = $dispatcher; 41 | $this->alias = "entity"; 42 | } 43 | 44 | public function getBaseQuery(): QueryBuilder 45 | { 46 | $q = $this->repository->createQueryBuilder($this->alias); 47 | 48 | //add criteria if exists 49 | if($criteria = $this->datatable->getCriteria()){ 50 | $q->addCriteria($criteria); 51 | } 52 | return $q; 53 | } 54 | 55 | public function getFilteredQuery(array $query,bool $withOrder = true,bool $withPagination = true): QueryBuilder 56 | { 57 | //create query 58 | $q = $this->getBaseQuery(); 59 | 60 | $event = new RenderSearchQueryEvent($this->datatable,$q); 61 | $this->dispatcher->dispatch($event,Events::SEARCH_QUERY); 62 | $q = $event->getQuery(); 63 | 64 | //add join entities 65 | /** @var EntityColumn $column */ 66 | foreach ($this->datatable->getColumnsByType(EntityColumn::class) as $column){ 67 | $column->join($q); 68 | } 69 | 70 | if($withOrder){ 71 | //add order in query base on columns 72 | foreach ($query['orders'] as $order){ 73 | $indexOfColumn = $order['column']; 74 | $dir = $order['dir']; 75 | $column = $this->datatable->getColumnByIndex($indexOfColumn); 76 | 77 | $column->order($q,$dir); 78 | } 79 | } 80 | 81 | //add global search query 82 | if($value = $query['search']['value']){ 83 | $search = []; 84 | foreach ($this->datatable->getSearchableColumns() as $column){ 85 | $search[] = $column->search($q,$value); 86 | } 87 | $q->andWhere($q->expr()->orX(...$search)); 88 | } 89 | 90 | //add filter columns query 91 | if($query['columns']){ 92 | foreach ($query['columns'] as $queryColumn){ 93 | if($value = $queryColumn['search']['value']){ 94 | $indexOfColumn = $queryColumn['data']; 95 | $column = $this->datatable->getColumn($indexOfColumn); 96 | 97 | $q->andWhere($column->search($q,$value)); 98 | } 99 | } 100 | } 101 | 102 | if($withPagination){ 103 | //add pagination to query 104 | $q->setFirstResult($query['start']) 105 | ->setMaxResults($query['length']); 106 | } 107 | 108 | return $q; 109 | } 110 | 111 | public function findRecords(array $query) 112 | { 113 | return $this->getFilteredQuery($query) 114 | ->getQuery() 115 | ->getResult(); 116 | } 117 | 118 | public function countRecords(array $query = null) 119 | { 120 | if($query){ 121 | $q = $this->getFilteredQuery($query,false,false); 122 | } 123 | else{ 124 | $q = $this->getBaseQuery(); 125 | } 126 | 127 | return $q->select("COUNT($this->alias.id)") 128 | ->getQuery() 129 | ->getSingleScalarResult(); 130 | } 131 | } -------------------------------------------------------------------------------- /src/Helper/DatatableTemplatingHelper.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Helper; 13 | 14 | use Aziz403\UX\Datatable\Model\AbstractDatatable; 15 | use Aziz403\UX\Datatable\Model\ArrayDatatable; 16 | use Aziz403\UX\Datatable\Model\EntityDatatable; 17 | use Aziz403\UX\Datatable\Service\DataService; 18 | use Symfony\Component\PropertyAccess\PropertyAccessorInterface; 19 | use Twig\Environment; 20 | 21 | /** 22 | * @author Aziz Benmallouk 23 | */ 24 | class DatatableTemplatingHelper 25 | { 26 | private Environment $environment; 27 | private PropertyAccessorInterface $propertyAccessor; 28 | private AbstractDatatable $datatable; 29 | 30 | private iterable $data; 31 | 32 | public function __construct(Environment $environment,PropertyAccessorInterface $propertyAccessor,AbstractDatatable $datatable,iterable $data) 33 | { 34 | $this->environment = $environment; 35 | $this->propertyAccessor = $propertyAccessor; 36 | $this->datatable = $datatable; 37 | $this->data = $data; 38 | } 39 | 40 | /** 41 | * convert data from array of entity objects to datatable result 42 | * @return array 43 | */ 44 | public function renderData() :array 45 | { 46 | $result = []; 47 | foreach ($this->data as $item){ 48 | $row = []; 49 | foreach ($this->datatable->getColumns() as $column){ 50 | //get column index 51 | $index = $this->datatable->getColumnIndex($column->getData()); 52 | $value = null; 53 | 54 | //get value from prop if column mapped on entity 55 | if($this->datatable instanceof EntityDatatable && $column->isMapped()) { 56 | $value = $this->propertyAccessor->getValue($item,$column->__toString()); 57 | } 58 | else if($this->datatable instanceof ArrayDatatable){ 59 | $value = $item[$index]; 60 | } 61 | 62 | //add special parts to each column value base on column type 63 | $column->setEnvironment($this->environment); 64 | $value = $column->render($item,$value); 65 | 66 | $row[$index] = "$value"; 67 | } 68 | $result[] = $row; 69 | } 70 | return $result; 71 | } 72 | } -------------------------------------------------------------------------------- /src/Model/AbstractDatatable.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Model; 13 | 14 | use Aziz403\UX\Datatable\Column\AbstractColumn; 15 | use Aziz403\UX\Datatable\Helper\Constants; 16 | use Aziz403\UX\Datatable\Helper\DatatableTemplatingHelper; 17 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 18 | use Symfony\Contracts\Translation\TranslatorInterface; 19 | use Twig\Environment; 20 | 21 | /** 22 | * @author Aziz Benmallouk 23 | */ 24 | abstract class AbstractDatatable 25 | { 26 | protected array $options = []; 27 | protected array $attributes = []; 28 | 29 | protected array $columns; 30 | 31 | protected ?string $globalController; 32 | 33 | protected string $language; 34 | protected string $locale; 35 | protected bool $isLangFromCDN; 36 | 37 | protected TranslatorInterface $translator; 38 | protected EventDispatcherInterface $dispatcher; 39 | 40 | public function __construct( 41 | EventDispatcherInterface $dispatcher, 42 | TranslatorInterface $translator, 43 | array $config, 44 | string $locale 45 | ) 46 | { 47 | $this->columns = []; 48 | $this->attributes['id'] = "datatable"; 49 | 50 | $this->language = $config['language']; 51 | $this->isLangFromCDN = $config['language_from_cdn']; 52 | $this->globalController = $config['global_controller'] ?? null; 53 | $this->locale = $locale; 54 | 55 | if(isset($config['template_parameters'])){ 56 | if(isset($config['template_parameters']['style'])){ 57 | $this->attributes['data-styling-choicer'] = $config['template_parameters']['style']; 58 | } 59 | if(isset($config['template_parameters']['className'])){ 60 | $this->attributes['class'] = ' '.$config['template_parameters']['className']; 61 | } 62 | } 63 | 64 | $this->translator = $translator; 65 | $this->dispatcher = $dispatcher; 66 | } 67 | 68 | /** 69 | * @return array 70 | */ 71 | public function getOptions(): array 72 | { 73 | return $this->options; 74 | } 75 | 76 | /** 77 | * @param array $options 78 | * @return $this 79 | */ 80 | public function setOptions(array $options): self 81 | { 82 | $this->options = $options; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * @param array $options 89 | * @return $this 90 | */ 91 | public function addOptions(array $options): self 92 | { 93 | $this->options = array_merge($this->options,$options); 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * @param string $key 100 | * @param $value 101 | * @return $this 102 | */ 103 | public function addOption(string $key, $value): self 104 | { 105 | $this->options[$key] = $value; 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * @return array 112 | */ 113 | public function getAttributes(): array 114 | { 115 | return $this->attributes; 116 | } 117 | 118 | /** 119 | * @param array $attributes 120 | * @return $this 121 | */ 122 | public function setAttributes(array $attributes): self 123 | { 124 | $this->attributes = $attributes; 125 | 126 | return $this; 127 | } 128 | 129 | /** 130 | * @param array $attributes 131 | * @return $this 132 | */ 133 | public function addAttributes(array $attributes): self 134 | { 135 | $this->attributes = array_merge($this->attributes,$attributes); 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * @param string $key 142 | * @param string $value 143 | * @return $this 144 | */ 145 | public function addAttribute(string $key, string $value): self 146 | { 147 | $this->attributes[$key] = $value; 148 | 149 | return $this; 150 | } 151 | 152 | /** 153 | * @return string|null 154 | */ 155 | public function getDataController(): ?string 156 | { 157 | return $this->attributes['data-controller'] ?? null; 158 | } 159 | 160 | /** 161 | * @param string $controllerName 162 | * @return void 163 | */ 164 | public function setDataController(string $controllerName) 165 | { 166 | $this->attributes['data-controller'] = $controllerName; 167 | } 168 | 169 | /** 170 | * @return AbstractColumn[] 171 | */ 172 | public function getColumns(): array 173 | { 174 | return $this->columns; 175 | } 176 | 177 | /** 178 | * @return AbstractColumn[] 179 | */ 180 | public function getSearchableColumns(): array 181 | { 182 | return array_filter($this->columns,function (AbstractColumn $column) { 183 | return $column->isSearchable(); 184 | }); 185 | } 186 | 187 | /** 188 | * @param string $type 189 | * @return array 190 | */ 191 | public function getColumnsByType(string $type): array 192 | { 193 | return array_filter($this->columns,function (AbstractColumn $column) use ($type) { 194 | return $column instanceof $type; 195 | }); 196 | } 197 | 198 | /** 199 | * @param string $data 200 | * @return AbstractColumn|null 201 | */ 202 | public function getColumn(string $data): ?AbstractColumn 203 | { 204 | $res = array_filter($this->columns,function (AbstractColumn $column) use($data){ 205 | return $column->getData()===$data; 206 | }); 207 | if(count($res)==0){ 208 | return null; 209 | } 210 | return reset($res); 211 | } 212 | 213 | /** 214 | * @param int $index 215 | * @return AbstractColumn|null 216 | */ 217 | public function getColumnByIndex(int $index): ?AbstractColumn 218 | { 219 | return $this->columns[$index]; 220 | } 221 | 222 | /** 223 | * @param string $data 224 | * @return AbstractColumn|null 225 | */ 226 | public function getColumnIndex(string $data): ?int 227 | { 228 | return array_search( 229 | $this->getColumn($data), 230 | $this->columns 231 | ); 232 | } 233 | 234 | /** 235 | * @param AbstractColumn[] $columns 236 | */ 237 | public function setColumns(array $columns): self 238 | { 239 | $this->columns = $columns; 240 | 241 | return $this; 242 | } 243 | 244 | /** 245 | * @param AbstractColumn[] $columns 246 | */ 247 | public function addColumns(array $columns): self 248 | { 249 | $this->columns = array_merge($this->columns,$columns); 250 | 251 | return $this; 252 | } 253 | 254 | /** 255 | * @param AbstractColumn $column 256 | */ 257 | public function addColumn(AbstractColumn $column): self 258 | { 259 | $this->columns[] = $column; 260 | 261 | return $this; 262 | } 263 | 264 | /** 265 | * @return array 266 | */ 267 | public function getLanguageData(): array 268 | { 269 | $this->getLanguage(); 270 | 271 | if ($this->isLangFromCDN) { 272 | return ['url' => "//cdn.datatables.net/plug-ins/1.10.15/i18n/{$this->getFullLanguage()}.json"]; 273 | } 274 | 275 | return [ 276 | 'processing' => $this->translator->trans('datatable.datatable.processing'), 277 | 'search' => $this->translator->trans('datatable.datatable.search'), 278 | 'lengthMenu' => $this->translator->trans('datatable.datatable.lengthMenu'), 279 | 'info' => $this->translator->trans('datatable.datatable.info'), 280 | 'infoEmpty' => $this->translator->trans('datatable.datatable.infoEmpty'), 281 | 'infoFiltered' => $this->translator->trans('datatable.datatable.infoFiltered'), 282 | 'infoPostFix' => $this->translator->trans('datatable.datatable.infoPostFix'), 283 | 'loadingRecords' => $this->translator->trans('datatable.datatable.loadingRecords'), 284 | 'zeroRecords' => $this->translator->trans('datatable.datatable.zeroRecords'), 285 | 'emptyTable' => $this->translator->trans('datatable.datatable.emptyTable'), 286 | 'searchPlaceholder' => $this->translator->trans('datatable.datatable.searchPlaceholder'), 287 | 'paginate' => [ 288 | 'first' => $this->translator->trans('datatable.datatable.paginate.first'), 289 | 'previous' => $this->translator->trans('datatable.datatable.paginate.previous'), 290 | 'next' => $this->translator->trans('datatable.datatable.paginate.next'), 291 | 'last' => $this->translator->trans('datatable.datatable.paginate.last'), 292 | ], 293 | 'aria' => [ 294 | 'sortAscending' => $this->translator->trans('datatable.datatable.aria.sortAscending'), 295 | 'sortDescending' => $this->translator->trans('datatable.datatable.aria.sortDescending'), 296 | ], 297 | ]; 298 | } 299 | 300 | /** 301 | * @return string 302 | */ 303 | public function getLanguage(): string 304 | { 305 | if($this->language==null || $this->language=='request'){ 306 | $this->language = $this->locale; 307 | } 308 | 309 | return $this->language; 310 | } 311 | 312 | /** 313 | * @return string 314 | */ 315 | public function getFullLanguage(): string 316 | { 317 | if(!isset(LANGUAGES[$this->language])){ 318 | throw new \Exception(sprintf("'%s' Not Accepted, The Language needs to be a shortcut and one of: %s, or 'request'",$this->language,implode(",",array_keys(LANGUAGES)))); 319 | } 320 | 321 | return LANGUAGES[$this->language]; 322 | } 323 | 324 | /** 325 | * @param string $language 326 | */ 327 | public function setLanguage(string $language): self 328 | { 329 | $this->language = $language; 330 | 331 | return $this; 332 | } 333 | 334 | /** 335 | * @return bool 336 | */ 337 | public function isLangFromCDN(): bool 338 | { 339 | return $this->isLangFromCDN; 340 | } 341 | 342 | /** 343 | * @param bool $isLangFromCDN 344 | */ 345 | public function setLangFromCDN(bool $isLangFromCDN): self 346 | { 347 | $this->isLangFromCDN = $isLangFromCDN; 348 | 349 | return $this; 350 | } 351 | 352 | /** 353 | * @return mixed 354 | */ 355 | public function getThemeStyling() 356 | { 357 | return $this->attributes['data-styling-choicer']; 358 | } 359 | 360 | /** 361 | * @return string 362 | */ 363 | public function getGlobalController(): ?string 364 | { 365 | return $this->globalController; 366 | } 367 | 368 | /** 369 | * @param string|null $globalController 370 | */ 371 | public function setGlobalController(?string $globalController): AbstractDatatable 372 | { 373 | $this->globalController = $globalController; 374 | return $this; 375 | } 376 | 377 | /** 378 | * @return array 379 | */ 380 | public function getColumnDefs(): array 381 | { 382 | $columnDefs = []; 383 | $i = 0; 384 | /** @var AbstractColumn $column */ 385 | foreach ($this->columns as $column){ 386 | $columnDefs[] = [ 387 | 'targets' => $i, 388 | 'visible' => $column->isVisible(), 389 | 'orderable' => $column->isOrderable() 390 | ]; 391 | $i++; 392 | } 393 | 394 | return $columnDefs; 395 | } 396 | 397 | public abstract function createView(): array; 398 | 399 | } 400 | 401 | const LANGUAGES = array( 402 | 'en' => 'English', 403 | 'fr' => 'French', 404 | 'de' => 'German', 405 | 'es' => 'Spanish', 406 | 'it' => 'Italian', 407 | 'pt' => 'Portuguese', 408 | 'ru' => 'Russian', 409 | 'zh' => 'Chinese', 410 | 'ja' => 'Japanese', 411 | 'ar' => 'Arabic', 412 | 'hi' => 'Hindi', 413 | 'bn' => 'Bengali', 414 | 'sw' => 'Swahili', 415 | 'mr' => 'Marathi', 416 | 'ta' => 'Tamil', 417 | 'tr' => 'Turkish', 418 | 'pl' => 'Polish', 419 | 'uk' => 'Ukrainian', 420 | 'fa' => 'Persian', 421 | 'ur' => 'Urdu', 422 | 'he' => 'Hebrew', 423 | 'th' => 'Thai' 424 | ); -------------------------------------------------------------------------------- /src/Model/ArrayDatatable.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Model; 13 | 14 | use Aziz403\UX\Datatable\Event\Events; 15 | use Aziz403\UX\Datatable\Event\RenderDataEvent; 16 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 17 | use Symfony\Contracts\Translation\TranslatorInterface; 18 | 19 | /** 20 | * @author Aziz Benmallouk 21 | */ 22 | class ArrayDatatable extends AbstractDatatable 23 | { 24 | protected array $data = []; 25 | 26 | public function __construct( 27 | EventDispatcherInterface $dispatcher, 28 | TranslatorInterface $translator, 29 | array $config, 30 | string $locale, 31 | array $data, 32 | ) 33 | { 34 | parent::__construct($dispatcher,$translator,$config,$locale); 35 | $this->data = $data; 36 | } 37 | 38 | /** 39 | * @return array|null 40 | */ 41 | public function getData(): ?array 42 | { 43 | return $this->data; 44 | } 45 | 46 | /** 47 | * @param array $data 48 | */ 49 | public function setData(array $data): void 50 | { 51 | $this->data = $data; 52 | } 53 | 54 | /** 55 | * @return array 56 | */ 57 | public function getColumnsView(): array 58 | { 59 | $fieldName = 'title'; 60 | if((array() === $this->data) || (array_keys($this->data) !== range(0, count($this->data) - 1))){ 61 | $fieldName = 'data'; 62 | } 63 | 64 | $columns = []; 65 | foreach($this->columns as $column){ 66 | $columns[] = [ 67 | $fieldName => $column->getData() 68 | ]; 69 | } 70 | return $columns; 71 | } 72 | 73 | public function renderData() 74 | { 75 | $event = new RenderDataEvent($this,$this->data); 76 | $this->dispatcher->dispatch($event,Events::PRE_DATA); 77 | 78 | return $event->getRecords(); 79 | } 80 | 81 | /** 82 | * @return array 83 | */ 84 | public function createView(): array 85 | { 86 | return [ 87 | "options" => array_merge( 88 | $this->options, 89 | [ 90 | "columns" => $this->getColumnsView(), 91 | "columnDefs" => $this->getColumnDefs(), 92 | "language" => $this->getLanguageData(), 93 | "data" => $this->renderData() 94 | ] 95 | ), 96 | ]; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Model/EntityDatatable.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Model; 13 | 14 | use Aziz403\UX\Datatable\Event\Events; 15 | use Aziz403\UX\Datatable\Event\RenderDataEvent; 16 | use Aziz403\UX\Datatable\Event\RenderQueryEvent; 17 | use Aziz403\UX\Datatable\Service\DataService; 18 | use Doctrine\Common\Collections\Criteria; 19 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 20 | use Symfony\Component\HttpFoundation\JsonResponse; 21 | use Symfony\Component\HttpFoundation\Request; 22 | use Symfony\Contracts\Translation\TranslatorInterface; 23 | 24 | /** 25 | * @author Aziz Benmallouk 26 | */ 27 | class EntityDatatable extends AbstractDatatable 28 | { 29 | const DEFAULT_DATATABLE_OPTIONS = [ 30 | 'processing' => true, 31 | 'serverSide' => true 32 | ]; 33 | 34 | protected string $className = ""; 35 | 36 | protected ?string $path = ""; 37 | 38 | private ?Criteria $criteria = null; 39 | 40 | private ?Request $request = null; 41 | 42 | public function __construct( 43 | string $className, 44 | EventDispatcherInterface $dispatcher, 45 | TranslatorInterface $translator, 46 | array $config, 47 | string $locale 48 | ) 49 | { 50 | parent::__construct($dispatcher,$translator,$config,$locale); 51 | 52 | $this->className = $className; 53 | $this->attributes['id'] = "datatable_".DataService::toSnakeCase($className); 54 | $this->options = array_merge(self::DEFAULT_DATATABLE_OPTIONS,$this->options); 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function getClassName(): string 61 | { 62 | return $this->className; 63 | } 64 | 65 | /** 66 | * @param string $className 67 | * @return EntityDatatable 68 | */ 69 | public function setClassName(string $className): EntityDatatable 70 | { 71 | $this->className = $className; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * @param string|null $path 78 | */ 79 | public function setPath(?string $path): void 80 | { 81 | $this->path = $path; 82 | } 83 | 84 | /** 85 | * @return Criteria|null 86 | */ 87 | public function getCriteria(): ?Criteria 88 | { 89 | return $this->criteria; 90 | } 91 | 92 | /** 93 | * @param Criteria $criteria 94 | */ 95 | public function setCriteria(Criteria $criteria): void 96 | { 97 | $this->criteria = $criteria; 98 | } 99 | 100 | public function addFilter(callable $function,int $priority = 0) 101 | { 102 | $this->dispatcher->addListener(Events::SEARCH_QUERY,$function,$priority); 103 | } 104 | 105 | /** 106 | * @return string 107 | */ 108 | public function getLanguage(): string 109 | { 110 | if($this->language==null || $this->language=='request'){ 111 | if($this->request){ 112 | $this->language = $this->request->getLocale(); 113 | } 114 | else{ 115 | $this->language = $this->locale; 116 | } 117 | } 118 | return $this->language; 119 | } 120 | 121 | /** 122 | * @return bool 123 | */ 124 | public function isSubmitted():bool 125 | { 126 | if(!$this->request){ 127 | return false; 128 | } 129 | return $this->request->get('draw',false); 130 | } 131 | 132 | /** 133 | * @param Request $request 134 | * @return $this 135 | */ 136 | public function handleRequest(Request $request): self 137 | { 138 | $this->request = $request; 139 | $this->path = $request->getPathInfo(); 140 | $this->options['ajax'] = $this->path; 141 | 142 | return $this; 143 | } 144 | 145 | /** 146 | * @return JsonResponse 147 | */ 148 | public function getResponse(): JsonResponse 149 | { 150 | //check if request handled 151 | if(!$this->request){ 152 | throw new \Exception(sprintf("%s Not Found, Maybe you forget send Request in EntityDatatable::handleRequest","Request")); 153 | } 154 | 155 | $query = [ 156 | 'columns' => $this->request->get('columns'), 157 | 'orders' => $this->request->get('order'), 158 | 'search' => $this->request->get('search'), 159 | 'start' => $this->request->get('start'), 160 | 'length' => $this->request->get('length') 161 | ]; 162 | 163 | $event = new RenderQueryEvent($this,$query); 164 | $this->dispatcher->dispatch($event,Events::PRE_QUERY); 165 | 166 | $recordsTotal = $event->getRecordsTotal(); 167 | $recordsFiltered = $event->getRecordsFiltered(); 168 | $records = $event->getRecords(); 169 | 170 | $event = new RenderDataEvent($this,$records); 171 | $this->dispatcher->dispatch($event,Events::PRE_DATA); 172 | 173 | $records = $event->getRecords(); 174 | 175 | //clear request 176 | $draw = $this->request->get('draw',1); 177 | $this->request = null; 178 | 179 | //save data to later usage 180 | return new JsonResponse([ 181 | "recordsTotal" => $recordsTotal, 182 | "recordsFiltered" => $recordsFiltered, 183 | "data" => $records, 184 | "draw" => $draw 185 | ]); 186 | } 187 | 188 | /** 189 | * @return array 190 | */ 191 | public function createView(): array 192 | { 193 | return [ 194 | "path" => $this->path, 195 | "options" => array_merge( 196 | array_merge( 197 | $this->options, 198 | ["columnDefs" => $this->getColumnDefs()] 199 | ), 200 | ["language" => $this->getLanguageData()] 201 | ), 202 | ]; 203 | } 204 | } -------------------------------------------------------------------------------- /src/Service/DataService.php: -------------------------------------------------------------------------------- 1 | 0) { 36 | $method = array_values($method)[0]; 37 | } else { 38 | throw new \Exception(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $property, $class)); 39 | } 40 | 41 | return $object->$method(...$argements); 42 | } 43 | } -------------------------------------------------------------------------------- /src/Twig/DatatableExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Aziz403\UX\Datatable\Twig; 13 | 14 | use Aziz403\UX\Datatable\Model\AbstractDatatable; 15 | use Symfony\WebpackEncoreBundle\Dto\StimulusControllersDto; 16 | use Symfony\WebpackEncoreBundle\Twig\StimulusTwigExtension; 17 | use Twig\Environment; 18 | use Twig\Extension\AbstractExtension; 19 | use Twig\TwigFunction; 20 | 21 | /** 22 | * @author Aziz Benmallouk 23 | */ 24 | class DatatableExtension extends AbstractExtension 25 | { 26 | private StimulusTwigExtension $stimulus; 27 | 28 | public function __construct(StimulusTwigExtension $stimulus) 29 | { 30 | $this->stimulus = $stimulus; 31 | } 32 | 33 | public function getFunctions(): array 34 | { 35 | return [ 36 | new TwigFunction('render_datatable', [$this, 'renderDatatable'], ['needs_environment' => true, 'is_safe' => ['html']]), 37 | ]; 38 | } 39 | 40 | public function renderDatatable(Environment $env,AbstractDatatable $datatable, array $attributes = []): string 41 | { 42 | $datatable->setAttributes(array_merge($datatable->getAttributes(), $attributes)); 43 | 44 | $controllers = []; 45 | $html = "getThemeStyling())!='none'){ 49 | $controllers['@aziz403/ux-datatable/styling_'.$theme] = []; 50 | } 51 | 52 | // add custom controller if exists 53 | if ($datatable->getDataController()) { 54 | $controllers[$datatable->getDataController()] = []; 55 | } 56 | 57 | // add global controller if exists 58 | if($datatable->getGlobalController()){ 59 | $controllers[$datatable->getGlobalController()] = []; 60 | } 61 | 62 | //add main controller 63 | $controllers['@aziz403/ux-datatable/datatable'] = ['view' => $datatable->createView()]; 64 | 65 | if (class_exists(StimulusControllersDto::class)) { 66 | $dto = new StimulusControllersDto($env); 67 | foreach ($controllers as $controllerName => $controllerValues) { 68 | $dto->addController($controllerName, $controllerValues); 69 | } 70 | $html .= $dto.''; 71 | } 72 | else { 73 | $html .= $this->stimulus->renderStimulusController($env, $controllers).''; 74 | } 75 | 76 | foreach ($datatable->getAttributes() as $name => $value) { 77 | if ('data-controller' === $name) { 78 | continue; 79 | } 80 | if (true === $value) { 81 | $html .= $name.'="'.$name.'" '; 82 | } elseif (false !== $value) { 83 | $html .= $name.'="'.$value.'" '; 84 | } 85 | } 86 | 87 | $html .= ">"; 88 | 89 | $html .= ''; 90 | foreach ($datatable->getColumns() as $column){ 91 | $html .= ""; 92 | } 93 | $html .= ''; 94 | 95 | return trim($html).'
{$column->getText()}
'; 96 | } 97 | } 98 | --------------------------------------------------------------------------------