Completed
Push — master ( 5addd2...7300e4 )
by Arjay
01:36
created

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

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
762
    }
763
764
    /**
765
     * Convert the object to its JSON representation.
766
     *
767
     * @param  int $options
768
     * @return string
769
     */
770
    public function toJson($options = 0)
771
    {
772
        if ($options) {
773
            $this->config->set('datatables.json.options', $options);
774
        }
775
776
        return $this->make();
777
    }
778
}
779