Completed
Push — master ( 5e5621...e7f8c9 )
by Arjay
02:02
created

BaseEngine::processResults()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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