Completed
Push — master ( 501027...a97a17 )
by Arjay
01:39
created

src/DataTableAbstract.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Yajra\DataTables;
4
5
use Illuminate\Support\Str;
6
use Psr\Log\LoggerInterface;
7
use Illuminate\Http\JsonResponse;
8
use Yajra\DataTables\Utilities\Helper;
9
use Illuminate\Support\Traits\Macroable;
10
use Yajra\DataTables\Contracts\DataTable;
11
use Illuminate\Contracts\Support\Jsonable;
12
use Yajra\DataTables\Exceptions\Exception;
13
use Illuminate\Contracts\Support\Arrayable;
14
use Yajra\DataTables\Processors\DataProcessor;
15
16
/**
17
 * @method DataTableAbstract setTransformer($transformer)
18
 * @method DataTableAbstract setSerializer($transformer)
19
 * @property mixed transformer
20
 * @property mixed serializer
21
 * @see     https://github.com/yajra/laravel-datatables-fractal for transformer related methods.
22
 */
23
abstract class DataTableAbstract implements DataTable, Arrayable, Jsonable
24
{
25
    use Macroable;
26
27
    /**
28
     * DataTables Request object.
29
     *
30
     * @var \Yajra\DataTables\Utilities\Request
31
     */
32
    public $request;
33
34
    /**
35
     * @var \Psr\Log\LoggerInterface
36
     */
37
    protected $logger;
38
39
    /**
40
     * Array of result columns/fields.
41
     *
42
     * @var array
43
     */
44
    protected $columns = [];
45
46
    /**
47
     * DT columns definitions container (add/edit/remove/filter/order/escape).
48
     *
49
     * @var array
50
     */
51
    protected $columnDef = [
52
        'index'  => false,
53
        'append' => [],
54
        'edit'   => [],
55
        'filter' => [],
56
        'order'  => [],
57
        'only'   => null,
58
    ];
59
60
    /**
61
     * Extra/Added columns.
62
     *
63
     * @var array
64
     */
65
    protected $extraColumns = [];
66
67
    /**
68
     * Total records.
69
     *
70
     * @var int
71
     */
72
    protected $totalRecords = 0;
73
74
    /**
75
     * Total filtered records.
76
     *
77
     * @var int
78
     */
79
    protected $filteredRecords = 0;
80
81
    /**
82
     * Auto-filter flag.
83
     *
84
     * @var bool
85
     */
86
    protected $autoFilter = true;
87
88
    /**
89
     * Callback to override global search.
90
     *
91
     * @var callable
92
     */
93
    protected $filterCallback;
94
95
    /**
96
     * DT row templates container.
97
     *
98
     * @var array
99
     */
100
    protected $templates = [
101
        'DT_RowId'    => '',
102
        'DT_RowClass' => '',
103
        'DT_RowData'  => [],
104
        'DT_RowAttr'  => [],
105
    ];
106
107
    /**
108
     * [internal] Track if any filter was applied for at least one column.
109
     *
110
     * @var bool
111
     */
112
    protected $isFilterApplied = false;
113
114
    /**
115
     * Custom ordering callback.
116
     *
117
     * @var callable
118
     */
119
    protected $orderCallback;
120
121
    /**
122
     * Skip paginate as needed.
123
     *
124
     * @var bool
125
     */
126
    protected $skipPaging = false;
127
128
    /**
129
     * Array of data to append on json response.
130
     *
131
     * @var array
132
     */
133
    protected $appends = [];
134
135
    /**
136
     * @var \Yajra\DataTables\Utilities\Config
137
     */
138
    protected $config;
139
140
    /**
141
     * @var mixed
142
     */
143
    protected $serializer;
144
145
    /**
146
     * Can the DataTable engine be created with these parameters.
147
     *
148
     * @param mixed $source
149
     * @return bool
150
     */
151
    public static function canCreate($source)
152
    {
153
        return false;
154
    }
155
156
    /**
157
     * Factory method, create and return an instance for the DataTable engine.
158
     *
159
     * @param mixed $source
160
     * @return DataTableAbstract
161
     */
162
    public static function create($source)
163
    {
164
        return new static($source);
165
    }
166
167
    /**
168
     * Add column in collection.
169
     *
170
     * @param string          $name
171
     * @param string|callable $content
172
     * @param bool|int        $order
173
     * @return $this
174
     */
175
    public function addColumn($name, $content, $order = false)
176
    {
177
        $this->extraColumns[] = $name;
178
179
        $this->columnDef['append'][] = ['name' => $name, 'content' => $content, 'order' => $order];
180
181
        return $this;
182
    }
183
184
    /**
185
     * Add DT row index column on response.
186
     *
187
     * @return $this
188
     */
189
    public function addIndexColumn()
190
    {
191
        $this->columnDef['index'] = true;
192
193
        return $this;
194
    }
195
196
    /**
197
     * Edit column's content.
198
     *
199
     * @param string          $name
200
     * @param string|callable $content
201
     * @return $this
202
     */
203
    public function editColumn($name, $content)
204
    {
205
        $this->columnDef['edit'][] = ['name' => $name, 'content' => $content];
206
207
        return $this;
208
    }
209
210
    /**
211
     * Remove column from collection.
212
     *
213
     * @return $this
214
     */
215
    public function removeColumn()
216
    {
217
        $names                     = func_get_args();
218
        $this->columnDef['excess'] = array_merge($this->getColumnsDefinition()['excess'], $names);
219
220
        return $this;
221
    }
222
223
    /**
224
     * Get only selected columns in response.
225
     *
226
     * @param array $columns
227
     * @return $this
228
     */
229
    public function only(array $columns = [])
230
    {
231
        $this->columnDef['only'] = $columns;
232
233
        return $this;
234
    }
235
236
    /**
237
     * Declare columns to escape values.
238
     *
239
     * @param string|array $columns
240
     * @return $this
241
     */
242
    public function escapeColumns($columns = '*')
243
    {
244
        $this->columnDef['escape'] = $columns;
245
246
        return $this;
247
    }
248
249
    /**
250
     * Set columns that should not be escaped.
251
     * Optionally merge the defaults from config.
252
     *
253
     * @param array $columns
254
     * @param bool $merge
255
     * @return $this
256
     */
257
    public function rawColumns(array $columns, $merge = false)
258
    {
259
        if ($merge) {
260
            $config = $this->config->get('datatables.columns');
261
262
            $this->columnDef['raw'] = array_merge($config['raw'], $columns);
263
        } else {
264
            $this->columnDef['raw'] = $columns;
265
        }
266
267
        return $this;
268
    }
269
270
    /**
271
     * Sets DT_RowClass template.
272
     * result: <tr class="output_from_your_template">.
273
     *
274
     * @param string|callable $content
275
     * @return $this
276
     */
277
    public function setRowClass($content)
278
    {
279
        $this->templates['DT_RowClass'] = $content;
280
281
        return $this;
282
    }
283
284
    /**
285
     * Sets DT_RowId template.
286
     * result: <tr id="output_from_your_template">.
287
     *
288
     * @param string|callable $content
289
     * @return $this
290
     */
291
    public function setRowId($content)
292
    {
293
        $this->templates['DT_RowId'] = $content;
294
295
        return $this;
296
    }
297
298
    /**
299
     * Set DT_RowData templates.
300
     *
301
     * @param array $data
302
     * @return $this
303
     */
304
    public function setRowData(array $data)
305
    {
306
        $this->templates['DT_RowData'] = $data;
307
308
        return $this;
309
    }
310
311
    /**
312
     * Add DT_RowData template.
313
     *
314
     * @param string          $key
315
     * @param string|callable $value
316
     * @return $this
317
     */
318
    public function addRowData($key, $value)
319
    {
320
        $this->templates['DT_RowData'][$key] = $value;
321
322
        return $this;
323
    }
324
325
    /**
326
     * Set DT_RowAttr templates.
327
     * result: <tr attr1="attr1" attr2="attr2">.
328
     *
329
     * @param array $data
330
     * @return $this
331
     */
332
    public function setRowAttr(array $data)
333
    {
334
        $this->templates['DT_RowAttr'] = $data;
335
336
        return $this;
337
    }
338
339
    /**
340
     * Add DT_RowAttr template.
341
     *
342
     * @param string          $key
343
     * @param string|callable $value
344
     * @return $this
345
     */
346
    public function addRowAttr($key, $value)
347
    {
348
        $this->templates['DT_RowAttr'][$key] = $value;
349
350
        return $this;
351
    }
352
353
    /**
354
     * Append data on json response.
355
     *
356
     * @param mixed $key
357
     * @param mixed $value
358
     * @return $this
359
     */
360
    public function with($key, $value = '')
361
    {
362
        if (is_array($key)) {
363
            $this->appends = $key;
364
        } elseif (is_callable($value)) {
365
            $this->appends[$key] = value($value);
366
        } else {
367
            $this->appends[$key] = value($value);
368
        }
369
370
        return $this;
371
    }
372
373
    /**
374
     * Add with query callback value on response.
375
     *
376
     * @param string   $key
377
     * @param callable $value
378
     * @return $this
379
     */
380
    public function withQuery($key, callable $value)
381
    {
382
        $this->appends[$key] = $value;
383
384
        return $this;
385
    }
386
387
    /**
388
     * Override default ordering method with a closure callback.
389
     *
390
     * @param callable $closure
391
     * @return $this
392
     */
393
    public function order(callable $closure)
394
    {
395
        $this->orderCallback = $closure;
396
397
        return $this;
398
    }
399
400
    /**
401
     * Update list of columns that is not allowed for search/sort.
402
     *
403
     * @param  array $blacklist
404
     * @return $this
405
     */
406
    public function blacklist(array $blacklist)
407
    {
408
        $this->columnDef['blacklist'] = $blacklist;
409
410
        return $this;
411
    }
412
413
    /**
414
     * Update list of columns that is allowed for search/sort.
415
     *
416
     * @param  string|array $whitelist
417
     * @return $this
418
     */
419
    public function whitelist($whitelist = '*')
420
    {
421
        $this->columnDef['whitelist'] = $whitelist;
422
423
        return $this;
424
    }
425
426
    /**
427
     * Set smart search config at runtime.
428
     *
429
     * @param bool $bool
430
     * @return $this
431
     */
432
    public function smart($bool = true)
433
    {
434
        $this->config->set(['datatables.search.smart' => $bool]);
435
436
        return $this;
437
    }
438
439
    /**
440
     * Set total records manually.
441
     *
442
     * @param int $total
443
     * @return $this
444
     */
445
    public function setTotalRecords($total)
446
    {
447
        $this->totalRecords = $total;
448
449
        return $this;
450
    }
451
452
    /**
453
     * Set filtered records manually.
454
     *
455
     * @param int $total
456
     * @return $this
457
     */
458
    public function setFilteredRecords($total)
459
    {
460
        $this->filteredRecords = $total;
461
462
        return $this;
463
    }
464
465
    /**
466
     * Skip pagination as needed.
467
     *
468
     * @return $this
469
     */
470
    public function skipPaging()
471
    {
472
        $this->skipPaging = true;
473
474
        return $this;
475
    }
476
477
    /**
478
     * Push a new column name to blacklist.
479
     *
480
     * @param string $column
481
     * @return $this
482
     */
483
    public function pushToBlacklist($column)
484
    {
485
        if (! $this->isBlacklisted($column)) {
486
            $this->columnDef['blacklist'][] = $column;
487
        }
488
489
        return $this;
490
    }
491
492
    /**
493
     * Check if column is blacklisted.
494
     *
495
     * @param string $column
496
     * @return bool
497
     */
498
    protected function isBlacklisted($column)
499
    {
500
        $colDef = $this->getColumnsDefinition();
501
502
        if (in_array($column, $colDef['blacklist'])) {
503
            return true;
504
        }
505
506
        if ($colDef['whitelist'] === '*' || in_array($column, $colDef['whitelist'])) {
507
            return false;
508
        }
509
510
        return true;
511
    }
512
513
    /**
514
     * Get columns definition.
515
     *
516
     * @return array
517
     */
518
    protected function getColumnsDefinition()
519
    {
520
        $config  = $this->config->get('datatables.columns');
521
        $allowed = ['excess', 'escape', 'raw', 'blacklist', 'whitelist'];
522
523
        return array_replace_recursive(array_only($config, $allowed), $this->columnDef);
0 ignored issues
show
Deprecated Code introduced by
The function array_only() has been deprecated with message: Arr::only() should be used directly instead. Will be removed in Laravel 5.9.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
524
    }
525
526
    /**
527
     * Perform sorting of columns.
528
     */
529
    public function ordering()
530
    {
531
        if ($this->orderCallback) {
532
            return call_user_func($this->orderCallback, $this->resolveCallbackParameter());
533
        }
534
535
        return $this->defaultOrdering();
536
    }
537
538
    /**
539
     * Resolve callback parameter instance.
540
     *
541
     * @return mixed
542
     */
543
    abstract protected function resolveCallbackParameter();
544
545
    /**
546
     * Perform default query orderBy clause.
547
     */
548
    abstract protected function defaultOrdering();
549
550
    /**
551
     * Set auto filter off and run your own filter.
552
     * Overrides global search.
553
     *
554
     * @param callable $callback
555
     * @param bool     $globalSearch
556
     * @return $this
557
     */
558
    public function filter(callable $callback, $globalSearch = false)
559
    {
560
        $this->autoFilter      = $globalSearch;
561
        $this->isFilterApplied = true;
562
        $this->filterCallback  = $callback;
563
564
        return $this;
565
    }
566
567
    /**
568
     * Convert instance to array.
569
     *
570
     * @return array
571
     */
572
    public function toArray()
573
    {
574
        return $this->make()->getData(true);
575
    }
576
577
    /**
578
     * Convert the object to its JSON representation.
579
     *
580
     * @param  int $options
581
     * @return \Illuminate\Http\JsonResponse
582
     */
583
    public function toJson($options = 0)
584
    {
585
        if ($options) {
586
            $this->config->set('datatables.json.options', $options);
587
        }
588
589
        return $this->make();
590
    }
591
592
    /**
593
     * Count filtered items.
594
     *
595
     * @return int
596
     */
597
    protected function filteredCount()
598
    {
599
        return $this->filteredRecords ? $this->filteredRecords : $this->count();
600
    }
601
602
    /**
603
     * Perform necessary filters.
604
     *
605
     * @return void
606
     */
607
    protected function filterRecords()
608
    {
609
        if ($this->autoFilter && $this->request->isSearchable()) {
610
            $this->filtering();
611
        }
612
613
        if (is_callable($this->filterCallback)) {
614
            call_user_func($this->filterCallback, $this->resolveCallbackParameter());
615
        }
616
617
        $this->columnSearch();
618
        $this->filteredRecords = $this->isFilterApplied ? $this->filteredCount() : $this->totalRecords;
619
    }
620
621
    /**
622
     * Perform global search.
623
     *
624
     * @return void
625
     */
626
    public function filtering()
627
    {
628
        $keyword = $this->request->keyword();
629
630
        if ($this->config->isMultiTerm()) {
631
            $this->smartGlobalSearch($keyword);
632
633
            return;
634
        }
635
636
        $this->globalSearch($keyword);
637
    }
638
639
    /**
640
     * Perform multi-term search by splitting keyword into
641
     * individual words and searches for each of them.
642
     *
643
     * @param string $keyword
644
     */
645
    protected function smartGlobalSearch($keyword)
646
    {
647
        collect(explode(' ', $keyword))
648
            ->reject(function ($keyword) {
649
                return trim($keyword) === '';
650
            })
651
            ->each(function ($keyword) {
652
                $this->globalSearch($keyword);
653
            });
654
    }
655
656
    /**
657
     * Perform global search for the given keyword.
658
     *
659
     * @param string $keyword
660
     */
661
    abstract protected function globalSearch($keyword);
662
663
    /**
664
     * Apply pagination.
665
     *
666
     * @return void
667
     */
668
    protected function paginate()
669
    {
670
        if ($this->request->isPaginationable() && ! $this->skipPaging) {
671
            $this->paging();
672
        }
673
    }
674
675
    /**
676
     * Transform output.
677
     *
678
     * @param mixed $results
679
     * @param mixed $processed
680
     * @return array
681
     */
682
    protected function transform($results, $processed)
683
    {
684
        if (isset($this->transformer) && class_exists('Yajra\\DataTables\\Transformers\\FractalTransformer')) {
685
            return app('datatables.transformer')->transform(
686
                $results,
687
                $this->transformer,
688
                $this->serializer ?? null
689
            );
690
        }
691
692
        return Helper::transform($processed);
693
    }
694
695
    /**
696
     * Get processed data.
697
     *
698
     * @param mixed $results
699
     * @param bool  $object
700
     * @return array
701
     */
702
    protected function processResults($results, $object = false)
703
    {
704
        $processor = new DataProcessor(
705
            $results,
706
            $this->getColumnsDefinition(),
707
            $this->templates,
708
            $this->request->input('start')
709
        );
710
711
        return $processor->process($object);
712
    }
713
714
    /**
715
     * Render json response.
716
     *
717
     * @param array $data
718
     * @return \Illuminate\Http\JsonResponse
719
     */
720
    protected function render(array $data)
721
    {
722
        $output = $this->attachAppends([
723
            'draw'            => (int) $this->request->input('draw'),
724
            'recordsTotal'    => $this->totalRecords,
725
            'recordsFiltered' => $this->filteredRecords,
726
            'data'            => $data,
727
        ]);
728
729
        if ($this->config->isDebugging()) {
730
            $output = $this->showDebugger($output);
731
        }
732
733
        return new JsonResponse(
734
            $output,
735
            200,
736
            $this->config->get('datatables.json.header', []),
737
            $this->config->get('datatables.json.options', 0)
738
        );
739
    }
740
741
    /**
742
     * Attach custom with meta on response.
743
     *
744
     * @param array $data
745
     * @return array
746
     */
747
    protected function attachAppends(array $data)
748
    {
749
        return array_merge($data, $this->appends);
750
    }
751
752
    /**
753
     * Append debug parameters on output.
754
     *
755
     * @param  array $output
756
     * @return array
757
     */
758
    protected function showDebugger(array $output)
759
    {
760
        $output['input'] = $this->request->all();
761
762
        return $output;
763
    }
764
765
    /**
766
     * Return an error json response.
767
     *
768
     * @param \Exception $exception
769
     * @return \Illuminate\Http\JsonResponse
770
     * @throws \Yajra\DataTables\Exceptions\Exception
771
     */
772
    protected function errorResponse(\Exception $exception)
773
    {
774
        $error = $this->config->get('datatables.error');
775
        $debug = $this->config->get('app.debug');
776
777
        if ($error === 'throw' || (! $error && ! $debug)) {
778
            throw new Exception($exception->getMessage(), $code = 0, $exception);
779
        }
780
781
        $this->getLogger()->error($exception);
782
783
        return new JsonResponse([
784
            'draw'            => (int) $this->request->input('draw'),
785
            'recordsTotal'    => $this->totalRecords,
786
            'recordsFiltered' => 0,
787
            'data'            => [],
788
            'error'           => $error ? __($error) : "Exception Message:\n\n".$exception->getMessage(),
789
        ]);
790
    }
791
792
    /**
793
     * Get monolog/logger instance.
794
     *
795
     * @return \Psr\Log\LoggerInterface
796
     */
797
    public function getLogger()
798
    {
799
        $this->logger = $this->logger ?: app(LoggerInterface::class);
800
801
        return $this->logger;
802
    }
803
804
    /**
805
     * Set monolog/logger instance.
806
     *
807
     * @param \Psr\Log\LoggerInterface $logger
808
     * @return $this
809
     */
810
    public function setLogger(LoggerInterface $logger)
811
    {
812
        $this->logger = $logger;
813
814
        return $this;
815
    }
816
817
    /**
818
     * Setup search keyword.
819
     *
820
     * @param  string $value
821
     * @return string
822
     */
823
    protected function setupKeyword($value)
824
    {
825
        if ($this->config->isSmartSearch()) {
826
            $keyword = '%'.$value.'%';
827
            if ($this->config->isWildcard()) {
828
                $keyword = Helper::wildcardLikeString($value);
829
            }
830
            // remove escaping slash added on js script request
831
            $keyword = str_replace('\\', '%', $keyword);
832
833
            return $keyword;
834
        }
835
836
        return $value;
837
    }
838
839
    /**
840
     * Get column name to be use for filtering and sorting.
841
     *
842
     * @param int  $index
843
     * @param bool $wantsAlias
844
     * @return string
845
     */
846
    protected function getColumnName($index, $wantsAlias = false)
847
    {
848
        $column = $this->request->columnName($index);
849
850
        // DataTables is using make(false)
851
        if (is_numeric($column)) {
852
            $column = $this->getColumnNameByIndex($index);
853
        }
854
855
        if (Str::contains(Str::upper($column), ' AS ')) {
856
            $column = Helper::extractColumnName($column, $wantsAlias);
857
        }
858
859
        return $column;
860
    }
861
862
    /**
863
     * Get column name by order column index.
864
     *
865
     * @param int $index
866
     * @return string
867
     */
868
    protected function getColumnNameByIndex($index)
869
    {
870
        $name = (isset($this->columns[$index]) && $this->columns[$index] != '*')
871
            ? $this->columns[$index] : $this->getPrimaryKeyName();
872
873
        return in_array($name, $this->extraColumns, true) ? $this->getPrimaryKeyName() : $name;
874
    }
875
876
    /**
877
     * If column name could not be resolved then use primary key.
878
     *
879
     * @return string
880
     */
881
    protected function getPrimaryKeyName()
882
    {
883
        return 'id';
884
    }
885
}
886