Completed
Push — master ( b1bb78...82c1ec )
by Arjay
01:57 queued 12s
created

src/DataTableAbstract.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Yajra\DataTables;
4
5
use Illuminate\Contracts\Support\Arrayable;
6
use Illuminate\Contracts\Support\Jsonable;
7
use Illuminate\Http\JsonResponse;
8
use Illuminate\Support\Arr;
9
use Illuminate\Support\Str;
10
use Illuminate\Support\Traits\Macroable;
11
use Psr\Log\LoggerInterface;
12
use Yajra\DataTables\Contracts\DataTable;
13
use Yajra\DataTables\Contracts\Formatter;
14
use Yajra\DataTables\Exceptions\Exception;
15
use Yajra\DataTables\Processors\DataProcessor;
16
use Yajra\DataTables\Utilities\Helper;
17
18
/**
19
 * @method DataTableAbstract setTransformer($transformer)
20
 * @method DataTableAbstract setSerializer($transformer)
21
 * @property mixed transformer
22
 * @property mixed serializer
23
 * @see     https://github.com/yajra/laravel-datatables-fractal for transformer related methods.
24
 */
25
abstract class DataTableAbstract implements DataTable, Arrayable, Jsonable
26
{
27
    use Macroable;
28
29
    /**
30
     * DataTables Request object.
31
     *
32
     * @var \Yajra\DataTables\Utilities\Request
33
     */
34
    public $request;
35
36
    /**
37
     * @var \Psr\Log\LoggerInterface
38
     */
39
    protected $logger;
40
41
    /**
42
     * Array of result columns/fields.
43
     *
44
     * @var array
45
     */
46
    protected $columns = [];
47
48
    /**
49
     * DT columns definitions container (add/edit/remove/filter/order/escape).
50
     *
51
     * @var array
52
     */
53
    protected $columnDef = [
54
        'index'       => false,
55
        'append'      => [],
56
        'edit'        => [],
57
        'filter'      => [],
58
        'order'       => [],
59
        'only'        => null,
60
        'hidden'      => [],
61
        'visible'     => [],
62
    ];
63
64
    /**
65
     * Extra/Added columns.
66
     *
67
     * @var array
68
     */
69
    protected $extraColumns = [];
70
71
    /**
72
     * Total records.
73
     *
74
     * @var int
75
     */
76
    protected $totalRecords = 0;
77
78
    /**
79
     * Total filtered records.
80
     *
81
     * @var int
82
     */
83
    protected $filteredRecords = 0;
84
85
    /**
86
     * Auto-filter flag.
87
     *
88
     * @var bool
89
     */
90
    protected $autoFilter = true;
91
92
    /**
93
     * Callback to override global search.
94
     *
95
     * @var callable
96
     */
97
    protected $filterCallback;
98
99
    /**
100
     * DT row templates container.
101
     *
102
     * @var array
103
     */
104
    protected $templates = [
105
        'DT_RowId'    => '',
106
        'DT_RowClass' => '',
107
        'DT_RowData'  => [],
108
        'DT_RowAttr'  => [],
109
    ];
110
111
    /**
112
     * [internal] Track if any filter was applied for at least one column.
113
     *
114
     * @var bool
115
     */
116
    protected $isFilterApplied = false;
117
118
    /**
119
     * Custom ordering callback.
120
     *
121
     * @var callable
122
     */
123
    protected $orderCallback;
124
125
    /**
126
     * Skip paginate as needed.
127
     *
128
     * @var bool
129
     */
130
    protected $skipPaging = false;
131
132
    /**
133
     * Array of data to append on json response.
134
     *
135
     * @var array
136
     */
137
    protected $appends = [];
138
139
    /**
140
     * @var \Yajra\DataTables\Utilities\Config
141
     */
142
    protected $config;
143
144
    /**
145
     * @var mixed
146
     */
147
    protected $serializer;
148
149
    /**
150
     * @var array
151
     */
152
    protected $searchPanes = [];
153
154
    /**
155
     * Can the DataTable engine be created with these parameters.
156
     *
157
     * @param mixed $source
158
     * @return bool
159
     */
160
    public static function canCreate($source)
161
    {
162
        return false;
163
    }
164
165
    /**
166
     * Factory method, create and return an instance for the DataTable engine.
167
     *
168
     * @param mixed $source
169
     * @return DataTableAbstract
170
     */
171
    public static function create($source)
172
    {
173
        return new static($source);
174
    }
175
176
    /**
177
     * Add column in collection.
178
     *
179
     * @param string          $name
180
     * @param string|callable $content
181
     * @param bool|int        $order
182
     * @return $this
183
     */
184
    public function addColumn($name, $content, $order = false)
185
    {
186
        $this->extraColumns[] = $name;
187
188
        $this->columnDef['append'][] = ['name' => $name, 'content' => $content, 'order' => $order];
189
190
        return $this;
191
    }
192
193
    /**
194
     * @param string|array $columns
195
     * @param mixed|\Yajra\DataTables\Contracts\Formatter $formatter
196
     * @return $this
197
     * @throws \Exception
198
     */
199
    public function formatColumn($columns, $formatter)
200
    {
201
        if (is_string($formatter) && class_exists($formatter)) {
202
            $formatter = app($formatter);
203
        }
204
205
        if (! $formatter instanceof Formatter) {
206
            throw new \Exception('$formatter must be an instance of '. Formatter::class);
207
        }
208
209
        foreach ((array) $columns as $column) {
210
            $this->addColumn($column . '_formatted', $formatter);
0 ignored issues
show
$formatter is of type object<Yajra\DataTables\Contracts\Formatter>, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
211
        }
212
213
        return $this;
214
    }
215
216
    /**
217
     * Add DT row index column on response.
218
     *
219
     * @return $this
220
     */
221
    public function addIndexColumn()
222
    {
223
        $this->columnDef['index'] = true;
224
225
        return $this;
226
    }
227
228
    /**
229
     * Edit column's content.
230
     *
231
     * @param string          $name
232
     * @param string|callable $content
233
     * @return $this
234
     */
235
    public function editColumn($name, $content)
236
    {
237
        $this->columnDef['edit'][] = ['name' => $name, 'content' => $content];
238
239
        return $this;
240
    }
241
242
    /**
243
     * Remove column from collection.
244
     *
245
     * @return $this
246
     */
247
    public function removeColumn()
248
    {
249
        $names                     = func_get_args();
250
        $this->columnDef['excess'] = array_merge($this->getColumnsDefinition()['excess'], $names);
251
252
        return $this;
253
    }
254
255
    /**
256
     * Get only selected columns in response.
257
     *
258
     * @param array $columns
259
     * @return $this
260
     */
261
    public function only(array $columns = [])
262
    {
263
        $this->columnDef['only'] = $columns;
264
265
        return $this;
266
    }
267
268
    /**
269
     * Declare columns to escape values.
270
     *
271
     * @param string|array $columns
272
     * @return $this
273
     */
274
    public function escapeColumns($columns = '*')
275
    {
276
        $this->columnDef['escape'] = $columns;
277
278
        return $this;
279
    }
280
281
    /**
282
     * Add a makeHidden() to the row object.
283
     *
284
     * @param array          $attributes
285
     * @return $this
286
     */
287
    public function makeHidden(array $attributes = [])
288
    {
289
        $this->columnDef['hidden'] = array_merge_recursive(Arr::get($this->columnDef, 'hidden', []), $attributes);
290
291
        return $this;
292
    }
293
294
    /**
295
     * Add a makeVisible() to the row object.
296
     *
297
     * @param array          $attributes
298
     * @return $this
299
     */
300
    public function makeVisible(array $attributes = [])
301
    {
302
        $this->columnDef['visible'] = array_merge_recursive(Arr::get($this->columnDef, 'visible', []), $attributes);
303
304
        return $this;
305
    }
306
307
    /**
308
     * Set columns that should not be escaped.
309
     * Optionally merge the defaults from config.
310
     *
311
     * @param array $columns
312
     * @param bool $merge
313
     * @return $this
314
     */
315
    public function rawColumns(array $columns, $merge = false)
316
    {
317
        if ($merge) {
318
            $config = $this->config->get('datatables.columns');
319
320
            $this->columnDef['raw'] = array_merge($config['raw'], $columns);
321
        } else {
322
            $this->columnDef['raw'] = $columns;
323
        }
324
325
        return $this;
326
    }
327
328
    /**
329
     * Sets DT_RowClass template.
330
     * result: <tr class="output_from_your_template">.
331
     *
332
     * @param string|callable $content
333
     * @return $this
334
     */
335
    public function setRowClass($content)
336
    {
337
        $this->templates['DT_RowClass'] = $content;
338
339
        return $this;
340
    }
341
342
    /**
343
     * Sets DT_RowId template.
344
     * result: <tr id="output_from_your_template">.
345
     *
346
     * @param string|callable $content
347
     * @return $this
348
     */
349
    public function setRowId($content)
350
    {
351
        $this->templates['DT_RowId'] = $content;
352
353
        return $this;
354
    }
355
356
    /**
357
     * Set DT_RowData templates.
358
     *
359
     * @param array $data
360
     * @return $this
361
     */
362
    public function setRowData(array $data)
363
    {
364
        $this->templates['DT_RowData'] = $data;
365
366
        return $this;
367
    }
368
369
    /**
370
     * Add DT_RowData template.
371
     *
372
     * @param string          $key
373
     * @param string|callable $value
374
     * @return $this
375
     */
376
    public function addRowData($key, $value)
377
    {
378
        $this->templates['DT_RowData'][$key] = $value;
379
380
        return $this;
381
    }
382
383
    /**
384
     * Set DT_RowAttr templates.
385
     * result: <tr attr1="attr1" attr2="attr2">.
386
     *
387
     * @param array $data
388
     * @return $this
389
     */
390
    public function setRowAttr(array $data)
391
    {
392
        $this->templates['DT_RowAttr'] = $data;
393
394
        return $this;
395
    }
396
397
    /**
398
     * Add DT_RowAttr template.
399
     *
400
     * @param string          $key
401
     * @param string|callable $value
402
     * @return $this
403
     */
404
    public function addRowAttr($key, $value)
405
    {
406
        $this->templates['DT_RowAttr'][$key] = $value;
407
408
        return $this;
409
    }
410
411
    /**
412
     * Append data on json response.
413
     *
414
     * @param mixed $key
415
     * @param mixed $value
416
     * @return $this
417
     */
418
    public function with($key, $value = '')
419
    {
420
        if (is_array($key)) {
421
            $this->appends = $key;
422
        } elseif (is_callable($value)) {
423
            $this->appends[$key] = value($value);
424
        } else {
425
            $this->appends[$key] = value($value);
426
        }
427
428
        return $this;
429
    }
430
431
    /**
432
     * Add with query callback value on response.
433
     *
434
     * @param string   $key
435
     * @param callable $value
436
     * @return $this
437
     */
438
    public function withQuery($key, callable $value)
439
    {
440
        $this->appends[$key] = $value;
441
442
        return $this;
443
    }
444
445
    /**
446
     * Override default ordering method with a closure callback.
447
     *
448
     * @param callable $closure
449
     * @return $this
450
     */
451
    public function order(callable $closure)
452
    {
453
        $this->orderCallback = $closure;
454
455
        return $this;
456
    }
457
458
    /**
459
     * Update list of columns that is not allowed for search/sort.
460
     *
461
     * @param  array $blacklist
462
     * @return $this
463
     */
464
    public function blacklist(array $blacklist)
465
    {
466
        $this->columnDef['blacklist'] = $blacklist;
467
468
        return $this;
469
    }
470
471
    /**
472
     * Update list of columns that is allowed for search/sort.
473
     *
474
     * @param  string|array $whitelist
475
     * @return $this
476
     */
477
    public function whitelist($whitelist = '*')
478
    {
479
        $this->columnDef['whitelist'] = $whitelist;
480
481
        return $this;
482
    }
483
484
    /**
485
     * Set smart search config at runtime.
486
     *
487
     * @param bool $state
488
     * @return $this
489
     */
490
    public function smart($state = true)
491
    {
492
        $this->config->set('datatables.search.smart', $state);
493
494
        return $this;
495
    }
496
497
    /**
498
     * Set starts_with search config at runtime.
499
     *
500
     * @param bool $state
501
     * @return $this
502
     */
503
    public function startsWithSearch($state = true)
504
    {
505
        $this->config->set('datatables.search.starts_with', $state);
506
507
        return $this;
508
    }
509
510
    /**
511
     * Set total records manually.
512
     *
513
     * @param int $total
514
     * @return $this
515
     */
516
    public function setTotalRecords($total)
517
    {
518
        $this->totalRecords = $total;
519
520
        return $this;
521
    }
522
523
    /**
524
     * Set filtered records manually.
525
     *
526
     * @param int $total
527
     * @return $this
528
     */
529
    public function setFilteredRecords($total)
530
    {
531
        $this->filteredRecords = $total;
532
533
        return $this;
534
    }
535
536
    /**
537
     * Skip pagination as needed.
538
     *
539
     * @return $this
540
     */
541
    public function skipPaging()
542
    {
543
        $this->skipPaging = true;
544
545
        return $this;
546
    }
547
548
    /**
549
     * Push a new column name to blacklist.
550
     *
551
     * @param string $column
552
     * @return $this
553
     */
554
    public function pushToBlacklist($column)
555
    {
556
        if (! $this->isBlacklisted($column)) {
557
            $this->columnDef['blacklist'][] = $column;
558
        }
559
560
        return $this;
561
    }
562
563
    /**
564
     * Check if column is blacklisted.
565
     *
566
     * @param string $column
567
     * @return bool
568
     */
569
    protected function isBlacklisted($column)
570
    {
571
        $colDef = $this->getColumnsDefinition();
572
573
        if (in_array($column, $colDef['blacklist'])) {
574
            return true;
575
        }
576
577
        if ($colDef['whitelist'] === '*' || in_array($column, $colDef['whitelist'])) {
578
            return false;
579
        }
580
581
        return true;
582
    }
583
584
    /**
585
     * Get columns definition.
586
     *
587
     * @return array
588
     */
589
    protected function getColumnsDefinition()
590
    {
591
        $config  = $this->config->get('datatables.columns');
592
        $allowed = ['excess', 'escape', 'raw', 'blacklist', 'whitelist'];
593
594
        return array_replace_recursive(Arr::only($config, $allowed), $this->columnDef);
595
    }
596
597
    /**
598
     * Perform sorting of columns.
599
     */
600
    public function ordering()
601
    {
602
        if ($this->orderCallback) {
603
            return call_user_func($this->orderCallback, $this->resolveCallbackParameter());
604
        }
605
606
        return $this->defaultOrdering();
607
    }
608
609
    /**
610
     * Resolve callback parameter instance.
611
     *
612
     * @return mixed
613
     */
614
    abstract protected function resolveCallbackParameter();
615
616
    /**
617
     * Perform default query orderBy clause.
618
     */
619
    abstract protected function defaultOrdering();
620
621
    /**
622
     * Set auto filter off and run your own filter.
623
     * Overrides global search.
624
     *
625
     * @param callable $callback
626
     * @param bool     $globalSearch
627
     * @return $this
628
     */
629
    public function filter(callable $callback, $globalSearch = false)
630
    {
631
        $this->autoFilter      = $globalSearch;
632
        $this->isFilterApplied = true;
633
        $this->filterCallback  = $callback;
634
635
        return $this;
636
    }
637
638
    /**
639
     * Convert instance to array.
640
     *
641
     * @return array
642
     */
643
    public function toArray()
644
    {
645
        return $this->make()->getData(true);
646
    }
647
648
    /**
649
     * Convert the object to its JSON representation.
650
     *
651
     * @param  int $options
652
     * @return \Illuminate\Http\JsonResponse
653
     */
654
    public function toJson($options = 0)
655
    {
656
        if ($options) {
657
            $this->config->set('datatables.json.options', $options);
658
        }
659
660
        return $this->make();
661
    }
662
663
    /**
664
     * Count filtered items.
665
     *
666
     * @return int
667
     */
668
    protected function filteredCount()
669
    {
670
        return $this->filteredRecords ? $this->filteredRecords : $this->count();
671
    }
672
673
    /**
674
     * Perform necessary filters.
675
     *
676
     * @return void
677
     */
678
    protected function filterRecords()
679
    {
680
        if ($this->autoFilter && $this->request->isSearchable()) {
681
            $this->filtering();
682
        }
683
684
        if (is_callable($this->filterCallback)) {
685
            call_user_func($this->filterCallback, $this->resolveCallbackParameter());
686
        }
687
688
        $this->columnSearch();
689
        $this->searchPanesSearch();
690
        $this->filteredRecords = $this->isFilterApplied ? $this->filteredCount() : $this->totalRecords;
691
    }
692
693
    /**
694
     * Perform search using search pane values.
695
     */
696
    protected function searchPanesSearch()
697
    {
698
        // Add support for search pane.
699
    }
700
701
    /**
702
     * Perform global search.
703
     *
704
     * @return void
705
     */
706
    public function filtering()
707
    {
708
        $keyword = $this->request->keyword();
709
710
        if ($this->config->isMultiTerm()) {
711
            $this->smartGlobalSearch($keyword);
712
713
            return;
714
        }
715
716
        $this->globalSearch($keyword);
717
    }
718
719
    /**
720
     * Perform multi-term search by splitting keyword into
721
     * individual words and searches for each of them.
722
     *
723
     * @param string $keyword
724
     */
725
    protected function smartGlobalSearch($keyword)
726
    {
727
        collect(explode(' ', $keyword))
728
            ->reject(function ($keyword) {
729
                return trim($keyword) === '';
730
            })
731
            ->each(function ($keyword) {
732
                $this->globalSearch($keyword);
733
            });
734
    }
735
736
    /**
737
     * Perform global search for the given keyword.
738
     *
739
     * @param string $keyword
740
     */
741
    abstract protected function globalSearch($keyword);
742
743
    /**
744
     * Apply pagination.
745
     *
746
     * @return void
747
     */
748
    protected function paginate()
749
    {
750
        if ($this->request->isPaginationable() && ! $this->skipPaging) {
751
            $this->paging();
752
        }
753
    }
754
755
    /**
756
     * Transform output.
757
     *
758
     * @param mixed $results
759
     * @param mixed $processed
760
     * @return array
761
     */
762
    protected function transform($results, $processed)
763
    {
764
        if (isset($this->transformer) && class_exists('Yajra\\DataTables\\Transformers\\FractalTransformer')) {
765
            return app('datatables.transformer')->transform(
766
                $results,
767
                $this->transformer,
768
                $this->serializer ?? null
769
            );
770
        }
771
772
        return Helper::transform($processed);
773
    }
774
775
    /**
776
     * Get processed data.
777
     *
778
     * @param mixed $results
779
     * @param bool  $object
780
     * @return array
781
     */
782
    protected function processResults($results, $object = false)
783
    {
784
        $processor = new DataProcessor(
785
            $results,
786
            $this->getColumnsDefinition(),
787
            $this->templates,
788
            $this->request->input('start')
789
        );
790
791
        return $processor->process($object);
792
    }
793
794
    /**
795
     * Render json response.
796
     *
797
     * @param array $data
798
     * @return \Illuminate\Http\JsonResponse
799
     */
800
    protected function render(array $data)
801
    {
802
        $output = $this->attachAppends([
803
            'draw'            => (int) $this->request->input('draw'),
804
            'recordsTotal'    => $this->totalRecords,
805
            'recordsFiltered' => $this->filteredRecords,
806
            'data'            => $data,
807
        ]);
808
809
        if ($this->config->isDebugging()) {
810
            $output = $this->showDebugger($output);
811
        }
812
813
        foreach ($this->searchPanes as $column => $searchPane) {
814
            $output['searchPanes']['options'][$column] = $searchPane['options'];
815
        }
816
817
        return new JsonResponse(
818
            $output,
819
            200,
820
            $this->config->get('datatables.json.header', []),
821
            $this->config->get('datatables.json.options', 0)
822
        );
823
    }
824
825
    /**
826
     * Attach custom with meta on response.
827
     *
828
     * @param array $data
829
     * @return array
830
     */
831
    protected function attachAppends(array $data)
832
    {
833
        return array_merge($data, $this->appends);
834
    }
835
836
    /**
837
     * Append debug parameters on output.
838
     *
839
     * @param  array $output
840
     * @return array
841
     */
842
    protected function showDebugger(array $output)
843
    {
844
        $output['input'] = $this->request->all();
845
846
        return $output;
847
    }
848
849
    /**
850
     * Return an error json response.
851
     *
852
     * @param \Exception $exception
853
     * @return \Illuminate\Http\JsonResponse
854
     * @throws \Yajra\DataTables\Exceptions\Exception
855
     */
856
    protected function errorResponse(\Exception $exception)
857
    {
858
        $error = $this->config->get('datatables.error');
859
        $debug = $this->config->get('app.debug');
860
861
        if ($error === 'throw' || (! $error && ! $debug)) {
862
            throw new Exception($exception->getMessage(), $code = 0, $exception);
863
        }
864
865
        $this->getLogger()->error($exception);
866
867
        return new JsonResponse([
868
            'draw'            => (int) $this->request->input('draw'),
869
            'recordsTotal'    => $this->totalRecords,
870
            'recordsFiltered' => 0,
871
            'data'            => [],
872
            'error'           => $error ? __($error) : "Exception Message:\n\n".$exception->getMessage(),
873
        ]);
874
    }
875
876
    /**
877
     * Get monolog/logger instance.
878
     *
879
     * @return \Psr\Log\LoggerInterface
880
     */
881
    public function getLogger()
882
    {
883
        $this->logger = $this->logger ?: app(LoggerInterface::class);
884
885
        return $this->logger;
886
    }
887
888
    /**
889
     * Set monolog/logger instance.
890
     *
891
     * @param \Psr\Log\LoggerInterface $logger
892
     * @return $this
893
     */
894
    public function setLogger(LoggerInterface $logger)
895
    {
896
        $this->logger = $logger;
897
898
        return $this;
899
    }
900
901
    /**
902
     * Setup search keyword.
903
     *
904
     * @param  string $value
905
     * @return string
906
     */
907
    protected function setupKeyword($value)
908
    {
909
        if ($this->config->isSmartSearch()) {
910
            $keyword = '%'.$value.'%';
911
            if ($this->config->isWildcard()) {
912
                $keyword = Helper::wildcardLikeString($value);
913
            }
914
            // remove escaping slash added on js script request
915
            $keyword = str_replace('\\', '%', $keyword);
916
917
            return $keyword;
918
        }
919
920
        return $value;
921
    }
922
923
    /**
924
     * Get column name to be use for filtering and sorting.
925
     *
926
     * @param int  $index
927
     * @param bool $wantsAlias
928
     * @return string
929
     */
930
    protected function getColumnName($index, $wantsAlias = false)
931
    {
932
        $column = $this->request->columnName($index);
933
934
        // DataTables is using make(false)
935
        if (is_numeric($column)) {
936
            $column = $this->getColumnNameByIndex($index);
937
        }
938
939
        if (Str::contains(Str::upper($column), ' AS ')) {
940
            $column = Helper::extractColumnName($column, $wantsAlias);
941
        }
942
943
        return $column;
944
    }
945
946
    /**
947
     * Get column name by order column index.
948
     *
949
     * @param int $index
950
     * @return string
951
     */
952
    protected function getColumnNameByIndex($index)
953
    {
954
        $name = (isset($this->columns[$index]) && $this->columns[$index] != '*')
955
            ? $this->columns[$index] : $this->getPrimaryKeyName();
956
957
        return in_array($name, $this->extraColumns, true) ? $this->getPrimaryKeyName() : $name;
958
    }
959
960
    /**
961
     * If column name could not be resolved then use primary key.
962
     *
963
     * @return string
964
     */
965
    protected function getPrimaryKeyName()
966
    {
967
        return 'id';
968
    }
969
970
    /**
971
     * Add a search pane options on response.
972
     *
973
     * @param string $column
974
     * @param mixed $options
975
     * @param callable|null $builder
976
     * @return $this
977
     */
978
    public function searchPane($column, $options, callable $builder = null)
979
    {
980
        $options = value($options);
981
982
        if ($options instanceof Arrayable) {
983
            $options = $options->toArray();
984
        }
985
986
        $this->searchPanes[$column]['options'] = $options;
987
        $this->searchPanes[$column]['builder'] = $builder;
988
989
        return $this;
990
    }
991
}
992