Completed
Push — master ( e96c6d...2336af )
by Arjay
01:52
created

BaseEngine::filter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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