Completed
Push — master ( 486225...c5b2a4 )
by Arjay
03:07
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\Http\JsonResponse;
6
use Illuminate\Support\Facades\Config;
7
use Illuminate\Support\Str;
8
use League\Fractal\Manager;
9
use League\Fractal\Resource\Collection;
10
use League\Fractal\Serializer\DataArraySerializer;
11
use Yajra\Datatables\Contracts\DataTableEngineContract;
12
use Yajra\Datatables\Helper;
13
use Yajra\Datatables\Processors\DataProcessor;
14
15
/**
16
 * Class BaseEngine.
17
 *
18
 * @package Yajra\Datatables\Engines
19
 * @author  Arjay Angeles <[email protected]>
20
 */
21
abstract class BaseEngine implements DataTableEngineContract
22
{
23
    /**
24
     * Datatables Request object.
25
     *
26
     * @var \Yajra\Datatables\Request
27
     */
28
    public $request;
29
30
    /**
31
     * Database connection used.
32
     *
33
     * @var \Illuminate\Database\Connection
34
     */
35
    protected $connection;
36
37
    /**
38
     * Builder object.
39
     *
40
     * @var \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder
41
     */
42
    protected $query;
43
44
    /**
45
     * Query builder object.
46
     *
47
     * @var \Illuminate\Database\Query\Builder
48
     */
49
    protected $builder;
50
51
    /**
52
     * Array of result columns/fields.
53
     *
54
     * @var array
55
     */
56
    protected $columns = [];
57
58
    /**
59
     * DT columns definitions container (add/edit/remove/filter/order/escape).
60
     *
61
     * @var array
62
     */
63
    protected $columnDef = [
64
        'index'     => false,
65
        'append'    => [],
66
        'edit'      => [],
67
        'excess'    => ['rn', 'row_num'],
68
        'filter'    => [],
69
        'order'     => [],
70
        'escape'    => [],
71
        'blacklist' => ['password', 'remember_token'],
72
        'whitelist' => '*',
73
    ];
74
75
    /**
76
     * Query type.
77
     *
78
     * @var string
79
     */
80
    protected $query_type;
81
82
    /**
83
     * Extra/Added columns.
84
     *
85
     * @var array
86
     */
87
    protected $extraColumns = [];
88
89
    /**
90
     * Total records.
91
     *
92
     * @var int
93
     */
94
    protected $totalRecords = 0;
95
96
    /**
97
     * Total filtered records.
98
     *
99
     * @var int
100
     */
101
    protected $filteredRecords = 0;
102
103
    /**
104
     * Auto-filter flag.
105
     *
106
     * @var bool
107
     */
108
    protected $autoFilter = true;
109
110
    /**
111
     * Callback to override global search.
112
     *
113
     * @var \Closure
114
     */
115
    protected $filterCallback;
116
117
    /**
118
     * Parameters to passed on filterCallback.
119
     *
120
     * @var mixed
121
     */
122
    protected $filterCallbackParameters;
123
124
    /**
125
     * DT row templates container.
126
     *
127
     * @var array
128
     */
129
    protected $templates = [
130
        'DT_RowId'    => '',
131
        'DT_RowClass' => '',
132
        'DT_RowData'  => [],
133
        'DT_RowAttr'  => [],
134
    ];
135
136
    /**
137
     * Output transformer.
138
     *
139
     * @var \League\Fractal\TransformerAbstract
140
     */
141
    protected $transformer = null;
142
143
    /**
144
     * Database prefix
145
     *
146
     * @var string
147
     */
148
    protected $prefix;
149
150
    /**
151
     * Database driver used.
152
     *
153
     * @var string
154
     */
155
    protected $database;
156
157
    /**
158
     * [internal] Track if any filter was applied for at least one column
159
     *
160
     * @var boolean
161
     */
162
    protected $isFilterApplied = false;
163
164
    /**
165
     * Fractal serializer class.
166
     *
167
     * @var string
168
     */
169
    protected $serializer;
170
171
    /**
172
     * Custom ordering callback.
173
     *
174
     * @var \Closure
175
     */
176
    protected $orderCallback;
177
178
    /**
179
     * Array of data to append on json response.
180
     *
181
     * @var array
182
     */
183
    private $appends = [];
184
185
    /**
186
     * Setup search keyword.
187
     *
188
     * @param  string $value
189
     * @return string
190
     */
191
    public function setupKeyword($value)
192
    {
193
        if ($this->isSmartSearch()) {
194
            $keyword = '%' . $value . '%';
195
            if ($this->isWildcard()) {
196
                $keyword = $this->wildcardLikeString($value);
197
            }
198
            // remove escaping slash added on js script request
199
            $keyword = str_replace('\\', '%', $keyword);
200
201
            return $keyword;
202
        }
203
204
        return $value;
205
    }
206
207
    /**
208
     * Check if DataTables uses smart search.
209
     *
210
     * @return bool
211
     */
212
    protected function isSmartSearch()
213
    {
214
        return Config::get('datatables.search.smart', true);
215
    }
216
217
    /**
218
     * Get config use wild card status.
219
     *
220
     * @return bool
221
     */
222
    public function isWildcard()
223
    {
224
        return Config::get('datatables.search.use_wildcards', false);
225
    }
226
227
    /**
228
     * Adds % wildcards to the given string.
229
     *
230
     * @param string $str
231
     * @param bool $lowercase
232
     * @return string
233
     */
234
    public function wildcardLikeString($str, $lowercase = true)
235
    {
236
        $wild   = '%';
237
        $length = Str::length($str);
238
        if ($length) {
239
            for ($i = 0; $i < $length; $i++) {
240
                $wild .= $str[$i] . '%';
241
            }
242
        }
243
        if ($lowercase) {
244
            $wild = Str::lower($wild);
245
        }
246
247
        return $wild;
248
    }
249
250
    /**
251
     * Will prefix column if needed.
252
     *
253
     * @param string $column
254
     * @return string
255
     */
256
    public function prefixColumn($column)
257
    {
258
        $table_names = $this->tableNames();
259
        if (count(
260
            array_filter($table_names, function ($value) use (&$column) {
261
                return strpos($column, $value . '.') === 0;
262
            })
263
        )) {
264
            // the column starts with one of the table names
265
            $column = $this->prefix . $column;
266
        }
267
268
        return $column;
269
    }
270
271
    /**
272
     * Will look through the query and all it's joins to determine the table names.
273
     *
274
     * @return array
275
     */
276
    public function tableNames()
277
    {
278
        $names          = [];
279
        $query          = $this->getQueryBuilder();
280
        $names[]        = $query->from;
281
        $joins          = $query->joins ?: [];
282
        $databasePrefix = $this->prefix;
283
        foreach ($joins as $join) {
284
            $table   = preg_split('/ as /i', $join->table);
285
            $names[] = $table[0];
286
            if (isset($table[1]) && ! empty($databasePrefix) && strpos($table[1], $databasePrefix) == 0) {
287
                $names[] = preg_replace('/^' . $databasePrefix . '/', '', $table[1]);
288
            }
289
        }
290
291
        return $names;
292
    }
293
294
    /**
295
     * Get Query Builder object.
296
     *
297
     * @param mixed $instance
298
     * @return mixed
299
     */
300
    public function getQueryBuilder($instance = null)
301
    {
302
        if (! $instance) {
303
            $instance = $this->query;
304
        }
305
306
        if ($this->isQueryBuilder()) {
307
            return $instance;
308
        }
309
310
        return $instance->getQuery();
311
    }
312
313
    /**
314
     * Check query type is a builder.
315
     *
316
     * @return bool
317
     */
318
    public function isQueryBuilder()
319
    {
320
        return $this->query_type == 'builder';
321
    }
322
323
    /**
324
     * Add column in collection.
325
     *
326
     * @param string $name
327
     * @param string|callable $content
328
     * @param bool|int $order
329
     * @return $this
330
     */
331
    public function addColumn($name, $content, $order = false)
332
    {
333
        $this->extraColumns[] = $name;
334
335
        $this->columnDef['append'][] = ['name' => $name, 'content' => $content, 'order' => $order];
336
337
        return $this;
338
    }
339
340
    /**
341
     * Add DT row index column on response.
342
     *
343
     * @return $this
344
     */
345
    public function addIndexColumn()
346
    {
347
        $this->columnDef['index'] = true;
348
349
        return $this;
350
    }
351
352
    /**
353
     * Edit column's content.
354
     *
355
     * @param string $name
356
     * @param string|callable $content
357
     * @return $this
358
     */
359
    public function editColumn($name, $content)
360
    {
361
        $this->columnDef['edit'][] = ['name' => $name, 'content' => $content];
362
363
        return $this;
364
    }
365
366
    /**
367
     * Remove column from collection.
368
     *
369
     * @return $this
370
     */
371
    public function removeColumn()
372
    {
373
        $names                     = func_get_args();
374
        $this->columnDef['excess'] = array_merge($this->columnDef['excess'], $names);
375
376
        return $this;
377
    }
378
379
    /**
380
     * Declare columns to escape values.
381
     *
382
     * @param string|array $columns
383
     * @return $this
384
     */
385
    public function escapeColumns($columns = '*')
386
    {
387
        $this->columnDef['escape'] = $columns;
388
389
        return $this;
390
    }
391
392
    /**
393
     * Allows previous API calls where the methods were snake_case.
394
     * Will convert a camelCase API call to a snake_case call.
395
     * Allow query builder method to be used by the engine.
396
     *
397
     * @param  string $name
398
     * @param  array $arguments
399
     * @return mixed
400
     */
401
    public function __call($name, $arguments)
402
    {
403
        $name = Str::camel(Str::lower($name));
404
        if (method_exists($this, $name)) {
405
            return call_user_func_array([$this, $name], $arguments);
406
        } elseif (method_exists($this->getQueryBuilder(), $name)) {
407
            call_user_func_array([$this->getQueryBuilder(), $name], $arguments);
408
        } else {
409
            trigger_error('Call to undefined method ' . __CLASS__ . '::' . $name . '()', E_USER_ERROR);
410
        }
411
412
        return $this;
413
    }
414
415
    /**
416
     * Sets DT_RowClass template.
417
     * result: <tr class="output_from_your_template">.
418
     *
419
     * @param string|callable $content
420
     * @return $this
421
     */
422
    public function setRowClass($content)
423
    {
424
        $this->templates['DT_RowClass'] = $content;
425
426
        return $this;
427
    }
428
429
    /**
430
     * Sets DT_RowId template.
431
     * result: <tr id="output_from_your_template">.
432
     *
433
     * @param string|callable $content
434
     * @return $this
435
     */
436
    public function setRowId($content)
437
    {
438
        $this->templates['DT_RowId'] = $content;
439
440
        return $this;
441
    }
442
443
    /**
444
     * Set DT_RowData templates.
445
     *
446
     * @param array $data
447
     * @return $this
448
     */
449
    public function setRowData(array $data)
450
    {
451
        $this->templates['DT_RowData'] = $data;
452
453
        return $this;
454
    }
455
456
    /**
457
     * Add DT_RowData template.
458
     *
459
     * @param string $key
460
     * @param string|callable $value
461
     * @return $this
462
     */
463
    public function addRowData($key, $value)
464
    {
465
        $this->templates['DT_RowData'][$key] = $value;
466
467
        return $this;
468
    }
469
470
    /**
471
     * Set DT_RowAttr templates.
472
     * result: <tr attr1="attr1" attr2="attr2">.
473
     *
474
     * @param array $data
475
     * @return $this
476
     */
477
    public function setRowAttr(array $data)
478
    {
479
        $this->templates['DT_RowAttr'] = $data;
480
481
        return $this;
482
    }
483
484
    /**
485
     * Add DT_RowAttr template.
486
     *
487
     * @param string $key
488
     * @param string|callable $value
489
     * @return $this
490
     */
491
    public function addRowAttr($key, $value)
492
    {
493
        $this->templates['DT_RowAttr'][$key] = $value;
494
495
        return $this;
496
    }
497
498
    /**
499
     * Override default column filter search.
500
     *
501
     * @param string $column
502
     * @param string|Closure $method
503
     * @return $this
504
     * @internal param $mixed ...,... All the individual parameters required for specified $method
505
     * @internal string $1 Special variable that returns the requested search keyword.
506
     */
507
    public function filterColumn($column, $method)
508
    {
509
        $params                             = func_get_args();
510
        $this->columnDef['filter'][$column] = ['method' => $method, 'parameters' => array_splice($params, 2)];
511
512
        return $this;
513
    }
514
515
    /**
516
     * Override default column ordering.
517
     *
518
     * @param string $column
519
     * @param string $sql
520
     * @param array $bindings
521
     * @return $this
522
     * @internal string $1 Special variable that returns the requested order direction of the column.
523
     */
524
    public function orderColumn($column, $sql, $bindings = [])
525
    {
526
        $this->columnDef['order'][$column] = ['method' => 'orderByRaw', 'parameters' => [$sql, $bindings]];
527
528
        return $this;
529
    }
530
531
    /**
532
     * Set data output transformer.
533
     *
534
     * @param \League\Fractal\TransformerAbstract $transformer
535
     * @return $this
536
     */
537
    public function setTransformer($transformer)
538
    {
539
        $this->transformer = $transformer;
540
541
        return $this;
542
    }
543
544
    /**
545
     * Set fractal serializer class.
546
     *
547
     * @param string $serializer
548
     * @return $this
549
     */
550
    public function setSerializer($serializer)
551
    {
552
        $this->serializer = $serializer;
553
554
        return $this;
555
    }
556
557
    /**
558
     * Organizes works.
559
     *
560
     * @param bool $mDataSupport
561
     * @param bool $orderFirst
562
     * @return \Illuminate\Http\JsonResponse
563
     */
564
    public function make($mDataSupport = false, $orderFirst = false)
565
    {
566
        $this->totalRecords = $this->totalCount();
567
568
        if ($this->totalRecords) {
569
            $this->orderRecords(! $orderFirst);
570
            $this->filterRecords();
571
            $this->orderRecords($orderFirst);
572
            $this->paginate();
573
        }
574
575
        return $this->render($mDataSupport);
576
    }
577
578
    /**
579
     * Sort records.
580
     *
581
     * @param  boolean $skip
582
     * @return void
583
     */
584
    public function orderRecords($skip)
585
    {
586
        if (! $skip) {
587
            $this->ordering();
588
        }
589
    }
590
591
    /**
592
     * Perform necessary filters.
593
     *
594
     * @return void
595
     */
596
    public function filterRecords()
597
    {
598
        if ($this->autoFilter && $this->request->isSearchable()) {
599
            $this->filtering();
600
        } else {
601
            if (is_callable($this->filterCallback)) {
602
                call_user_func($this->filterCallback, $this->filterCallbackParameters);
603
            }
604
        }
605
606
        $this->columnSearch();
607
        $this->filteredRecords = $this->isFilterApplied ? $this->count() : $this->totalRecords;
608
    }
609
610
    /**
611
     * Apply pagination.
612
     *
613
     * @return void
614
     */
615
    public function paginate()
616
    {
617
        if ($this->request->isPaginationable()) {
618
            $this->paging();
619
        }
620
    }
621
622
    /**
623
     * Render json response.
624
     *
625
     * @param bool $object
626
     * @return \Illuminate\Http\JsonResponse
627
     */
628
    public function render($object = false)
629
    {
630
        $output = array_merge([
631
            'draw'            => (int) $this->request['draw'],
632
            'recordsTotal'    => $this->totalRecords,
633
            'recordsFiltered' => $this->filteredRecords,
634
        ], $this->appends);
635
636
        if (isset($this->transformer)) {
637
            $fractal = new Manager();
638
            if ($this->request->get('include')) {
639
                $fractal->parseIncludes($this->request->get('include'));
640
            }
641
642
            $serializer = $this->serializer ?: Config::get('datatables.fractal.serializer', DataArraySerializer::class);
643
            $fractal->setSerializer(new $serializer);
644
645
            //Get transformer reflection
646
            //Firs method parameter should be data/object to transform
647
            $reflection = new \ReflectionMethod($this->transformer, 'transform');
648
            $parameter  = $reflection->getParameters()[0];
649
650
            //If parameter is class assuming it requires object
651
            //Else just pass array by default
652
            if ($parameter->getClass()) {
653
                $resource = new Collection($this->results(), new $this->transformer());
654
            } else {
655
                $resource = new Collection(
656
                    $this->getProcessedData($object),
657
                    new $this->transformer()
658
                );
659
            }
660
661
            $collection     = $fractal->createData($resource)->toArray();
662
            $output['data'] = $collection['data'];
663
        } else {
664
            $output['data'] = Helper::transform($this->getProcessedData($object));
665
        }
666
667
        if ($this->isDebugging()) {
668
            $output = $this->showDebugger($output);
669
        }
670
671
        return new JsonResponse($output);
672
    }
673
674
    /**
675
     * Get processed data
676
     *
677
     * @param bool|false $object
678
     * @return array
679
     */
680
    private function getProcessedData($object = false)
681
    {
682
        $processor = new DataProcessor(
683
            $this->results(),
684
            $this->columnDef,
685
            $this->templates,
686
            $this->request['start']
687
        );
688
689
        return $processor->process($object);
690
    }
691
692
    /**
693
     * Check if app is in debug mode.
694
     *
695
     * @return bool
696
     */
697
    public function isDebugging()
698
    {
699
        return Config::get('app.debug', false);
700
    }
701
702
    /**
703
     * Append debug parameters on output.
704
     *
705
     * @param  array $output
706
     * @return array
707
     */
708
    public function showDebugger(array $output)
709
    {
710
        $output['queries'] = $this->connection->getQueryLog();
711
        $output['input']   = $this->request->all();
712
713
        return $output;
714
    }
715
716
    /**
717
     * Update flags to disable global search
718
     *
719
     * @param  \Closure $callback
720
     * @param  mixed $parameters
721
     * @return void
722
     */
723
    public function overrideGlobalSearch(\Closure $callback, $parameters)
724
    {
725
        $this->autoFilter               = false;
726
        $this->isFilterApplied          = true;
727
        $this->filterCallback           = $callback;
728
        $this->filterCallbackParameters = $parameters;
729
    }
730
731
    /**
732
     * Get config is case insensitive status.
733
     *
734
     * @return bool
735
     */
736
    public function isCaseInsensitive()
737
    {
738
        return Config::get('datatables.search.case_insensitive', false);
739
    }
740
741
    /**
742
     * Append data on json response.
743
     *
744
     * @param mixed $key
745
     * @param mixed $value
746
     * @return $this
747
     */
748
    public function with($key, $value = '')
749
    {
750
        if (is_array($key)) {
751
            $this->appends = $key;
752
        } elseif (is_callable($value)) {
753
            $this->appends[$key] = value($value);
754
        } else {
755
            $this->appends[$key] = value($value);
756
        }
757
758
        return $this;
759
    }
760
761
    /**
762
     * Override default ordering method with a closure callback.
763
     *
764
     * @param \Closure $closure
765
     * @return $this
766
     */
767
    public function order(\Closure $closure)
768
    {
769
        $this->orderCallback = $closure;
770
771
        return $this;
772
    }
773
774
    /**
775
     * Update list of columns that is not allowed for search/sort.
776
     *
777
     * @param  array $blacklist
778
     * @return $this
779
     */
780
    public function blacklist(array $blacklist)
781
    {
782
        $this->columnDef['blacklist'] = $blacklist;
783
784
        return $this;
785
    }
786
787
    /**
788
     * Update list of columns that is not allowed for search/sort.
789
     *
790
     * @param  string|array $whitelist
791
     * @return $this
792
     */
793
    public function whitelist($whitelist = '*')
794
    {
795
        $this->columnDef['whitelist'] = $whitelist;
796
797
        return $this;
798
    }
799
800
    /**
801
     * Set smart search config at runtime.
802
     *
803
     * @param bool $bool
804
     * @return $this
805
     */
806
    public function smart($bool = true)
807
    {
808
        Config::set('datatables.search.smart', $bool);
809
810
        return $this;
811
    }
812
813
    /**
814
     * Check if column is blacklisted.
815
     *
816
     * @param string $column
817
     * @return bool
818
     */
819
    protected function isBlacklisted($column)
820
    {
821
        if (in_array($column, $this->columnDef['blacklist'])) {
822
            return true;
823
        }
824
825
        if ($this->columnDef['whitelist'] === '*' || in_array($column, $this->columnDef['whitelist'])) {
826
            return false;
827
        }
828
829
        return true;
830
    }
831
832
    /**
833
     * Get column name to be use for filtering and sorting.
834
     *
835
     * @param integer $index
836
     * @param bool $wantsAlias
837
     * @return string
838
     */
839
    protected function getColumnName($index, $wantsAlias = false)
840
    {
841
        $column = $this->request->columnName($index);
842
843
        // DataTables is using make(false)
844
        if (is_numeric($column)) {
845
            $column = $this->getColumnNameByIndex($index);
846
        }
847
848
        if (Str::contains(Str::upper($column), ' AS ')) {
849
            $column = $this->extractColumnName($column, $wantsAlias);
850
        }
851
852
        return $column;
853
    }
854
855
    /**
856
     * Get column name by order column index.
857
     *
858
     * @param int $index
859
     * @return mixed
860
     */
861
    protected function getColumnNameByIndex($index)
862
    {
863
        $name = isset($this->columns[$index]) && $this->columns[$index] <> '*' ? $this->columns[$index] : $this->getPrimaryKeyName();
864
865
        return in_array($name, $this->extraColumns, true) ? $this->getPrimaryKeyName() : $name;
866
    }
867
868
    /**
869
     * If column name could not be resolved then use primary key.
870
     *
871
     * @return string
872
     */
873
    protected function getPrimaryKeyName()
874
    {
875
        if ($this->isEloquent()) {
876
            return $this->query->getModel()->getKeyName();
0 ignored issues
show
Bug introduced by
The method getModel does only exist in Illuminate\Database\Eloquent\Builder, but not in Illuminate\Database\Query\Builder.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
877
        }
878
879
        return 'id';
880
    }
881
882
    /**
883
     * Check if the engine used was eloquent.
884
     *
885
     * @return bool
886
     */
887
    protected function isEloquent()
888
    {
889
        return $this->query_type === 'eloquent';
890
    }
891
892
    /**
893
     * Get column name from string.
894
     *
895
     * @param string $str
896
     * @param bool $wantsAlias
897
     * @return string
898
     */
899
    protected function extractColumnName($str, $wantsAlias)
900
    {
901
        $matches = explode(' as ', Str::lower($str));
902
903
        if (! empty($matches)) {
904
            if ($wantsAlias) {
905
                return array_pop($matches);
906
            } else {
907
                return array_shift($matches);
908
            }
909
        } elseif (strpos($str, '.')) {
910
            $array = explode('.', $str);
911
912
            return array_pop($array);
913
        }
914
915
        return $str;
916
    }
917
918
    /**
919
     * Check if the current sql language is based on oracle syntax.
920
     *
921
     * @return bool
922
     */
923
    protected function isOracleSql()
924
    {
925
        return in_array($this->database, ['oracle', 'oci8']);
926
    }
927
}
928