├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── phpunit.xml ├── public └── .gitkeep ├── src ├── Bllim │ └── Datatables │ │ ├── Datatables.php │ │ ├── DatatablesServiceProvider.php │ │ └── Facade │ │ └── Datatables.php ├── config │ ├── .gitkeep │ └── config.php ├── lang │ └── .gitkeep ├── migrations │ └── .gitkeep └── views │ └── .gitkeep └── tests ├── .gitkeep ├── Datatables.php └── bootstrap ├── autoload.php ├── paths.php └── start.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | 7 | before_script: 8 | - curl -s http://getcomposer.org/installer | php 9 | - php composer.phar install --dev 10 | 11 | script: phpunit -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Datatables Bundle for Laravel 4 2 | 3 | ###WARNING 4 | 5 | Do not use this package directly, use [Bllim](https://github.com/bllim/laravel4-datatables-package) instead. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pragmarx/datatables", 3 | "description": "Server-side handler of DataTables Jquery Plugin for Laravel 4", 4 | "keywords" : ["laravel4","laravel","datatables","datatable","datatables jquery plugin"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Bilal Gultekin", 9 | "email": "bilal@bilal.im" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.3.0", 14 | "illuminate/support": ">=4.0.0", 15 | "illuminate/database": ">=4.0.0", 16 | "illuminate/view": ">=4.0.0", 17 | "illuminate/filesystem": ">=4.0.0" 18 | }, 19 | "require-dev": { 20 | "laravel/laravel": ">=4.0.0", 21 | "mockery/mockery": "0.7.2", 22 | "phpunit/phpunit": "3.7.*" 23 | }, 24 | "autoload": { 25 | "classmap": [ 26 | "src/migrations" 27 | ], 28 | "psr-0": { 29 | "Bllim\\Datatables": "src/" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/laravel4-datatables-package/142631346d01c074248b80c8bc55fdfea255e23e/public/.gitkeep -------------------------------------------------------------------------------- /src/Bllim/Datatables/Datatables.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | use Illuminate\Support\Facades\DB; 15 | use Illuminate\Support\Facades\Input; 16 | use Illuminate\Support\Facades\Config; 17 | use Illuminate\Support\Facades\Response; 18 | use Illuminate\Support\Arr; 19 | use Illuminate\Support\Str; 20 | use Illuminate\View\Compilers\BladeCompiler; 21 | use Illuminate\Filesystem\Filesystem; 22 | 23 | class Datatables 24 | { 25 | /** 26 | * @var \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query 27 | */ 28 | public $query; 29 | 30 | /** 31 | * @var string $query_type 'eloquent' | 'fluent' 32 | */ 33 | protected $query_type; 34 | 35 | protected $added_columns = array(); 36 | protected $removed_columns = array(); 37 | protected $edit_columns = array(); 38 | protected $filter_columns = array(); 39 | protected $sColumns = array(); 40 | 41 | public $columns = array(); 42 | public $aliased_ordered_columns = array(); 43 | 44 | protected $count_all = 0; 45 | protected $display_all = 0; 46 | 47 | protected $result_object; 48 | protected $result_array = array(); 49 | protected $result_array_return = array(); 50 | 51 | protected $input = array(); 52 | protected $mDataSupport; //previous support included only returning columns as object with key names 53 | protected $dataFullSupport; //new support that better implements dot notation without reliance on name column 54 | 55 | protected $index_column; 56 | protected $row_class_tmpl = null; 57 | protected $row_data_tmpls = array(); 58 | 59 | 60 | /** 61 | * Read Input into $this->input according to jquery.dataTables.js version 62 | * 63 | */ 64 | public function __construct() 65 | { 66 | 67 | $this->setData($this->processData(Input::get())); 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Will take an input array and return the formatted dataTables data as an array 74 | * 75 | * @param array $input 76 | * 77 | * @return array 78 | */ 79 | public function processData($input = []) 80 | { 81 | $formatted_input = []; 82 | 83 | if (isset($input['draw'])) { 84 | // DT version 1.10+ 85 | 86 | $input['version'] = '1.10'; 87 | 88 | $formatted_input = $input; 89 | 90 | } else { 91 | // DT version < 1.10 92 | 93 | $formatted_input['version'] = '1.9'; 94 | 95 | $formatted_input['draw'] = Arr::get($input, 'sEcho', ''); 96 | $formatted_input['start'] = Arr::get($input, 'iDisplayStart', 0); 97 | $formatted_input['length'] = Arr::get($input, 'iDisplayLength', 10); 98 | $formatted_input['search'] = array( 99 | 'value' => Arr::get($input, 'sSearch', ''), 100 | 'regex' => Arr::get($input, 'bRegex', ''), 101 | ); 102 | $formatted_input['_'] = Arr::get($input, '_', ''); 103 | 104 | $columns = explode(',', Arr::get($input, 'sColumns', '')); 105 | $formatted_input['columns'] = array(); 106 | for ($i = 0; $i < Arr::get($input, 'iColumns', 0); $i++) { 107 | $arr = array(); 108 | $arr['name'] = isset($columns[$i]) ? $columns[$i] : ''; 109 | $arr['data'] = Arr::get($input, 'mDataProp_' . $i, ''); 110 | $arr['searchable'] = Arr::get($input, 'bSearchable_' . $i, ''); 111 | $arr['search'] = array(); 112 | $arr['search']['value'] = Arr::get($input, 'sSearch_' . $i, ''); 113 | $arr['search']['regex'] = Arr::get($input, 'bRegex_' . $i, ''); 114 | $arr['orderable'] = Arr::get($input, 'bSortable_' . $i, ''); 115 | $formatted_input['columns'][] = $arr; 116 | } 117 | 118 | $formatted_input['order'] = array(); 119 | for ($i = 0; $i < Arr::get($input, 'iSortingCols', 0); $i++) { 120 | $arr = array(); 121 | $arr['column'] = Arr::get($input, 'iSortCol_' . $i, ''); 122 | $arr['dir'] = Arr::get($input, 'sSortDir_' . $i, ''); 123 | $formatted_input['order'][] = $arr; 124 | } 125 | } 126 | 127 | return $formatted_input; 128 | } 129 | 130 | /** 131 | * @return array $this->input 132 | */ 133 | public function getData() 134 | { 135 | return $this->input; 136 | } 137 | 138 | /** 139 | * Sets input data. 140 | * Can be used when not wanting to use default Input data. 141 | * 142 | * @param array $data 143 | */ 144 | public function setData($data) 145 | { 146 | $this->input = $data; 147 | } 148 | 149 | /** 150 | * Gets query and returns instance of class 151 | * 152 | * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query 153 | * @param null $dataFullSupport 154 | * 155 | * @return Datatables 156 | */ 157 | public static function of($query, $dataFullSupport = null) 158 | { 159 | $ins = new static; 160 | $ins->dataFullSupport = ($dataFullSupport) ?: Config::get('datatables::dataFullSupport', false); 161 | $ins->saveQuery($query); 162 | 163 | return $ins; 164 | } 165 | 166 | /** 167 | * Organizes works 168 | * 169 | * @param bool $mDataSupport 170 | * @param bool $raw 171 | * 172 | * @return array|json 173 | */ 174 | public function make($mDataSupport = false, $raw = false) 175 | { 176 | $this->mDataSupport = $mDataSupport; 177 | $this->createAliasedOrderedColumns(); 178 | $this->prepareQuery(); 179 | $this->getResult(); 180 | $this->modifyColumns(); 181 | $this->regulateArray(); 182 | 183 | return $this->output($raw); 184 | } 185 | 186 | /** 187 | * Gets results from prepared query 188 | * 189 | * @return null 190 | */ 191 | protected function getResult() 192 | { 193 | if ($this->query_type == 'eloquent') { 194 | $this->result_object = $this->query->get(); 195 | $this->result_array = $this->result_object->toArray(); 196 | } else { 197 | $this->result_object = $this->query->get(); 198 | $this->result_array = array_map(function ($object) { 199 | return (array)$object; 200 | }, $this->result_object); 201 | } 202 | 203 | if ($this->dataFullSupport) { 204 | $walk = function ($value, $key, $prefix = null) use (&$walk, &$result_array) { 205 | $key = (!is_null($prefix)) ? ($prefix . "." . $key) : $key; 206 | if (is_array($value)) { 207 | array_walk($value, $walk, $key); 208 | } else { 209 | $result_array = Arr::add($result_array, $key, $value); 210 | } 211 | }; 212 | 213 | $result_array = array(); 214 | array_walk($this->result_array, $walk); 215 | $this->result_array = $result_array; 216 | 217 | } 218 | 219 | } 220 | 221 | /** 222 | * Prepares variables according to Datatables parameters 223 | * 224 | * @return null 225 | */ 226 | protected function prepareQuery() 227 | { 228 | $this->count('count_all'); //Total records 229 | $this->filtering(); 230 | $this->count('display_all'); // Filtered records 231 | $this->paging(); 232 | $this->ordering(); 233 | } 234 | 235 | /** 236 | * Adds additional columns to added_columns 237 | * 238 | * @param string $name 239 | * @param string|callable $content 240 | * @param bool $order 241 | * 242 | * @return $this 243 | */ 244 | public function addColumn($name, $content, $order = false) 245 | { 246 | $this->sColumns[] = $name; 247 | 248 | $this->added_columns[] = array('name' => $name, 'content' => $content, 'order' => $order); 249 | 250 | return $this; 251 | } 252 | 253 | /** 254 | * Adds column names to edit_columns 255 | * 256 | * @param string $name 257 | * @param string|callable $content 258 | * 259 | * @return $this 260 | */ 261 | public function editColumn($name, $content) 262 | { 263 | $this->edit_columns[] = array('name' => $name, 'content' => $content); 264 | 265 | return $this; 266 | } 267 | 268 | 269 | /** 270 | * This will remove the columns from the returned data. It will also cause it to skip any filters for those removed columns. 271 | * Adds a list of columns to removed_columns 272 | * 273 | * @params strings ...,... As many individual string parameters matching column names 274 | * 275 | * @return $this 276 | */ 277 | public function removeColumn() 278 | { 279 | $names = func_get_args(); 280 | $this->removed_columns = array_merge($this->removed_columns, $names); 281 | 282 | return $this; 283 | } 284 | 285 | /** 286 | * The filtered columns will add query sql options for the specified columns 287 | * Adds column filter to filter_columns 288 | * 289 | * @param string $column 290 | * @param string $method 291 | * @param mixed ...,... All the individual parameters required for specified $method 292 | * 293 | * @return $this 294 | */ 295 | public function filterColumn($column, $method) 296 | { 297 | $params = func_get_args(); 298 | $this->filter_columns[$column] = array('method' => $method, 'parameters' => array_splice($params, 2)); 299 | 300 | return $this; 301 | } 302 | 303 | 304 | /** 305 | * Sets the DT_RowID for the DataTables index column (as used to set, e.g., id of the tags) to the named column 306 | * If the index matches a column, then that column value will be set as the id of th . 307 | * If the index doesn't, it will be parsed as either a callback or blade template and that returned value will be the 308 | * id of the 309 | * 310 | * @param string $name 311 | * 312 | * @return $this 313 | */ 314 | public function setIndexColumn($name) 315 | { 316 | $this->index_column = $name; 317 | 318 | return $this; 319 | } 320 | 321 | /** 322 | * Sets DT_RowClass template 323 | * result: 324 | * 325 | * @param string|callable $content 326 | * 327 | * @return $this 328 | */ 329 | public function setRowClass($content) 330 | { 331 | $this->row_class_tmpl = $content; 332 | 333 | return $this; 334 | } 335 | 336 | /** 337 | * Sets DT_RowData template for given attribute name 338 | * result: Datatables invoking $(row).data(name, output_from_your_template) 339 | * 340 | * @param string $name 341 | * @param string|callable $content 342 | * 343 | * @return $this 344 | */ 345 | public function setRowData($name, $content) 346 | { 347 | $this->row_data_tmpls[$name] = $content; 348 | 349 | return $this; 350 | } 351 | 352 | /** 353 | * Saves given query and determines its type 354 | * 355 | * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query 356 | * 357 | * @return null 358 | */ 359 | protected function saveQuery($query) 360 | { 361 | $this->query = $query; 362 | $this->query_type = $query instanceof \Illuminate\Database\Query\Builder ? 'fluent' : 'eloquent'; 363 | if ($this->dataFullSupport) { 364 | if ($this->query_type == 'eloquent') { 365 | $this->columns = array_map(function ($column) { 366 | return trim(DB::connection()->getPdo()->quote($column['data']), "'"); 367 | }, $this->input['columns']); 368 | } else { 369 | $this->columns = ($this->query->columns ?: array()); 370 | } 371 | } else { 372 | $this->columns = $this->query_type == 'eloquent' ? ($this->query->getQuery()->columns ?: array()) : ($this->query->columns ?: array()); 373 | } 374 | } 375 | 376 | /** 377 | * Places extra columns 378 | * 379 | * @return null 380 | */ 381 | protected function modifyColumns() 382 | { 383 | foreach ($this->result_array as $rkey => &$rvalue) { 384 | foreach ($this->added_columns as $key => $value) { 385 | $value['content'] = $this->getContent($value['content'], $rvalue, $this->result_object[$rkey]); 386 | 387 | if ($this->dataFullSupport) { 388 | Arr::set($rvalue, $value['name'], $value['content']); 389 | } else { 390 | $rvalue = $this->includeInArray($value, $rvalue); 391 | } 392 | } 393 | 394 | foreach ($this->edit_columns as $key => $value) { 395 | $value['content'] = $this->getContent($value['content'], $rvalue, $this->result_object[$rkey]); 396 | 397 | if ($this->dataFullSupport) { 398 | Arr::set($rvalue, $value['name'], $value['content']); 399 | } else { 400 | $rvalue[$value['name']] = $value['content']; 401 | } 402 | } 403 | } 404 | } 405 | 406 | /** 407 | * Converts result_array number indexed array and consider excess columns 408 | * 409 | * @return null 410 | * @throws \Exception 411 | */ 412 | protected function regulateArray() 413 | { 414 | foreach ($this->result_array as $key => $value) { 415 | foreach ($this->removed_columns as $remove_col_name) { 416 | if ($this->dataFullSupport) { 417 | Arr::forget($value, $remove_col_name); 418 | } else { 419 | unset($value[$remove_col_name]); 420 | } 421 | } 422 | 423 | if ($this->mDataSupport || $this->dataFullSupport) { 424 | $row = $value; 425 | } else { 426 | $row = array_values($value); 427 | } 428 | 429 | if ($this->index_column !== null) { 430 | if (array_key_exists($this->index_column, $value)) { 431 | $row['DT_RowId'] = $value[$this->index_column]; 432 | } else { 433 | $row['DT_RowId'] = $this->getContent($this->index_column, $value, $this->result_object[$key]); 434 | } 435 | } 436 | 437 | if ($this->row_class_tmpl !== null) { 438 | $row['DT_RowClass'] = $this->getContent($this->row_class_tmpl, $value, $this->result_object[$key]); 439 | } 440 | 441 | if (count($this->row_data_tmpls)) { 442 | $row['DT_RowData'] = array(); 443 | foreach ($this->row_data_tmpls as $tkey => $tvalue) { 444 | $row['DT_RowData'][$tkey] = $this->getContent($tvalue, $value, $this->result_object[$key]); 445 | } 446 | } 447 | 448 | $this->result_array_return[] = $row; 449 | } 450 | } 451 | 452 | /** 453 | * 454 | * Inject searched string into $1 in filter_column parameters 455 | * 456 | * @param array|callable|string|Expression &$params 457 | * @param string $value 458 | * 459 | * @return array 460 | */ 461 | private function injectVariable(&$params, $value) 462 | { 463 | if (is_array($params)) { 464 | foreach ($params as $key => $param) { 465 | $params[$key] = $this->injectVariable($param, $value); 466 | } 467 | 468 | } elseif ($params instanceof \Illuminate\Database\Query\Expression) { 469 | $params = DB::raw(str_replace('$1', $value, $params)); 470 | 471 | } elseif (is_callable($params)) { 472 | $params = $params($value); 473 | 474 | } elseif (is_string($params)) { 475 | $params = str_replace('$1', $value, $params); 476 | } 477 | 478 | return $params; 479 | } 480 | 481 | /** 482 | * Creates an array which contains published aliased ordered columns in sql with their index 483 | * 484 | * Creates an array of column names using column aliases where applicable. 485 | * If an added column has a particular order number, it will skip that array key # 486 | * and continue to the next. Leaves dot notation in column names alone. 487 | * 488 | * @return null 489 | */ 490 | protected function createAliasedOrderedColumns() 491 | { 492 | $added_columns_indexes = array(); 493 | $aliased_ordered_columns = array(); 494 | $count = 0; 495 | 496 | foreach ($this->added_columns as $key => $value) { 497 | if ($value['order'] === false) { 498 | continue; 499 | } 500 | $added_columns_indexes[] = $value['order']; 501 | } 502 | 503 | for ($i = 0, $c = count($this->columns); $i < $c; $i++) { 504 | 505 | if (in_array($this->getColumnName($this->columns[$i]), $this->removed_columns)) { 506 | continue; 507 | } 508 | 509 | if (in_array($count, $added_columns_indexes)) { 510 | $count++; 511 | $i--; 512 | continue; 513 | } 514 | 515 | // previous regex #^(\S*?)\s+as\s+(\S*?)$# prevented subqueries and functions from being detected as alias 516 | preg_match('#\s+as\s+(\S*?)$#si', $this->columns[$i], $matches); 517 | $aliased_ordered_columns[$count] = empty($matches) ? $this->columns[$i] : $matches[1]; 518 | $count++; 519 | } 520 | 521 | $this->aliased_ordered_columns = $aliased_ordered_columns; 522 | } 523 | 524 | /** 525 | * Determines if content is callable or blade string, processes and returns 526 | * 527 | * @param string|callable $content Pre-processed content 528 | * @param mixed $data data to use with blade template 529 | * @param mixed $param parameter to call with callable 530 | * 531 | * @return string Processed content 532 | */ 533 | protected function getContent($content, $data = null, $param = null) 534 | { 535 | if (is_string($content)) { 536 | $return = $this->blader($content, $data); 537 | } elseif (is_callable($content)) { 538 | $return = $content($param); 539 | } else { 540 | $return = $content; 541 | } 542 | 543 | return $return; 544 | } 545 | 546 | /** 547 | * Parses and compiles strings by using Blade Template System 548 | * 549 | * @param string $str 550 | * @param array $data 551 | * 552 | * @return string 553 | * @throws \Exception 554 | */ 555 | protected function blader($str, $data = array()) 556 | { 557 | $empty_filesystem_instance = new Filesystem; 558 | $blade = new BladeCompiler($empty_filesystem_instance, 'datatables'); 559 | $parsed_string = $blade->compileString($str); 560 | 561 | ob_start() and extract($data, EXTR_SKIP); 562 | 563 | try { 564 | eval('?>' . $parsed_string); 565 | } 566 | catch (\Exception $e) { 567 | ob_end_clean(); 568 | throw $e; 569 | } 570 | 571 | $str = ob_get_contents(); 572 | ob_end_clean(); 573 | 574 | return $str; 575 | } 576 | 577 | /** 578 | * Places item of extra columns into result_array by care of their order 579 | * Only necessary if not using mData 580 | * 581 | * @param array $item 582 | * @param array $array 583 | * 584 | * @return null 585 | */ 586 | protected function includeInArray($item, $array) 587 | { 588 | if ($item['order'] === false) { 589 | return array_merge($array, array($item['name'] => $item['content'])); 590 | } else { 591 | $count = 0; 592 | $last = $array; 593 | $first = array(); 594 | 595 | if (count($array) <= $item['order']) { 596 | return $array + array($item['name'] => $item['content']); 597 | } 598 | 599 | foreach ($array as $key => $value) { 600 | if ($count == $item['order']) { 601 | return array_merge($first, array($item['name'] => $item['content']), $last); 602 | } 603 | 604 | unset($last[$key]); 605 | $first[$key] = $value; 606 | 607 | $count++; 608 | } 609 | } 610 | } 611 | 612 | /** 613 | * Datatable paging 614 | * 615 | * @return null 616 | */ 617 | protected function paging() 618 | { 619 | if (!is_null($this->input['start']) && !is_null($this->input['length']) && $this->input['length'] != -1) { 620 | $this->query->skip($this->input['start'])->take((int)$this->input['length'] > 0 ? $this->input['length'] : 10); 621 | } 622 | } 623 | 624 | /** 625 | * Datatable ordering 626 | * 627 | * @return null 628 | */ 629 | protected function ordering() 630 | { 631 | if (array_key_exists('order', $this->input) && count($this->input['order']) > 0) { 632 | $columns = $this->cleanColumns($this->aliased_ordered_columns); 633 | 634 | for ($i = 0, $c = count($this->input['order']); $i < $c; $i++) { 635 | $order_col = (int)$this->input['order'][$i]['column']; 636 | if (isset($columns[$order_col])) { 637 | if ($this->input['columns'][$order_col]['orderable'] == "true") { 638 | $this->query->orderBy($columns[$order_col], $this->input['order'][$i]['dir']); 639 | } 640 | } 641 | } 642 | 643 | } 644 | } 645 | 646 | /** 647 | * @param array $cols 648 | * @param bool $use_alias weather to get the column/function or the alias 649 | * 650 | * @return array 651 | */ 652 | protected function cleanColumns($cols, $use_alias = true) 653 | { 654 | $return = array(); 655 | foreach ($cols as $i => $col) { 656 | preg_match('#^(.*?)\s+as\s+(\S*?)\s*$#si', $col, $matches); 657 | if (empty($matches)) { 658 | $return[$i] = $use_alias ? $this->getColumnName($col) : $col; 659 | } else { 660 | $return[$i] = $matches[$use_alias ? 2 : 1]; 661 | } 662 | 663 | } 664 | 665 | return $return; 666 | } 667 | 668 | /** 669 | * Datatable filtering 670 | * 671 | * @return null 672 | */ 673 | protected function filtering() 674 | { 675 | 676 | // copy of $this->columns without columns removed by remove_column 677 | $columns_not_removed = $this->columns; 678 | for ($i = 0, $c = count($columns_not_removed); $i < $c; $i++) { 679 | if (in_array($this->getColumnName($columns_not_removed[$i]), $this->removed_columns)) { 680 | unset($columns_not_removed[$i]); 681 | } 682 | } 683 | 684 | //reindex keys if columns were removed 685 | $columns_not_removed = array_values($columns_not_removed); 686 | 687 | // copy of $this->columns cleaned for database queries 688 | $column_names = $this->cleanColumns($columns_not_removed, false); 689 | $column_aliases = $this->cleanColumns($columns_not_removed, !$this->dataFullSupport); 690 | 691 | // global search 692 | if ($this->input['search']['value'] != '') { 693 | $that = $this; 694 | 695 | $this->query->where(function ($query) use (&$that, $column_aliases, $column_names) { 696 | 697 | for ($i = 0, $c = count($that->input['columns']); $i < $c; $i++) { 698 | if (isset($column_aliases[$i]) && $that->input['columns'][$i]['searchable'] == "true") { 699 | 700 | // if filter column exists for this columns then use user defined method 701 | if (isset($that->filter_columns[$column_aliases[$i]])) { 702 | 703 | $filter = $that->filter_columns[$column_aliases[$i]]; 704 | 705 | // check if "or" equivalent exists for given function 706 | // and if the number of parameters given is not excess 707 | // than call the "or" equivalent 708 | 709 | $method_name = 'or' . ucfirst($filter['method']); 710 | 711 | if (method_exists($query->getQuery(), $method_name) 712 | && count($filter['parameters']) <= with(new \ReflectionMethod($query->getQuery(), $method_name))->getNumberOfParameters() 713 | ) { 714 | 715 | if (isset($filter['parameters'][1]) 716 | && strtoupper(trim($filter['parameters'][1])) == "LIKE" 717 | ) { 718 | $keyword = $that->formatKeyword($that->input['search']['value']); 719 | } else { 720 | $keyword = $that->input['search']['value']; 721 | } 722 | 723 | call_user_func_array( 724 | array( 725 | $query, 726 | $method_name 727 | ), 728 | $that->injectVariable( 729 | $filter['parameters'], 730 | $keyword 731 | ) 732 | ); 733 | } 734 | 735 | } else { 736 | // otherwise do simple LIKE search 737 | 738 | $keyword = $that->formatKeyword($that->input['search']['value']); 739 | 740 | // Check if the database driver is PostgreSQL 741 | // If it is, cast the current column to TEXT datatype 742 | $cast_begin = null; 743 | $cast_end = null; 744 | if ($this->databaseDriver() === 'pgsql') { 745 | $cast_begin = "CAST("; 746 | $cast_end = " as TEXT)"; 747 | } 748 | 749 | //there's no need to put the prefix unless the column name is prefixed with the table name. 750 | $column = $this->prefixColumn($column_names[$i]); 751 | 752 | if (Config::get('datatables::search.case_insensitive', false)) { 753 | $query->orwhere(DB::raw('LOWER(' . $cast_begin . $column . $cast_end . ')'), 'LIKE', Str::lower($keyword)); 754 | } else { 755 | $query->orwhere(DB::raw($cast_begin . $column . $cast_end), 'LIKE', $keyword); 756 | } 757 | } 758 | 759 | } 760 | } 761 | }); 762 | 763 | } 764 | 765 | // column search 766 | for ($i = 0, $c = count($this->input['columns']); $i < $c; $i++) { 767 | if (isset($column_aliases[$i]) && $this->input['columns'][$i]['searchable'] == "true" && $this->input['columns'][$i]['search']['value'] != '') { 768 | // if filter column exists for this columns then use user defined method 769 | if (isset($this->filter_columns[$column_aliases[$i]])) { 770 | 771 | $filter = $this->filter_columns[$column_aliases[$i]]; 772 | 773 | if (isset($filter['parameters'][1]) 774 | && strtoupper(trim($filter['parameters'][1])) == "LIKE" 775 | ) { 776 | $keyword = $this->formatKeyword($this->input['columns'][$i]['search']['value']); 777 | } else { 778 | $keyword = $this->input['columns'][$i]['search']['value']; 779 | } 780 | 781 | 782 | call_user_func_array( 783 | array( 784 | $this->query, 785 | $filter['method'] 786 | ), 787 | $this->injectVariable( 788 | $filter['parameters'], 789 | $keyword 790 | ) 791 | ); 792 | 793 | } else // otherwise do simple LIKE search 794 | { 795 | 796 | $keyword = $this->formatKeyword($this->input['columns'][$i]['search']['value']); 797 | 798 | //there's no need to put the prefix unless the column name is prefixed with the table name. 799 | $column = $this->prefixColumn($column_names[$i]); 800 | 801 | if (Config::get('datatables::search.case_insensitive', false)) { 802 | $this->query->where(DB::raw('LOWER(' . $column . ')'), 'LIKE', Str::lower($keyword)); 803 | } else { 804 | //note: so, when would a ( be in the columns? It will break a select if that's put in the columns 805 | //without a DB::raw. It could get there in filter columns, but it wouldn't be delt with here. 806 | //why is it searching for ( ? 807 | $col = strstr($column_names[$i], '(') ? DB::raw($column) : $column; 808 | $this->query->where($col, 'LIKE', $keyword); 809 | } 810 | } 811 | } 812 | } 813 | } 814 | 815 | /** 816 | * This will format the keyword as needed for "LIKE" based on config settings 817 | * If $value already has %, it doesn't motify and just returns the value. 818 | * 819 | * @param string $value 820 | * 821 | * @return string 822 | */ 823 | public function formatKeyword($value) 824 | { 825 | if (strpos($value, '%') !== false) { 826 | return $value; 827 | } 828 | 829 | if (Config::get('datatables::search.use_wildcards', false)) { 830 | $keyword = '%' . $this->formatWildcard($value) . '%'; 831 | } else { 832 | $keyword = '%' . trim($value) . '%'; 833 | } 834 | 835 | return $keyword; 836 | } 837 | 838 | /** 839 | * Adds % wildcards to the given string 840 | * 841 | * @param $str 842 | * @param bool $lowercase 843 | * 844 | * @return string 845 | */ 846 | public function formatWildcard($str, $lowercase = true) 847 | { 848 | if ($lowercase) { 849 | $str = lowercase($str); 850 | } 851 | 852 | return preg_replace('\s+', '%', $str); 853 | } 854 | 855 | /** 856 | * Returns current database prefix 857 | * 858 | * @return string 859 | */ 860 | public function databasePrefix() 861 | { 862 | if ($this->query_type == 'eloquent') { 863 | $query = $this->query->getQuery(); 864 | } else { 865 | $query = $this->query; 866 | } 867 | return $query->getGrammar()->getTablePrefix(); 868 | //return Config::get('database.connections.' . Config::get('database.default') . '.prefix', ''); 869 | } 870 | 871 | /** 872 | * Returns current database driver 873 | */ 874 | protected function databaseDriver() 875 | { 876 | if ($this->query_type == 'eloquent') { 877 | $query = $this->query->getQuery(); 878 | } else { 879 | $query = $this->query; 880 | } 881 | return $query->getConnection()->getDriverName(); 882 | } 883 | 884 | /** 885 | * Will prefix column if needed 886 | * 887 | * @param string $column 888 | * @return string 889 | */ 890 | protected function prefixColumn($column) 891 | { 892 | // $query = ($this->query_type == 'eloquent') ? $this->query->getQuery() : $this->query; 893 | // return $query->getGrammar()->wrap($column); 894 | 895 | $table_names = $this->tableNames(); 896 | if (count(array_filter($table_names, function($value) use (&$column) { return strpos($column, $value.".") === 0; }))) { 897 | //the column starts with one of the table names 898 | $column = $this->databasePrefix() . $column; 899 | } 900 | return $column; 901 | } 902 | 903 | /** 904 | * Will look through the query and all it's joins to determine the table names 905 | * 906 | * @return array 907 | */ 908 | protected function tableNames() 909 | { 910 | $names = []; 911 | 912 | $query = ($this->query_type == 'eloquent') ? $this->query->getQuery() : $this->query; 913 | 914 | $names[] = $query->from; 915 | $joins = $query->joins?:array(); 916 | $databasePrefix = $this->databasePrefix(); 917 | foreach ($joins as $join) { 918 | $table = preg_split("/ as /i", $join->table); 919 | $names[] = $table[0]; 920 | if (isset($table[1]) && !empty($databasePrefix) && strpos($table[1], $databasePrefix) == 0) { 921 | $names[] = preg_replace('/^'.$databasePrefix.'/', '', $table[1]); 922 | } 923 | } 924 | 925 | return $names; 926 | 927 | } 928 | 929 | /** 930 | * Counts current query 931 | * 932 | * @param string $count variable to store to 'count_all' for iTotalRecords, 'display_all' for iTotalDisplayRecords 933 | * 934 | * @return null 935 | */ 936 | protected function count($count = 'count_all') 937 | { 938 | 939 | //Get columns to temp var. 940 | if ($this->query_type == 'eloquent') { 941 | $query = $this->query->getQuery(); 942 | $connection = $this->query->getModel()->getConnection()->getName(); 943 | } else { 944 | $query = $this->query; 945 | $connection = $query->getConnection()->getName(); 946 | } 947 | 948 | // if its a normal query ( no union ) replace the select with static text to improve performance 949 | $countQuery = clone $query; 950 | if (!preg_match('/UNION/i', $countQuery->toSql())) { 951 | $countQuery->select(DB::raw("'1' as `row`")); 952 | 953 | // if query has "having" clause add select columns 954 | if ($countQuery->havings) { 955 | foreach ($countQuery->havings as $having) { 956 | if (isset($having['column'])) { 957 | $countQuery->addSelect($having['column']); 958 | } else { 959 | // search filter_columns for query string to get column name from an array key 960 | $found = false; 961 | foreach ($this->filter_columns as $column => $filter) { 962 | if ($filter['parameters'][0] == $having['sql']) { 963 | $found = $column; 964 | break; 965 | } 966 | } 967 | // then correct it if it's an alias and add to columns 968 | if ($found !== false) { 969 | foreach ($this->columns as $col) { 970 | $arr = preg_split('/ as /i', $col); 971 | if (isset($arr[1]) && $arr[1] == $found) { 972 | $found = $arr[0]; 973 | break; 974 | } 975 | } 976 | $countQuery->addSelect($found); 977 | } 978 | } 979 | } 980 | } 981 | } 982 | 983 | // Clear the orders, since they are not relevant for count 984 | $countQuery->orders = null; 985 | 986 | $this->$count = DB::connection($connection) 987 | ->table(DB::raw('(' . $countQuery->toSql() . ') AS count_row_table')) 988 | ->setBindings($countQuery->getBindings())->count(); 989 | 990 | } 991 | 992 | /** 993 | * Returns column name from . 994 | * 995 | * For processing select statement columns like $query->column data 996 | * 997 | * @param string $str 998 | * 999 | * @return string 1000 | */ 1001 | protected function getColumnName($str) 1002 | { 1003 | 1004 | preg_match('#^(\S*?)\s+as\s+(\S*?)$#si', $str, $matches); 1005 | 1006 | if (!empty($matches)) { 1007 | return $matches[2]; 1008 | } elseif (strpos($str, '.')) { 1009 | $array = explode('.', $str); 1010 | 1011 | return array_pop($array); 1012 | } 1013 | 1014 | return $str; 1015 | } 1016 | 1017 | /** 1018 | * Prints output 1019 | * 1020 | * @param bool $raw If raw will output array data, otherwise json 1021 | * 1022 | * @return array|json 1023 | */ 1024 | protected function output($raw = false) 1025 | { 1026 | if (Arr::get($this->input, 'version') == '1.10') { 1027 | 1028 | $output = array( 1029 | "draw" => intval($this->input['draw']), 1030 | "recordsTotal" => $this->count_all, 1031 | "recordsFiltered" => $this->display_all, 1032 | "data" => $this->result_array_return, 1033 | ); 1034 | 1035 | } else { 1036 | 1037 | $sColumns = array_merge_recursive($this->columns, $this->sColumns); 1038 | 1039 | $output = array( 1040 | "sEcho" => intval($this->input['draw']), 1041 | "iTotalRecords" => $this->count_all, 1042 | "iTotalDisplayRecords" => $this->display_all, 1043 | "aaData" => $this->result_array_return, 1044 | "sColumns" => $sColumns 1045 | ); 1046 | 1047 | } 1048 | 1049 | if (Config::get('app.debug', false)) { 1050 | $output['aQueries'] = DB::getQueryLog(); 1051 | } 1052 | 1053 | if ($raw) { 1054 | return $output; 1055 | } else { 1056 | return Response::json($output); 1057 | } 1058 | } 1059 | 1060 | /** 1061 | * originally PR #93 1062 | * Allows previous API calls where the methods were snake_case. 1063 | * Will convert a camelCase API call to a snake_case call. 1064 | */ 1065 | public function __call($name, $arguments) 1066 | { 1067 | $name = Str::camel(Str::lower($name)); 1068 | if (method_exists($this, $name)) { 1069 | return call_user_func_array(array($this, $name), $arguments); 1070 | } else { 1071 | trigger_error('Call to undefined method ' . __CLASS__ . '::' . $name . '()', E_USER_ERROR); 1072 | } 1073 | } 1074 | } 1075 | -------------------------------------------------------------------------------- /src/Bllim/Datatables/DatatablesServiceProvider.php: -------------------------------------------------------------------------------- 1 | package('bllim/datatables', 'datatables', __DIR__); 15 | } 16 | 17 | /** 18 | * Register the service provider. 19 | * 20 | * @return void 21 | */ 22 | public function register() 23 | { 24 | $this->app->singleton('bllim.datatables', function($app) { 25 | return new Datatables; 26 | }); 27 | } 28 | 29 | /** 30 | * Register the package's component namespaces. 31 | * 32 | * @param string $package 33 | * @param string $namespace 34 | * @param string $path 35 | * @return void 36 | */ 37 | public function package($package, $namespace = null, $path = null) 38 | { 39 | 40 | // Is it possible to register the config? 41 | if (method_exists($this->app['config'], 'package')) { 42 | $this->app['config']->package($package, $path.'/../../config', $namespace); 43 | } else { 44 | // Load the config for now.. 45 | $config = $this->app['files']->getRequire($path .'/../../config/config.php'); 46 | foreach($config as $key => $value){ 47 | $this->app['config']->set($namespace.'::'.$key, $value); 48 | } 49 | } 50 | 51 | // Register view files 52 | $appView = $this->app['path']."/views/packages/{$package}"; 53 | if ($this->app['files']->isDirectory($appView)) 54 | { 55 | $this->app['view']->addNamespace($namespace, $appView); 56 | } 57 | 58 | $this->app['view']->addNamespace($namespace, $path.'/views'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Bllim/Datatables/Facade/Datatables.php: -------------------------------------------------------------------------------- 1 | array( 6 | 'case_insensitive' => true, 7 | 'use_wildcards' => false, 8 | ), 9 | 10 | 'dataFullSupport' => false, 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /src/lang/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/laravel4-datatables-package/142631346d01c074248b80c8bc55fdfea255e23e/src/lang/.gitkeep -------------------------------------------------------------------------------- /src/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/laravel4-datatables-package/142631346d01c074248b80c8bc55fdfea255e23e/src/migrations/.gitkeep -------------------------------------------------------------------------------- /src/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/laravel4-datatables-package/142631346d01c074248b80c8bc55fdfea255e23e/src/views/.gitkeep -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/laravel4-datatables-package/142631346d01c074248b80c8bc55fdfea255e23e/tests/.gitkeep -------------------------------------------------------------------------------- /tests/Datatables.php: -------------------------------------------------------------------------------- 1 | increments('id'); 32 | $table->string('name'); 33 | }); 34 | Demo::insert(array('name'=>'demo datatables')); 35 | } 36 | 37 | public function tearDown() 38 | { 39 | Schema::dropIfExists('demos'); 40 | } 41 | 42 | public function test_demo_count() 43 | { 44 | $this->assertEquals(1, Demo::count()); 45 | } 46 | 47 | public function test_datatables_make_function() 48 | { 49 | $demo = DB::table('demos')->select('id','name'); 50 | $output = Datatables::of($demo)->make(); 51 | $this->assertInstanceOf('Illuminate\Http\JsonResponse', $output); 52 | } 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /tests/bootstrap/autoload.php: -------------------------------------------------------------------------------- 1 | __DIR__.'/../../vendor/laravel/laravel/app', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Public Path 21 | |-------------------------------------------------------------------------- 22 | | 23 | | The public path contains the assets for your web application, such as 24 | | your JavaScript and CSS files, and also contains the primary entry 25 | | point for web requests into these applications from the outside. 26 | | 27 | */ 28 | 29 | 'public' => __DIR__.'/../../vendor/laravel/laravel/public', 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Base Path 34 | |-------------------------------------------------------------------------- 35 | | 36 | | The base path is the root of the Laravel installation. Most likely you 37 | | will not need to change this value. But, if for some wild reason it 38 | | is necessary you will do so here, just proceed with some caution. 39 | | 40 | */ 41 | 42 | 'base' => __DIR__.'/../..', 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Storage Path 47 | |-------------------------------------------------------------------------- 48 | | 49 | | The storage path is used by Laravel to store cached Blade views, logs 50 | | and other pieces of information. You may modify the path here when 51 | | you want to change the location of this directory for your apps. 52 | | 53 | */ 54 | 55 | 'storage' => __DIR__.'/../../vendor/laravel/laravel/app/storage', 56 | 57 | ); 58 | -------------------------------------------------------------------------------- /tests/bootstrap/start.php: -------------------------------------------------------------------------------- 1 | detectEnvironment(array( 28 | 29 | 'local' => array('your-machine-name'), 30 | 31 | )); 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Bind Paths 36 | |-------------------------------------------------------------------------- 37 | | 38 | | Here we are binding the paths configured in paths.php to the app. You 39 | | should not be changing these here. If you need to change these you 40 | | may do so within the paths.php file and they will be bound here. 41 | | 42 | */ 43 | 44 | $app->bindInstallPaths(require __DIR__.'/paths.php'); 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Load The Application 49 | |-------------------------------------------------------------------------- 50 | | 51 | | Here we will load this Illuminate application. We will keep this in a 52 | | separate location so we can isolate the creation of an application 53 | | from the actual running of the application with a given request. 54 | | 55 | */ 56 | 57 | $framework = $app['path.base'].'/vendor/laravel/framework/src'; 58 | 59 | require $framework.'/Illuminate/Foundation/start.php'; 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Return The Application 64 | |-------------------------------------------------------------------------- 65 | | 66 | | This script returns the application instance. The instance is given to 67 | | the calling script so we can separate the building of the instances 68 | | from the actual running of the application and sending responses. 69 | | 70 | */ 71 | 72 | return $app; 73 | --------------------------------------------------------------------------------