Completed
Push — master ( f58995...61d3f4 )
by Arjay
01:46
created

BaseEngine::filtering()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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