Completed
Push — master ( bb6a5a...d505a7 )
by Arjay
05:09
created

BaseEngine::order()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
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
     * Setup column name to be use for filtering.
237
     *
238
     * @param integer $i
239
     * @param bool $wantsAlias
240
     * @return string
241
     */
242
    public function setupColumnName($i, $wantsAlias = false)
243
    {
244
        $column = $this->getColumnName($i);
245
        if (Str::contains(Str::upper($column), ' AS ')) {
246
            $column = $this->extractColumnName($column, $wantsAlias);
247
        }
248
249
        return $column;
250
    }
251
252
    /**
253
     * Get column name by order column index.
254
     *
255
     * @param int $column
256
     * @return mixed
257
     */
258
    protected function getColumnName($column)
259
    {
260
        $name = $this->request->columnName($column) ?: (isset($this->columns[$column]) ? $this->columns[$column] : $this->columns[0]);
261
262
        return in_array($name, $this->extraColumns, true) ? $this->columns[0] : $name;
263
    }
264
265
    /**
266
     * Get column name from string.
267
     *
268
     * @param string $str
269
     * @param bool $wantsAlias
270
     * @return string
271
     */
272
    public function extractColumnName($str, $wantsAlias)
273
    {
274
        $matches = explode(' as ', Str::lower($str));
275
276
        if (! empty($matches)) {
277
            if ($wantsAlias) {
278
                return array_pop($matches);
279
            } else {
280
                return array_shift($matches);
281
            }
282
        } elseif (strpos($str, '.')) {
283
            $array = explode('.', $str);
284
285
            return array_pop($array);
286
        }
287
288
        return $str;
289
    }
290
291
    /**
292
     * Will prefix column if needed.
293
     *
294
     * @param string $column
295
     * @return string
296
     */
297
    public function prefixColumn($column)
298
    {
299
        $table_names = $this->tableNames();
300
        if (count(
301
            array_filter($table_names, function ($value) use (&$column) {
302
                return strpos($column, $value . '.') === 0;
303
            })
304
        )) {
305
            // the column starts with one of the table names
306
            $column = $this->prefix . $column;
307
        }
308
309
        return $column;
310
    }
311
312
    /**
313
     * Will look through the query and all it's joins to determine the table names.
314
     *
315
     * @return array
316
     */
317
    public function tableNames()
318
    {
319
        $names          = [];
320
        $query          = $this->getQueryBuilder();
321
        $names[]        = $query->from;
322
        $joins          = $query->joins ?: [];
323
        $databasePrefix = $this->prefix;
324
        foreach ($joins as $join) {
325
            $table   = preg_split('/ as /i', $join->table);
326
            $names[] = $table[0];
327
            if (isset($table[1]) && ! empty($databasePrefix) && strpos($table[1], $databasePrefix) == 0) {
328
                $names[] = preg_replace('/^' . $databasePrefix . '/', '', $table[1]);
329
            }
330
        }
331
332
        return $names;
333
    }
334
335
    /**
336
     * Get Query Builder object.
337
     *
338
     * @param mixed $instance
339
     * @return mixed
340
     */
341
    public function getQueryBuilder($instance = null)
342
    {
343
        if (! $instance) {
344
            $instance = $this->query;
345
        }
346
347
        if ($this->isQueryBuilder()) {
348
            return $instance;
349
        }
350
351
        return $instance->getQuery();
352
    }
353
354
    /**
355
     * Check query type is a builder.
356
     *
357
     * @return bool
358
     */
359
    public function isQueryBuilder()
360
    {
361
        return $this->query_type == 'builder';
362
    }
363
364
    /**
365
     * Add column in collection.
366
     *
367
     * @param string $name
368
     * @param string $content
369
     * @param bool|int $order
370
     * @return $this
371
     */
372
    public function addColumn($name, $content, $order = false)
373
    {
374
        $this->extraColumns[] = $name;
375
376
        $this->columnDef['append'][] = ['name' => $name, 'content' => $content, 'order' => $order];
377
378
        return $this;
379
    }
380
381
    /**
382
     * Edit column's content.
383
     *
384
     * @param string $name
385
     * @param string $content
386
     * @return $this
387
     */
388
    public function editColumn($name, $content)
389
    {
390
        $this->columnDef['edit'][] = ['name' => $name, 'content' => $content];
391
392
        return $this;
393
    }
394
395
    /**
396
     * Remove column from collection.
397
     *
398
     * @return $this
399
     */
400
    public function removeColumn()
401
    {
402
        $names                     = func_get_args();
403
        $this->columnDef['excess'] = array_merge($this->columnDef['excess'], $names);
404
405
        return $this;
406
    }
407
408
    /**
409
     * Declare columns to escape values.
410
     *
411
     * @param string|array $columns
412
     * @return $this
413
     */
414
    public function escapeColumns($columns = '*')
415
    {
416
        $this->columnDef['escape'] = $columns;
417
418
        return $this;
419
    }
420
421
    /**
422
     * Allows previous API calls where the methods were snake_case.
423
     * Will convert a camelCase API call to a snake_case call.
424
     *
425
     * @param  $name
426
     * @param  $arguments
427
     * @return $this|mixed
428
     */
429
    public function __call($name, $arguments)
430
    {
431
        $name = Str::camel(Str::lower($name));
432
        if (method_exists($this, $name)) {
433
            return call_user_func_array([$this, $name], $arguments);
434
        } elseif (method_exists($this->getQueryBuilder(), $name)) {
435
            call_user_func_array([$this->getQueryBuilder(), $name], $arguments);
436
        } else {
437
            trigger_error('Call to undefined method ' . __CLASS__ . '::' . $name . '()', E_USER_ERROR);
438
        }
439
440
        return $this;
441
    }
442
443
    /**
444
     * Sets DT_RowClass template
445
     * result: <tr class="output_from_your_template">.
446
     *
447
     * @param string|callable $content
448
     * @return $this
449
     */
450
    public function setRowClass($content)
451
    {
452
        $this->templates['DT_RowClass'] = $content;
453
454
        return $this;
455
    }
456
457
    /**
458
     * Sets DT_RowId template
459
     * result: <tr id="output_from_your_template">.
460
     *
461
     * @param string|callable $content
462
     * @return $this
463
     */
464
    public function setRowId($content)
465
    {
466
        $this->templates['DT_RowId'] = $content;
467
468
        return $this;
469
    }
470
471
    /**
472
     * Set DT_RowData templates.
473
     *
474
     * @param array $data
475
     * @return $this
476
     */
477
    public function setRowData(array $data)
478
    {
479
        $this->templates['DT_RowData'] = $data;
480
481
        return $this;
482
    }
483
484
    /**
485
     * Add DT_RowData template.
486
     *
487
     * @param string $key
488
     * @param string|callable $value
489
     * @return $this
490
     */
491
    public function addRowData($key, $value)
492
    {
493
        $this->templates['DT_RowData'][$key] = $value;
494
495
        return $this;
496
    }
497
498
    /**
499
     * Set DT_RowAttr templates
500
     * result: <tr attr1="attr1" attr2="attr2">.
501
     *
502
     * @param array $data
503
     * @return $this
504
     */
505
    public function setRowAttr(array $data)
506
    {
507
        $this->templates['DT_RowAttr'] = $data;
508
509
        return $this;
510
    }
511
512
    /**
513
     * Add DT_RowAttr template.
514
     *
515
     * @param string $key
516
     * @param string|callable $value
517
     * @return $this
518
     */
519
    public function addRowAttr($key, $value)
520
    {
521
        $this->templates['DT_RowAttr'][$key] = $value;
522
523
        return $this;
524
    }
525
526
    /**
527
     * Override default column filter search.
528
     *
529
     * @param string $column
530
     * @param string $method
531
     * @return $this
532
     * @internal param $mixed ...,... All the individual parameters required for specified $method
533
     * @internal string $1 Special variable that returns the requested search keyword.
534
     */
535
    public function filterColumn($column, $method)
536
    {
537
        $params                             = func_get_args();
538
        $this->columnDef['filter'][$column] = ['method' => $method, 'parameters' => array_splice($params, 2)];
539
540
        return $this;
541
    }
542
543
    /**
544
     * Override default column ordering.
545
     *
546
     * @param string $column
547
     * @param string $sql
548
     * @param array $bindings
549
     * @return $this
550
     * @internal string $1 Special variable that returns the requested order direction of the column.
551
     */
552
    public function orderColumn($column, $sql, $bindings = [])
553
    {
554
        $this->columnDef['order'][$column] = ['method' => 'orderByRaw', 'parameters' => [$sql, $bindings]];
555
556
        return $this;
557
    }
558
559
    /**
560
     * Set data output transformer.
561
     *
562
     * @param \League\Fractal\TransformerAbstract $transformer
563
     * @return $this
564
     */
565
    public function setTransformer($transformer)
566
    {
567
        $this->transformer = $transformer;
568
569
        return $this;
570
    }
571
572
    /**
573
     * Set fractal serializer class.
574
     *
575
     * @param string $serializer
576
     * @return $this
577
     */
578
    public function setSerializer($serializer)
579
    {
580
        $this->serializer = $serializer;
581
582
        return $this;
583
    }
584
585
    /**
586
     * Organizes works.
587
     *
588
     * @param bool $mDataSupport
589
     * @param bool $orderFirst
590
     * @return \Illuminate\Http\JsonResponse
591
     */
592
    public function make($mDataSupport = false, $orderFirst = false)
593
    {
594
        $this->totalRecords = $this->totalCount();
595
596
        if ($this->totalRecords) {
597
            $this->orderRecords(! $orderFirst);
598
            $this->filterRecords();
599
            $this->orderRecords($orderFirst);
600
            $this->paginate();
601
        }
602
603
        return $this->render($mDataSupport);
604
    }
605
606
    /**
607
     * Count total items.
608
     *
609
     * @return integer
610
     */
611
    abstract public function totalCount();
612
613
    /**
614
     * Sort records.
615
     *
616
     * @param  boolean $skip
617
     * @return void
618
     */
619
    public function orderRecords($skip)
620
    {
621
        if (! $skip) {
622
            $this->ordering();
623
        }
624
    }
625
626
    /**
627
     * Perform sorting of columns.
628
     *
629
     * @return void
630
     */
631
    abstract public function ordering();
632
633
    /**
634
     * Perform necessary filters.
635
     *
636
     * @return void
637
     */
638
    public function filterRecords()
639
    {
640
        if ($this->autoFilter && $this->request->isSearchable()) {
641
            $this->filtering();
642
        } else {
643
            if (is_callable($this->filterCallback)) {
644
                call_user_func($this->filterCallback, $this->filterCallbackParameters);
645
            }
646
        }
647
648
        $this->columnSearch();
649
        $this->filteredRecords = $this->isFilterApplied ? $this->count() : $this->totalRecords;
650
    }
651
652
    /**
653
     * Perform global search.
654
     *
655
     * @return void
656
     */
657
    abstract public function filtering();
658
659
    /**
660
     * Perform column search.
661
     *
662
     * @return void
663
     */
664
    abstract public function columnSearch();
665
666
    /**
667
     * Count results.
668
     *
669
     * @return integer
670
     */
671
    abstract public function count();
672
673
    /**
674
     * Apply pagination.
675
     *
676
     * @return void
677
     */
678
    public function paginate()
679
    {
680
        if ($this->request->isPaginationable()) {
681
            $this->paging();
682
        }
683
    }
684
685
    /**
686
     * Perform pagination
687
     *
688
     * @return void
689
     */
690
    abstract public function paging();
691
692
    /**
693
     * Render json response.
694
     *
695
     * @param bool $object
696
     * @return \Illuminate\Http\JsonResponse
697
     */
698
    public function render($object = false)
699
    {
700
        $output = array_merge([
701
            'draw'            => (int) $this->request['draw'],
702
            'recordsTotal'    => $this->totalRecords,
703
            'recordsFiltered' => $this->filteredRecords,
704
        ], $this->appends);
705
706
        if (isset($this->transformer)) {
707
            $fractal = new Manager();
708
            if ($this->request->get('include')) {
709
                $fractal->parseIncludes($this->request->get('include'));
710
            }
711
712
            $serializer = $this->serializer ?: Config::get('datatables.fractal.serializer', DataArraySerializer::class);
713
            $fractal->setSerializer(new $serializer);
714
715
            //Get transformer reflection
716
            //Firs method parameter should be data/object to transform
717
            $reflection = new \ReflectionMethod($this->transformer, 'transform');
718
            $parameter  = $reflection->getParameters()[0];
719
720
            //If parameter is class assuming it requires object
721
            //Else just pass array by default
722
            if ($parameter->getClass()) {
723
                $resource = new Collection($this->results(), new $this->transformer());
724
            } else {
725
                $resource = new Collection(
726
                    $this->getProcessedData($object),
727
                    new $this->transformer()
728
                );
729
            }
730
731
            $collection     = $fractal->createData($resource)->toArray();
732
            $output['data'] = $collection['data'];
733
        } else {
734
            $output['data'] = Helper::transform($this->getProcessedData($object));
735
        }
736
737
        if ($this->isDebugging()) {
738
            $output = $this->showDebugger($output);
739
        }
740
741
        return new JsonResponse($output);
742
    }
743
744
    /**
745
     * Get results
746
     *
747
     * @return array
748
     */
749
    abstract public function results();
750
751
    /**
752
     * Get processed data
753
     *
754
     * @param bool|false $object
755
     * @return array
756
     */
757
    private function getProcessedData($object = false)
758
    {
759
        $processor = new DataProcessor(
760
            $this->results(),
761
            $this->columnDef,
762
            $this->templates
763
        );
764
765
        return $processor->process($object);
766
    }
767
768
    /**
769
     * Check if app is in debug mode.
770
     *
771
     * @return bool
772
     */
773
    public function isDebugging()
774
    {
775
        return Config::get('app.debug', false);
776
    }
777
778
    /**
779
     * Append debug parameters on output.
780
     *
781
     * @param  array $output
782
     * @return array
783
     */
784
    public function showDebugger(array $output)
785
    {
786
        $output['queries'] = $this->connection->getQueryLog();
787
        $output['input']   = $this->request->all();
788
789
        return $output;
790
    }
791
792
    /**
793
     * Set auto filter off and run your own filter.
794
     * Overrides global search
795
     *
796
     * @param \Closure $callback
797
     * @return $this
798
     */
799
    abstract public function filter(\Closure $callback);
800
801
    /**
802
     * Update flags to disable global search
803
     *
804
     * @param  \Closure $callback
805
     * @param  mixed $parameters
806
     * @return void
807
     */
808
    public function overrideGlobalSearch(\Closure $callback, $parameters)
809
    {
810
        $this->autoFilter               = false;
811
        $this->isFilterApplied          = true;
812
        $this->filterCallback           = $callback;
813
        $this->filterCallbackParameters = $parameters;
814
    }
815
816
    /**
817
     * Get config is case insensitive status.
818
     *
819
     * @return bool
820
     */
821
    public function isCaseInsensitive()
822
    {
823
        return Config::get('datatables.search.case_insensitive', false);
824
    }
825
826
    /**
827
     * Append data on json response.
828
     *
829
     * @param mixed $key
830
     * @param mixed $value
831
     * @return $this
832
     */
833
    public function with($key, $value = '')
834
    {
835
        if (is_array($key)) {
836
            $this->appends = $key;
837
        } elseif (is_callable($value)) {
838
            $this->appends[$key] = value($value);
839
        } else {
840
            $this->appends[$key] = value($value);
841
        }
842
843
        return $this;
844
    }
845
846
    /**
847
     * Override default ordering method with a closure callback.
848
     *
849
     * @param \Closure $closure
850
     * @return $this
851
     */
852
    public function order(\Closure $closure)
853
    {
854
        $this->orderCallback = $closure;
855
856
        return $this;
857
    }
858
}
859