├── .gitignore ├── .travis.yml ├── DependencyInjection ├── Configuration.php └── WaldoDatatableExtension.php ├── LICENSE ├── Listener └── KernelTerminateListener.php ├── README.md ├── Resources ├── config │ └── services.xml ├── doc │ ├── images │ │ └── sample_01.png │ └── test.md ├── translations │ ├── messages.ar.yml │ ├── messages.cn.yml │ ├── messages.de.yml │ ├── messages.en.yml │ ├── messages.es.yml │ ├── messages.fr.yml │ ├── messages.it.yml │ ├── messages.pl.yml │ ├── messages.ru.yml │ ├── messages.tr.yml │ └── messages.ua.yml └── views │ ├── Main │ ├── datatableHtml.html.twig │ ├── datatableJs.html.twig │ └── index.html.twig │ ├── Renderers │ └── _default.html.twig │ └── Snippet │ ├── individualSearchField.js.twig │ └── multipleRaw.js.twig ├── Tests ├── BaseClient.php ├── Functional │ ├── Datatable │ │ ├── DatatableStaticTest.php │ │ ├── DatatableTest.php │ │ └── DoctrineBuilderTest.php │ └── Entity │ │ ├── Feature.php │ │ └── Product.php ├── Unit │ ├── DependencyInjection │ │ └── WaldoDatatableExtensionTest.php │ ├── Listner │ │ └── KernelTerminateListenerTest.php │ └── Twig │ │ └── DatatableTwigExtensionTest.php ├── app │ ├── AppKernel.php │ ├── Resources │ │ └── views │ │ │ └── Renderers │ │ │ └── _actions.html.twig │ └── config │ │ ├── config_test.yml │ │ └── routing.yml └── bootstrap.php ├── Twig └── Extension │ └── DatatableExtension.php ├── Util ├── ArrayMerge.php ├── Datatable.php ├── Factory │ └── Query │ │ ├── DoctrineBuilder.php │ │ └── QueryInterface.php └── Formatter │ └── Renderer.php ├── WaldoDatatableBundle.php ├── composer.json └── phpunit.xml.dist /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | !/vendor/vendors.php 3 | composer.lock 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - 7.0 8 | 9 | 10 | env: 11 | - SYMFONY_VERSION=v2.6 12 | - SYMFONY_VERSION=v2.7 13 | - SYMFONY_VERSION=v2.8 14 | - SYMFONY_VERSION=v3.0 15 | - SYMFONY_VERSION=v3.1 16 | 17 | install: composer --prefer-source install 18 | 19 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('waldo_datatable'); 22 | 23 | $rootNode 24 | ->children() 25 | ->arrayNode('all') 26 | ->addDefaultsIfNotSet() 27 | ->children() 28 | ->scalarNode('action')->defaultTrue()->end() 29 | ->scalarNode('search')->defaultFalse()->end() 30 | ->end() 31 | ->end() 32 | ->arrayNode('js') 33 | ->useAttributeAsKey('name') 34 | ->prototype('variable') 35 | ->end() 36 | ->end() 37 | ->end() 38 | ; 39 | 40 | return $treeBuilder; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /DependencyInjection/WaldoDatatableExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 25 | 26 | $config = $this->applyDefaultConfig($config); 27 | 28 | 29 | $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 30 | $loader->load('services.xml'); 31 | 32 | $container->setParameter('datatable', $config); 33 | } 34 | 35 | /** 36 | * Datatable options 37 | * 38 | * @see https://datatables.net/reference/option/ 39 | * @param type $config 40 | */ 41 | private function applyDefaultConfig($config) 42 | { 43 | $defaultJsConfig = array( 44 | "jQueryUI" => true, 45 | "pagingType" => "full_numbers", 46 | "lengthMenu" => [[10, 25, 50, -1], [10, 25, 50, "All"]], 47 | "pageLength" => 10, 48 | "serverSide" => true, 49 | "processing" => true, 50 | "paging" => true, 51 | "lengthChange" => true, 52 | "ordering" => true, 53 | "searching" => true, 54 | "autoWidth" => false, 55 | "order" => array() 56 | ); 57 | 58 | $config['js'] = array_merge($defaultJsConfig, $config['js']); 59 | 60 | return $config; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014 Ali Hichem 2 | Copyright (c) 2015 Valérian Girard 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Listener/KernelTerminateListener.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class KernelTerminateListener 11 | { 12 | public function onKernelTerminate() 13 | { 14 | Datatable::clearInstance(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DatatableBundle 2 | =============== 3 | 4 | Fork of [AliHichem/DatatableBundle](https://github.com/AliHichem/DatatableBundle), this bundle will add some great features 5 | and evolve in a different way than it source. 6 | 7 | [![Build Status](https://travis-ci.org/waldo2188/DatatableBundle.svg?branch=master)](https://travis-ci.org/waldo2188/DatatableBundle) 8 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/bb7b64f6-4203-45ca-b99a-2d15c4d272ec/small.png)](https://insight.sensiolabs.com/projects/bb7b64f6-4203-45ca-b99a-2d15c4d272ec) 9 | 10 | > **Warning**: The [jQuery Datatable plugin](http://datatables.net/) has evolved (version 1.10) with a all new API and option. 11 | > You **MUST** use the version **2** of DatatableBundle with the jQuery Datatable plugin version lower than 1.10. 12 | > You **MUST** use the version **3** of DatatableBundle with the jQuery Datatable plugin version equal or greater than 1.10. 13 | 14 | The Datatable bundle for symfony2 allow for easily integration of the [jQuery Datatable plugin](http://datatables.net/) with 15 | the doctrine2 entities. 16 | This bundle provides a way to make a projection of a doctrine2 entity to a powerful jquery datagrid. It mainly includes: 17 | 18 | * datatable service container: to manage the datatable as a service. 19 | * twig extension: for view integration. 20 | * dynamic pager handler : no need to set your pager. 21 | * support doctrine2 association. 22 | * support of Doctrine Query Builder. 23 | * support of doctrine subquery. 24 | * support of column search. 25 | * support of custom twig/phpClosure renderers. 26 | 27 |
Screenshot
28 | 29 | ------------------------------------- 30 | ## Summary 31 | - [Installation](#installation) 32 | 1. [Download DatatableBundle using Composer](#Download-DatatableBundle-using-Composer) 33 | 2. [Enable the Bundle](#Enable-the-Bundle) 34 | 3. [Configure the Bundle](#Configure-the-Bundle) 35 | - [How to use DatatableBundle ?](#how-to-use-datatablebundle-) 36 | - [Rendering inside Twig](#rendering-inside-twig) 37 | - [Advanced Use of DatatableBundle](#advanced-use-of-DatatableBundle) 38 | - [Use of search filters](#use-of-search-filters) 39 | - [Activate search globally](#activate-search-globally) 40 | - [Set search fields](#set-search-fields) 41 | - [Multiple actions, how to had checkbox for each row ?](#Multiple-actions-how-to-had-checkbox-for-each-row-) 42 | - [Custom renderer for cells](#custom-renderer-for-cells) 43 | - [Datatable Callbacks options](#datatable-callbacks-options) 44 | - [Translation](#translation) 45 | - [Doctrine Query Builder](#doctrine-query-builder) 46 | - [Multiple datatable in the same view](#multiple-datatable-in-the-same-view) 47 | - [Use specific jQuery Datatable options](#Use-specific-jQuery-Datatable-options) 48 | - [Launch the test suite](/Resources/doc/test.md) 49 | 50 | --------------------------------------- 51 | 52 | ## Installation 53 | 54 | Installation is a quick (I promise!) 3 step process: 55 | 1. [Download DatatableBundle using Composer](#Download-DatatableBundle-using-Composer) 56 | 2. [Enable the Bundle](#Enable-the-Bundle) 57 | 3. [Configure the Bundle](#Configure-the-Bundle) 58 | 59 | ### Download DatatableBundle using Composer 60 | 61 | #### Using Composer 62 | 63 | Install with this command : `composer require waldo/datatable-bundle` 64 | 65 | Generate the assets symlinks : 66 | ```bash 67 | php app/console assets:install --symlink web 68 | ``` 69 | 70 | ### Enable the Bundle 71 | 72 | Add the bundle to the `AppKernel.php` 73 | ```php 74 | $bundles = array( 75 | \\... 76 | new Waldo\DatatableBundle\WaldoDatatableBundle(), 77 | ) 78 | ``` 79 | 80 | ### Configure the Bundle 81 | 82 | In this section you can put the global config that you want to set for all the instance of DataTable in your project. 83 | 84 | #### To keep it to default 85 | 86 | ``` 87 | # app/config/config.yml 88 | waldo_datatable: 89 | all: ~ 90 | js: ~ 91 | ``` 92 | 93 | The `js` config will be applied to DataTable exactly like you do with `$().datatable({ your config });` in a javascript part. 94 | > Note: all your js config have to be string typed, make sure to use (") as delimiters. 95 | 96 | #### Config sample 97 | 98 | ``` 99 | waldo_datatable: 100 | all: 101 | search: false 102 | js: 103 | pageLength: "10" 104 | lengthMenu: [[5,10, 25, 50, -1], [5,10, 25, 50, 'All']] 105 | dom: '<"clearfix"lf>rtip' 106 | jQueryUI: "false" 107 | ``` 108 | 109 | ## How to use DatatableBundle ? 110 | 111 | Assuming for example that you need a grid in your "index" action, create in your controller method as below : 112 | 113 | **Warning alias `as` is case-sensitive, always write it in lower case** 114 | 115 | ```php 116 | /** 117 | * set datatable configs 118 | * @return \Waldo\DatatableBundle\Util\Datatable 119 | */ 120 | private function datatable() { 121 | return $this->get('datatable') 122 | ->setEntity("XXXMyBundle:Entity", "x") // replace "XXXMyBundle:Entity" by your entity 123 | ->setFields( 124 | array( 125 | "Name" => 'x.name', // Declaration for fields: 126 | "Address" => 'x.address', // "label" => "alias.field_attribute_for_dql" 127 | "Total" => 'COUNT(x.people) as total', // Use SQL commands, you must always define an alias 128 | "Sub" => '(SELECT i FROM ... ) as sub', // you can set sub DQL request, you MUST ALWAYS define an alias 129 | "_identifier_" => 'x.id') // you have to put the identifier field without label. Do not replace the "_identifier_" 130 | ) 131 | ->setWhere( // set your dql where statement 132 | 'x.address = :address', 133 | array('address' => 'Paris') 134 | ) 135 | ->setOrder("x.created", "desc"); // it's also possible to set the default order 136 | } 137 | 138 | 139 | /** 140 | * Grid action 141 | * @Route("/", name="datatable") 142 | * @return Response 143 | */ 144 | public function gridAction() 145 | { 146 | return $this->datatable()->execute(); // call the "execute" method in your grid action 147 | } 148 | 149 | /** 150 | * Lists all entities. 151 | * @Route("/list", name="datatable_list") 152 | * @return Response 153 | */ 154 | public function indexAction() 155 | { 156 | $this->datatable(); // call the datatable config initializer 157 | return $this->render('XXXMyBundle:Module:index.html.twig'); // replace "XXXMyBundle:Module:index.html.twig" by yours 158 | } 159 | ``` 160 | 161 | ## Rendering inside Twig 162 | 163 | You have the choice, you can render the HTML table part and Javascript part in just one time with the Twig function `datatable`, 164 | like below. 165 | 166 | ```twig 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | {{ datatable({ 175 | 'js' : { 176 | 'ajax' : path('route_for_your_datatable_action') 177 | } 178 | }) 179 | }} 180 | ``` 181 | 182 | Or, render each part separatly. 183 | 184 | `datatable_html` is the Twig function for the HTML part. 185 | `datatable_js` is the Twig function for the Javascript part. 186 | 187 | ```twig 188 | {% block body %} 189 | 190 | {{ datatable_html({ 191 | 'id' : 'dta-offres' 192 | }) 193 | }} 194 | 195 | {% endblock %} 196 | 197 | {% block javascripts %} 198 | 199 | 200 | {{ datatable_js({ 201 | 'id' : 'dta-offres', 202 | 'js' : { 203 | 'dom': '<"clearfix"lf>rtip', 204 | 'ajax': path('route_for_your_datatable_action'), 205 | } 206 | }) 207 | }} 208 | {% endblock javascripts %} 209 | ``` 210 | 211 | ## Advanced Use of DatatableBundle 212 | 213 | ### Advanced php config 214 | 215 | Assuming the example above, you can add your joins and where statements. 216 | 217 | ```php 218 | /** 219 | * set datatable configs 220 | * 221 | * @return \Waldo\DatatableBundle\Util\Datatable 222 | */ 223 | private function datatable() 224 | { 225 | return $this->get('datatable') 226 | ->setEntity("XXXMyBundle:Entity", "x") // replace "XXXMyBundle:Entity" by your entity 227 | ->setFields( 228 | array( 229 | "Name" => 'x.name', // Declaration for fields: 230 | "Address" => 'x.address', // "label" => "alias.field_attribute_for_dql" 231 | "Group" => 'g.name', 232 | "Team" => 't.name', 233 | "_identifier_" => 'x.id') // you have to put the identifier field without label. Do not replace the "_identifier_" 234 | ) 235 | ->addJoin('x.group', 'g', \Doctrine\ORM\Query\Expr\Join::INNER_JOIN) 236 | ->addJoin('x.team', 't', \Doctrine\ORM\Query\Expr\Join::LEFT_JOIN) 237 | ->addJoin('x.something', 's', \Doctrine\ORM\Query\Expr\Join::LEFT_JOIN, \Doctrine\ORM\Query\Expr\Join::WITH, 's.id = :someId') 238 | ->setWhere( // set your dql where statement 239 | 'x.address = :address', 240 | array('address' => 'Paris') 241 | ) 242 | ->setOrder("x.created", "desc") // it's also possible to set the default order. 243 | ->setParameter('someId', 12) 244 | ; 245 | } 246 | ``` 247 | 248 | ### Use of search filter 249 | * [Activate search globally](#activate-search-globally) 250 | * [Set search fields](#set-search-fields) 251 | 252 | #### Activate search globally 253 | 254 | The searching functionality that is very useful for quickly search through the information from the database. 255 | This bundle provide two way of searching, who can be used together : **global search** and **individual column search**. 256 | 257 | By default the filtering functionality is disabled, to get it working you just need to activate it from your configuration method like this : 258 | 259 | ```php 260 | private function datatable() 261 | { 262 | return $this->get('datatable') 263 | //... 264 | ->setSearch(true); // for individual column search 265 | // or 266 | ->setGlobalSearch(true); 267 | } 268 | ``` 269 | #### Set search fields 270 | 271 | You can set fields where you want to enable your search. 272 | Let say you want search to be active only for "field 1" and "field3 ", you just need to activate search for the approriate column key 273 | and your datatable config should be : 274 | 275 | ```php 276 | /** 277 | * set datatable configs 278 | * 279 | * @return \Waldo\DatatableBundle\Util\Datatable 280 | */ 281 | private function datatable() 282 | { 283 | $datatable = $this->get('datatable'); 284 | return $datatable->setEntity("XXXMyBundle:Entity", "x") 285 | ->setFields( 286 | array( 287 | "label of field 1" => 'x.field1', // column key 0 288 | "label of field 2" => 'x.field2', // column key 1 289 | "label of field 3" => 'x.field3', // column key 2 290 | "_identifier_" => 'x.id') // column key 3 291 | ) 292 | ->setSearch(true) 293 | ->setSearchFields(array(0,2)) 294 | ; 295 | } 296 | ``` 297 | 298 | ### Multiple actions, how to had checkbox for each row ? 299 | 300 | Sometimes, it's good to be able to do the same action on multiple records like deleting, activating, moving ... 301 | Well this is very easy to add to your datatable: all what you need is to declare your multiple action as follow. 302 | 303 | ```php 304 | /** 305 | * set datatable configs 306 | * @return \Waldo\DatatableBundle\Util\Datatable 307 | */ 308 | private function datatable() 309 | { 310 | $datatable = $this->get('datatable'); 311 | return $datatable->setEntity("XXXMyBundle:Entity", "x") 312 | ->setFields( 313 | array( 314 | "label of field1" => 'x.field1', // column key 0 315 | "label of field2" => 'x.field2', // column key 1 316 | "_identifier_" => 'x.id') // column key 2 317 | ) 318 | ->setMultiple( 319 | array( 320 | 'delete' => array( 321 | 'title' => 'Delete', 322 | 'route' => 'multiple_delete_route' // path to multiple delete route action 323 | ), 324 | 'move' => array( 325 | 'title' => 'Move', 326 | 'route' => 'multiple_move_route' // path to multiple move route action 327 | ), 328 | ) 329 | ) 330 | ; 331 | } 332 | ``` 333 | 334 | Then all what you have to do is to add the necessary logic in your "multiple_delete_route" (or whatever your route is for). 335 | In that action, you can get the selected ids by : 336 | 337 | ```php 338 | $data = $this->getRequest()->get('dataTables'); 339 | $ids = $data['actions']; 340 | ``` 341 | 342 | ### Custom renderer for cells 343 | 344 | #### Twig renderers 345 | 346 | To set your own column structure, you can use a custom twig renderer as below : 347 | In this example you can find how to set the use of the default twig renderer for action fields which you can override as 348 | your own needs. 349 | 350 | ```php 351 | /** 352 | * set datatable configs 353 | * @return \Waldo\DatatableBundle\Util\Datatable 354 | */ 355 | private function datatable() 356 | { 357 | $datatable = $this->get('datatable'); 358 | return $datatable->setEntity("XXXMyBundle:Entity", "x") 359 | ->setFields( 360 | array( 361 | "label of field1" => 'x.field1', 362 | "label of field2" => 'x.field2', 363 | "_identifier_" => 'x.id') 364 | ) 365 | ->setRenderers( 366 | array( 367 | 2 => array( 368 | 'view' => 'XXXMyBundle:Renderers:_actions.html.twig', // Path to the template 369 | 'params' => array( // All the parameters you need (same as a twig template) 370 | 'edit_route' => 'route_edit', 371 | 'delete_route' => 'route_delete' 372 | ), 373 | ), 374 | ) 375 | ); 376 | } 377 | ``` 378 | 379 | In a twig renderer you can have access the the field value using `dt_item` variable, 380 | ``` 381 | // XXXMyBundle:Renderers:_actions.html.twig 382 | {{ dt_item }} 383 | ``` 384 | or access the entire entity object using `dt_obj` variable. 385 | ``` 386 | // XXXMyBundle:Renderers:_actions.html.twig 387 | {{ dt_obj.username }} 388 | ``` 389 | 390 | > NOTE: be careful of Doctrine's LAZY LOADING when using dt_obj ! 391 | 392 | #### PHP Closures 393 | 394 | Assuming the example above, you can set your custom fields renderer using [PHP Closures](http://php.net/manual/en/class.closure.php). 395 | 396 | ```php 397 | /** 398 | * set datatable configs 399 | * @return \Waldo\DatatableBundle\Util\Datatable 400 | */ 401 | private function datatable() { 402 | 403 | $controller_instance = $this; 404 | return $this->get('datatable') 405 | ->setEntity("XXXMyBundle:Entity", "x") // replace "XXXMyBundle:Entity" by your entity 406 | ->setFields( 407 | array( 408 | "Name" => 'x.name', // Declaration for fields: 409 | "Address" => 'x.address', // "label" => "alias.field_attribute_for_dql" 410 | "_identifier_" => 'x.id') // you have to put the identifier field without label. Do not replace the "_identifier_" 411 | ) 412 | ->setRenderer( 413 | function(&$data) use ($controller_instance) { 414 | foreach ($data as $key => $value) { 415 | if ($key == 1) { // 1 => address field 416 | $data[$key] = $controller_instance 417 | ->get('templating') 418 | ->render( 419 | 'XXXMyBundle:Module:_grid_entity.html.twig', 420 | array('data' => $value) 421 | ); 422 | } 423 | } 424 | } 425 | ); 426 | } 427 | ``` 428 | 429 | ### DataTable Callbacks options 430 | If you need to put some Javascript Callbacks like [`drawCallback`](http://datatables.net/reference/option/drawCallback), you can 431 | do it localy with the Datatable `js` option. See the two examples below: 432 | 433 | ```twig 434 | // XXXMyBundle:Welcome:list.html.twig 435 | {{ datatable_js({ 436 | 'id' : 'datable-id', 437 | 'js' : { 438 | 'ajax': "/some/path", 439 | 'createdRow': 'function(){console.log("Do something useful here");}' 440 | } 441 | }) 442 | }} 443 | 444 | {# or #} 445 | 446 | {{ datatable_js({ 447 | 'id' : 'datable-id', 448 | 'js' : { 449 | 'ajax': "/some/path", 450 | 'createdRow': 'myUsefullThing' 451 | } 452 | }) 453 | }} 454 | 459 | ``` 460 | 461 | You can also define a Callback globally by setting it up in the `config.yml` like below : 462 | 463 | ```yaml 464 | waldo_datatable: 465 | js: 466 | createdRow: | 467 | function(){console.log("Do something useful here");} 468 | 469 | ``` 470 | 471 | 472 | ### Translation 473 | 474 | You can set your own translated labels by adding in your translation catalog entries as define in `Resources/translations/messages.en.yml` 475 | 476 | You can also get the translated labels from *official DataTable translation* repository, by configuring the bundle like below : 477 | ```yaml 478 | waldo_datatable: 479 | all: ~ 480 | js: 481 | language: 482 | url: "//cdn.datatables.net/plug-ins/1.10.9/i18n/Chinese.json" 483 | ``` 484 | 485 | This bundle includes nine translation catalogs: Arabic, Chinese, Dutch, English, Spanish, French, Italian, Polish, Russian and Turkish 486 | To get more translated entries, you can follow the [official DataTable translation](https://datatables.net/manual/i18n) 487 | 488 | 489 | ### Doctrine Query Builder 490 | 491 | To use your own query object to supply to the datatable object, you can perform this action using your own 492 | "Doctrine Query object": DatatableBundle allow to manipulate the query object provider which is now a Doctrine Query Builder object, 493 | you can use it to update the query in all its components except of course in the selected field part. 494 | 495 | This is a classic config before using the Doctrine Query Builder: 496 | 497 | ```php 498 | private function datatable() 499 | { 500 | $datatable = $this->get('datatable') 501 | ->setEntity("XXXBundle:Entity", "e") 502 | ->setFields( 503 | array( 504 | "column1 label" => 'e.column1', 505 | "_identifier_" => 'e.id') 506 | ) 507 | ->setWhere( 508 | 'e.column1 = :column1', 509 | array('column1' => '1' ) 510 | ) 511 | ->setOrder("e.created", "desc"); 512 | 513 | $qb = $datatable->getQueryBuilder()->getDoctrineQueryBuilder(); 514 | // This is the Doctrine Query Builder object, you can 515 | // retrieve it and include your own change 516 | 517 | return $datatable; 518 | } 519 | ``` 520 | 521 | This is a config that uses a Doctrine Query object a query builder : 522 | 523 | ```php 524 | private function datatable() 525 | { 526 | $qb = $this->getDoctrine()->getEntityManager()->createQueryBuilder(); 527 | $qb->from("XXXBundle:Entity", "e") 528 | ->where('e.column1 = :column1') 529 | ->setParameters(array('column1' = 0)) 530 | ->orderBy("e.created", "desc"); 531 | 532 | $datatable = $this->get('datatable') 533 | ->setFields( 534 | array( 535 | "Column 1 label" => 'e.column1', 536 | "_identifier_" => 'e.id') 537 | ); 538 | 539 | $datatable->getQueryBuilder()->setDoctrineQueryBuilder($qb); 540 | 541 | return $datatable; 542 | } 543 | ``` 544 | 545 | ### Multiple DataTable in the same view 546 | 547 | To declare multiple DataTables in the same view, you have to set the datatable identifier in you controller with `setDatatableId` : 548 | Each of your DataTable config methods ( datatable() , datatable_1() .. datatable_n() ) needs to set the same identifier used in your view: 549 | 550 | #### In the controller 551 | 552 | ```php 553 | protected function datatable() 554 | { 555 | // ... 556 | return $this->get('datatable') 557 | ->setDatatableId('dta-unique-id_1') 558 | ->setEntity("XXXMyBundle:Entity", "x") 559 | // ... 560 | } 561 | 562 | protected function datatableSecond() 563 | { 564 | // ... 565 | return $this->get('datatable') 566 | ->setDatatableId('dta-unique-id_2') 567 | ->setEntity("YYYMyBundle:Entity", "y") 568 | // ... 569 | } 570 | ``` 571 | 572 | #### In the view 573 | 574 | ```js 575 | {{ 576 | datatable({ 577 | 'id' : 'dta-unique-id_1', 578 | ... 579 | 'js' : { 580 | 'ajax' : path('route_for_your_datatable_action_1') 581 | } 582 | }) 583 | }} 584 | 585 | {{ 586 | datatable({ 587 | 'id' : 'dta-unique-id_2', 588 | ... 589 | 'js' : { 590 | 'ajax' : path('route_for_your_datatable_action_2') 591 | } 592 | }) 593 | }} 594 | ``` 595 | 596 | ## Use specific jQuery DataTable options 597 | 598 | Some time we need to apply some specific options to a grid, like a specific width for the second column. 599 | DataTable comes with a [lot a feature](http://datatables.net/reference/option/) that you can always use, even with this bundle. 600 | 601 | In the code below, we use the `columnDefs` option to fix the width of the second column. 602 | ```js 603 | {{ 604 | datatable({ 605 | 'id' : 'dta-id', 606 | 'js' : { 607 | 'ajax' : path('route_for_your_datatable_action'), 608 | 'columnDefs': [ 609 | { "width": "15%", "targets": 1 } 610 | ] 611 | } 612 | }) 613 | }} 614 | ``` 615 | 616 | You really can play with all the DataTable's options. 617 | 618 | -------------------------------------------------------------------------------- /Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | %datatable% 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Resources/doc/images/sample_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waldo2188/DatatableBundle/7ed6f754441a561caf1265d102144b15284a9e6a/Resources/doc/images/sample_01.png -------------------------------------------------------------------------------- /Resources/doc/test.md: -------------------------------------------------------------------------------- 1 | Tests 2 | ===== 3 | 4 | For launch the test suite follow the command below : 5 | 6 | ## Install the required vendors 7 | 8 | ```bash 9 | composer update 10 | ``` 11 | 12 | ## Launch the test suite 13 | 14 | ```bash 15 | ./vendor/phpunit/phpunit/phpunit -c phpunit.xml.dist 16 | ``` 17 | -------------------------------------------------------------------------------- /Resources/translations/messages.ar.yml: -------------------------------------------------------------------------------- 1 | datatable: 2 | common: 3 | are_you_sure: 'هل أنت متأكد ؟' 4 | you_need_to_select_at_least_one_element: 'تحتاج إلى تحديد عنصر واحد على الأقل.' 5 | search: 'بحث' 6 | execute: تنفيذ 7 | ok: حسنا 8 | datatable: 9 | searchPlaceholder: "" 10 | processing: "جاري التحميل..." 11 | lengthMenu: "أظهر مُدخلات _MENU_" 12 | zeroRecords: "لم يُعثر على أية سجلات" 13 | info: "إظهار _START_ إلى _END_ من أصل _TOTAL_ مُدخل" 14 | infoEmpty: "يعرض 0 إلى 0 من أصل 0 سجلّ" 15 | infoFiltered: "(منتقاة من مجموع _MAX_ مُدخل)" 16 | infoPostFix: "" 17 | search: "ابحث:" 18 | paginate: 19 | first: "الأول" 20 | previous: "السابق" 21 | next: "التالي" 22 | last: "الأخير" 23 | aria: 24 | sortAscending: "تفعيل لفرز العمود تصاعدي" 25 | sortDescending: "تفعيل لفرز العمود الهابطة" 26 | -------------------------------------------------------------------------------- /Resources/translations/messages.cn.yml: -------------------------------------------------------------------------------- 1 | datatable: 2 | common: 3 | are_you_sure: "你确定吗 ?" 4 | you_need_to_select_at_least_one_element: "您需要至少选择一个元素" 5 | search: "搜索" 6 | execute: "执" 7 | ok: "OK" 8 | datatable: 9 | searchPlaceholder: "" 10 | emptyTable: "表中数据为空" 11 | info: "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项" 12 | infoEmpty: "显示第 0 至 0 项结果,共 0 项" 13 | infoFiltered: "(由 _MAX_ 项结果过滤)" 14 | infoPostFix: "" 15 | infoThousands: "," 16 | lengthMenu: "显示 _MENU_ 项结果" 17 | loadingRecords: "载入中..." 18 | processing: "处理中..." 19 | search: "搜索:" 20 | zeroRecords: "没有匹配结果" 21 | paginate: 22 | first: "首页" 23 | last: "末页" 24 | next: "下页" 25 | previous: "上页" 26 | aria: 27 | sortAscending: ": 以升序排列此列" 28 | sortDescending: ": 以降序排列此列" 29 | -------------------------------------------------------------------------------- /Resources/translations/messages.de.yml: -------------------------------------------------------------------------------- 1 | datatable: 2 | common: 3 | are_you_sure: "Bist du sicher ?" 4 | you_need_to_select_at_least_one_element: "Sie müssen mindestens ein Element auswählen." 5 | search: Zoeken 6 | execute: "Ausführen" 7 | ok: "OK" 8 | datatable: 9 | searchPlaceholder: "" 10 | emptyTable: "Geen resultaten aanwezig in de tabel" 11 | info: "_START_ tot _END_ van _TOTAL_ resultaten" 12 | infoEmpty: "Geen resultaten om weer te geven" 13 | infoFiltered: " (gefilterd uit _MAX_ resultaten)" 14 | infoPostFix: "" 15 | infoThousands: "." 16 | lengthMenu: "_MENU_ resultaten weergeven" 17 | loadingRecords: "Een moment geduld aub - bezig met laden..." 18 | processing: "Bezig..." 19 | search: "Zoeken:" 20 | zeroRecords: "Geen resultaten gevonden" 21 | paginate: 22 | first: "Eerste" 23 | last: "Laatste" 24 | next: "Volgende" 25 | previous: "Vorige" 26 | aria: 27 | sortAscending: ": zu aktivieren, um Spalte aufsteigend zu sortieren" 28 | sortDescending: ": zu aktivieren, um Spalte absteigend sortieren" 29 | -------------------------------------------------------------------------------- /Resources/translations/messages.en.yml: -------------------------------------------------------------------------------- 1 | datatable: 2 | common: 3 | are_you_sure: Are you sure ? 4 | you_need_to_select_at_least_one_element: You need to select at least one element. 5 | search: Search 6 | execute: Execute 7 | ok: OK 8 | datatable: 9 | searchPlaceholder: "" 10 | emptyTable: "No data available in table" 11 | info: "Showing _START_ to _END_ of _TOTAL_ entries" 12 | infoEmpty: "Showing 0 to 0 of 0 entries" 13 | infoFiltered: "(filtered from _MAX_ total entries)" 14 | infoPostFix: "" 15 | infoThousands: "," 16 | lengthMenu: "Show _MENU_ entries" 17 | loadingRecords: "Loading..." 18 | processing: "Processing..." 19 | search: "Search:" 20 | zeroRecords: "No matching records found" 21 | paginate: 22 | first: "First" 23 | last: "Last" 24 | next: "Next" 25 | previous: "Previous" 26 | aria: 27 | sortAscending: ": activate to sort column ascending" 28 | sortDescending: ": activate to sort column descending" 29 | 30 | -------------------------------------------------------------------------------- /Resources/translations/messages.es.yml: -------------------------------------------------------------------------------- 1 | datatable: 2 | common: 3 | are_you_sure: Estas seguro ? 4 | you_need_to_select_at_least_one_element: Debe seleccionar al menos un elemento. 5 | search: Buscar 6 | execute: "" 7 | ok: "OK" 8 | datatable: 9 | searchPlaceholder: "" 10 | emptyTable: "Ningún dato disponible en esta tabla" 11 | info: "Mostrando registros del _START_ al _END_ de un total de _TOTAL_ registros" 12 | infoEmpty: "Mostrando registros del 0 al 0 de un total de 0 registros" 13 | infoFiltered: "(filtrado de un total de _MAX_ registros)" 14 | infoPostFix: "" 15 | infoThousands: "," 16 | lengthMenu: "Mostrar _MENU_ registros" 17 | loadingRecords: "Cargando..." 18 | processing: "Procesando..." 19 | search: "Buscar:" 20 | zeroRecords: "No se encontraron resultados" 21 | paginate: 22 | first: "Primero" 23 | last: "Último" 24 | next: "Siguiente" 25 | previous: "Anterior" 26 | aria: 27 | sortAscending: ": Activar para ordenar la columna de manera ascendente" 28 | sortDescending: ": Activar para ordenar la columna de manera descendente" 29 | -------------------------------------------------------------------------------- /Resources/translations/messages.fr.yml: -------------------------------------------------------------------------------- 1 | datatable: 2 | common: 3 | are_you_sure: Êtes-vous sûr ? 4 | you_need_to_select_at_least_one_element: Vous devez sélectionner au moins un élément. 5 | search: Rechercher 6 | execute: Exécuter 7 | ok: OK 8 | datatable: 9 | searchPlaceholder: "" 10 | processing: "Traitement en cours..." 11 | search: "Rechercher :" 12 | lengthMenu: "Afficher _MENU_ éléments" 13 | info: "Affichage de l'élément _START_ à _END_ sur _TOTAL_ éléments" 14 | infoEmpty: "Affichage de l'élément 0 à 0 sur 0 éléments" 15 | infoFiltered: "(filtré de _MAX_ éléments au total)" 16 | infoPostFix: "" 17 | infoThousands: "." 18 | loadingRecords: "Chargement en cours..." 19 | zeroRecords: "Aucun élément à afficher" 20 | emptyTable: "Aucune donnée disponible dans le tableau" 21 | paginate: 22 | first: "Premier" 23 | previous: "Précédent" 24 | next: "Suivant" 25 | last: "Dernier" 26 | aria: 27 | sortAscending: ": activer pour trier la colonne par ordre croissant" 28 | sortDescending: ": activer pour trier la colonne par ordre décroissant" 29 | -------------------------------------------------------------------------------- /Resources/translations/messages.it.yml: -------------------------------------------------------------------------------- 1 | datatable: 2 | common: 3 | are_you_sure: Sei sicuro ? 4 | you_need_to_select_at_least_one_element: È necessario selezionare almeno un elemento. 5 | search: Cerca 6 | execute: "Eseguire" 7 | ok: "OK" 8 | datatable: 9 | searchPlaceholder: "" 10 | emptyTable: "Nessun dato presente nella tabella" 11 | info: "Vista da _START_ a _END_ di _TOTAL_ elementi" 12 | infoEmpty: "Vista da 0 a 0 di 0 elementi" 13 | infoFiltered: "(filtrati da _MAX_ elementi totali)" 14 | infoPostFix: "" 15 | infoThousands: "," 16 | lengthMenu: "Visualizza _MENU_ elementi" 17 | loadingRecords: "Caricamento..." 18 | processing: "Elaborazione..." 19 | search: "Cerca:" 20 | zeroRecords: "La ricerca non ha portato alcun risultato." 21 | paginate: 22 | first: "Inizio" 23 | last: "Fine" 24 | next: "Successivo" 25 | previous: "Precedente" 26 | aria: 27 | sortAscending: ": attiva per ordinare la colonna in ordine crescente" 28 | sortDescending: ": attiva per ordinare la colonna in ordine decrescente" 29 | -------------------------------------------------------------------------------- /Resources/translations/messages.pl.yml: -------------------------------------------------------------------------------- 1 | datatable: 2 | common: 3 | are_you_sure: Jesteś pewny ? 4 | you_need_to_select_at_least_one_element: Musisz wybrać co najmniej jeden element. 5 | search: Szukaj 6 | execute: "Wykonać" 7 | ok: "OK" 8 | datatable: 9 | searchPlaceholder: "" 10 | emptyTable: "Brak danych" 11 | info: "Pozycje od _START_ do _END_ z _TOTAL_ łącznie" 12 | infoEmpty: "Pozycji 0 z 0 dostępnych" 13 | infoFiltered: "(filtrowanie spośród _MAX_ dostępnych pozycji)" 14 | infoPostFix: "" 15 | infoThousands: " " 16 | lengthMenu: "Pokaż _MENU_ pozycji" 17 | loadingRecords: "Wczytywanie..." 18 | processing: "Przetwarzanie..." 19 | search: "Szukaj:" 20 | zeroRecords: "Nie znaleziono pasujących pozycji" 21 | paginate: 22 | first: "Pierwsza" 23 | last: "Ostatnia" 24 | next: "Następna" 25 | previous: "Poprzednia" 26 | aria: 27 | sortAscending: ": aktywuj, by posortować kolumnę rosnąco" 28 | sortDescending: ": aktywuj, by posortować kolumnę malejąco" 29 | -------------------------------------------------------------------------------- /Resources/translations/messages.ru.yml: -------------------------------------------------------------------------------- 1 | datatable: 2 | common: 3 | are_you_sure: Вы уверены ? 4 | you_need_to_select_at_least_one_element: Вы должны выбрать хотя бы один элемент. 5 | search: Поиск 6 | execute: "выполнять" 7 | ok: "OK" 8 | datatable: 9 | searchPlaceholder: "" 10 | emptyTable: "В таблице отсутствуют данные" 11 | info: "Записи с _START_ до _END_ из _TOTAL_ записей" 12 | infoEmpty: "Записи с 0 до 0 из 0 записей" 13 | infoFiltered: "(отфильтровано из _MAX_ записей)" 14 | infoPostFix: "" 15 | infoThousands: "," 16 | lengthMenu: "Показать _MENU_ записей" 17 | loadingRecords: "Загрузка записей..." 18 | processing: "Подождите..." 19 | search: "Поиск:" 20 | zeroRecords: "Записи отсутствуют." 21 | paginate: 22 | first: "Первая" 23 | last: "Последняя" 24 | next: "Следующая" 25 | previous: "Предыдущая" 26 | aria: 27 | sortAscending: ": активировать для сортировки столбца по возрастанию" 28 | sortDescending: ": активировать для сортировки столбца по убыванию" 29 | -------------------------------------------------------------------------------- /Resources/translations/messages.tr.yml: -------------------------------------------------------------------------------- 1 | datatable: 2 | common: 3 | are_you_sure: Emin misiniz 4 | you_need_to_select_at_least_one_element: En az bir eleman seçmeniz gerekir. 5 | search: Bul 6 | execute: "Yürütme" 7 | ok: "OK" 8 | datatable: 9 | searchPlaceholder: "" 10 | emptyTable: "" 11 | info: " _TOTAL_ Kayıttan _START_ - _END_ Arası Kayıtlar" 12 | infoEmpty: "Kayıt Yok" 13 | infoFiltered: "( _MAX_ Kayıt İçerisinden Bulunan)" 14 | infoPostFix: "" 15 | infoThousands: "," 16 | lengthMenu: "Sayfada _MENU_ Kayıt Göster" 17 | loadingRecords: "" 18 | processing: "İşleniyor..." 19 | search: "Bul:" 20 | zeroRecords: "Eşleşen Kayıt Bulunmadı" 21 | paginate: 22 | first: "İlk" 23 | last: "Son" 24 | next: "Sonraki" 25 | previous: "Önceki" 26 | aria: 27 | sortAscending: ": activate to sort column ascending" 28 | sortDescending: ": activate to sort column descending" 29 | 30 | -------------------------------------------------------------------------------- /Resources/translations/messages.ua.yml: -------------------------------------------------------------------------------- 1 | datatable: 2 | common: 3 | are_you_sure: Ти впевнений ? 4 | you_need_to_select_at_least_one_element: Ви повинні вибрати хоча б один елемент. 5 | search: Пошук 6 | execute: "виконувати" 7 | ok: "OK" 8 | datatable: 9 | searchPlaceholder: "" 10 | emptyTable: "" 11 | info: "Записи з _START_ по _END_ із _TOTAL_ записів" 12 | infoEmpty: "Записи з 0 по 0 із 0 записів" 13 | infoFiltered: "(відфільтровано з _MAX_ записів)" 14 | infoPostFix: "" 15 | infoThousands: "," 16 | lengthMenu: "Показати _MENU_ записів" 17 | loadingRecords: "" 18 | processing: "Зачекайте..." 19 | search: "Пошук:" 20 | zeroRecords: "Записи відсутні." 21 | paginate: 22 | first: "Перша" 23 | last: "Остання" 24 | next: "Наступна" 25 | previous: "Попередня" 26 | aria: 27 | sortAscending: ": активувати для сортування стовпців за зростанням" 28 | sortDescending: ": активувати для сортування стовпців за спаданням" 29 | -------------------------------------------------------------------------------- /Resources/views/Main/datatableHtml.html.twig: -------------------------------------------------------------------------------- 1 | {% block main %} 2 | {% if multiple %}
{% endif %} 3 | 4 | 5 | 6 | {% if multiple %} 7 | 8 | {% endif %} 9 | {% for label,key in fields %} 10 | {% if label != '_identifier_' %} 11 | 12 | {% endif %} 13 | {% endfor %} 14 | 15 | 16 | {% if search %} 17 | {% set i = 0 %} 18 | 19 | 20 | {% if multiple %} 21 | 22 | {% endif %} 23 | {% for label,key in fields %} 24 | {% if label != '_identifier_' %} 25 | {% if searchFields is not empty %} 26 | {% if i in searchFields %} 27 | 28 | {% else %} 29 | 30 | {% endif %} 31 | {% endif %} 32 | {% endif %} 33 | {% set i = i+1 %} 34 | {% endfor %} 35 | 36 | 37 | {% endif %} 38 |
{{ label }}
39 | {% if multiple %}
{% endif %} 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /Resources/views/Main/datatableJs.html.twig: -------------------------------------------------------------------------------- 1 | {% block main %} 2 | 3 | {% spaceless %} 4 | 15 | {% endspaceless %} 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /Resources/views/Main/index.html.twig: -------------------------------------------------------------------------------- 1 | {% block main %} 2 | {% include "WaldoDatatableBundle:Main:datatableHtml.html.twig" %} 3 | {% include "WaldoDatatableBundle:Main:datatableJs.html.twig" %} 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /Resources/views/Renderers/_default.html.twig: -------------------------------------------------------------------------------- 1 | {{dt_item}} 2 | -------------------------------------------------------------------------------- /Resources/views/Snippet/individualSearchField.js.twig: -------------------------------------------------------------------------------- 1 | $('#{{ id }}').one('draw.dt', function() { 2 | 3 | var delay = (function(){ 4 | var timer = 0; 5 | return function(callback, ms){ 6 | clearTimeout (timer); 7 | timer = setTimeout(callback, ms); 8 | }; 9 | })(); 10 | 11 | $('#{{ id }}').DataTable().columns().every(function() { 12 | 13 | $(this.footer()).find('input[type="text"][searchable="true"]').each(function() { 14 | var me = this; 15 | $(this).on('keyup change', function() { 16 | 17 | column = $('#{{ id }}').DataTable().columns(me.getAttribute('index')); 18 | 19 | if ( column.search() !== me.value ) { 20 | delay(function(){ 21 | column.search( me.value ).draw(); 22 | }, 400); 23 | } 24 | }); 25 | }); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /Resources/views/Snippet/multipleRaw.js.twig: -------------------------------------------------------------------------------- 1 | $('#{{ id }}').one('draw.dt', function() { 2 | var multiple_rawhtml = ''; 3 | if((el = $('#{{ id }}_wrapper div[id^="{{ id }}"]')).length > 0 ) { 4 | $(el[0]).prepend(multiple_rawhtml); 5 | } 6 | 7 | {% set chbox %}input:checkbox[name="dataTables[actions][]"]{% endset %} 8 | {% set chboxAll %}input:checkbox[name="datatable_action_all"]{% endset %} 9 | 10 | $('#{{ id }}_wrapper .btn-datatable-multiple:not(.search_init)').on('click', function(e) { 11 | 12 | if($('input:focus', $('#{{ id }}_wrapper')).length > 0){ 13 | return false; 14 | } 15 | 16 | e.preventDefault(); 17 | 18 | if($('{{ chbox }}:checked').length > 0){ 19 | if (!confirm('{% trans %}datatable.common.are_you_sure{% endtrans %}')) { 20 | return false; 21 | } 22 | 23 | var form = $(this).parents('form:eq(0)'); 24 | var action = $('select[name="dataTables[select]"]',$('#{{ id }}_wrapper')).val(); 25 | 26 | $.ajax({ 27 | type: "POST", 28 | url: action, 29 | data: form.serialize(), 30 | success: function(msg) { 31 | $('#{{ id }}').trigger('dt.draw'); 32 | } 33 | }); 34 | } else { 35 | alert('{% trans %}datatable.common.you_need_to_select_at_least_one_element{% endtrans %}'); 36 | } 37 | }); 38 | 39 | $('#{{ id }}_wrapper').on('click', '{{ chboxAll }}', function() { 40 | if($(this).is(':checked')) { 41 | $('{{ chbox }}', $('#{{ id }}_wrapper')).attr("checked",false).click(); 42 | } else { 43 | $('{{ chbox }}', $('#{{ id }}_wrapper')).attr("checked",true).click(); 44 | } 45 | }); 46 | 47 | $('#{{ id }}_wrapper {{ chboxAll }}').attr('checked', false); 48 | }); 49 | -------------------------------------------------------------------------------- /Tests/BaseClient.php: -------------------------------------------------------------------------------- 1 | getContainer()->get('doctrine.orm.default_entity_manager'); 15 | 16 | $schemaTool = new \Doctrine\ORM\Tools\SchemaTool($em); 17 | $classes = array( 18 | $em->getClassMetadata("\Waldo\DatatableBundle\Tests\Functional\Entity\Product"), 19 | $em->getClassMetadata("\Waldo\DatatableBundle\Tests\Functional\Entity\Feature"), 20 | ); 21 | $schemaTool->dropSchema($classes); 22 | $schemaTool->createSchema($classes); 23 | 24 | $this->insertData($em); 25 | } 26 | 27 | protected function insertData($em) 28 | { 29 | $p = new Product; 30 | $p->setName('Laptop') 31 | ->setPrice(1000) 32 | ->setDescription('New laptop'); 33 | $em->persist($p); 34 | 35 | $p = new Product; 36 | $p->setName('Desktop') 37 | ->setPrice(5000) 38 | ->setDescription('New Desktop'); 39 | $em->persist($p); 40 | 41 | 42 | $f = new Feature; 43 | $f->setName('CPU I7 Generation') 44 | ->setProduct($p); 45 | 46 | $f1 = new Feature; 47 | $f1->setName('SolidState drive') 48 | ->setProduct($p); 49 | 50 | $f2 = new Feature; 51 | $f2->setName('SLI graphic card ') 52 | ->setProduct($p); 53 | 54 | 55 | $em->persist($f); 56 | $em->persist($f1); 57 | $em->persist($f2); 58 | 59 | $em->flush(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Tests/Functional/Datatable/DatatableStaticTest.php: -------------------------------------------------------------------------------- 1 | initDatatable(); 35 | } 36 | 37 | private function initDatatable($query = array()) 38 | { 39 | if(count($query) > 0) { 40 | unset($this->client); 41 | unset($this->datatable); 42 | } 43 | 44 | $this->client = static::createClient(); 45 | $this->buildDatabase($this->client); 46 | 47 | // Inject a fake request 48 | $requestStack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack'); 49 | 50 | $requestStack 51 | ->expects($this->any()) 52 | ->method('getCurrentRequest') 53 | ->willReturn(new Request($query)); 54 | 55 | $this->client->getContainer()->set("request_stack", $requestStack); 56 | 57 | $this->em = $this->client->getContainer()->get('doctrine.orm.default_entity_manager'); 58 | 59 | Datatable::clearInstance(); 60 | 61 | $this->datatable = $this->client->getContainer()->get('datatable'); 62 | } 63 | 64 | public function test_chainingClassBehavior() 65 | { 66 | $this->assertInstanceOf( 67 | '\Waldo\DatatableBundle\Util\Datatable', 68 | $this->datatable->setEntity('$entity_name', '$entity_alias') 69 | ); 70 | 71 | $this->assertInstanceOf('\Waldo\DatatableBundle\Util\Datatable', $this->datatable->setFields(array())); 72 | $this->assertInstanceOf('\Waldo\DatatableBundle\Util\Datatable', $this->datatable->setFixedData('$data')); 73 | $this->assertInstanceOf('\Waldo\DatatableBundle\Util\Datatable', $this->datatable->setOrder('$order_field', '$order_type')); 74 | 75 | $this->assertInstanceOf('\Waldo\DatatableBundle\Util\Datatable', 76 | $this->datatable->setRenderer(function($value, $key) { 77 | return true; 78 | })); 79 | } 80 | 81 | public function test_addJoin() 82 | { 83 | $this->datatable 84 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 85 | ->addJoin('p.features', 'f'); 86 | 87 | /* @var $qb \Doctrine\ORM\QueryBuilder */ 88 | $qb = $this->datatable->getQueryBuilder()->getDoctrineQueryBuilder(); 89 | $parts = $qb->getDQLParts(); 90 | $this->assertNotEmpty($parts['join']); 91 | $this->assertTrue(array_key_exists('p', $parts['join'])); 92 | } 93 | 94 | public function test_addJoinWithCondition() 95 | { 96 | $this->datatable 97 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 98 | ->addJoin('p.features', 'f', \Doctrine\ORM\Query\Expr\Join::INNER_JOIN, 'p.id = 1'); 99 | 100 | /* @var $qb \Doctrine\ORM\QueryBuilder */ 101 | $qb = $this->datatable->getQueryBuilder()->getDoctrineQueryBuilder(); 102 | $parts = $qb->getDQLParts(); 103 | $this->assertNotEmpty($parts['join']); 104 | $this->assertTrue(array_key_exists('p', $parts['join'])); 105 | } 106 | 107 | 108 | public function test_execute() 109 | { 110 | $r = $this->datatable 111 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 112 | ->setFields( 113 | array( 114 | "title" => 'p.name', 115 | "_identifier_" => 'p.id') 116 | ) 117 | ->execute(); 118 | 119 | $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $r); 120 | } 121 | 122 | public function test_executeWithGroupBy() 123 | { 124 | $r = $this->datatable 125 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 126 | ->setFields( 127 | array( 128 | "title" => 'p.name', 129 | "_identifier_" => 'p.id') 130 | ) 131 | ->setGroupBy("p.id") 132 | ->execute(); 133 | 134 | $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $r); 135 | } 136 | 137 | public function test_executeWithFixedData() 138 | { 139 | $r = $this->datatable 140 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 141 | ->setFields( 142 | array( 143 | "title" => 'p.name', 144 | "_identifier_" => 'p.id') 145 | ) 146 | ->setFixedData(array("plop")) 147 | ->execute(); 148 | 149 | $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $r); 150 | } 151 | 152 | public function test_executeWithMultiple() 153 | { 154 | $r = $this->datatable 155 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 156 | ->setFields( 157 | array( 158 | "title" => 'p.name', 159 | "_identifier_" => 'p.id') 160 | ) 161 | ->setMultiple(array('delete' => array ('title' => "Delete", 'route' => 'route_to_delete'))) 162 | ->execute(); 163 | 164 | $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $r); 165 | } 166 | 167 | public function test_getInstance() 168 | { 169 | $this->datatable 170 | ->setDatatableId('test') 171 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 172 | ->setFields( 173 | array( 174 | "title" => 'p.name', 175 | "_identifier_" => 'p.id') 176 | ); 177 | $i = $this->datatable->getInstance('test'); 178 | $this->assertInstanceOf('\Waldo\DatatableBundle\Util\Datatable', $i); 179 | $this->assertEquals('p', $i->getEntityAlias()); 180 | } 181 | 182 | public function test_getInstanceWithFaketId() 183 | { 184 | $this->datatable 185 | 186 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 187 | ->setFields( 188 | array( 189 | "title" => 'p.name', 190 | "_identifier_" => 'p.id') 191 | ); 192 | 193 | $i = $this->datatable->getInstance("fake"); 194 | 195 | $this->assertInstanceOf('\Waldo\DatatableBundle\Util\Datatable', $i); 196 | $this->assertEquals('p', $i->getEntityAlias()); 197 | } 198 | 199 | public function test_clearInstance() 200 | { 201 | $this->datatable->setDatatableId('fake1') 202 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 203 | ->setFields( 204 | array( 205 | "title" => 'p.name', 206 | "_identifier_" => 'p.id') 207 | ); 208 | 209 | Datatable::clearInstance(); 210 | } 211 | 212 | /** 213 | * @expectedException Exception 214 | * @expectedExceptionMessage Identifer already exists 215 | */ 216 | public function test_setDatatableIdException() 217 | { 218 | $this->datatable->setDatatableId('fake1'); 219 | $this->datatable->setDatatableId('fake1'); 220 | } 221 | 222 | public function test_queryBuilder() 223 | { 224 | $qb = $this->getMockBuilder("Waldo\DatatableBundle\Util\Factory\Query\QueryInterface")->getMock(); 225 | 226 | 227 | $this->datatable->setQueryBuilder($qb); 228 | 229 | $qbGet = $this->datatable->getQueryBuilder(); 230 | 231 | $this->assertEquals($qb, $qbGet); 232 | } 233 | 234 | public function test_getEntityName() 235 | { 236 | $this->datatable 237 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 238 | ->setFields( 239 | array( 240 | "title" => 'p.name', 241 | "_identifier_" => 'p.id') 242 | ); 243 | $this->assertEquals('Waldo\DatatableBundle\Tests\Functional\Entity\Product', $this->datatable->getEntityName()); 244 | } 245 | 246 | public function test_getEntityAlias() 247 | { 248 | $this->datatable 249 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 250 | ->setFields( 251 | array( 252 | "title" => 'p.name', 253 | "_identifier_" => 'p.id') 254 | ); 255 | $this->assertEquals('p', $this->datatable->getEntityAlias()); 256 | } 257 | 258 | public function test_getFields() 259 | { 260 | $this->datatable 261 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 262 | ->setFields( 263 | array( 264 | "title" => 'p.name', 265 | "_identifier_" => 'p.id') 266 | ); 267 | $this->assertInternalType('array', $this->datatable->getFields()); 268 | } 269 | 270 | public function test_getOrderField() 271 | { 272 | $this->datatable 273 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 274 | ->setFields( 275 | array( 276 | "title" => 'p.name', 277 | "_identifier_" => 'p.id')) 278 | ->setOrder('p.id', 'asc') 279 | ; 280 | $this->assertInternalType('string', $this->datatable->getOrderField()); 281 | } 282 | 283 | public function test_getOrderFieldWithAlias() 284 | { 285 | $this->initDatatable(array("iSortCol_0" => 0)); 286 | 287 | 288 | $data = $this->datatable 289 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 290 | ->setFields( 291 | array( 292 | "title" => "(SELECT Product.name 293 | FROM \Waldo\DatatableBundle\Tests\Functional\Entity\Product as Product 294 | WHERE Product.id = 1) as someAliasName", 295 | "_identifier_" => 'p.id')) 296 | ->getQueryBuilder()->getData(null); 297 | 298 | $this->assertEquals("Laptop", $data[0][0][0]); 299 | } 300 | 301 | public function test_getOrderType() 302 | { 303 | $this->datatable 304 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 305 | ->setFields( 306 | array( 307 | "title" => 'p.name', 308 | "_identifier_" => 'p.id')) 309 | ->setOrder('p.id', 'asc') 310 | ; 311 | $this->assertInternalType('string', $this->datatable->getOrderType()); 312 | } 313 | 314 | public function test_getQueryBuilder() 315 | { 316 | $this->datatable 317 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 318 | ->setFields( 319 | array( 320 | "title" => 'p.name', 321 | "_identifier_" => 'p.id')) 322 | ->setOrder('p.id', 'asc') 323 | ; 324 | $this->assertInstanceOf('Waldo\DatatableBundle\Util\Factory\Query\DoctrineBuilder', $this->datatable->getQueryBuilder()); 325 | } 326 | 327 | public function test_alias() 328 | { 329 | $r = $this->datatable 330 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 331 | ->setFields( 332 | array( 333 | "title" => 'p.name as someAliasName', 334 | "_identifier_" => 'p.id') 335 | )->getQueryBuilder()->getData(null); 336 | 337 | 338 | $this->assertArrayHasKey("someAliasName", $r[1][0]); 339 | } 340 | 341 | public function test_multipleAlias() 342 | { 343 | $r = $this->datatable 344 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 345 | ->setFields( 346 | array( 347 | "title" => "(SELECT Product.name 348 | FROM \Waldo\DatatableBundle\Tests\Functional\Entity\Product as Product 349 | WHERE Product.id = 1) as someAliasName", 350 | "_identifier_" => 'p.id') 351 | )->getQueryBuilder()->getData(null); 352 | 353 | 354 | $this->assertArrayHasKey("someAliasName", $r[1][0]); 355 | } 356 | 357 | public function test_setWhere() 358 | { 359 | $r = $this->datatable 360 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 361 | ->setFields( 362 | array( 363 | "title" => "(SELECT Product.name 364 | FROM \Waldo\DatatableBundle\Tests\Functional\Entity\Product as Product 365 | WHERE Product.id = 1) as someAliasName", 366 | "_identifier_" => 'p.id') 367 | ) 368 | ->setWhere("thisIsAWhere") 369 | ->getQueryBuilder()->getDoctrineQueryBuilder()->getDQL(); 370 | 371 | $this->assertContains("thisIsAWhere", $r); 372 | } 373 | 374 | public function test_setGroupBy() 375 | { 376 | $r = $this->datatable 377 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 378 | ->setFields( 379 | array( 380 | "title" => "(SELECT Product.name 381 | FROM \Waldo\DatatableBundle\Tests\Functional\Entity\Product as Product 382 | WHERE Product.id = 1) as someAliasName", 383 | "_identifier_" => 'p.id') 384 | ) 385 | ->setGroupBy("thisIsAGroupBy") 386 | ->getQueryBuilder()->getDoctrineQueryBuilder()->getDQL(); 387 | 388 | $this->assertContains("thisIsAGroupBy", $r); 389 | } 390 | 391 | public function test_multiple() 392 | { 393 | $expectedArray = array( 394 | 'add' => array('title' => "Add", 'route' => 'route_to_add'), 395 | 'delete' => array('title' => "Delete", 'route' => 'route_to_delete'), 396 | ); 397 | 398 | $this->datatable 399 | ->setMultiple($expectedArray); 400 | 401 | 402 | $this->assertEquals($expectedArray, $this->datatable->getMultiple()); 403 | } 404 | 405 | public function test_getConfig() 406 | { 407 | $c = $this->datatable->getConfiguration(); 408 | 409 | $this->assertTrue(is_array($c)); 410 | } 411 | 412 | public function test_searchFields() 413 | { 414 | $expectedArray = array("field 1", "field 2"); 415 | 416 | $this->datatable->setSearchFields($expectedArray); 417 | 418 | $this->assertEquals($expectedArray, $this->datatable->getSearchFields()); 419 | } 420 | 421 | public function test_notFilterableFields() 422 | { 423 | $expectedArray = array("field 1", "field 2"); 424 | 425 | $this->datatable->setNotFilterableFields($expectedArray); 426 | 427 | $this->assertEquals($expectedArray, $this->datatable->getNotFilterableFields()); 428 | } 429 | 430 | public function test_notSortableFields() 431 | { 432 | $expectedArray = array("field 1", "field 2"); 433 | 434 | $this->datatable->setNotSortableFields($expectedArray); 435 | 436 | $this->assertEquals($expectedArray, $this->datatable->getNotSortableFields()); 437 | } 438 | 439 | public function test_hiddenFields() 440 | { 441 | $expectedArray = array("field 1", "field 2"); 442 | 443 | $this->datatable->setHiddenFields($expectedArray); 444 | 445 | $this->assertEquals($expectedArray, $this->datatable->getHiddenFields()); 446 | } 447 | 448 | public function test_filteringType() 449 | { 450 | $this->initDatatable(array( 451 | "search" => array("regex" => "false", "value" => "desktop"), 452 | "columns" => array( 453 | 0 => array( 454 | "searchable" => "true", 455 | "search" => array("regex" => "false", "value" => "") 456 | ), 457 | 1 => array( 458 | "searchable" => "true", 459 | "search" => array("regex" => "false", "value" => "") 460 | ), 461 | 2 => array( 462 | "searchable" => "true", 463 | "search" => array("regex" => "false", "value" => "") 464 | ), 465 | 3 => array( 466 | "searchable" => "true", 467 | "search" => array("regex" => "false", "value" => "") 468 | ) 469 | ) 470 | )); 471 | 472 | /* @var $res \Symfony\Component\HttpFoundation\JsonResponse */ 473 | $res = $this->datatable 474 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 475 | ->setFields(array( 476 | "name" => "p.name", 477 | "price" => "p.price", 478 | "description" => "p.description", 479 | "_identifier_" => "p.id" 480 | )) 481 | ->setFilteringType(array( 482 | 0 => "s", 483 | 1 => "f", 484 | 2 => "b", 485 | )) 486 | ->setSearch(true) 487 | ->execute(); 488 | 489 | $res = json_decode($res->getContent()); 490 | 491 | $this->assertEquals(1, $res->recordsFiltered); 492 | $this->assertEquals(2, $res->recordsTotal); 493 | $this->assertEquals("Desktop", $res->data[0][0]); 494 | } 495 | 496 | public function test_SQLCommandInFields() 497 | { 498 | $datatable = $this->datatable 499 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 500 | ->setFields( 501 | array( 502 | "total" => 'COUNT(p.id) as total', 503 | "_identifier_" => 'p.id') 504 | ); 505 | 506 | /* @var $qb \Doctrine\ORM\QueryBuilder */ 507 | $qb = $datatable->getQueryBuilder()->getDoctrineQueryBuilder(); 508 | $qb->groupBy('p.id'); 509 | 510 | $r = $datatable->getQueryBuilder()->getData(null); 511 | 512 | $this->assertArrayHasKey("total", $r[1][0]); 513 | $this->assertEquals(1, $r[0][0][1]); 514 | $this->assertEquals(1, $r[1][0]['total']); 515 | } 516 | 517 | public function test_getSearch() 518 | { 519 | $this->datatable 520 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 521 | ->setFields( 522 | array( 523 | "title" => 'p.name', 524 | "_identifier_" => 'p.id')) 525 | ->setOrder('p.id', 'asc') 526 | ; 527 | 528 | $this->assertInternalType('boolean', $this->datatable->getSearch()); 529 | } 530 | 531 | public function test_getSearchWithSubQuery() 532 | { 533 | $this->initDatatable(array( 534 | "search" => array("regex" => "false", "value" => "Laptop"), 535 | "columns" => array( 536 | 0 => array( 537 | "searchable" => "true", 538 | "search" => array("regex" => "false", "value" => "") 539 | ), 540 | 1 => array( 541 | "searchable" => "true", 542 | "search" => array("regex" => "false", "value" => "") 543 | ), 544 | 2 => array( 545 | "searchable" => "true", 546 | "search" => array("regex" => "false", "value" => "") 547 | ), 548 | 3 => array( 549 | "searchable" => "true", 550 | "search" => array("regex" => "false", "value" => "") 551 | ) 552 | ) 553 | )); 554 | 555 | $this->datatable 556 | ->setEntity('Waldo\DatatableBundle\Tests\Functional\Entity\Product', 'p') 557 | ->setFields( 558 | array( 559 | "title" => "(SELECT Product.name 560 | FROM Waldo\DatatableBundle\Tests\Functional\Entity\Product as Product 561 | WHERE Product.id = p.id) as someAliasName", 562 | "id" => 'p.id', 563 | "_identifier_" => 'p.id') 564 | ) 565 | ->setSearch(true) 566 | ; 567 | 568 | $data = $this->datatable->execute(); 569 | 570 | $this->assertEquals('{"draw":0,"recordsTotal":"2","recordsFiltered":"1","data":[["Laptop",1,1]]}', $data->getContent()); 571 | } 572 | 573 | public function test_setRenderders() 574 | { 575 | $tpl = new \SplFileInfo(__DIR__ . '/../../app/Resources/views/Renderers/_actions.html.twig'); 576 | 577 | $out = $this->datatable 578 | ->setEntity('\Waldo\DatatableBundle\Tests\Functional\Entity\Feature', 'f') 579 | ->setFields( 580 | array( 581 | "title" => 'f.name', 582 | "_identifier_" => 'f.id') 583 | ) 584 | ->setRenderers( 585 | array( 586 | 1 => array( 587 | 'view' => $tpl->getRealPath(), 588 | 'params' => array( 589 | 'edit_route' => '_edit', 590 | 'delete_route' => '_delete' 591 | ), 592 | ), 593 | ) 594 | ) 595 | ->execute() 596 | ; 597 | $json = (array) json_decode($out->getContent()); 598 | $this->assertContains('form', $json['data'][0][1]); 599 | } 600 | 601 | public function test_setRenderer() 602 | { 603 | $datatable = $this->datatable; 604 | 605 | $templating = $this->client->getContainer()->get('templating'); 606 | $out = $datatable 607 | ->setEntity('\Waldo\DatatableBundle\Tests\Functional\Entity\Feature', 'f') 608 | ->setFields( 609 | array( 610 | "title" => 'f.name', 611 | "_identifier_" => 'f.id') 612 | ) 613 | ->setRenderer( 614 | function(&$data) use ($templating, $datatable) { 615 | 616 | $tpl = new \SplFileInfo(__DIR__ . '/../../app/Resources/views/Renderers/_actions.html.twig'); 617 | 618 | foreach ($data as $key => $value) 619 | { 620 | if ($key == 1) // 1 => adress field 621 | { 622 | $data[$key] = $templating 623 | ->render($tpl->getRealPath(), array( 624 | 'edit_route' => '_edit', 625 | 'delete_route' => '_delete' 626 | ) 627 | ); 628 | } 629 | } 630 | } 631 | ) 632 | ->execute() 633 | ; 634 | $json = (array) json_decode($out->getContent()); 635 | $this->assertContains('form', $json['data'][0][1]); 636 | } 637 | 638 | public function test_globalSearch() 639 | { 640 | $this->datatable->setGlobalSearch(true); 641 | $this->assertTrue($this->datatable->getGlobalSearch()); 642 | } 643 | 644 | } 645 | -------------------------------------------------------------------------------- /Tests/Functional/Datatable/DoctrineBuilderTest.php: -------------------------------------------------------------------------------- 1 | initDatatable(); 36 | } 37 | 38 | private function initDatatable($query = array()) 39 | { 40 | if (count($query) > 0) { 41 | unset($this->client); 42 | unset($this->datatable); 43 | } 44 | 45 | $this->client = static::createClient(); 46 | $this->buildDatabase($this->client); 47 | 48 | // Inject a fake request 49 | $requestStack = $this->getMock("Symfony\Component\HttpFoundation\RequestStack"); 50 | 51 | $requestStack 52 | ->expects($this->any()) 53 | ->method('getCurrentRequest') 54 | ->willReturn(new Request($query)); 55 | 56 | $this->client->getContainer()->set("request_stack", $requestStack); 57 | 58 | $this->em = $this->client->getContainer()->get('doctrine.orm.default_entity_manager'); 59 | 60 | Datatable::clearInstance(); 61 | 62 | $this->datatable = $this->client->getContainer()->get('datatable'); 63 | } 64 | 65 | public function providerAddSearch() 66 | { 67 | return array( 68 | array("s", "Laptop"), 69 | array("f", "pto"), 70 | array("b", "ptop"), 71 | array("e", "Lap"), 72 | array(null, "Lap"), 73 | ); 74 | } 75 | 76 | /** 77 | * @dataProvider providerAddSearch 78 | */ 79 | public function test_addSearch($searchType, $searchString) 80 | { 81 | $query = array( 82 | "search" => array("regex" => "false", "value" => $searchString), 83 | "columns" => array( 84 | 0 => array( 85 | "searchable" => "true", 86 | "search" => array("regex" => "false", "value" => "") 87 | ), 88 | 1 => array( 89 | "searchable" => "true", 90 | "search" => array("regex" => "false", "value" => "") 91 | ), 92 | 2 => array( 93 | "searchable" => "true", 94 | "search" => array("regex" => "false", "value" => "") 95 | ), 96 | ) 97 | ); 98 | 99 | $this->initDatatable($query); 100 | 101 | $requestStack = $this->getRequestStackMock(); 102 | $requestStack->expects($this->any()) 103 | ->method("getCurrentRequest") 104 | ->willReturn(new Request($query)); 105 | 106 | $doctrineBuilder = new DoctrineBuilder($this->em, $requestStack); 107 | 108 | $doctrineBuilder->setFields(array( 109 | "name" => "p.name", 110 | "description" => "p.description", 111 | "price" => "p.price", 112 | "_identifier_" => "p.id" 113 | )) 114 | ->setEntity("Waldo\DatatableBundle\Tests\Functional\Entity\Product", "p") 115 | ->setSearch(true) 116 | ; 117 | 118 | if($searchType !== null) { 119 | $doctrineBuilder->setFilteringType(array( 120 | 0 => $searchType 121 | )); 122 | } 123 | 124 | $res = $doctrineBuilder->getData(); 125 | 126 | $this->assertCount(1, $res[0]); 127 | $this->assertEquals("Laptop", $res[0][0][0]); 128 | } 129 | 130 | /** 131 | * @dataProvider providerAddSearch 132 | */ 133 | public function test_addSearchWithoutColumns($searchType, $searchString) 134 | { 135 | $query = array( 136 | "search" => array("regex" => "false", "value" => $searchString) 137 | ); 138 | 139 | $this->initDatatable($query); 140 | 141 | $requestStack = $this->getRequestStackMock(); 142 | $requestStack->expects($this->any()) 143 | ->method("getCurrentRequest") 144 | ->willReturn(new Request($query)); 145 | 146 | $doctrineBuilder = new DoctrineBuilder($this->em, $requestStack); 147 | 148 | $doctrineBuilder->setFields(array( 149 | "name" => "p.name", 150 | "description" => "p.description", 151 | "price" => "p.price", 152 | "_identifier_" => "p.id" 153 | )) 154 | ->setEntity("Waldo\DatatableBundle\Tests\Functional\Entity\Product", "p") 155 | ->setSearch(true) 156 | ; 157 | 158 | if($searchType !== null) { 159 | $doctrineBuilder->setFilteringType(array( 160 | 0 => $searchType 161 | )); 162 | } 163 | 164 | $res = $doctrineBuilder->getData(); 165 | 166 | $this->assertGreaterThan(1, $res[0]); 167 | } 168 | 169 | public function test_setDoctrineQueryBuilder() 170 | { 171 | $this->initDatatable(); 172 | 173 | $requestStack = $this->getRequestStackMock(); 174 | 175 | $doctrineBuilder = new DoctrineBuilder($this->em, $requestStack); 176 | 177 | $qbMock = $this->getMockBuilder("Doctrine\ORM\QueryBuilder") 178 | ->disableOriginalConstructor() 179 | ->getMock(); 180 | 181 | $res = $doctrineBuilder->setDoctrineQueryBuilder($qbMock); 182 | 183 | $this->assertEquals($qbMock, $res->getDoctrineQueryBuilder()); 184 | } 185 | 186 | private function getRequestStackMock() 187 | { 188 | return $this->getMockBuilder("Symfony\Component\HttpFoundation\RequestStack") 189 | ->disableOriginalConstructor() 190 | ->getMock(); 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /Tests/Functional/Entity/Feature.php: -------------------------------------------------------------------------------- 1 | id; 35 | } 36 | 37 | public function setId($id) 38 | { 39 | $this->id = $id; 40 | return $this; 41 | } 42 | 43 | public function getName() 44 | { 45 | return $this->name; 46 | } 47 | 48 | public function setName($name) 49 | { 50 | $this->name = $name; 51 | return $this; 52 | } 53 | 54 | public function getProduct() 55 | { 56 | return $this->product; 57 | } 58 | 59 | public function setProduct($product) 60 | { 61 | $this->product = $product; 62 | return $this; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/Functional/Entity/Product.php: -------------------------------------------------------------------------------- 1 | features = new \Doctrine\Common\Collections\ArrayCollection(); 44 | } 45 | 46 | public function getId() 47 | { 48 | return $this->id; 49 | } 50 | 51 | public function setId($id) 52 | { 53 | $this->id = $id; 54 | return $this; 55 | } 56 | 57 | public function getName() 58 | { 59 | return $this->name; 60 | } 61 | 62 | public function setName($name) 63 | { 64 | $this->name = $name; 65 | return $this; 66 | } 67 | 68 | public function getPrice() 69 | { 70 | return $this->price; 71 | } 72 | 73 | public function setPrice($price) 74 | { 75 | $this->price = $price; 76 | return $this; 77 | } 78 | 79 | public function getDescription() 80 | { 81 | return $this->description; 82 | } 83 | 84 | public function setDescription($description) 85 | { 86 | $this->description = $description; 87 | return $this; 88 | } 89 | 90 | public function getFeatures() 91 | { 92 | return $this->features; 93 | } 94 | 95 | public function setFeatures($features) 96 | { 97 | $this->features = $features; 98 | return $this; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Tests/Unit/DependencyInjection/WaldoDatatableExtensionTest.php: -------------------------------------------------------------------------------- 1 | extension = new WaldoDatatableExtension(); 26 | 27 | $this->container = new ContainerBuilder(); 28 | $this->container->registerExtension($this->extension); 29 | } 30 | 31 | public function testWithoutConfiguration() 32 | { 33 | $this->container->loadFromExtension($this->extension->getAlias()); 34 | $this->extension->load(array(), $this->container); 35 | 36 | $this->assertTrue($this->container->hasDefinition("datatable")); 37 | $this->assertTrue($this->container->hasDefinition("datatable.renderer")); 38 | $this->assertTrue($this->container->hasDefinition("datatable.twig.extension")); 39 | $this->assertTrue($this->container->hasDefinition("datatable.kernel.listener.terminate")); 40 | $this->assertArrayHasKey("datatable", $this->container->getParameterBag()->all()); 41 | } 42 | 43 | public function testWithYamlConfig() 44 | { 45 | $configYaml = <<parse($configYaml); 63 | 64 | $this->container->loadFromExtension($this->extension->getAlias()); 65 | $this->extension->load($config, $this->container); 66 | 67 | $this->assertArrayHasKey("datatable", $this->container->getParameterBag()->all()); 68 | 69 | $parseConfig = $this->container->getParameterBag()->get("datatable"); 70 | 71 | $this->assertEquals("[[5,10, 25, 50, -1], [5,10, 25, 50, 'All']]", $parseConfig['js']['aLengthMenu']); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /Tests/Unit/Listner/KernelTerminateListenerTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder("Doctrine\ORM\EntityManager")->disableOriginalConstructor()->getMock(), 18 | $this->getMockBuilder("Symfony\Component\HttpFoundation\RequestStack")->disableOriginalConstructor()->getMock(), 19 | $this->getMockBuilder("Waldo\DatatableBundle\Util\Factory\Query\DoctrineBuilder")->disableOriginalConstructor()->getMock(), 20 | $this->getMockBuilder("Waldo\DatatableBundle\Util\Formatter\Renderer")->disableOriginalConstructor()->getMock(), 21 | array("js" => array()) 22 | ); 23 | 24 | $dt->setDatatableId("testOnKernelTerminate"); 25 | 26 | $listner = new KernelTerminateListener(); 27 | 28 | $listner->onKernelTerminate(); 29 | 30 | $this->assertFalse($dt->hasInstanceId("testOnKernelTerminate")); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Tests/Unit/Twig/DatatableTwigExtensionTest.php: -------------------------------------------------------------------------------- 1 | translator = $this->getMockBuilder("Symfony\Component\Translation\DataCollectorTranslator") 25 | ->disableOriginalConstructor() 26 | ->getMock(); 27 | 28 | 29 | 30 | $this->extentsion = new DatatableExtension($this->translator); 31 | } 32 | 33 | public function testGetName() 34 | { 35 | $this->assertEquals("DatatableBundle", $this->extentsion->getName()); 36 | } 37 | 38 | 39 | public function testGetFunctions() 40 | { 41 | /* @var $functions array<\Twig_SimpleFunction> */ 42 | $functions = $this->extentsion->getFunctions(); 43 | 44 | $this->assertEquals("datatable", $functions[0]->getName()); 45 | $this->assertEquals("datatable_html", $functions[1]->getName()); 46 | $this->assertEquals("datatable_js", $functions[2]->getName()); 47 | } 48 | 49 | public function testDatatable() 50 | { 51 | $dbMock = $this->getMockBuilder("Waldo\DatatableBundle\Util\Factory\Query\DoctrineBuilder") 52 | ->disableOriginalConstructor() 53 | ->getMock(); 54 | 55 | $dbMock->expects($this->any()) 56 | ->method("getOrderField") 57 | ->willReturn("oHYes"); 58 | 59 | $dbMock->expects($this->any()) 60 | ->method("getFields") 61 | ->willReturn(array()); 62 | 63 | $dt = $this->getDatatable(); 64 | $dt->setQueryBuilder($dbMock); 65 | $dt->setDatatableId("testDatatable"); 66 | 67 | $twig = $this->getMock("\Twig_Environment"); 68 | $twig->expects($this->once()) 69 | ->method("render") 70 | ->with($this->equalTo("WaldoDatatableBundle:Main:index.html.twig")) 71 | ->willReturn("OK"); 72 | 73 | $res = $this->extentsion->datatable($twig, array( 74 | "id" => "testDatatable", 75 | "js" => array(), 76 | "action" => "", 77 | "action_twig" => "", 78 | "fields" => "", 79 | "delete_form" => "", 80 | "search" => "", 81 | "global_search" => "", 82 | "searchFields" => "", 83 | "multiple" => "", 84 | "sort" => "" 85 | )); 86 | 87 | $this->assertEquals("OK", $res); 88 | } 89 | 90 | public function testDatatableJs() 91 | { 92 | $dt = $this->getDatatable(); 93 | $dt->setDatatableId("testDatatableJs"); 94 | 95 | $twig = $this->getMock("\Twig_Environment"); 96 | $twig->expects($this->any()) 97 | ->method("render") 98 | ->with($this->equalTo("WaldoDatatableBundle:Main:datatableJs.html.twig")) 99 | ->willReturnArgument(1); 100 | 101 | $configRow = array( 102 | "js" => array( 103 | 'dom'=> "<'row'<'span6'fr>>t<'row'<'span7'il><'span5 align-right'p>>", 104 | 'ajax'=> "urlDatatable" 105 | ), 106 | "action" => "", 107 | "action_twig" => "", 108 | "fields" => "", 109 | "delete_form" => "", 110 | "search" => "", 111 | "global_search" => "", 112 | "searchFields" => "", 113 | "multiple" => "", 114 | "sort" => "" 115 | ); 116 | 117 | $res = $this->extentsion->datatableJs($twig, $configRow); 118 | 119 | 120 | $this->assertEquals("<'row'<'span6'fr>>t<'row'<'span7'il><'span5 align-right'p>>", $res['js']['dom']); 121 | $this->assertEquals("urlDatatable", $res['js']['ajax']['url']); 122 | $this->assertTrue($res['js']['paging']); 123 | 124 | $configRow["js"]['paging'] = false; 125 | 126 | $res = $this->extentsion->datatableJs($twig, $configRow); 127 | 128 | $this->assertFalse($res['js']['paging']); 129 | } 130 | 131 | public function testDatatableJsTranslation() 132 | { 133 | $dt = $this->getDatatable(); 134 | $dt->setDatatableId("testDatatableJsTranslation"); 135 | 136 | $twig = $this->getMock("\Twig_Environment"); 137 | $twig->expects($this->once()) 138 | ->method("render") 139 | ->with( 140 | $this->equalTo("WaldoDatatableBundle:Main:datatableJs.html.twig"), 141 | $this->callback(function($option){ 142 | 143 | return $option['js']['language']["searchPlaceholder"] === "find Me" && 144 | $option['js']['language']["paginate"]["first"] === "coucou" && 145 | array_key_exists("next", $option['js']['language']["paginate"]) 146 | ; 147 | 148 | }) 149 | ) 150 | ->willReturn("OK"); 151 | 152 | $res = $this->extentsion->datatableJs($twig, array( 153 | "js" => array("language" => array( 154 | "searchPlaceholder" => "find Me", 155 | "paginate" => array( 156 | "first" => "coucou" 157 | ) 158 | )), 159 | "action" => "", 160 | "action_twig" => "", 161 | "fields" => "", 162 | "delete_form" => "", 163 | "search" => "", 164 | "global_search" => "", 165 | "searchFields" => "", 166 | "multiple" => "", 167 | "sort" => "" 168 | )); 169 | 170 | 171 | $this->assertEquals("OK", $res); 172 | } 173 | 174 | public function testDatatableHtml() 175 | { 176 | $dt = $this->getDatatable(); 177 | $dt->setDatatableId("testDatatableHtml"); 178 | 179 | $twig = $this->getMock("\Twig_Environment"); 180 | $twig->expects($this->once()) 181 | ->method("render") 182 | ->with($this->equalTo("myHtmlTemplate")) 183 | ->willReturn("OK"); 184 | 185 | $res = $this->extentsion->datatableHtml($twig, array( 186 | "html_template" => "myHtmlTemplate", 187 | "js" => array(), 188 | "action" => "", 189 | "action_twig" => "", 190 | "fields" => "", 191 | "delete_form" => "", 192 | "search" => "", 193 | "global_search" => "", 194 | "searchFields" => "", 195 | "multiple" => "", 196 | "sort" => "" 197 | )); 198 | 199 | $this->assertEquals("OK", $res); 200 | } 201 | 202 | private function getDatatable() 203 | { 204 | return new \Waldo\DatatableBundle\Util\Datatable( 205 | $this->getMockBuilder("Doctrine\ORM\EntityManager")->disableOriginalConstructor()->getMock(), 206 | $this->getMockBuilder("Symfony\Component\HttpFoundation\RequestStack")->disableOriginalConstructor()->getMock(), 207 | $this->getMockBuilder("Waldo\DatatableBundle\Util\Factory\Query\DoctrineBuilder")->disableOriginalConstructor()->getMock(), 208 | $this->getMockBuilder("Waldo\DatatableBundle\Util\Formatter\Renderer")->disableOriginalConstructor()->getMock(), 209 | array("js" => array( 210 | 'paging' => true 211 | )) 212 | ); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /Tests/app/AppKernel.php: -------------------------------------------------------------------------------- 1 | load(__DIR__ . '/config/config_test.yml'); 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function getCacheDir() 27 | { 28 | $cacheDir = sys_get_temp_dir() . '/cache'; 29 | if (!is_dir($cacheDir)) { 30 | mkdir($cacheDir, 0777, true); 31 | } 32 | 33 | return $cacheDir; 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function getLogDir() 40 | { 41 | $logDir = sys_get_temp_dir() . '/logs'; 42 | if (!is_dir($logDir)) { 43 | mkdir($logDir, 0777, true); 44 | } 45 | 46 | return $logDir; 47 | } 48 | } -------------------------------------------------------------------------------- /Tests/app/Resources/views/Renderers/_actions.html.twig: -------------------------------------------------------------------------------- 1 |
2 | {{ delete_form_prototype | replace ({'@id':dt_item}) | raw }} 3 | Delete 4 |
5 | edit -------------------------------------------------------------------------------- /Tests/app/config/config_test.yml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: secret 3 | test: ~ 4 | router: { resource: "%kernel.root_dir%/config/routing.yml" } 5 | form: true 6 | csrf_protection: true 7 | validation: { enable_annotations: true } 8 | templating: { engines: ['twig'] } 9 | session: 10 | storage_id: session.storage.filesystem 11 | 12 | doctrine: 13 | dbal: 14 | driver: pdo_sqlite 15 | host: ~ 16 | port: ~ 17 | dbname: datatableDB 18 | user: ~ 19 | password: ~ 20 | memory: true 21 | charset: utf8 22 | orm: 23 | auto_generate_proxy_classes: true 24 | # auto_mapping: true 25 | mappings: 26 | WaldoDatatableBundle: 27 | mapping: true 28 | type: annotation 29 | dir: 'Tests/Functional/Entity' 30 | alias: 'WaldoDatatableBundle' 31 | prefix: 'Waldo\DatatableBundle\Tests' 32 | is_bundle: false 33 | -------------------------------------------------------------------------------- /Tests/app/config/routing.yml: -------------------------------------------------------------------------------- 1 | _homepage: 2 | path: / 3 | _delete: 4 | path: / 5 | _edit: 6 | path: / 7 | -------------------------------------------------------------------------------- /Tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | translator = $translator; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function getFunctions() 51 | { 52 | return array( 53 | new Twig_SimpleFunction('datatable', array($this, 'datatable'), 54 | array("is_safe" => array("html"), 'needs_environment' => true)), 55 | new Twig_SimpleFunction('datatable_html', array($this, 'datatableHtml'), 56 | array("is_safe" => array("html"), 'needs_environment' => true)), 57 | new Twig_SimpleFunction('datatable_js', array($this, 'datatableJs'), 58 | array("is_safe" => array("html"), 'needs_environment' => true)) 59 | ); 60 | } 61 | 62 | public function getFilters() 63 | { 64 | return array( 65 | new \Twig_SimpleFilter('printDatatableOption', array($this, 'printDatatableOption'), 66 | array("is_safe" => array("html"))) 67 | ); 68 | } 69 | 70 | /** 71 | * Converts a string to time 72 | * 73 | * @param string $string 74 | * @return int 75 | */ 76 | public function datatable(\Twig_Environment $twig, $options) 77 | { 78 | $options = $this->buildDatatableTemplate($options); 79 | 80 | $mainTemplate = array_key_exists('main_template', $options) ? $options['main_template'] : 'WaldoDatatableBundle:Main:index.html.twig'; 81 | 82 | return $twig->render($mainTemplate, $options); 83 | } 84 | 85 | /** 86 | * Converts a string to time 87 | * 88 | * @param string $string 89 | * @return int 90 | */ 91 | public function datatableJs(\Twig_Environment $twig, $options) 92 | { 93 | $options = $this->buildDatatableTemplate($options, "js"); 94 | 95 | $mainTemplate = array_key_exists('main_template', $options) ? $options['js_template'] : 'WaldoDatatableBundle:Main:datatableJs.html.twig'; 96 | 97 | return $twig->render($mainTemplate, $options); 98 | } 99 | 100 | /** 101 | * Converts a string to time 102 | * 103 | * @param string $string 104 | * @return int 105 | */ 106 | public function datatableHtml(\Twig_Environment $twig, $options) 107 | { 108 | if (!isset($options['id'])) { 109 | $options['id'] = 'ali-dta_' . md5(mt_rand(1, 100)); 110 | } 111 | $dt = Datatable::getInstance($options['id']); 112 | 113 | $options['fields'] = $dt->getFields(); 114 | $options['search'] = $dt->getSearch(); 115 | $options['searchFields'] = $dt->getSearchFields(); 116 | $options['multiple'] = $dt->getMultiple(); 117 | 118 | $mainTemplate = 'WaldoDatatableBundle:Main:datatableHtml.html.twig'; 119 | 120 | if (isset($options['html_template'])) { 121 | $mainTemplate = $options['html_template']; 122 | } 123 | 124 | return $twig->render($mainTemplate, $options); 125 | } 126 | 127 | private function buildDatatableTemplate($options, $type = null) 128 | { 129 | if (!isset($options['id'])) { 130 | $options['id'] = 'ali-dta_' . md5(mt_rand(1, 100)); 131 | } 132 | 133 | $dt = Datatable::getInstance($options['id']); 134 | 135 | $config = $dt->getConfiguration(); 136 | 137 | $options['js'] = array_merge($config['js'], $options['js']); 138 | $options['fields'] = $dt->getFields(); 139 | $options['search'] = $dt->getSearch(); 140 | $options['global_search'] = $dt->getGlobalSearch(); 141 | $options['multiple'] = $dt->getMultiple(); 142 | $options['searchFields'] = $dt->getSearchFields(); 143 | $options['sort'] = $dt->getOrderField() === null ? null : array( 144 | array_search($dt->getOrderField(), array_values($dt->getFields())), 145 | $dt->getOrderType() 146 | ); 147 | 148 | if ($type == "js") { 149 | $this->buildJs($options, $dt); 150 | } 151 | 152 | return $options; 153 | } 154 | 155 | private function buildJs(&$options, $dt) 156 | { 157 | if(array_key_exists("ajax", $options['js']) && !is_array($options['js']['ajax'])) { 158 | $options['js']['ajax'] = array( 159 | "url" => $options['js']['ajax'], 160 | "type" => "POST" 161 | ); 162 | } 163 | 164 | if (count($dt->getHiddenFields()) > 0) { 165 | $options['js']['columnDefs'][] = array( 166 | "visible" => false, 167 | "targets" => $dt->getHiddenFields() 168 | ); 169 | } 170 | if (count($dt->getNotSortableFields()) > 0) { 171 | $options['js']['columnDefs'][] = array( 172 | "orderable" => false, 173 | "targets" => $dt->getNotSortableFields() 174 | ); 175 | } 176 | if (count($dt->getNotFilterableFields()) > 0) { 177 | $options['js']['columnDefs'][] = array( 178 | "searchable" => false, 179 | "targets" => $dt->getNotFilterableFields() 180 | ); 181 | } 182 | 183 | $this->buildTranslation($options); 184 | } 185 | 186 | private function buildTranslation(&$options) 187 | { 188 | if(!array_key_exists("language", $options['js'])) { 189 | $options['js']['language'] = array(); 190 | } 191 | 192 | $baseLanguage = array( 193 | "processing" => $this->translator->trans("datatable.datatable.processing"), 194 | "search"=> $this->translator->trans("datatable.datatable.search"), 195 | "lengthMenu"=> $this->translator->trans("datatable.datatable.lengthMenu"), 196 | "info"=> $this->translator->trans("datatable.datatable.info"), 197 | "infoEmpty"=> $this->translator->trans("datatable.datatable.infoEmpty"), 198 | "infoFiltered"=> $this->translator->trans("datatable.datatable.infoFiltered"), 199 | "infoPostFix"=> $this->translator->trans("datatable.datatable.infoPostFix"), 200 | "loadingRecords"=> $this->translator->trans("datatable.datatable.loadingRecords"), 201 | "zeroRecords"=> $this->translator->trans("datatable.datatable.zeroRecords"), 202 | "emptyTable"=> $this->translator->trans("datatable.datatable.emptyTable"), 203 | "searchPlaceholder" => $this->translator->trans("datatable.datatable.searchPlaceholder"), 204 | "paginate"=> array ( 205 | "first"=> $this->translator->trans("datatable.datatable.paginate.first"), 206 | "previous"=> $this->translator->trans("datatable.datatable.paginate.previous"), 207 | "next"=> $this->translator->trans("datatable.datatable.paginate.next"), 208 | "last"=> $this->translator->trans("datatable.datatable.paginate.last") 209 | ), 210 | "aria"=> array( 211 | "sortAscending"=> $this->translator->trans("datatable.datatable.aria.sortAscending"), 212 | "sortDescending"=> $this->translator->trans("datatable.datatable.aria.sortDescending") 213 | )); 214 | 215 | $options['js']['language'] = $this->arrayMergeRecursiveDistinct($baseLanguage, $options['js']['language']); 216 | } 217 | 218 | public function printDatatableOption($var, $elementName) 219 | { 220 | if(is_bool($var)) { 221 | return $var === true ? 'true' : 'false'; 222 | } 223 | 224 | if(is_array($var)) { 225 | return json_encode($var); 226 | } 227 | 228 | if(in_array($elementName, $this->callbackMethodName)) { 229 | return $var; 230 | } 231 | 232 | return json_encode($var); 233 | } 234 | 235 | /** 236 | * Returns the name of the extension. 237 | * 238 | * @return string The extension name 239 | */ 240 | public function getName() 241 | { 242 | return 'DatatableBundle'; 243 | } 244 | 245 | } 246 | -------------------------------------------------------------------------------- /Util/ArrayMerge.php: -------------------------------------------------------------------------------- 1 | 'org value'), array('key' => 'new value')); 17 | * => array('key' => array('org value', 'new value')); 18 | * 19 | * array_merge_recursive_distinct does not change the datatypes of the values in the arrays. 20 | * Matching keys' values in the second array overwrite those in the first array, as is the 21 | * case with array_merge, i.e.: 22 | * 23 | * arrayMergeRecursiveDistinct(array('key' => 'org value'), array('key' => 'new value')); 24 | * => array('key' => array('new value')); 25 | * 26 | * Parameters are passed by reference, though only for performance reasons. They're not 27 | * altered by this function. 28 | * 29 | * @param array $array1 30 | * @param array $array2 31 | * @return array 32 | * @author Daniel 33 | * @author Gabriel Sobrinho 34 | */ 35 | public function arrayMergeRecursiveDistinct(array &$array1, array &$array2) 36 | { 37 | $merged = $array1; 38 | foreach ($array2 as $key => &$value) { 39 | if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { 40 | $merged[$key] = $this->arrayMergeRecursiveDistinct($merged[$key], $value); 41 | } else { 42 | $merged[$key] = $value; 43 | } 44 | } 45 | return $merged; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Util/Datatable.php: -------------------------------------------------------------------------------- 1 | em = $entityManager; 126 | $this->request = $request; 127 | $this->queryBuilder = $doctrineBuilder; 128 | $this->rendererEngine = $renderer; 129 | $this->config = $config; 130 | 131 | self::$currentInstance = $this; 132 | 133 | $this->applyDefaults(); 134 | } 135 | 136 | /** 137 | * apply default value from datatable config 138 | * 139 | * @return void 140 | */ 141 | protected function applyDefaults() 142 | { 143 | if (isset($this->config['all'])) { 144 | $this->search = $this->config['all']['search']; 145 | } 146 | } 147 | 148 | /** 149 | * add join 150 | * 151 | * @example: 152 | * ->setJoin( 153 | * 'r.event', 154 | * 'e', 155 | * \Doctrine\ORM\Query\Expr\Join::INNER_JOIN, 156 | * \Doctrine\ORM\Query\Expr\Join::WITH, 157 | * 'e.name like %test%') 158 | * 159 | * @param string $join The relationship to join. 160 | * @param string $alias The alias of the join. 161 | * @param string|Join::INNER_JOIN $type The type of the join Join::INNER_JOIN | Join::LEFT_JOIN 162 | * @param string|null $conditionType The condition type constant. Either ON or WITH. 163 | * @param string|null $condition The condition for the join. 164 | * 165 | * @return Datatable 166 | */ 167 | public function addJoin($join, $alias, $type = Join::INNER_JOIN, $conditionType = null, $condition = null) 168 | { 169 | $this->queryBuilder->addJoin($join, $alias, $type, $conditionType, $condition); 170 | return $this; 171 | } 172 | 173 | /** 174 | * execute 175 | * 176 | * @param int $hydrationMode 177 | * 178 | * @return JsonResponse 179 | */ 180 | public function execute() 181 | { 182 | $request = $this->request->getCurrentRequest(); 183 | 184 | $iTotalRecords = $this->queryBuilder->getTotalRecords(); 185 | $iTotalDisplayRecords = $this->queryBuilder->getTotalDisplayRecords(); 186 | 187 | list($data, $objects) = $this->queryBuilder->getData($this->multiple); 188 | 189 | $id_index = array_search('_identifier_', array_keys($this->getFields())); 190 | $ids = array(); 191 | 192 | array_walk($data, function($val, $key) use ($id_index, &$ids) { 193 | $ids[$key] = $val[$id_index]; 194 | }); 195 | 196 | if (!is_null($this->fixedData)) { 197 | $this->fixedData = array_reverse($this->fixedData); 198 | foreach ($this->fixedData as $item) { 199 | array_unshift($data, $item); 200 | } 201 | } 202 | 203 | if (!is_null($this->renderer)) { 204 | array_walk($data, $this->renderer); 205 | } 206 | 207 | if (!is_null($this->rendererObj)) { 208 | $this->rendererObj->applyTo($data, $objects); 209 | } 210 | 211 | if (!empty($this->multiple)) { 212 | array_walk($data, 213 | function($val, $key) use(&$data, $ids) { 214 | array_unshift($val, sprintf('', $ids[$key])); 215 | $data[$key] = $val; 216 | }); 217 | } 218 | 219 | $output = array( 220 | "draw" => $request->query->getInt('draw'), 221 | "recordsTotal" => $iTotalRecords, 222 | "recordsFiltered" => $iTotalDisplayRecords, 223 | "data" => $data 224 | ); 225 | 226 | return new JsonResponse($output); 227 | } 228 | 229 | /** 230 | * get datatable instance by id 231 | * return current instance if null 232 | * 233 | * @param string $id 234 | * 235 | * @return Datatable . 236 | */ 237 | public static function getInstance($id) 238 | { 239 | $instance = null; 240 | 241 | if (array_key_exists($id, self::$instances)) { 242 | $instance = self::$instances[$id]; 243 | } else { 244 | $instance = self::$currentInstance; 245 | } 246 | 247 | if ($instance === null) { 248 | throw new \Exception('No instance found for datatable, you should set a datatable id in your action with "setDatatableId" using the id from your view'); 249 | } 250 | 251 | return $instance; 252 | } 253 | 254 | public static function clearInstance() 255 | { 256 | self::$instances = array(); 257 | } 258 | 259 | /** 260 | * get entity name 261 | * 262 | * @return string 263 | */ 264 | public function getEntityName() 265 | { 266 | return $this->queryBuilder->getEntityName(); 267 | } 268 | 269 | /** 270 | * get entity alias 271 | * 272 | * @return string 273 | */ 274 | public function getEntityAlias() 275 | { 276 | return $this->queryBuilder->getEntityAlias(); 277 | } 278 | 279 | /** 280 | * get fields 281 | * 282 | * @return array 283 | */ 284 | public function getFields() 285 | { 286 | return $this->queryBuilder->getFields(); 287 | } 288 | 289 | /** 290 | * get order field 291 | * 292 | * @return string 293 | */ 294 | public function getOrderField() 295 | { 296 | return $this->queryBuilder->getOrderField(); 297 | } 298 | 299 | /** 300 | * get order type 301 | * 302 | * @return string 303 | */ 304 | public function getOrderType() 305 | { 306 | return $this->queryBuilder->getOrderType(); 307 | } 308 | 309 | /** 310 | * get query builder 311 | * 312 | * @return QueryInterface 313 | */ 314 | public function getQueryBuilder() 315 | { 316 | return $this->queryBuilder; 317 | } 318 | 319 | /** 320 | * get search 321 | * 322 | * @return boolean 323 | */ 324 | public function getSearch() 325 | { 326 | return $this->search; 327 | } 328 | 329 | /** 330 | * get global_search 331 | * 332 | * @return boolean 333 | */ 334 | public function getGlobalSearch() 335 | { 336 | return $this->globalSearch; 337 | } 338 | 339 | /** 340 | * set entity 341 | * 342 | * @param type $entityName 343 | * @param type $entityAlias 344 | * 345 | * @return Datatable 346 | */ 347 | public function setEntity($entityName, $entityAlias) 348 | { 349 | $this->queryBuilder->setEntity($entityName, $entityAlias); 350 | return $this; 351 | } 352 | 353 | /** 354 | * set fields 355 | * 356 | * @param array $fields 357 | * 358 | * @return Datatable 359 | */ 360 | public function setFields(array $fields) 361 | { 362 | $this->queryBuilder->setFields($fields); 363 | return $this; 364 | } 365 | 366 | /** 367 | * set order 368 | * 369 | * @param type $orderField 370 | * @param type $orderType 371 | * 372 | * @return Datatable 373 | */ 374 | public function setOrder($orderField, $orderType) 375 | { 376 | $this->queryBuilder->setOrder($orderField, $orderType); 377 | return $this; 378 | } 379 | 380 | /** 381 | * set fixed data 382 | * 383 | * @param type $data 384 | * 385 | * @return Datatable 386 | */ 387 | public function setFixedData($data) 388 | { 389 | $this->fixedData = $data; 390 | return $this; 391 | } 392 | 393 | /** 394 | * set query builder 395 | * 396 | * @param QueryInterface $queryBuilder 397 | */ 398 | public function setQueryBuilder(QueryInterface $queryBuilder) 399 | { 400 | $this->queryBuilder = $queryBuilder; 401 | } 402 | 403 | /** 404 | * set a php closure as renderer 405 | * 406 | * @example: 407 | * 408 | * $controller_instance = $this; 409 | * $datatable = $this->get('datatable') 410 | * ->setEntity("BaseBundle:Entity", "e") 411 | * ->setFields($fields) 412 | * ->setOrder("e.created", "desc") 413 | * ->setRenderer( 414 | * function(&$data) use ($controller_instance) 415 | * { 416 | * foreach ($data as $key => $value) 417 | * { 418 | * if ($key == 1) 419 | * { 420 | * $data[$key] = $controller_instance 421 | * ->get('templating') 422 | * ->render('BaseBundle:Entity:_decorator.html.twig', 423 | * array( 424 | * 'data' => $value 425 | * ) 426 | * ); 427 | * } 428 | * } 429 | * } 430 | * ) 431 | * 432 | * @param \Closure $renderer 433 | * 434 | * @return Datatable 435 | */ 436 | public function setRenderer(\Closure $renderer) 437 | { 438 | $this->renderer = $renderer; 439 | return $this; 440 | } 441 | 442 | /** 443 | * set renderers as twig views 444 | * 445 | * @example: To override the actions column 446 | * 447 | * ->setFields( 448 | * array( 449 | * "field label 1" => 'x.field1', 450 | * "field label 2" => 'x.field2', 451 | * "_identifier_" => 'x.id' 452 | * ) 453 | * ) 454 | * ->setRenderers( 455 | * array( 456 | * 2 => array( 457 | * 'view' => 'WaldoDatatableBundle:Renderers:_actions.html.twig', 458 | * 'params' => array( 459 | * 'edit_route' => 'matche_edit', 460 | * 'delete_route' => 'matche_delete', 461 | * 'delete_form_prototype' => $datatable->getPrototype('delete_form') 462 | * ), 463 | * ), 464 | * ) 465 | * ) 466 | * 467 | * @param array $renderers 468 | * 469 | * @return Datatable 470 | */ 471 | public function setRenderers(array $renderers) 472 | { 473 | $this->renderers = $renderers; 474 | 475 | if (!empty($this->renderers)) { 476 | $this->rendererObj = $this->rendererEngine->build($this->renderers, $this->getFields()); 477 | } 478 | 479 | return $this; 480 | } 481 | 482 | /** 483 | * set query where 484 | * 485 | * @param string $where 486 | * @param array $params 487 | * 488 | * @return Datatable 489 | */ 490 | public function setWhere($where, array $params = array()) 491 | { 492 | $this->queryBuilder->setWhere($where, $params); 493 | return $this; 494 | } 495 | 496 | /** 497 | * set query group 498 | * 499 | * @param string $groupby 500 | * 501 | * @return Datatable 502 | */ 503 | public function setGroupBy($groupby) 504 | { 505 | $this->queryBuilder->setGroupBy($groupby); 506 | return $this; 507 | } 508 | 509 | /** 510 | * set search 511 | * 512 | * @param bool $search 513 | * 514 | * @return Datatable 515 | */ 516 | public function setSearch($search) 517 | { 518 | $this->search = $search; 519 | $this->queryBuilder->setSearch($search || $this->globalSearch); 520 | return $this; 521 | } 522 | 523 | /** 524 | * set global search 525 | * 526 | * @param bool $globalSearch 527 | * 528 | * @return Datatable 529 | */ 530 | public function setGlobalSearch($globalSearch) 531 | { 532 | $this->globalSearch = $globalSearch; 533 | $this->queryBuilder->setSearch($globalSearch || $this->search); 534 | return $this; 535 | } 536 | 537 | /** 538 | * set datatable identifier 539 | * 540 | * @param string $id 541 | * 542 | * @return Datatable 543 | */ 544 | public function setDatatableId($id) 545 | { 546 | if (!array_key_exists($id, self::$instances)) { 547 | self::$instances[$id] = $this; 548 | } else { 549 | throw new \Exception('Identifer already exists'); 550 | } 551 | 552 | return $this; 553 | } 554 | 555 | /** 556 | * hasInstanceId 557 | * 558 | * @param strin $id 559 | */ 560 | public function hasInstanceId($id) 561 | { 562 | return array_key_exists($id, self::$instances); 563 | } 564 | 565 | /** 566 | * get multiple 567 | * 568 | * @return array 569 | */ 570 | public function getMultiple() 571 | { 572 | return $this->multiple; 573 | } 574 | 575 | /** 576 | * set multiple 577 | * 578 | * @example 579 | * 580 | * ->setMultiple('delete' => array ('title' => "Delete", 'route' => 'route_to_delete')); 581 | * 582 | * @param array $multiple 583 | * 584 | * @return \Waldo\DatatableBundle\Util\Datatable 585 | */ 586 | public function setMultiple(array $multiple) 587 | { 588 | $this->multiple = $multiple; 589 | 590 | if(count($this->multiple) > 0) { 591 | if(!in_array(0, $this->notFilterableFields)) { 592 | $this->notFilterableFields[] = 0; 593 | } 594 | if(!in_array(0, $this->notSortableFields)) { 595 | $this->notSortableFields[] = 0; 596 | } 597 | } 598 | 599 | return $this; 600 | } 601 | 602 | /** 603 | * get global configuration (read it from config.yml under datatable) 604 | * 605 | * @return array 606 | */ 607 | public function getConfiguration() 608 | { 609 | return $this->config; 610 | } 611 | 612 | /** 613 | * get search field 614 | * 615 | * @return array 616 | */ 617 | public function getSearchFields() 618 | { 619 | return $this->searchFields; 620 | } 621 | 622 | /** 623 | * set search fields 624 | * 625 | * @example 626 | * 627 | * ->setSearchFields(array(0,2,5)) 628 | * 629 | * @param array $searchFields 630 | * 631 | * @return \Waldo\DatatableBundle\Util\Datatable 632 | */ 633 | public function setSearchFields(array $searchFields) 634 | { 635 | $this->searchFields = $searchFields; 636 | return $this; 637 | } 638 | 639 | /** 640 | * set not filterable fields 641 | * 642 | * @example 643 | * 644 | * ->setNotFilterableFields(array(0,2,5)) 645 | * 646 | * @param array $notFilterableFields 647 | * 648 | * @return \Waldo\DatatableBundle\Util\Datatable 649 | */ 650 | public function setNotFilterableFields(array $notFilterableFields) 651 | { 652 | $this->notFilterableFields = $notFilterableFields; 653 | return $this; 654 | } 655 | 656 | /** 657 | * get not filterable field 658 | * 659 | * @return array 660 | */ 661 | public function getNotFilterableFields() 662 | { 663 | return $this->notFilterableFields; 664 | } 665 | 666 | /** 667 | * set not sortable fields 668 | * 669 | * @example 670 | * 671 | * ->setNotSortableFields(array(0,2,5)) 672 | * 673 | * @param array $notSortableFields 674 | * 675 | * @return \Waldo\DatatableBundle\Util\Datatable 676 | */ 677 | public function setNotSortableFields(array $notSortableFields) 678 | { 679 | $this->notSortableFields = $notSortableFields; 680 | return $this; 681 | } 682 | 683 | /** 684 | * get not sortable field 685 | * 686 | * @return array 687 | */ 688 | public function getNotSortableFields() 689 | { 690 | return $this->notSortableFields; 691 | } 692 | 693 | /** 694 | * set hidden fields 695 | * 696 | * @example 697 | * 698 | * ->setHiddenFields(array(0,2,5)) 699 | * 700 | * @param array $hiddenFields 701 | * 702 | * @return \Waldo\DatatableBundle\Util\Datatable 703 | */ 704 | public function setHiddenFields(array $hiddenFields) 705 | { 706 | $this->hiddenFields = $hiddenFields; 707 | return $this; 708 | } 709 | 710 | /** 711 | * get hidden field 712 | * 713 | * @return array 714 | */ 715 | public function getHiddenFields() 716 | { 717 | return $this->hiddenFields; 718 | } 719 | 720 | /** 721 | * set filtering type 722 | * 's' strict 723 | * 'f' full => LIKE '%' . $value . '%' 724 | * 'b' begin => LIKE '%' . $value 725 | * 'e' end => LIKE $value . '%' 726 | * 727 | * @example 728 | * 729 | * ->setFilteringType(array(0 => 's',2 => 'f',5 => 'b')) 730 | * 731 | * @param array $filteringType 732 | * 733 | * @return \Waldo\DatatableBundle\Util\Datatable 734 | */ 735 | public function setFilteringType(array $filteringType) 736 | { 737 | $this->queryBuilder->setFilteringType($filteringType); 738 | return $this; 739 | } 740 | } 741 | -------------------------------------------------------------------------------- /Util/Factory/Query/DoctrineBuilder.php: -------------------------------------------------------------------------------- 1 | em = $entityManager; 93 | $this->request = $request; 94 | 95 | $this->queryBuilder = $this->em->createQueryBuilder(); 96 | } 97 | 98 | /** 99 | * get the search DQL 100 | * 101 | * @return string 102 | */ 103 | protected function addSearch(QueryBuilder $queryBuilder) 104 | { 105 | if ($this->search !== true) { 106 | return; 107 | } 108 | 109 | $request = $this->request->getCurrentRequest(); 110 | 111 | $columns = $request->get('columns', array()); 112 | 113 | $searchFields = array_intersect_key(array_values($this->fields), $columns); 114 | 115 | $globalSearch = $request->get('search'); 116 | 117 | $orExpr = $queryBuilder->expr()->orX(); 118 | 119 | $filteringType = $this->getFilteringType(); 120 | 121 | foreach ($searchFields as $i => $searchField) { 122 | 123 | $searchField = $this->getSearchField($searchField); 124 | 125 | // Global filtering 126 | if ((!empty($globalSearch) || $globalSearch['value'] == '0') && $columns[$i]['searchable'] === "true") { 127 | 128 | $qbParam = "sSearch_global_" . $i; 129 | 130 | if ($this->isStringDQLQuery($searchField)) { 131 | 132 | $orExpr->add( 133 | $queryBuilder->expr()->eq($searchField, ':' . $qbParam) 134 | ); 135 | $queryBuilder->setParameter($qbParam, $globalSearch['value']); 136 | 137 | } else { 138 | 139 | $orExpr->add($queryBuilder->expr()->like($searchField, ":" . $qbParam)); 140 | $queryBuilder->setParameter($qbParam, "%" . $globalSearch['value'] . "%"); 141 | 142 | } 143 | } 144 | 145 | // Individual filtering 146 | $searchName = "sSearch_" . $i; 147 | 148 | if($columns[$i]['searchable'] === "true" && $columns[$i]['search']['value'] != "") { 149 | 150 | $queryBuilder->andWhere($queryBuilder->expr()->like($searchField, ":" . $searchName)); 151 | 152 | if (array_key_exists($i, $filteringType)) { 153 | switch ($filteringType[$i]) { 154 | case 's': 155 | $queryBuilder->setParameter($searchName, $columns[$i]['search']['value']); 156 | break; 157 | case 'f': 158 | $queryBuilder->setParameter($searchName, sprintf("%%%s%%", $columns[$i]['search']['value'])); 159 | break; 160 | case 'b': 161 | $queryBuilder->setParameter($searchName, sprintf("%%%s", $columns[$i]['search']['value'])); 162 | break; 163 | case 'e': 164 | $queryBuilder->setParameter($searchName, sprintf("%s%%", $columns[$i]['search']['value'])); 165 | break; 166 | } 167 | } else { 168 | $queryBuilder->setParameter($searchName, sprintf("%%%s%%", $columns[$i]['search']['value'])); 169 | } 170 | } 171 | } 172 | 173 | if ((!empty($globalSearch) || $globalSearch == '0') && $orExpr->count() > 0) { 174 | $queryBuilder->andWhere($orExpr); 175 | } 176 | } 177 | 178 | /** 179 | * add join 180 | * 181 | * @example: 182 | * ->setJoin( 183 | * 'r.event', 184 | * 'e', 185 | * \Doctrine\ORM\Query\Expr\Join::INNER_JOIN, 186 | * \Doctrine\ORM\Query\Expr\Join::WITH, 187 | * 'e.name like %test%') 188 | * 189 | * @param string $join The relationship to join. 190 | * @param string $alias The alias of the join. 191 | * @param string|Join::INNER_JOIN $type The type of the join Join::INNER_JOIN | Join::LEFT_JOIN 192 | * @param string|null $conditionType The condition type constant. Either ON or WITH. 193 | * @param string|null $condition The condition for the join. 194 | * 195 | * @return Datatable 196 | */ 197 | public function addJoin($join, $alias, $type = Join::INNER_JOIN, $conditionType = null, $condition = null) 198 | { 199 | if($type === Join::INNER_JOIN) { 200 | $this->queryBuilder->innerJoin($join, $alias, $conditionType, $condition); 201 | } elseif($type === Join::LEFT_JOIN) { 202 | $this->queryBuilder->leftJoin($join, $alias, $conditionType, $condition); 203 | } 204 | 205 | $this->joins[] = array($join, $alias, $type, $conditionType, $condition); 206 | 207 | return $this; 208 | } 209 | 210 | /** 211 | * get total records 212 | * 213 | * @return integer 214 | */ 215 | public function getTotalRecords() 216 | { 217 | $qb = clone $this->queryBuilder; 218 | $qb->resetDQLPart('orderBy'); 219 | 220 | $gb = $qb->getDQLPart('groupBy'); 221 | 222 | if (empty($gb) || !in_array($this->fields['_identifier_'], $gb)) { 223 | $qb->select( 224 | $qb->expr()->count($this->fields['_identifier_']) 225 | ); 226 | 227 | return $qb->getQuery()->getSingleScalarResult(); 228 | } else { 229 | $qb->resetDQLPart('groupBy'); 230 | $qb->select( 231 | $qb->expr()->countDistinct($this->fields['_identifier_']) 232 | ); 233 | 234 | return $qb->getQuery()->getSingleScalarResult(); 235 | } 236 | } 237 | 238 | /** 239 | * get total records after filtering 240 | * 241 | * @return integer 242 | */ 243 | public function getTotalDisplayRecords() 244 | { 245 | $qb = clone $this->queryBuilder; 246 | 247 | $this->addSearch($qb); 248 | 249 | $qb->resetDQLPart('orderBy'); 250 | 251 | $gb = $qb->getDQLPart('groupBy'); 252 | 253 | if (empty($gb) || !in_array($this->fields['_identifier_'], $gb)) { 254 | $qb->select( 255 | $qb->expr()->count($this->fields['_identifier_']) 256 | ); 257 | 258 | return $qb->getQuery()->getSingleScalarResult(); 259 | 260 | } else { 261 | $qb->resetDQLPart('groupBy'); 262 | $qb->select( 263 | $qb->expr()->countDistinct($this->fields['_identifier_']) 264 | ); 265 | 266 | return $qb->getQuery()->getSingleScalarResult(); 267 | } 268 | } 269 | 270 | /** 271 | * get data 272 | * 273 | * @return array 274 | */ 275 | public function getData($multiple) 276 | { 277 | $request = $this->request->getCurrentRequest(); 278 | $order = $request->get('order', array()); 279 | 280 | $dqlFields = array_values($this->fields); 281 | 282 | $qb = clone $this->queryBuilder; 283 | 284 | // add sorting 285 | if (array_key_exists(0, $order)) { 286 | if ($multiple) $orderColumn = $order[0]['column']-1; else $orderColumn = $order[0]['column']; 287 | $orderField = explode(' as ', $dqlFields[$orderColumn]); 288 | end($orderField); 289 | 290 | $qb->orderBy(current($orderField), $order[0]['dir']); 291 | } elseif($this->orderField === null) { 292 | $qb->resetDQLPart('orderBy'); 293 | } 294 | 295 | // extract alias selectors 296 | $select = array($this->entityAlias); 297 | foreach ($this->joins as $join) { 298 | $select[] = $join[1]; 299 | } 300 | 301 | foreach ($this->fields as $key => $field) { 302 | if (stripos($field, " as ") !== false || stripos($field, "(") !== false) { 303 | $select[] = $field; 304 | } 305 | } 306 | 307 | $qb->select(implode(',', $select)); 308 | 309 | // add search 310 | $this->addSearch($qb); 311 | 312 | // get results and process data formatting 313 | $query = $qb->getQuery(); 314 | $length = (int) $request->get('length', 0); 315 | 316 | if ($length > 0) { 317 | $query->setMaxResults($length) 318 | ->setFirstResult((int) $request->get('start')); 319 | } 320 | 321 | $objects = $query->getResult(Query::HYDRATE_OBJECT); 322 | $maps = $query->getResult(Query::HYDRATE_SCALAR); 323 | $data = array(); 324 | 325 | $aliasPattern = self::DQL_ALIAS_PATTERN; 326 | 327 | $get_scalar_key = function($field) use($aliasPattern) { 328 | 329 | $has_alias = (bool) preg_match_all($aliasPattern, $field, $matches); 330 | $_f = ( $has_alias === true ) ? $matches[2][0] : $field; 331 | $_f = str_replace('.', '_', $_f); 332 | 333 | return $_f; 334 | }; 335 | 336 | $fields = array(); 337 | 338 | foreach ($this->fields as $field) { 339 | $fields[] = $get_scalar_key($field); 340 | } 341 | 342 | foreach ($maps as $map) { 343 | $item = array(); 344 | foreach ($fields as $_field) { 345 | $item[] = $map[$_field]; 346 | } 347 | $data[] = $item; 348 | } 349 | 350 | return array($data, $objects); 351 | } 352 | 353 | /** 354 | * get entity name 355 | * 356 | * @return string 357 | */ 358 | public function getEntityName() 359 | { 360 | return $this->entityName; 361 | } 362 | 363 | /** 364 | * get entity alias 365 | * 366 | * @return string 367 | */ 368 | public function getEntityAlias() 369 | { 370 | return $this->entityAlias; 371 | } 372 | 373 | /** 374 | * get fields 375 | * 376 | * @return array 377 | */ 378 | public function getFields() 379 | { 380 | return $this->fields; 381 | } 382 | 383 | /** 384 | * get order field 385 | * 386 | * @return string 387 | */ 388 | public function getOrderField() 389 | { 390 | return $this->orderField; 391 | } 392 | 393 | /** 394 | * get order type 395 | * 396 | * @return string 397 | */ 398 | public function getOrderType() 399 | { 400 | return $this->orderType; 401 | } 402 | 403 | /** 404 | * get doctrine query builder 405 | * 406 | * @return \Doctrine\ORM\QueryBuilder 407 | */ 408 | public function getDoctrineQueryBuilder() 409 | { 410 | return $this->queryBuilder; 411 | } 412 | 413 | /** 414 | * set entity 415 | * 416 | * @param type $entity_name 417 | * @param type $entity_alias 418 | * 419 | * @return Datatable 420 | */ 421 | public function setEntity($entity_name, $entity_alias) 422 | { 423 | $this->entityName = $entity_name; 424 | $this->entityAlias = $entity_alias; 425 | $this->queryBuilder->from($entity_name, $entity_alias); 426 | 427 | return $this; 428 | } 429 | 430 | /** 431 | * set fields 432 | * 433 | * @param array $fields 434 | * 435 | * @return Datatable 436 | */ 437 | public function setFields(array $fields) 438 | { 439 | $this->fields = $fields; 440 | $this->queryBuilder->select(implode(', ', $fields)); 441 | 442 | return $this; 443 | } 444 | 445 | /** 446 | * set order 447 | * 448 | * @param type $order_field 449 | * @param type $order_type 450 | * 451 | * @return Datatable 452 | */ 453 | public function setOrder($order_field, $order_type) 454 | { 455 | $this->orderField = $order_field; 456 | $this->orderType = $order_type; 457 | $this->queryBuilder->orderBy($order_field, $order_type); 458 | 459 | return $this; 460 | } 461 | 462 | /** 463 | * set query where 464 | * 465 | * @param string $where 466 | * @param array $params 467 | * 468 | * @return Datatable 469 | */ 470 | public function setWhere($where, array $params = array()) 471 | { 472 | $this->queryBuilder->where($where); 473 | $this->queryBuilder->setParameters($params); 474 | return $this; 475 | } 476 | 477 | /** 478 | * set query group 479 | * 480 | * @param string $group 481 | * 482 | * @return Datatable 483 | */ 484 | public function setGroupBy($group) 485 | { 486 | $this->queryBuilder->groupBy($group); 487 | return $this; 488 | } 489 | 490 | /** 491 | * set search 492 | * 493 | * @param bool $search 494 | * 495 | * @return Datatable 496 | */ 497 | public function setSearch($search) 498 | { 499 | $this->search = $search; 500 | return $this; 501 | } 502 | 503 | /** 504 | * set doctrine query builder 505 | * 506 | * @param \Doctrine\ORM\QueryBuilder $queryBuilder 507 | * 508 | * @return DoctrineBuilder 509 | */ 510 | public function setDoctrineQueryBuilder(QueryBuilder $queryBuilder) 511 | { 512 | $this->queryBuilder = $queryBuilder; 513 | return $this; 514 | } 515 | 516 | /** 517 | * set filtering type 518 | * 's' strict 519 | * 'f' full => LIKE '%' . $value . '%' 520 | * 'b' begin => LIKE '%' . $value 521 | * 'e' end => LIKE $value . '%' 522 | * 523 | * @example 524 | * 525 | * ->setFilteringType(array(0 => 's',2 => 'f',5 => 'b')) 526 | * 527 | * @param array $filtering_type 528 | * 529 | * @return DoctrineBuilder 530 | */ 531 | public function setFilteringType(array $filtering_type) 532 | { 533 | $this->filteringType = $filtering_type; 534 | return $this; 535 | } 536 | 537 | public function getFilteringType() 538 | { 539 | return $this->filteringType; 540 | } 541 | 542 | /** 543 | * The most of time $search_field is a string that represent the name of a field in data base. 544 | * But some times, $search_field is a DQL subquery 545 | * 546 | * @param string $field 547 | * @return string 548 | */ 549 | private function getSearchField($field) 550 | { 551 | if ($this->isStringDQLQuery($field)) { 552 | 553 | $dqlQuery = $field; 554 | 555 | $lexer = new Query\Lexer($field); 556 | 557 | // We have to rename some identifier or the execution will crash 558 | do { 559 | $lexer->moveNext(); 560 | 561 | if ($this->isTheIdentifierILookingFor($lexer)) { 562 | 563 | $replacement = sprintf("$1%s_%d$3", $lexer->lookahead['value'], mt_rand()); 564 | $pattern = sprintf("/([\(\s])(%s)([\s\.])/", $lexer->lookahead['value']); 565 | 566 | $dqlQuery = preg_replace($pattern, $replacement, $dqlQuery); 567 | } 568 | 569 | } while($lexer->lookahead !== null); 570 | 571 | $dqlQuery = substr($dqlQuery, 0, strripos($dqlQuery, ")") + 1); 572 | 573 | return $dqlQuery; 574 | } 575 | 576 | $field = explode(' ', trim($field)); 577 | 578 | return $field[0]; 579 | } 580 | 581 | /** 582 | * Check if it's the lexer part is the identifier I looking for 583 | * 584 | * @param \Doctrine\ORM\Query\Lexer $lexer 585 | * @return boolean 586 | */ 587 | private function isTheIdentifierILookingFor(Query\Lexer $lexer) 588 | { 589 | if ($lexer->token['type'] === Query\Lexer::T_IDENTIFIER && $lexer->isNextToken(Query\Lexer::T_IDENTIFIER)) { 590 | return true; 591 | } 592 | 593 | if ($lexer->token['type'] === Query\Lexer::T_IDENTIFIER && $lexer->isNextToken(Query\Lexer::T_AS)) { 594 | 595 | $lexer->moveNext(); 596 | 597 | if ($lexer->lookahead['type'] === Query\Lexer::T_IDENTIFIER) { 598 | return true; 599 | } 600 | } 601 | 602 | return false; 603 | } 604 | 605 | /** 606 | * Check if a sring is a DQL query 607 | * 608 | * @param string $value 609 | * @return boolean 610 | */ 611 | private function isStringDQLQuery($value) 612 | { 613 | $keysWord = array( 614 | "SELECT ", 615 | " FROM ", 616 | " WHERE " 617 | ); 618 | 619 | foreach ($keysWord as $keyWord) { 620 | if (stripos($value, $keyWord) !== false) { 621 | return true; 622 | } 623 | } 624 | 625 | return false; 626 | } 627 | } 628 | -------------------------------------------------------------------------------- /Util/Factory/Query/QueryInterface.php: -------------------------------------------------------------------------------- 1 | setJoin( 114 | * 'r.event', 115 | * 'e', 116 | * \Doctrine\ORM\Query\Expr\Join::INNER_JOIN, 117 | * \Doctrine\ORM\Query\Expr\Join::WITH, 118 | * 'e.name like %test%') 119 | * 120 | * @param string $join The relationship to join. 121 | * @param string $alias The alias of the join. 122 | * @param string|Join::INNER_JOIN $type The type of the join Join::INNER_JOIN | Join::LEFT_JOIN 123 | * @param string|null $conditionType The condition type constant. Either ON or WITH. 124 | * @param string|null $condition The condition for the join. 125 | * 126 | * @return Datatable 127 | */ 128 | public function addJoin($join, $alias, $type = Join::INNER_JOIN, $conditionType = null, $condition = null); 129 | 130 | /** 131 | * set filtering type 132 | * 's' strict 133 | * 'f' full => LIKE '%' . $value . '%' 134 | * 'b' begin => LIKE '%' . $value 135 | * 'e' end => LIKE $value . '%' 136 | * 137 | * @example 138 | * 139 | * ->setFilteringType(array(0 => 's',2 => 'f',5 => 'b')) 140 | * 141 | * @param array $filtering_type 142 | * 143 | * @return Datatable 144 | */ 145 | function setFilteringType(array $filtering_type); 146 | 147 | /** 148 | * @return \Doctrine\ORM\QueryBuilder; 149 | */ 150 | function getDoctrineQueryBuilder(); 151 | 152 | function getFilteringType(); 153 | } 154 | -------------------------------------------------------------------------------- /Util/Formatter/Renderer.php: -------------------------------------------------------------------------------- 1 | templating = $templating; 39 | } 40 | 41 | /** 42 | * Build the renderer 43 | * 44 | * @param array $renderers 45 | * @param array $fields 46 | * @return \Waldo\DatatableBundle\Util\Formatter\Renderer 47 | */ 48 | public function build(array $renderers, array $fields) 49 | { 50 | $this->renderers = $renderers; 51 | $this->fields = $fields; 52 | $this->prepare(); 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * return the rendered view using the given content 59 | * 60 | * @param string $view_path 61 | * @param array $params 62 | * 63 | * @return string 64 | */ 65 | public function applyView($view_path, array $params) 66 | { 67 | $out = $this->templating 68 | ->render($view_path, $params); 69 | 70 | return html_entity_decode($out); 71 | } 72 | 73 | /** 74 | * prepare the renderer : 75 | * - guess the identifier index 76 | * 77 | * @return void 78 | */ 79 | protected function prepare() 80 | { 81 | $this->identifierIndex = array_search("_identifier_", array_keys($this->fields)); 82 | } 83 | 84 | /** 85 | * apply foreach given cell content the given (if exists) view 86 | * 87 | * @param array $data 88 | * @param array $objects 89 | * 90 | * @return void 91 | */ 92 | public function applyTo(array &$data, array $objects) 93 | { 94 | foreach ($data as $row_index => $row) { 95 | $identifier_raw = $data[$row_index][$this->identifierIndex]; 96 | foreach ($row as $column_index => $column) { 97 | $params = array(); 98 | if (array_key_exists($column_index, $this->renderers)) { 99 | $view = $this->renderers[$column_index]['view']; 100 | $params = isset($this->renderers[$column_index]['params']) ? $this->renderers[$column_index]['params'] : array(); 101 | } else { 102 | $view = 'WaldoDatatableBundle:Renderers:_default.html.twig'; 103 | } 104 | $params = array_merge($params, 105 | array( 106 | 'dt_obj' => $objects[$row_index], 107 | 'dt_item' => $data[$row_index][$column_index], 108 | 'dt_id' => $identifier_raw, 109 | 'dt_line' => $data[$row_index] 110 | ) 111 | ); 112 | $data[$row_index][$column_index] = $this->applyView( 113 | $view, $params 114 | ); 115 | } 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /WaldoDatatableBundle.php: -------------------------------------------------------------------------------- 1 | =5.4", 26 | "twig/twig": "~1.8", 27 | "doctrine/common": "~2.1" 28 | }, 29 | "require-dev": { 30 | "symfony/symfony": ">=2.6.0,<=4.0", 31 | "doctrine/doctrine-bundle": "~1.4", 32 | "doctrine/orm": "^2.4.8", 33 | "phpunit/phpunit": "~4.8" 34 | }, 35 | "autoload": { 36 | "psr-4": { "Waldo\\DatatableBundle\\": ""} 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ./Tests 12 | 13 | 14 | 15 | 16 | 17 | ./ 18 | 19 | ./Tests 20 | ./vendor 21 | 22 | 23 | 24 | 25 | --------------------------------------------------------------------------------