Completed
Push — master ( 45b368...c11199 )
by Arjay
02:00
created

DataTableAbstract::withQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 6
rs 9.4285
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
    ];
58
59
    /**
60
     * Extra/Added columns.
61
     *
62
     * @var array
63
     */
64
    protected $extraColumns = [];
65
66
    /**
67
     * Total records.
68
     *
69
     * @var int
70
     */
71
    protected $totalRecords = 0;
72
73
    /**
74
     * Total filtered records.
75
     *
76
     * @var int
77
     */
78
    protected $filteredRecords = 0;
79
80
    /**
81
     * Auto-filter flag.
82
     *
83
     * @var bool
84
     */
85
    protected $autoFilter = true;
86
87
    /**
88
     * Callback to override global search.
89
     *
90
     * @var callable
91
     */
92
    protected $filterCallback;
93
94
    /**
95
     * DT row templates container.
96
     *
97
     * @var array
98
     */
99
    protected $templates = [
100
        'DT_RowId'    => '',
101
        'DT_RowClass' => '',
102
        'DT_RowData'  => [],
103
        'DT_RowAttr'  => [],
104
    ];
105
106
    /**
107
     * [internal] Track if any filter was applied for at least one column.
108
     *
109
     * @var bool
110
     */
111
    protected $isFilterApplied = false;
112
113
    /**
114
     * Custom ordering callback.
115
     *
116
     * @var callable
117
     */
118
    protected $orderCallback;
119
120
    /**
121
     * Skip paginate as needed.
122
     *
123
     * @var bool
124
     */
125
    protected $skipPaging = false;
126
127
    /**
128
     * Array of data to append on json response.
129
     *
130
     * @var array
131
     */
132
    protected $appends = [];
133
134
    /**
135
     * @var \Yajra\DataTables\Utilities\Config
136
     */
137
    protected $config;
138
139
    /**
140
     * @var mixed
141
     */
142
    protected $serializer;
143
144
    /**
145
     * Can the DataTable engine be created with these parameters.
146
     *
147
     * @param mixed $source
148
     * @return bool
149
     */
150
    public static function canCreate($source)
151
    {
152
        return false;
153
    }
154
155
    /**
156
     * Factory method, create and return an instance for the DataTable engine.
157
     *
158
     * @param mixed $source
159
     * @return DataTableAbstract
160
     */
161
    public static function create($source)
162
    {
163
        return new static($source);
0 ignored issues
show
Unused Code introduced by
The call to DataTableAbstract::__construct() has too many arguments starting with $source.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

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