Completed
Push — master ( 3a0338...c73ed2 )
by Arjay
01:57
created

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