1: <?php
2:
3: namespace Yajra\Datatables\Engines;
4:
5: use Illuminate\Http\JsonResponse;
6: use Illuminate\Support\Facades\Config;
7: use Illuminate\Support\Str;
8: use League\Fractal\Resource\Collection;
9: use Yajra\Datatables\Contracts\DataTableEngineContract;
10: use Yajra\Datatables\Helper;
11: use Yajra\Datatables\Processors\DataProcessor;
12:
13: 14: 15: 16: 17: 18:
19: abstract class BaseEngine implements DataTableEngineContract
20: {
21: 22: 23: 24: 25:
26: public $request;
27:
28: 29: 30: 31: 32:
33: protected $connection;
34:
35: 36: 37: 38: 39:
40: protected $query;
41:
42: 43: 44: 45: 46:
47: protected $builder;
48:
49: 50: 51: 52: 53:
54: protected $columns = [];
55:
56: 57: 58: 59: 60:
61: protected $columnDef = [
62: 'index' => false,
63: 'append' => [],
64: 'edit' => [],
65: 'excess' => ['rn', 'row_num'],
66: 'filter' => [],
67: 'order' => [],
68: 'escape' => [],
69: 'blacklist' => ['password', 'remember_token'],
70: 'whitelist' => '*',
71: ];
72:
73: 74: 75: 76: 77:
78: protected $query_type;
79:
80: 81: 82: 83: 84:
85: protected $extraColumns = [];
86:
87: 88: 89: 90: 91:
92: protected $totalRecords = 0;
93:
94: 95: 96: 97: 98:
99: protected $filteredRecords = 0;
100:
101: 102: 103: 104: 105:
106: protected $autoFilter = true;
107:
108: 109: 110: 111: 112: 113:
114: protected $withTrashed = false;
115:
116: 117: 118: 119: 120:
121: protected $filterCallback;
122:
123: 124: 125: 126: 127:
128: protected $filterCallbackParameters;
129:
130: 131: 132: 133: 134:
135: protected $templates = [
136: 'DT_RowId' => '',
137: 'DT_RowClass' => '',
138: 'DT_RowData' => [],
139: 'DT_RowAttr' => [],
140: ];
141:
142: 143: 144: 145: 146:
147: protected $transformer = null;
148:
149: 150: 151: 152: 153:
154: protected $prefix;
155:
156: 157: 158: 159: 160:
161: protected $database;
162:
163: 164: 165: 166: 167:
168: protected $isFilterApplied = false;
169:
170: 171: 172: 173: 174:
175: protected $serializer = null;
176:
177: 178: 179: 180: 181:
182: protected $orderCallback;
183:
184: 185: 186: 187: 188:
189: protected $skipPaging = false;
190:
191: 192: 193: 194: 195:
196: private $appends = [];
197:
198: 199: 200: 201: 202: 203:
204: public function setupKeyword($value)
205: {
206: if ($this->isSmartSearch()) {
207: $keyword = '%' . $value . '%';
208: if ($this->isWildcard()) {
209: $keyword = $this->wildcardLikeString($value);
210: }
211:
212: $keyword = str_replace('\\', '%', $keyword);
213:
214: return $keyword;
215: }
216:
217: return $value;
218: }
219:
220: 221: 222: 223: 224:
225: protected function isSmartSearch()
226: {
227: return Config::get('datatables.search.smart', true);
228: }
229:
230: 231: 232: 233: 234:
235: public function isWildcard()
236: {
237: return Config::get('datatables.search.use_wildcards', false);
238: }
239:
240: 241: 242: 243: 244: 245: 246:
247: public function wildcardLikeString($str, $lowercase = true)
248: {
249: $wild = '%';
250: $length = Str::length($str);
251: if ($length) {
252: for ($i = 0; $i < $length; $i++) {
253: $wild .= $str[$i] . '%';
254: }
255: }
256: if ($lowercase) {
257: $wild = Str::lower($wild);
258: }
259:
260: return $wild;
261: }
262:
263: 264: 265: 266: 267: 268:
269: public function prefixColumn($column)
270: {
271: $table_names = $this->tableNames();
272: if (count(
273: array_filter($table_names, function ($value) use (&$column) {
274: return strpos($column, $value . '.') === 0;
275: })
276: )) {
277:
278: $column = $this->prefix . $column;
279: }
280:
281: return $column;
282: }
283:
284: 285: 286: 287: 288:
289: public function tableNames()
290: {
291: $names = [];
292: $query = $this->getQueryBuilder();
293: $names[] = $query->from;
294: $joins = $query->joins ?: [];
295: $databasePrefix = $this->prefix;
296: foreach ($joins as $join) {
297: $table = preg_split('/ as /i', $join->table);
298: $names[] = $table[0];
299: if (isset($table[1]) && ! empty($databasePrefix) && strpos($table[1], $databasePrefix) == 0) {
300: $names[] = preg_replace('/^' . $databasePrefix . '/', '', $table[1]);
301: }
302: }
303:
304: return $names;
305: }
306:
307: 308: 309: 310: 311: 312:
313: public function getQueryBuilder($instance = null)
314: {
315: if (! $instance) {
316: $instance = $this->query;
317: }
318:
319: if ($this->isQueryBuilder()) {
320: return $instance;
321: }
322:
323: return $instance->getQuery();
324: }
325:
326: 327: 328: 329: 330:
331: public function isQueryBuilder()
332: {
333: return $this->query_type == 'builder';
334: }
335:
336: 337: 338: 339: 340: 341: 342: 343:
344: public function addColumn($name, $content, $order = false)
345: {
346: $this->extraColumns[] = $name;
347:
348: $this->columnDef['append'][] = ['name' => $name, 'content' => $content, 'order' => $order];
349:
350: return $this;
351: }
352:
353: 354: 355: 356: 357:
358: public function addIndexColumn()
359: {
360: $this->columnDef['index'] = true;
361:
362: return $this;
363: }
364:
365: 366: 367: 368: 369: 370: 371:
372: public function editColumn($name, $content)
373: {
374: $this->columnDef['edit'][] = ['name' => $name, 'content' => $content];
375:
376: return $this;
377: }
378:
379: 380: 381: 382: 383:
384: public function removeColumn()
385: {
386: $names = func_get_args();
387: $this->columnDef['excess'] = array_merge($this->columnDef['excess'], $names);
388:
389: return $this;
390: }
391:
392: 393: 394: 395: 396: 397:
398: public function escapeColumns($columns = '*')
399: {
400: $this->columnDef['escape'] = $columns;
401:
402: return $this;
403: }
404:
405: 406: 407: 408: 409: 410:
411: public function withTrashed($withTrashed = true)
412: {
413: $this->withTrashed = $withTrashed;
414:
415: return $this;
416: }
417:
418: 419: 420: 421: 422: 423: 424: 425: 426:
427: public function __call($name, $arguments)
428: {
429: $name = Str::camel(Str::lower($name));
430: if (method_exists($this, $name)) {
431: return call_user_func_array([$this, $name], $arguments);
432: } elseif (method_exists($this->getQueryBuilder(), $name)) {
433: call_user_func_array([$this->getQueryBuilder(), $name], $arguments);
434: } else {
435: trigger_error('Call to undefined method ' . __CLASS__ . '::' . $name . '()', E_USER_ERROR);
436: }
437:
438: return $this;
439: }
440:
441: 442: 443: 444: 445: 446: 447:
448: public function setRowClass($content)
449: {
450: $this->templates['DT_RowClass'] = $content;
451:
452: return $this;
453: }
454:
455: 456: 457: 458: 459: 460: 461:
462: public function setRowId($content)
463: {
464: $this->templates['DT_RowId'] = $content;
465:
466: return $this;
467: }
468:
469: 470: 471: 472: 473: 474:
475: public function setRowData(array $data)
476: {
477: $this->templates['DT_RowData'] = $data;
478:
479: return $this;
480: }
481:
482: 483: 484: 485: 486: 487: 488:
489: public function addRowData($key, $value)
490: {
491: $this->templates['DT_RowData'][$key] = $value;
492:
493: return $this;
494: }
495:
496: 497: 498: 499: 500: 501: 502:
503: public function setRowAttr(array $data)
504: {
505: $this->templates['DT_RowAttr'] = $data;
506:
507: return $this;
508: }
509:
510: 511: 512: 513: 514: 515: 516:
517: public function addRowAttr($key, $value)
518: {
519: $this->templates['DT_RowAttr'][$key] = $value;
520:
521: return $this;
522: }
523:
524: 525: 526: 527: 528: 529: 530: 531: 532:
533: public function filterColumn($column, $method)
534: {
535: $params = func_get_args();
536: $this->columnDef['filter'][$column] = ['method' => $method, 'parameters' => array_splice($params, 2)];
537:
538: return $this;
539: }
540:
541: 542: 543: 544: 545: 546: 547: 548:
549: public function orderColumns(array $columns, $sql, $bindings = [])
550: {
551: foreach ($columns as $column) {
552: $this->orderColumn($column, str_replace(':column', $column, $sql), $bindings);
553: }
554:
555: return $this;
556: }
557:
558: 559: 560: 561: 562: 563: 564: 565: 566:
567: public function orderColumn($column, $sql, $bindings = [])
568: {
569: $this->columnDef['order'][$column] = ['method' => 'orderByRaw', 'parameters' => [$sql, $bindings]];
570:
571: return $this;
572: }
573:
574: 575: 576: 577: 578: 579:
580: public function setTransformer($transformer)
581: {
582: $this->transformer = $transformer;
583:
584: return $this;
585: }
586:
587: 588: 589: 590: 591: 592:
593: public function setSerializer($serializer)
594: {
595: $this->serializer = $serializer;
596:
597: return $this;
598: }
599:
600: 601: 602: 603: 604: 605: 606:
607: public function make($mDataSupport = false, $orderFirst = false)
608: {
609: $this->totalRecords = $this->totalCount();
610:
611: if ($this->totalRecords) {
612: $this->orderRecords(! $orderFirst);
613: $this->filterRecords();
614: $this->orderRecords($orderFirst);
615: $this->paginate();
616: }
617:
618: return $this->render($mDataSupport);
619: }
620:
621: 622: 623: 624: 625: 626:
627: public function orderRecords($skip)
628: {
629: if (! $skip) {
630: $this->ordering();
631: }
632: }
633:
634: 635: 636: 637: 638:
639: public function filterRecords()
640: {
641: if ($this->autoFilter && $this->request->isSearchable()) {
642: $this->filtering();
643: }
644:
645: if (is_callable($this->filterCallback)) {
646: call_user_func($this->filterCallback, $this->filterCallbackParameters);
647: }
648:
649: $this->columnSearch();
650: $this->filteredRecords = $this->isFilterApplied ? $this->count() : $this->totalRecords;
651: }
652:
653: 654: 655: 656: 657:
658: public function paginate()
659: {
660: if ($this->request->isPaginationable() && ! $this->skipPaging) {
661: $this->paging();
662: }
663: }
664:
665: 666: 667: 668: 669: 670:
671: public function render($object = false)
672: {
673: $output = array_merge([
674: 'draw' => (int) $this->request['draw'],
675: 'recordsTotal' => $this->totalRecords,
676: 'recordsFiltered' => $this->filteredRecords,
677: ], $this->appends);
678:
679: if (isset($this->transformer)) {
680: $fractal = app('datatables.fractal');
681:
682: if ($this->serializer) {
683: $fractal->setSerializer(new $this->serializer);
684: }
685:
686:
687:
688: $reflection = new \ReflectionMethod($this->transformer, 'transform');
689: $parameter = $reflection->getParameters()[0];
690:
691:
692:
693: if ($parameter->getClass()) {
694: $resource = new Collection($this->results(), $this->createTransformer());
695: } else {
696: $resource = new Collection(
697: $this->getProcessedData($object),
698: $this->createTransformer()
699: );
700: }
701:
702: $collection = $fractal->createData($resource)->toArray();
703: $output['data'] = $collection['data'];
704: } else {
705: $output['data'] = Helper::transform($this->getProcessedData($object));
706: }
707:
708: if ($this->isDebugging()) {
709: $output = $this->showDebugger($output);
710: }
711:
712: return new JsonResponse($output);
713: }
714:
715: 716: 717: 718: 719:
720: protected function createTransformer()
721: {
722: if ($this->transformer instanceof \League\Fractal\TransformerAbstract) {
723: return $this->transformer;
724: }
725:
726: return new $this->transformer();
727: }
728:
729: 730: 731: 732: 733: 734:
735: private function getProcessedData($object = false)
736: {
737: $processor = new DataProcessor(
738: $this->results(),
739: $this->columnDef,
740: $this->templates,
741: $this->request['start']
742: );
743:
744: return $processor->process($object);
745: }
746:
747: 748: 749: 750: 751:
752: public function isDebugging()
753: {
754: return Config::get('app.debug', false);
755: }
756:
757: 758: 759: 760: 761: 762:
763: public function showDebugger(array $output)
764: {
765: $output['queries'] = $this->connection->getQueryLog();
766: $output['input'] = $this->request->all();
767:
768: return $output;
769: }
770:
771: 772: 773: 774: 775: 776: 777:
778: public function overrideGlobalSearch(\Closure $callback, $parameters, $autoFilter = false)
779: {
780: $this->autoFilter = $autoFilter;
781: $this->isFilterApplied = true;
782: $this->filterCallback = $callback;
783: $this->filterCallbackParameters = $parameters;
784: }
785:
786: 787: 788: 789: 790:
791: public function isCaseInsensitive()
792: {
793: return Config::get('datatables.search.case_insensitive', false);
794: }
795:
796: 797: 798: 799: 800: 801: 802:
803: public function with($key, $value = '')
804: {
805: if (is_array($key)) {
806: $this->appends = $key;
807: } elseif (is_callable($value)) {
808: $this->appends[$key] = value($value);
809: } else {
810: $this->appends[$key] = value($value);
811: }
812:
813: return $this;
814: }
815:
816: 817: 818: 819: 820: 821:
822: public function order(\Closure $closure)
823: {
824: $this->orderCallback = $closure;
825:
826: return $this;
827: }
828:
829: 830: 831: 832: 833: 834:
835: public function blacklist(array $blacklist)
836: {
837: $this->columnDef['blacklist'] = $blacklist;
838:
839: return $this;
840: }
841:
842: 843: 844: 845: 846: 847:
848: public function whitelist($whitelist = '*')
849: {
850: $this->columnDef['whitelist'] = $whitelist;
851:
852: return $this;
853: }
854:
855: 856: 857: 858: 859: 860:
861: public function smart($bool = true)
862: {
863: Config::set('datatables.search.smart', $bool);
864:
865: return $this;
866: }
867:
868: 869: 870: 871: 872: 873:
874: public function setTotalRecords($total)
875: {
876: $this->totalRecords = $total;
877:
878: return $this;
879: }
880:
881: 882: 883:
884: public function skipPaging()
885: {
886: $this->skipPaging = true;
887:
888: return $this;
889: }
890:
891: 892: 893: 894: 895: 896:
897: protected function isBlacklisted($column)
898: {
899: if (in_array($column, $this->columnDef['blacklist'])) {
900: return true;
901: }
902:
903: if ($this->columnDef['whitelist'] === '*' || in_array($column, $this->columnDef['whitelist'])) {
904: return false;
905: }
906:
907: return true;
908: }
909:
910: 911: 912: 913: 914: 915: 916:
917: protected function getColumnName($index, $wantsAlias = false)
918: {
919: $column = $this->request->columnName($index);
920:
921:
922: if (is_numeric($column)) {
923: $column = $this->getColumnNameByIndex($index);
924: }
925:
926: if (Str::contains(Str::upper($column), ' AS ')) {
927: $column = $this->extractColumnName($column, $wantsAlias);
928: }
929:
930: return $column;
931: }
932:
933: 934: 935: 936: 937: 938:
939: protected function getColumnNameByIndex($index)
940: {
941: $name = isset($this->columns[$index]) && $this->columns[$index] <> '*' ? $this->columns[$index] : $this->getPrimaryKeyName();
942:
943: return in_array($name, $this->extraColumns, true) ? $this->getPrimaryKeyName() : $name;
944: }
945:
946: 947: 948: 949: 950:
951: protected function getPrimaryKeyName()
952: {
953: if ($this->isEloquent()) {
954: return $this->query->getModel()->getKeyName();
955: }
956:
957: return 'id';
958: }
959:
960: 961: 962: 963: 964:
965: protected function isEloquent()
966: {
967: return $this->query_type === 'eloquent';
968: }
969:
970: 971: 972: 973: 974: 975: 976:
977: protected function extractColumnName($str, $wantsAlias)
978: {
979: $matches = explode(' as ', Str::lower($str));
980:
981: if (! empty($matches)) {
982: if ($wantsAlias) {
983: return array_pop($matches);
984: } else {
985: return array_shift($matches);
986: }
987: } elseif (strpos($str, '.')) {
988: $array = explode('.', $str);
989:
990: return array_pop($array);
991: }
992:
993: return $str;
994: }
995:
996: 997: 998: 999: 1000:
1001: protected function isOracleSql()
1002: {
1003: return in_array($this->database, ['oracle', 'oci8']);
1004: }
1005: }
1006: