Completed
Push — master ( 89c4c4...0563f6 )
by Arjay
02:35
created

BaseEngine::paginate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

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