Completed
Push — master ( 2a9bf3...618bf6 )
by Arjay
01:57
created

BaseEngine::editColumn()   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\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
13
/**
14
 * Class BaseEngine.
15
 *
16
 * @package Yajra\Datatables\Engines
17
 * @author  Arjay Angeles <[email protected]>
18
 */
19
abstract class BaseEngine implements DataTableEngine
20
{
21
    /**
22
     * Datatables Request object.
23
     *
24
     * @var \Yajra\Datatables\Request
25
     */
26
    public $request;
27
28
    /**
29
     * @var \Illuminate\Contracts\Logging\Log
30
     */
31
    protected $logger;
32
33
    /**
34
     * Array of result columns/fields.
35
     *
36
     * @var array
37
     */
38
    protected $columns = [];
39
40
    /**
41
     * DT columns definitions container (add/edit/remove/filter/order/escape).
42
     *
43
     * @var array
44
     */
45
    protected $columnDef = [
46
        'index'  => false,
47
        'append' => [],
48
        'edit'   => [],
49
        'filter' => [],
50
        'order'  => [],
51
    ];
52
53
    /**
54
     * Extra/Added columns.
55
     *
56
     * @var array
57
     */
58
    protected $extraColumns = [];
59
60
    /**
61
     * Total records.
62
     *
63
     * @var int
64
     */
65
    protected $totalRecords = 0;
66
67
    /**
68
     * Total filtered records.
69
     *
70
     * @var int
71
     */
72
    protected $filteredRecords = 0;
73
74
    /**
75
     * Auto-filter flag.
76
     *
77
     * @var bool
78
     */
79
    protected $autoFilter = true;
80
81
    /**
82
     * Callback to override global search.
83
     *
84
     * @var callable
85
     */
86
    protected $filterCallback;
87
88
    /**
89
     * Parameters to passed on filterCallback.
90
     *
91
     * @var mixed
92
     */
93
    protected $filterCallbackParameters;
94
95
    /**
96
     * DT row templates container.
97
     *
98
     * @var array
99
     */
100
    protected $templates = [
101
        'DT_RowId'    => '',
102
        'DT_RowClass' => '',
103
        'DT_RowData'  => [],
104
        'DT_RowAttr'  => [],
105
    ];
106
107
    /**
108
     * Output transformer.
109
     *
110
     * @var \League\Fractal\TransformerAbstract
111
     */
112
    protected $transformer = null;
113
114
    /**
115
     * [internal] Track if any filter was applied for at least one column.
116
     *
117
     * @var boolean
118
     */
119
    protected $isFilterApplied = false;
120
121
    /**
122
     * Fractal serializer class.
123
     *
124
     * @var string|null
125
     */
126
    protected $serializer = null;
127
128
    /**
129
     * Custom ordering callback.
130
     *
131
     * @var callable
132
     */
133
    protected $orderCallback;
134
135
    /**
136
     * Skip paginate as needed.
137
     *
138
     * @var bool
139
     */
140
    protected $skipPaging = false;
141
142
    /**
143
     * Array of data to append on json response.
144
     *
145
     * @var array
146
     */
147
    protected $appends = [];
148
149
    /**
150
     * Flag for ordering NULLS LAST option.
151
     *
152
     * @var bool
153
     */
154
    protected $nullsLast = false;
155
156
    /**
157
     * Add column in collection.
158
     *
159
     * @param string          $name
160
     * @param string|callable $content
161
     * @param bool|int        $order
162
     * @return $this
163
     */
164
    public function addColumn($name, $content, $order = false)
165
    {
166
        $this->extraColumns[] = $name;
167
168
        $this->columnDef['append'][] = ['name' => $name, 'content' => $content, 'order' => $order];
169
170
        return $this;
171
    }
172
173
    /**
174
     * Add DT row index column on response.
175
     *
176
     * @return $this
177
     */
178
    public function addIndexColumn()
179
    {
180
        $this->columnDef['index'] = true;
181
182
        return $this;
183
    }
184
185
    /**
186
     * Edit column's content.
187
     *
188
     * @param string          $name
189
     * @param string|callable $content
190
     * @return $this
191
     */
192
    public function editColumn($name, $content)
193
    {
194
        $this->columnDef['edit'][] = ['name' => $name, 'content' => $content];
195
196
        return $this;
197
    }
198
199
    /**
200
     * Remove column from collection.
201
     *
202
     * @return $this
203
     */
204
    public function removeColumn()
205
    {
206
        $names                     = func_get_args();
207
        $this->columnDef['excess'] = array_merge($this->columnDef['excess'], $names);
208
209
        return $this;
210
    }
211
212
    /**
213
     * Declare columns to escape values.
214
     *
215
     * @param string|array $columns
216
     * @return $this
217
     */
218
    public function escapeColumns($columns = '*')
219
    {
220
        $this->columnDef['escape'] = $columns;
221
222
        return $this;
223
    }
224
225
    /**
226
     * Set columns that should not be escaped.
227
     *
228
     * @param array $columns
229
     * @return $this
230
     */
231
    public function rawColumns(array $columns)
232
    {
233
        $this->columnDef['raw'] = $columns;
234
235
        return $this;
236
    }
237
238
    /**
239
     * Sets DT_RowClass template.
240
     * result: <tr class="output_from_your_template">.
241
     *
242
     * @param string|callable $content
243
     * @return $this
244
     */
245
    public function setRowClass($content)
246
    {
247
        $this->templates['DT_RowClass'] = $content;
248
249
        return $this;
250
    }
251
252
    /**
253
     * Sets DT_RowId template.
254
     * result: <tr id="output_from_your_template">.
255
     *
256
     * @param string|callable $content
257
     * @return $this
258
     */
259
    public function setRowId($content)
260
    {
261
        $this->templates['DT_RowId'] = $content;
262
263
        return $this;
264
    }
265
266
    /**
267
     * Set DT_RowData templates.
268
     *
269
     * @param array $data
270
     * @return $this
271
     */
272
    public function setRowData(array $data)
273
    {
274
        $this->templates['DT_RowData'] = $data;
275
276
        return $this;
277
    }
278
279
    /**
280
     * Add DT_RowData template.
281
     *
282
     * @param string          $key
283
     * @param string|callable $value
284
     * @return $this
285
     */
286
    public function addRowData($key, $value)
287
    {
288
        $this->templates['DT_RowData'][$key] = $value;
289
290
        return $this;
291
    }
292
293
    /**
294
     * Set DT_RowAttr templates.
295
     * result: <tr attr1="attr1" attr2="attr2">.
296
     *
297
     * @param array $data
298
     * @return $this
299
     */
300
    public function setRowAttr(array $data)
301
    {
302
        $this->templates['DT_RowAttr'] = $data;
303
304
        return $this;
305
    }
306
307
    /**
308
     * Add DT_RowAttr template.
309
     *
310
     * @param string          $key
311
     * @param string|callable $value
312
     * @return $this
313
     */
314
    public function addRowAttr($key, $value)
315
    {
316
        $this->templates['DT_RowAttr'][$key] = $value;
317
318
        return $this;
319
    }
320
321
    /**
322
     * Add custom filter handler for the give column.
323
     *
324
     * @param string   $column
325
     * @param callable $callback
326
     * @return $this
327
     */
328
    public function filterColumn($column, callable $callback)
329
    {
330
        $this->columnDef['filter'][$column] = ['method' => $callback];
331
332
        return $this;
333
    }
334
335
    /**
336
     * Order each given columns versus the given custom sql.
337
     *
338
     * @param array  $columns
339
     * @param string $sql
340
     * @param array  $bindings
341
     * @return $this
342
     */
343
    public function orderColumns(array $columns, $sql, $bindings = [])
344
    {
345
        foreach ($columns as $column) {
346
            $this->orderColumn($column, str_replace(':column', $column, $sql), $bindings);
347
        }
348
349
        return $this;
350
    }
351
352
    /**
353
     * Override default column ordering.
354
     *
355
     * @param string $column
356
     * @param string $sql
357
     * @param array  $bindings
358
     * @return $this
359
     * @internal string $1 Special variable that returns the requested order direction of the column.
360
     */
361
    public function orderColumn($column, $sql, $bindings = [])
362
    {
363
        $this->columnDef['order'][$column] = compact('sql', 'bindings');
364
365
        return $this;
366
    }
367
368
    /**
369
     * Set data output transformer.
370
     *
371
     * @param \League\Fractal\TransformerAbstract $transformer
372
     * @return $this
373
     */
374
    public function setTransformer($transformer)
375
    {
376
        $this->transformer = $transformer;
377
378
        return $this;
379
    }
380
381
    /**
382
     * Set fractal serializer class.
383
     *
384
     * @param string $serializer
385
     * @return $this
386
     */
387
    public function setSerializer($serializer)
388
    {
389
        $this->serializer = $serializer;
390
391
        return $this;
392
    }
393
394
    /**
395
     * Check if config uses case insensitive search.
396
     *
397
     * @return bool
398
     */
399
    public function isCaseInsensitive()
400
    {
401
        return config('datatables.search.case_insensitive', false);
402
    }
403
404
    /**
405
     * Append data on json response.
406
     *
407
     * @param mixed $key
408
     * @param mixed $value
409
     * @return $this
410
     */
411
    public function with($key, $value = '')
412
    {
413
        if (is_array($key)) {
414
            $this->appends = $key;
415
        } elseif (is_callable($value)) {
416
            $this->appends[$key] = value($value);
417
        } else {
418
            $this->appends[$key] = value($value);
419
        }
420
421
        return $this;
422
    }
423
424
    /**
425
     * Override default ordering method with a closure callback.
426
     *
427
     * @param callable $closure
428
     * @return $this
429
     */
430
    public function order(callable $closure)
431
    {
432
        $this->orderCallback = $closure;
433
434
        return $this;
435
    }
436
437
    /**
438
     * Update list of columns that is not allowed for search/sort.
439
     *
440
     * @param  array $blacklist
441
     * @return $this
442
     */
443
    public function blacklist(array $blacklist)
444
    {
445
        $this->columnDef['blacklist'] = $blacklist;
446
447
        return $this;
448
    }
449
450
    /**
451
     * Update list of columns that is allowed for search/sort.
452
     *
453
     * @param  string|array $whitelist
454
     * @return $this
455
     */
456
    public function whitelist($whitelist = '*')
457
    {
458
        $this->columnDef['whitelist'] = $whitelist;
459
460
        return $this;
461
    }
462
463
    /**
464
     * Set smart search config at runtime.
465
     *
466
     * @param bool $bool
467
     * @return $this
468
     */
469
    public function smart($bool = true)
470
    {
471
        config(['datatables.search.smart' => $bool]);
472
473
        return $this;
474
    }
475
476
    /**
477
     * Set total records manually.
478
     *
479
     * @param int $total
480
     * @return $this
481
     */
482
    public function setTotalRecords($total)
483
    {
484
        $this->totalRecords = $total;
485
486
        return $this;
487
    }
488
489
    /**
490
     * Skip pagination as needed.
491
     *
492
     * @return $this
493
     */
494
    public function skipPaging()
495
    {
496
        $this->skipPaging = true;
497
498
        return $this;
499
    }
500
501
    /**
502
     * Set datatables to do ordering with NULLS LAST option.
503
     *
504
     * @return $this
505
     */
506
    public function orderByNullsLast()
507
    {
508
        $this->nullsLast = true;
509
510
        return $this;
511
    }
512
513
    /**
514
     * Push a new column name to blacklist.
515
     *
516
     * @param string $column
517
     * @return $this
518
     */
519
    public function pushToBlacklist($column)
520
    {
521
        if (!$this->isBlacklisted($column)) {
522
            $this->columnDef['blacklist'][] = $column;
523
        }
524
525
        return $this;
526
    }
527
528
    /**
529
     * Check if column is blacklisted.
530
     *
531
     * @param string $column
532
     * @return bool
533
     */
534
    protected function isBlacklisted($column)
535
    {
536
        $colDef = $this->getColumnsDefinition();
537
538
        if (in_array($column, $colDef['blacklist'])) {
539
            return true;
540
        }
541
542
        if ($colDef['whitelist'] === '*' || in_array($column, $colDef['whitelist'])) {
543
            return false;
544
        }
545
546
        return true;
547
    }
548
549
    /**
550
     * Perform necessary filters.
551
     *
552
     * @return void
553
     */
554
    protected function filterRecords()
555
    {
556
        if ($this->autoFilter && $this->request->isSearchable()) {
557
            $this->filtering();
558
        }
559
560
        if (is_callable($this->filterCallback)) {
561
            call_user_func($this->filterCallback, $this->filterCallbackParameters);
562
        }
563
564
        $this->columnSearch();
565
        $this->filteredRecords = $this->isFilterApplied ? $this->count() : $this->totalRecords;
566
    }
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
        $fractal = app('datatables.fractal');
593
594
        if ($this->serializer) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->serializer of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
595
            $fractal->setSerializer($this->createSerializer());
596
        }
597
598
        //Get transformer reflection
599
        //Firs method parameter should be data/object to transform
600
        $reflection = new \ReflectionMethod($this->transformer, 'transform');
601
        $parameter  = $reflection->getParameters()[0];
602
603
        //If parameter is class assuming it requires object
604
        //Else just pass array by default
605
        if ($parameter->getClass()) {
606
            $resource = new Collection($output, $this->createTransformer());
607
        } else {
608
            $resource = new Collection(
609
                $output,
610
                $this->createTransformer()
611
            );
612
        }
613
614
        $collection = $fractal->createData($resource)->toArray();
615
616
        return $collection['data'];
617
    }
618
619
    /**
620
     * Get or create transformer serializer instance.
621
     *
622
     * @return \League\Fractal\Serializer\SerializerAbstract
623
     */
624
    protected function createSerializer()
625
    {
626
        if ($this->serializer instanceof \League\Fractal\Serializer\SerializerAbstract) {
0 ignored issues
show
Bug introduced by
The class League\Fractal\Serializer\SerializerAbstract does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
627
            return $this->serializer;
628
        }
629
630
        return new $this->serializer();
631
    }
632
633
    /**
634
     * Get or create transformer instance.
635
     *
636
     * @return \League\Fractal\TransformerAbstract
637
     */
638
    protected function createTransformer()
639
    {
640
        if ($this->transformer instanceof \League\Fractal\TransformerAbstract) {
0 ignored issues
show
Bug introduced by
The class League\Fractal\TransformerAbstract does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
641
            return $this->transformer;
642
        }
643
644
        return new $this->transformer();
645
    }
646
647
    /**
648
     * Get processed data.
649
     *
650
     * @param bool|false $object
651
     * @return array
652
     */
653
    protected function getProcessedData($object = false)
654
    {
655
        $processor = new DataProcessor(
656
            $this->results(),
657
            $this->getColumnsDefinition(),
658
            $this->templates,
659
            $this->request->input('start')
660
        );
661
662
        return $processor->process($object);
663
    }
664
665
    /**
666
     * Get columns definition.
667
     *
668
     * @return array
669
     */
670
    protected function getColumnsDefinition()
671
    {
672
        $config  = config('datatables.columns');
673
        $allowed = ['excess', 'escape', 'raw', 'blacklist', 'whitelist'];
674
675
        return array_merge(array_only($config, $allowed), $this->columnDef);
676
    }
677
678
    /**
679
     * Render json response.
680
     *
681
     * @param array $data
682
     * @return \Illuminate\Http\JsonResponse
683
     */
684
    protected function render(array $data)
685
    {
686
        $output = array_merge([
687
            'draw'            => (int) $this->request->input('draw'),
688
            'recordsTotal'    => $this->totalRecords,
689
            'recordsFiltered' => $this->filteredRecords,
690
            'data'            => $data,
691
        ], $this->appends);
692
693
        if ($this->isDebugging()) {
694
            $output = $this->showDebugger($output);
695
        }
696
697
        return new JsonResponse(
698
            $output,
699
            200,
700
            config('datatables.json.header', []),
701
            config('datatables.json.options', 0)
702
        );
703
    }
704
705
    /**
706
     * Check if app is in debug mode.
707
     *
708
     * @return bool
709
     */
710
    public function isDebugging()
711
    {
712
        return config('app.debug', false);
713
    }
714
715
    /**
716
     * Append debug parameters on output.
717
     *
718
     * @param  array $output
719
     * @return array
720
     */
721
    abstract protected function showDebugger(array $output);
722
723
    /**
724
     * Return an error json response.
725
     *
726
     * @param \Exception $exception
727
     * @return \Illuminate\Http\JsonResponse
728
     * @throws \Yajra\Datatables\Exception
729
     */
730
    protected function errorResponse(\Exception $exception)
731
    {
732
        $error = config('datatables.error');
733
        if ($error === 'throw') {
734
            throw new Exception($exception->getMessage(), $code = 0, $exception);
735
        }
736
737
        $this->getLogger()->error($exception);
738
739
        return new JsonResponse([
740
            'draw'            => (int) $this->request->input('draw'),
741
            'recordsTotal'    => (int) $this->totalRecords,
742
            'recordsFiltered' => 0,
743
            'data'            => [],
744
            'error'           => $error ? __($error) : "Exception Message:\n\n" . $exception->getMessage(),
745
        ]);
746
    }
747
748
    /**
749
     * Get monolog/logger instance.
750
     *
751
     * @return \Illuminate\Contracts\Logging\Log
752
     */
753
    public function getLogger()
754
    {
755
        $this->logger = $this->logger ?: resolve(Log::class);
756
757
        return $this->logger;
758
    }
759
760
    /**
761
     * Set monolog/logger instance.
762
     *
763
     * @param \Illuminate\Contracts\Logging\Log $logger
764
     * @return $this
765
     */
766
    public function setLogger(Log $logger)
767
    {
768
        $this->logger = $logger;
769
770
        return $this;
771
    }
772
773
    /**
774
     * Setup search keyword.
775
     *
776
     * @param  string $value
777
     * @return string
778
     */
779
    protected function setupKeyword($value)
780
    {
781
        if ($this->isSmartSearch()) {
782
            $keyword = '%' . $value . '%';
783
            if ($this->isWildcard()) {
784
                $keyword = $this->wildcardLikeString($value);
785
            }
786
            // remove escaping slash added on js script request
787
            $keyword = str_replace('\\', '%', $keyword);
788
789
            return $keyword;
790
        }
791
792
        return $value;
793
    }
794
795
    /**
796
     * Check if config uses smart search.
797
     *
798
     * @return bool
799
     */
800
    public function isSmartSearch()
801
    {
802
        return config('datatables.search.smart', true);
803
    }
804
805
    /**
806
     * Check if config uses wild card search.
807
     *
808
     * @return bool
809
     */
810
    public function isWildcard()
811
    {
812
        return config('datatables.search.use_wildcards', false);
813
    }
814
815
    /**
816
     * Adds % wildcards to the given string.
817
     *
818
     * @param string $str
819
     * @param bool   $lowercase
820
     * @return string
821
     */
822
    protected function wildcardLikeString($str, $lowercase = true)
823
    {
824
        $wild  = '%';
825
        $chars = preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY);
826
827
        if (count($chars) > 0) {
828
            foreach ($chars as $char) {
829
                $wild .= $char . '%';
830
            }
831
        }
832
833
        if ($lowercase) {
834
            $wild = Str::lower($wild);
835
        }
836
837
        return $wild;
838
    }
839
840
    /**
841
     * Update flags to disable global search.
842
     *
843
     * @param  callable $callback
844
     * @param  mixed    $parameters
845
     * @param  bool     $autoFilter
846
     */
847
    protected function overrideGlobalSearch(callable $callback, $parameters, $autoFilter = false)
848
    {
849
        $this->autoFilter               = $autoFilter;
850
        $this->isFilterApplied          = true;
851
        $this->filterCallback           = $callback;
852
        $this->filterCallbackParameters = $parameters;
853
    }
854
855
    /**
856
     * Get column name to be use for filtering and sorting.
857
     *
858
     * @param integer $index
859
     * @param bool    $wantsAlias
860
     * @return string
861
     */
862
    protected function getColumnName($index, $wantsAlias = false)
863
    {
864
        $column = $this->request->columnName($index);
865
866
        // DataTables is using make(false)
867
        if (is_numeric($column)) {
868
            $column = $this->getColumnNameByIndex($index);
869
        }
870
871
        if (Str::contains(Str::upper($column), ' AS ')) {
872
            $column = $this->extractColumnName($column, $wantsAlias);
873
        }
874
875
        return $column;
876
    }
877
878
    /**
879
     * Get column name by order column index.
880
     *
881
     * @param int $index
882
     * @return string
883
     */
884
    protected function getColumnNameByIndex($index)
885
    {
886
        $name = (isset($this->columns[$index]) && $this->columns[$index] != '*') ? $this->columns[$index] : $this->getPrimaryKeyName();
887
888
        return in_array($name, $this->extraColumns, true) ? $this->getPrimaryKeyName() : $name;
889
    }
890
891
    /**
892
     * If column name could not be resolved then use primary key.
893
     *
894
     * @return string
895
     */
896
    protected function getPrimaryKeyName()
897
    {
898
        return 'id';
899
    }
900
901
    /**
902
     * Get column name from string.
903
     *
904
     * @param string $str
905
     * @param bool   $wantsAlias
906
     * @return string
907
     */
908
    protected function extractColumnName($str, $wantsAlias)
909
    {
910
        $matches = explode(' as ', Str::lower($str));
911
912
        if (!empty($matches)) {
913
            if ($wantsAlias) {
914
                return array_pop($matches);
915
            } else {
916
                return array_shift($matches);
917
            }
918
        } elseif (strpos($str, '.')) {
919
            $array = explode('.', $str);
920
921
            return array_pop($array);
922
        }
923
924
        return $str;
925
    }
926
}
927