Completed
Push — master ( c73ed2...8e1c61 )
by Arjay
01:57
created

BaseEngine::errorResponse()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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