Completed
Branch develop (770b90)
by Arjay
02:23
created

BaseEngine::orderColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 3
1
<?php
2
3
namespace Yajra\Datatables\Engines;
4
5
/*
6
 * Laravel Datatables Base Engine
7
 *
8
 * @package  Laravel
9
 * @category Package
10
 * @author   Arjay Angeles <[email protected]>
11
 */
12
13
use Illuminate\Http\JsonResponse;
14
use Illuminate\Support\Facades\Config;
15
use Illuminate\Support\Str;
16
use League\Fractal\Manager;
17
use League\Fractal\Resource\Collection;
18
use League\Fractal\Serializer\DataArraySerializer;
19
use Yajra\Datatables\Contracts\DataTableEngineContract;
20
use Yajra\Datatables\Helper;
21
use Yajra\Datatables\Processors\DataProcessor;
22
23
abstract class BaseEngine implements DataTableEngineContract
24
{
25
    /**
26
     * Datatables Request object.
27
     *
28
     * @var \Yajra\Datatables\Request
29
     */
30
    public $request;
31
32
    /**
33
     * Database connection used.
34
     *
35
     * @var \Illuminate\Database\Connection
36
     */
37
    protected $connection;
38
39
    /**
40
     * Builder object.
41
     *
42
     * @var mixed
43
     */
44
    protected $query;
45
46
    /**
47
     * Query builder object.
48
     *
49
     * @var \Illuminate\Database\Query\Builder
50
     */
51
    protected $builder;
52
53
    /**
54
     * Array of result columns/fields.
55
     *
56
     * @var array
57
     */
58
    protected $columns = [];
59
60
    /**
61
     * DT columns definitions container (add/edit/remove/filter/order/escape).
62
     *
63
     * @var array
64
     */
65
    protected $columnDef = [
66
        'append' => [],
67
        'edit'   => [],
68
        'excess' => ['rn', 'row_num'],
69
        'filter' => [],
70
        'order'  => [],
71
        'escape' => [],
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  $name
371
     * @param  $arguments
372
     * @return $this|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 $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
     * Check if the current sql language is based on oracle syntax.
748
     *
749
     * @return bool
750
     */
751
    public function isOracleSql()
752
    {
753
        return Config::get('datatables.oracle_sql', false);
754
    }
755
756
    /**
757
     * Get column name to be use for filtering and sorting.
758
     *
759
     * @param integer $index
760
     * @param bool $wantsAlias
761
     * @return string
762
     */
763
    protected function getColumnName($index, $wantsAlias = false)
764
    {
765
        $column = $this->request->columnName($index);
766
767
        // DataTables is using make(false)
768
        if (is_numeric($column)) {
769
            $column = $this->getColumnNameByIndex($index);
770
        }
771
772
        if (Str::contains(Str::upper($column), ' AS ')) {
773
            $column = $this->extractColumnName($column, $wantsAlias);
774
        }
775
776
        return $column;
777
    }
778
779
    /**
780
     * Get column name by order column index.
781
     *
782
     * @param int $index
783
     * @return mixed
784
     */
785
    protected function getColumnNameByIndex($index)
786
    {
787
        $name = isset($this->columns[$index]) && $this->columns[$index] <> '*' ? $this->columns[$index] : $this->getPrimaryKeyName();
788
789
        return in_array($name, $this->extraColumns, true) ? $this->getPrimaryKeyName() : $name;
790
    }
791
792
    /**
793
     * If column name could not be resolved then use primary key.
794
     *
795
     * @return string
796
     */
797
    protected function getPrimaryKeyName()
798
    {
799
        if ($this->isEloquent()) {
800
            return $this->query->getModel()->getKeyName();
801
        }
802
803
        return 'id';
804
    }
805
806
    /**
807
     * Check if the engine used was eloquent.
808
     *
809
     * @return bool
810
     */
811
    protected function isEloquent()
812
    {
813
        return $this->query_type === 'eloquent';
814
    }
815
816
    /**
817
     * Get column name from string.
818
     *
819
     * @param string $str
820
     * @param bool $wantsAlias
821
     * @return string
822
     */
823
    protected function extractColumnName($str, $wantsAlias)
824
    {
825
        $matches = explode(' as ', Str::lower($str));
826
827
        if (! empty($matches)) {
828
            if ($wantsAlias) {
829
                return array_pop($matches);
830
            } else {
831
                return array_shift($matches);
832
            }
833
        } elseif (strpos($str, '.')) {
834
            $array = explode('.', $str);
835
836
            return array_pop($array);
837
        }
838
839
        return $str;
840
    }
841
}
842