Completed
Push — master ( 485f62...d2a601 )
by Arjay
02:05 queued 11s
created

DataTableAbstract::setTotalRecords()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
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
        'hidden'      => [],
59
    ];
60
61
    /**
62
     * Extra/Added columns.
63
     *
64
     * @var array
65
     */
66
    protected $extraColumns = [];
67
68
    /**
69
     * Total records.
70
     *
71
     * @var int
72
     */
73
    protected $totalRecords = 0;
74
75
    /**
76
     * Total filtered records.
77
     *
78
     * @var int
79
     */
80
    protected $filteredRecords = 0;
81
82
    /**
83
     * Auto-filter flag.
84
     *
85
     * @var bool
86
     */
87
    protected $autoFilter = true;
88
89
    /**
90
     * Callback to override global search.
91
     *
92
     * @var callable
93
     */
94
    protected $filterCallback;
95
96
    /**
97
     * DT row templates container.
98
     *
99
     * @var array
100
     */
101
    protected $templates = [
102
        'DT_RowId'    => '',
103
        'DT_RowClass' => '',
104
        'DT_RowData'  => [],
105
        'DT_RowAttr'  => [],
106
    ];
107
108
    /**
109
     * [internal] Track if any filter was applied for at least one column.
110
     *
111
     * @var bool
112
     */
113
    protected $isFilterApplied = false;
114
115
    /**
116
     * Custom ordering callback.
117
     *
118
     * @var callable
119
     */
120
    protected $orderCallback;
121
122
    /**
123
     * Skip paginate as needed.
124
     *
125
     * @var bool
126
     */
127
    protected $skipPaging = false;
128
129
    /**
130
     * Array of data to append on json response.
131
     *
132
     * @var array
133
     */
134
    protected $appends = [];
135
136
    /**
137
     * @var \Yajra\DataTables\Utilities\Config
138
     */
139
    protected $config;
140
141
    /**
142
     * @var mixed
143
     */
144
    protected $serializer;
145
146
    /**
147
     * Can the DataTable engine be created with these parameters.
148
     *
149
     * @param mixed $source
150
     * @return bool
151
     */
152
    public static function canCreate($source)
153
    {
154
        return false;
155
    }
156
157
    /**
158
     * Factory method, create and return an instance for the DataTable engine.
159
     *
160
     * @param mixed $source
161
     * @return DataTableAbstract
162
     */
163
    public static function create($source)
164
    {
165
        return new static($source);
166
    }
167
168
    /**
169
     * Add column in collection.
170
     *
171
     * @param string          $name
172
     * @param string|callable $content
173
     * @param bool|int        $order
174
     * @return $this
175
     */
176
    public function addColumn($name, $content, $order = false)
177
    {
178
        $this->extraColumns[] = $name;
179
180
        $this->columnDef['append'][] = ['name' => $name, 'content' => $content, 'order' => $order];
181
182
        return $this;
183
    }
184
185
    /**
186
     * Add DT row index column on response.
187
     *
188
     * @return $this
189
     */
190
    public function addIndexColumn()
191
    {
192
        $this->columnDef['index'] = true;
193
194
        return $this;
195
    }
196
197
    /**
198
     * Edit column's content.
199
     *
200
     * @param string          $name
201
     * @param string|callable $content
202
     * @return $this
203
     */
204
    public function editColumn($name, $content)
205
    {
206
        $this->columnDef['edit'][] = ['name' => $name, 'content' => $content];
207
208
        return $this;
209
    }
210
211
    /**
212
     * Remove column from collection.
213
     *
214
     * @return $this
215
     */
216
    public function removeColumn()
217
    {
218
        $names                     = func_get_args();
219
        $this->columnDef['excess'] = array_merge($this->getColumnsDefinition()['excess'], $names);
220
221
        return $this;
222
    }
223
224
    /**
225
     * Get only selected columns in response.
226
     *
227
     * @param array $columns
228
     * @return $this
229
     */
230
    public function only(array $columns = [])
231
    {
232
        $this->columnDef['only'] = $columns;
233
234
        return $this;
235
    }
236
237
    /**
238
     * Declare columns to escape values.
239
     *
240
     * @param string|array $columns
241
     * @return $this
242
     */
243
    public function escapeColumns($columns = '*')
244
    {
245
        $this->columnDef['escape'] = $columns;
246
247
        return $this;
248
    }
249
250
    /**
251
     * Add a makeHidden() to the row object.
252
     *
253
     * @param array          $attributes
254
     * @return $this
255
     */
256
    public function makeHidden(array $attributes = [])
257
    {
258
        $this->columnDef['hidden'] = array_merge_recursive(array_get($this->columnDef, 'hidden', []), $attributes);
259
260
        return $this;
261
    }
262
263
    /**
264
     * Set columns that should not be escaped.
265
     * Optionally merge the defaults from config.
266
     *
267
     * @param array $columns
268
     * @param bool $merge
269
     * @return $this
270
     */
271
    public function rawColumns(array $columns, $merge = false)
272
    {
273
        if ($merge) {
274
            $config = $this->config->get('datatables.columns');
275
276
            $this->columnDef['raw'] = array_merge($config['raw'], $columns);
277
        } else {
278
            $this->columnDef['raw'] = $columns;
279
        }
280
281
        return $this;
282
    }
283
284
    /**
285
     * Sets DT_RowClass template.
286
     * result: <tr class="output_from_your_template">.
287
     *
288
     * @param string|callable $content
289
     * @return $this
290
     */
291
    public function setRowClass($content)
292
    {
293
        $this->templates['DT_RowClass'] = $content;
294
295
        return $this;
296
    }
297
298
    /**
299
     * Sets DT_RowId template.
300
     * result: <tr id="output_from_your_template">.
301
     *
302
     * @param string|callable $content
303
     * @return $this
304
     */
305
    public function setRowId($content)
306
    {
307
        $this->templates['DT_RowId'] = $content;
308
309
        return $this;
310
    }
311
312
    /**
313
     * Set DT_RowData templates.
314
     *
315
     * @param array $data
316
     * @return $this
317
     */
318
    public function setRowData(array $data)
319
    {
320
        $this->templates['DT_RowData'] = $data;
321
322
        return $this;
323
    }
324
325
    /**
326
     * Add DT_RowData template.
327
     *
328
     * @param string          $key
329
     * @param string|callable $value
330
     * @return $this
331
     */
332
    public function addRowData($key, $value)
333
    {
334
        $this->templates['DT_RowData'][$key] = $value;
335
336
        return $this;
337
    }
338
339
    /**
340
     * Set DT_RowAttr templates.
341
     * result: <tr attr1="attr1" attr2="attr2">.
342
     *
343
     * @param array $data
344
     * @return $this
345
     */
346
    public function setRowAttr(array $data)
347
    {
348
        $this->templates['DT_RowAttr'] = $data;
349
350
        return $this;
351
    }
352
353
    /**
354
     * Add DT_RowAttr template.
355
     *
356
     * @param string          $key
357
     * @param string|callable $value
358
     * @return $this
359
     */
360
    public function addRowAttr($key, $value)
361
    {
362
        $this->templates['DT_RowAttr'][$key] = $value;
363
364
        return $this;
365
    }
366
367
    /**
368
     * Append data on json response.
369
     *
370
     * @param mixed $key
371
     * @param mixed $value
372
     * @return $this
373
     */
374
    public function with($key, $value = '')
375
    {
376
        if (is_array($key)) {
377
            $this->appends = $key;
378
        } elseif (is_callable($value)) {
379
            $this->appends[$key] = value($value);
380
        } else {
381
            $this->appends[$key] = value($value);
382
        }
383
384
        return $this;
385
    }
386
387
    /**
388
     * Add with query callback value on response.
389
     *
390
     * @param string   $key
391
     * @param callable $value
392
     * @return $this
393
     */
394
    public function withQuery($key, callable $value)
395
    {
396
        $this->appends[$key] = $value;
397
398
        return $this;
399
    }
400
401
    /**
402
     * Override default ordering method with a closure callback.
403
     *
404
     * @param callable $closure
405
     * @return $this
406
     */
407
    public function order(callable $closure)
408
    {
409
        $this->orderCallback = $closure;
410
411
        return $this;
412
    }
413
414
    /**
415
     * Update list of columns that is not allowed for search/sort.
416
     *
417
     * @param  array $blacklist
418
     * @return $this
419
     */
420
    public function blacklist(array $blacklist)
421
    {
422
        $this->columnDef['blacklist'] = $blacklist;
423
424
        return $this;
425
    }
426
427
    /**
428
     * Update list of columns that is allowed for search/sort.
429
     *
430
     * @param  string|array $whitelist
431
     * @return $this
432
     */
433
    public function whitelist($whitelist = '*')
434
    {
435
        $this->columnDef['whitelist'] = $whitelist;
436
437
        return $this;
438
    }
439
440
    /**
441
     * Set smart search config at runtime.
442
     *
443
     * @param bool $state
444
     * @return $this
445
     */
446
    public function smart($state = true)
447
    {
448
        $this->config->set('datatables.search.smart', $state);
449
450
        return $this;
451
    }
452
453
    /**
454
     * Set starts_with search config at runtime.
455
     *
456
     * @param bool $state
457
     * @return $this
458
     */
459
    public function startsWithSearch($state = true)
460
    {
461
        $this->config->set('datatables.search.starts_with', $state);
462
463
        return $this;
464
    }
465
466
    /**
467
     * Set total records manually.
468
     *
469
     * @param int $total
470
     * @return $this
471
     */
472
    public function setTotalRecords($total)
473
    {
474
        $this->totalRecords = $total;
475
476
        return $this;
477
    }
478
479
    /**
480
     * Set filtered records manually.
481
     *
482
     * @param int $total
483
     * @return $this
484
     */
485
    public function setFilteredRecords($total)
486
    {
487
        $this->filteredRecords = $total;
488
489
        return $this;
490
    }
491
492
    /**
493
     * Skip pagination as needed.
494
     *
495
     * @return $this
496
     */
497
    public function skipPaging()
498
    {
499
        $this->skipPaging = true;
500
501
        return $this;
502
    }
503
504
    /**
505
     * Push a new column name to blacklist.
506
     *
507
     * @param string $column
508
     * @return $this
509
     */
510
    public function pushToBlacklist($column)
511
    {
512
        if (! $this->isBlacklisted($column)) {
513
            $this->columnDef['blacklist'][] = $column;
514
        }
515
516
        return $this;
517
    }
518
519
    /**
520
     * Check if column is blacklisted.
521
     *
522
     * @param string $column
523
     * @return bool
524
     */
525
    protected function isBlacklisted($column)
526
    {
527
        $colDef = $this->getColumnsDefinition();
528
529
        if (in_array($column, $colDef['blacklist'])) {
530
            return true;
531
        }
532
533
        if ($colDef['whitelist'] === '*' || in_array($column, $colDef['whitelist'])) {
534
            return false;
535
        }
536
537
        return true;
538
    }
539
540
    /**
541
     * Get columns definition.
542
     *
543
     * @return array
544
     */
545
    protected function getColumnsDefinition()
546
    {
547
        $config  = $this->config->get('datatables.columns');
548
        $allowed = ['excess', 'escape', 'raw', 'blacklist', 'whitelist'];
549
550
        return array_replace_recursive(array_only($config, $allowed), $this->columnDef);
551
    }
552
553
    /**
554
     * Perform sorting of columns.
555
     */
556
    public function ordering()
557
    {
558
        if ($this->orderCallback) {
559
            return call_user_func($this->orderCallback, $this->resolveCallbackParameter());
560
        }
561
562
        return $this->defaultOrdering();
563
    }
564
565
    /**
566
     * Resolve callback parameter instance.
567
     *
568
     * @return mixed
569
     */
570
    abstract protected function resolveCallbackParameter();
571
572
    /**
573
     * Perform default query orderBy clause.
574
     */
575
    abstract protected function defaultOrdering();
576
577
    /**
578
     * Set auto filter off and run your own filter.
579
     * Overrides global search.
580
     *
581
     * @param callable $callback
582
     * @param bool     $globalSearch
583
     * @return $this
584
     */
585
    public function filter(callable $callback, $globalSearch = false)
586
    {
587
        $this->autoFilter      = $globalSearch;
588
        $this->isFilterApplied = true;
589
        $this->filterCallback  = $callback;
590
591
        return $this;
592
    }
593
594
    /**
595
     * Convert instance to array.
596
     *
597
     * @return array
598
     */
599
    public function toArray()
600
    {
601
        return $this->make()->getData(true);
602
    }
603
604
    /**
605
     * Convert the object to its JSON representation.
606
     *
607
     * @param  int $options
608
     * @return \Illuminate\Http\JsonResponse
609
     */
610
    public function toJson($options = 0)
611
    {
612
        if ($options) {
613
            $this->config->set('datatables.json.options', $options);
614
        }
615
616
        return $this->make();
617
    }
618
619
    /**
620
     * Count filtered items.
621
     *
622
     * @return int
623
     */
624
    protected function filteredCount()
625
    {
626
        return $this->filteredRecords ? $this->filteredRecords : $this->count();
627
    }
628
629
    /**
630
     * Perform necessary filters.
631
     *
632
     * @return void
633
     */
634
    protected function filterRecords()
635
    {
636
        if ($this->autoFilter && $this->request->isSearchable()) {
637
            $this->filtering();
638
        }
639
640
        if (is_callable($this->filterCallback)) {
641
            call_user_func($this->filterCallback, $this->resolveCallbackParameter());
642
        }
643
644
        $this->columnSearch();
645
        $this->filteredRecords = $this->isFilterApplied ? $this->filteredCount() : $this->totalRecords;
646
    }
647
648
    /**
649
     * Perform global search.
650
     *
651
     * @return void
652
     */
653
    public function filtering()
654
    {
655
        $keyword = $this->request->keyword();
656
657
        if ($this->config->isMultiTerm()) {
658
            $this->smartGlobalSearch($keyword);
659
660
            return;
661
        }
662
663
        $this->globalSearch($keyword);
664
    }
665
666
    /**
667
     * Perform multi-term search by splitting keyword into
668
     * individual words and searches for each of them.
669
     *
670
     * @param string $keyword
671
     */
672
    protected function smartGlobalSearch($keyword)
673
    {
674
        collect(explode(' ', $keyword))
675
            ->reject(function ($keyword) {
676
                return trim($keyword) === '';
677
            })
678
            ->each(function ($keyword) {
679
                $this->globalSearch($keyword);
680
            });
681
    }
682
683
    /**
684
     * Perform global search for the given keyword.
685
     *
686
     * @param string $keyword
687
     */
688
    abstract protected function globalSearch($keyword);
689
690
    /**
691
     * Apply pagination.
692
     *
693
     * @return void
694
     */
695
    protected function paginate()
696
    {
697
        if ($this->request->isPaginationable() && ! $this->skipPaging) {
698
            $this->paging();
699
        }
700
    }
701
702
    /**
703
     * Transform output.
704
     *
705
     * @param mixed $results
706
     * @param mixed $processed
707
     * @return array
708
     */
709
    protected function transform($results, $processed)
710
    {
711
        if (isset($this->transformer) && class_exists('Yajra\\DataTables\\Transformers\\FractalTransformer')) {
712
            return app('datatables.transformer')->transform(
713
                $results,
714
                $this->transformer,
715
                $this->serializer ?? null
716
            );
717
        }
718
719
        return Helper::transform($processed);
720
    }
721
722
    /**
723
     * Get processed data.
724
     *
725
     * @param mixed $results
726
     * @param bool  $object
727
     * @return array
728
     */
729
    protected function processResults($results, $object = false)
730
    {
731
        $processor = new DataProcessor(
732
            $results,
733
            $this->getColumnsDefinition(),
734
            $this->templates,
735
            $this->request->input('start')
736
        );
737
738
        return $processor->process($object);
739
    }
740
741
    /**
742
     * Render json response.
743
     *
744
     * @param array $data
745
     * @return \Illuminate\Http\JsonResponse
746
     */
747
    protected function render(array $data)
748
    {
749
        $output = $this->attachAppends([
750
            'draw'            => (int) $this->request->input('draw'),
751
            'recordsTotal'    => $this->totalRecords,
752
            'recordsFiltered' => $this->filteredRecords,
753
            'data'            => $data,
754
        ]);
755
756
        if ($this->config->isDebugging()) {
757
            $output = $this->showDebugger($output);
758
        }
759
760
        return new JsonResponse(
761
            $output,
762
            200,
763
            $this->config->get('datatables.json.header', []),
764
            $this->config->get('datatables.json.options', 0)
765
        );
766
    }
767
768
    /**
769
     * Attach custom with meta on response.
770
     *
771
     * @param array $data
772
     * @return array
773
     */
774
    protected function attachAppends(array $data)
775
    {
776
        return array_merge($data, $this->appends);
777
    }
778
779
    /**
780
     * Append debug parameters on output.
781
     *
782
     * @param  array $output
783
     * @return array
784
     */
785
    protected function showDebugger(array $output)
786
    {
787
        $output['input'] = $this->request->all();
788
789
        return $output;
790
    }
791
792
    /**
793
     * Return an error json response.
794
     *
795
     * @param \Exception $exception
796
     * @return \Illuminate\Http\JsonResponse
797
     * @throws \Yajra\DataTables\Exceptions\Exception
798
     */
799
    protected function errorResponse(\Exception $exception)
800
    {
801
        $error = $this->config->get('datatables.error');
802
        $debug = $this->config->get('app.debug');
803
804
        if ($error === 'throw' || (! $error && ! $debug)) {
805
            throw new Exception($exception->getMessage(), $code = 0, $exception);
806
        }
807
808
        $this->getLogger()->error($exception);
809
810
        return new JsonResponse([
811
            'draw'            => (int) $this->request->input('draw'),
812
            'recordsTotal'    => $this->totalRecords,
813
            'recordsFiltered' => 0,
814
            'data'            => [],
815
            'error'           => $error ? __($error) : "Exception Message:\n\n".$exception->getMessage(),
816
        ]);
817
    }
818
819
    /**
820
     * Get monolog/logger instance.
821
     *
822
     * @return \Psr\Log\LoggerInterface
823
     */
824
    public function getLogger()
825
    {
826
        $this->logger = $this->logger ?: app(LoggerInterface::class);
827
828
        return $this->logger;
829
    }
830
831
    /**
832
     * Set monolog/logger instance.
833
     *
834
     * @param \Psr\Log\LoggerInterface $logger
835
     * @return $this
836
     */
837
    public function setLogger(LoggerInterface $logger)
838
    {
839
        $this->logger = $logger;
840
841
        return $this;
842
    }
843
844
    /**
845
     * Setup search keyword.
846
     *
847
     * @param  string $value
848
     * @return string
849
     */
850
    protected function setupKeyword($value)
851
    {
852
        if ($this->config->isSmartSearch()) {
853
            $keyword = '%'.$value.'%';
854
            if ($this->config->isWildcard()) {
855
                $keyword = Helper::wildcardLikeString($value);
856
            }
857
            // remove escaping slash added on js script request
858
            $keyword = str_replace('\\', '%', $keyword);
859
860
            return $keyword;
861
        }
862
863
        return $value;
864
    }
865
866
    /**
867
     * Get column name to be use for filtering and sorting.
868
     *
869
     * @param int  $index
870
     * @param bool $wantsAlias
871
     * @return string
872
     */
873
    protected function getColumnName($index, $wantsAlias = false)
874
    {
875
        $column = $this->request->columnName($index);
876
877
        // DataTables is using make(false)
878
        if (is_numeric($column)) {
879
            $column = $this->getColumnNameByIndex($index);
880
        }
881
882
        if (Str::contains(Str::upper($column), ' AS ')) {
883
            $column = Helper::extractColumnName($column, $wantsAlias);
884
        }
885
886
        return $column;
887
    }
888
889
    /**
890
     * Get column name by order column index.
891
     *
892
     * @param int $index
893
     * @return string
894
     */
895
    protected function getColumnNameByIndex($index)
896
    {
897
        $name = (isset($this->columns[$index]) && $this->columns[$index] != '*')
898
            ? $this->columns[$index] : $this->getPrimaryKeyName();
899
900
        return in_array($name, $this->extraColumns, true) ? $this->getPrimaryKeyName() : $name;
901
    }
902
903
    /**
904
     * If column name could not be resolved then use primary key.
905
     *
906
     * @return string
907
     */
908
    protected function getPrimaryKeyName()
909
    {
910
        return 'id';
911
    }
912
}
913