Completed
Push — master ( 4cc8d8...e56a52 )
by Arjay
04:43
created

BaseEngine::ordering()

Size

Total Lines 1

Duplication

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