Completed
Push — master ( 467d47...6f2bb4 )
by Anton
14s
created

Grid::processSource()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.1825

Importance

Changes 0
Metric Value
nc 3
dl 0
loc 17
ccs 8
cts 11
cp 0.7272
c 0
b 0
f 0
cc 3
eloc 11
nop 0
crap 3.1825
rs 9.4285
1
<?php
2
/**
3
 * Bluz Framework Component
4
 *
5
 * @copyright Bluz PHP Team
6
 * @link      https://github.com/bluzphp/framework
7
 */
8
9
declare(strict_types=1);
10
11
namespace Bluz\Grid;
12
13
use Bluz\Common\Helper;
14
use Bluz\Common\Options;
15
use Bluz\Proxy\Request;
16
use Bluz\Proxy\Router;
17
18
/**
19
 * Grid
20
 *
21
 * @package  Bluz\Grid
22
 * @author   Anton Shevchuk
23
 * @link     https://github.com/bluzphp/framework/wiki/Grid
24
 *
25
 * @method string filter($column, $filter, $value, $reset = true)
26
 * @method string first()
27
 * @method string last()
28
 * @method string limit($limit = 25)
29
 * @method string next()
30
 * @method string order($column, $order = null, $defaultOrder = Grid::ORDER_ASC, $reset = true)
31
 * @method string page($page = 1)
32
 * @method string pages()
33
 * @method string prev()
34
 * @method string reset()
35
 * @method string total()
36
 */
37
abstract class Grid
38
{
39
    use Options;
40
    use Helper;
41
42
    public const ORDER_ASC = 'asc';
43
    public const ORDER_DESC = 'desc';
44
45
    public const FILTER_LIKE = 'like'; // like
46
    public const FILTER_ENUM = 'enum'; // one from .., .., ..
47
48
    public const FILTER_EQ = 'eq'; // equal to ..
49
    public const FILTER_NE = 'ne'; // not equal to ..
50
    public const FILTER_GT = 'gt'; // greater than ..
51
    public const FILTER_GE = 'ge'; // greater than .. or equal
52
    public const FILTER_LT = 'lt'; // less than ..
53
    public const FILTER_LE = 'le'; // less than .. or equal
54
55
    /**
56
     * @var Source\AbstractSource instance of Source
57
     */
58
    protected $adapter;
59
60
    /**
61
     * @var Data instance of Data
62
     */
63
    protected $data;
64
65
    /**
66
     * @var string unique identification of grid
67
     */
68
    protected $uid;
69
70
    /**
71
     * @var string unique prefix of grid
72
     */
73
    protected $prefix = '';
74
75
    /**
76
     * @var string location of Grid
77
     */
78
    protected $module;
79
80
    /**
81
     * @var string location of Grid
82
     */
83
    protected $controller;
84
85
    /**
86
     * @var array custom array params
87
     */
88
    protected $params = [];
89
90
    /**
91
     * @var integer start from first page
92
     */
93
    protected $page = 1;
94
95
    /**
96
     * @var integer limit per page
97
     */
98
    protected $limit = 25;
99
100
    /**
101
     * @var integer default value of page limit
102
     * @see Grid::$limit
103
     */
104
    protected $defaultLimit = 25;
105
106
    /**
107
     * List of orders
108
     *
109
     * Example
110
     *     'first' => 'ASC',
111
     *     'last' => 'ASC'
112
     *
113
     * @var array
114
     */
115
    protected $orders = [];
116
117
    /**
118
     * @var array default order
119
     * @see Grid::$orders
120
     */
121
    protected $defaultOrder;
122
123
    /**
124
     * @var array list of allow orders
125
     * @see Grid::$orders
126
     */
127
    protected $allowOrders = [];
128
129
    /**
130
     * @var array list of filters
131
     */
132
    protected $filters = [];
133
134
    /**
135
     * List of allow filters
136
     *
137
     * Example
138
     *     ['id', 'status' => ['active', 'disable']]
139
     *
140
     * @var array
141
     * @see Grid::$filters
142
     */
143
    protected $allowFilters = [];
144
145
    /**
146
     * List of allow filter names
147
     *
148
     * @var array
149
     * @see Grid::$filters
150
     */
151
    protected $allowFilterNames = [
152
        self::FILTER_LIKE,
153
        self::FILTER_ENUM,
154
        self::FILTER_EQ,
155
        self::FILTER_NE,
156
        self::FILTER_GT,
157
        self::FILTER_GE,
158
        self::FILTER_LT,
159
        self::FILTER_LE
160
    ];
161
162
    /**
163
     * List of aliases for columns in DB
164
     *
165
     * @var array
166
     */
167
    protected $aliases = [];
168
169
    /**
170
     * Grid constructor
171
     *
172
     * @param array $options
173
     *
174
     * @throws \Bluz\Common\Exception\CommonException
175
     */
176 34
    public function __construct($options = null)
177
    {
178
        // initial default helper path
179 34
        $this->addHelperPath(__DIR__ . '/Helper/');
180
181 34
        if ($options) {
182
            $this->setOptions($options);
183
        }
184
185 34
        if ($this->getUid()) {
186 34
            $this->prefix = $this->getUid() . '-';
187
        }
188
189 34
        $this->init();
190
191 34
        $this->processRequest();
192 34
    }
193
194
    /**
195
     * Initialize Grid
196
     *
197
     * @return void
198
     */
199
    abstract public function init() : void;
200
201
    /**
202
     * Set source adapter
203
     *
204
     * @param Source\AbstractSource $adapter
205
     *
206
     * @return void
207
     */
208 34
    public function setAdapter(Source\AbstractSource $adapter) : void
209
    {
210 34
        $this->adapter = $adapter;
211 34
    }
212
213
    /**
214
     * Get source adapter
215
     *
216
     * @return Source\AbstractSource
217
     * @throws GridException
218
     */
219 15
    public function getAdapter() : Source\AbstractSource
220
    {
221 15
        if (null === $this->adapter) {
222
            throw new GridException('Grid adapter is not initialized');
223
        }
224 15
        return $this->adapter;
225
    }
226
227
    /**
228
     * Get unique Grid Id
229
     *
230
     * @return string
231
     */
232 34
    public function getUid() : string
233
    {
234 34
        return $this->uid;
235
    }
236
237
    /**
238
     * Get prefix
239
     *
240
     * @return string
241
     */
242 1
    public function getPrefix() : string
243
    {
244 1
        return $this->prefix;
245
    }
246
247
    /**
248
     * Set module
249
     *
250
     * @param string $module
251
     *
252
     * @return void
253
     */
254 1
    public function setModule(string $module) : void
255
    {
256 1
        $this->module = $module;
257 1
    }
258
259
    /**
260
     * Get module
261
     *
262
     * @return string
263
     */
264 13
    public function getModule() : ?string
265
    {
266 13
        return $this->module;
267
    }
268
269
    /**
270
     * Set controller
271
     *
272
     * @param  string $controller
273
     *
274
     * @return void
275
     */
276 1
    public function setController(string $controller) : void
277
    {
278 1
        $this->controller = $controller;
279 1
    }
280
281
    /**
282
     * Get controller
283
     *
284
     * @return string
285
     */
286 13
    public function getController() : ?string
287
    {
288 13
        return $this->controller;
289
    }
290
291
    /**
292
     * Process request
293
     *
294
     * Example of request url
295
     * - http://domain.com/pages/grid/
296
     * - http://domain.com/pages/grid/page/2/
297
     * - http://domain.com/pages/grid/page/2/order-alias/desc/
298
     * - http://domain.com/pages/grid/page/2/order-created/desc/order-alias/asc/
299
     *
300
     * with prefix for support more than one grid on page
301
     * - http://domain.com/users/grid/users-page/2/users-order-created/desc/
302
     * - http://domain.com/users/grid/users-page/2/users-filter-status/active/
303
     *
304
     * hash support
305
     * - http://domain.com/pages/grid/#/page/2/order-created/desc/order-alias/asc/
306
     *
307
     * @return void
308
     * @throws GridException
309
     */
310 34
    public function processRequest() : void
311
    {
312 34
        $this->module = Request::getModule();
313 34
        $this->controller = Request::getController();
314
315 34
        $page = (int)Request::getParam($this->prefix . 'page', 1);
316 34
        $this->setPage($page);
317
318 34
        $limit = (int)Request::getParam($this->prefix . 'limit', $this->limit);
319 34
        $this->setLimit($limit);
320
321 34
        foreach ($this->allowOrders as $column) {
322 34
            $alias = $this->applyAlias($column);
323 34
            $order = Request::getParam($this->prefix . 'order-' . $alias);
324 34
            if (null !== $order) {
325 34
                $this->addOrder($column, $order);
326
            }
327
        }
328 34
        foreach ($this->allowFilters as $column) {
329 34
            $alias = $this->applyAlias($column);
330 34
            $filter = Request::getParam($this->prefix . 'filter-' . $alias);
331
332 34
            if (null !== $filter) {
333 1
                $filter = trim($filter, ' -');
334 1
                if (strpos($filter, '-')) {
335
                    /**
336
                     * Example of filters
337
                     * - http://domain.com/users/grid/users-filter-roleId/gt-2      - roleId greater than 2
338
                     * - http://domain.com/users/grid/users-filter-roleId/gt-1-lt-4 - 1 < roleId < 4
339
                     * - http://domain.com/users/grid/users-filter-login/eq-admin   - login == admin
340
                     */
341 1
                    while ($filter) {
342 1
                        [$filterName, $filterValue, $filter] = array_pad(explode('-', $filter, 3), 3, null);
0 ignored issues
show
Bug introduced by
The variable $filterName does not exist. Did you mean $filter?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
Bug introduced by
The variable $filterValue does not exist. Did you mean $filter?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
343 1
                        $this->addFilter($column, $filterName, $filterValue);
0 ignored issues
show
Bug introduced by
The variable $filterName does not exist. Did you mean $filter?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
Bug introduced by
The variable $filterValue does not exist. Did you mean $filter?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
344
                    }
345
                } else {
346
                    /**
347
                     * Example of filters
348
                     * - http://domain.com/users/grid/users-filter-roleId/2
349
                     * - http://domain.com/users/grid/users-filter-login/admin
350
                     */
351 34
                    $this->addFilter($column, self::FILTER_EQ, $filter);
352
                }
353
            }
354
        }
355 34
    }
356
357
    /**
358
     * Process source
359
     *
360
     * @return void
361
     * @throws GridException
362
     */
363 15
    public function processSource() : void
364
    {
365 15
        if (null === $this->adapter) {
366
            throw new GridException('Grid Adapter is not initiated, please update method `init()` and try again');
367
        }
368
369
        try {
370 15
            $this->data = $this->getAdapter()->process(
371 15
                $this->getPage(),
372 15
                $this->getLimit(),
373 15
                $this->getFilters(),
374 15
                $this->getOrders()
375
            );
376
        } catch (\Exception $e) {
377
            throw new GridException('Grid Adapter can\'t process request: '. $e->getMessage());
378
        }
379 15
    }
380
381
    /**
382
     * Get data
383
     *
384
     * @return Data
385
     * @throws \Bluz\Grid\GridException
386
     */
387 15
    public function getData() : Data
388
    {
389 15
        if (!$this->data) {
390 15
            $this->processSource();
391
        }
392 15
        return $this->data;
393
    }
394
395
    /**
396
     * Setup params
397
     *
398
     * @param  $params
399
     *
400
     * @return void
401
     */
402
    public function setParams($params) : void
403
    {
404
        $this->params = $params;
405
    }
406
407
    /**
408
     * Return params prepared for url builder
409
     *
410
     * @param  array $rewrite
411
     *
412
     * @return array
413
     */
414 13
    public function getParams(array $rewrite = []) : array
415
    {
416 13
        $params = $this->params;
417
418
        // change page to first for each new grid (with new filters or orders, or other stuff)
419 13
        $page = $rewrite['page'] ?? 1;
420
421 13
        if ($page > 1) {
422 4
            $params[$this->prefix . 'page'] = $page;
423
        }
424
425
        // change limit
426 13
        $limit = $rewrite['limit'] ?? $this->getLimit();
427
428 13
        if ($limit !== $this->defaultLimit) {
429 1
            $params[$this->prefix . 'limit'] = $limit;
430
        }
431
432
        // change orders
433 13
        $orders = $rewrite['orders'] ?? $this->getOrders();
434
435 13
        foreach ($orders as $column => $order) {
436 3
            $column = $this->applyAlias($column);
437 3
            $params[$this->prefix . 'order-' . $column] = $order;
438
        }
439
440
        // change filters
441 13
        $filters = $rewrite['filters'] ?? $this->getFilters();
442
443 13
        foreach ($filters as $column => $columnFilters) {
444
            /** @var array $columnFilters */
445 3
            $column = $this->applyAlias($column);
446 3
            if (count($columnFilters) === 1 && isset($columnFilters[self::FILTER_EQ])) {
447 2
                $params[$this->prefix . 'filter-' . $column] = $columnFilters[self::FILTER_EQ];
448 2
                continue;
449
            }
450
451 2
            $columnFilter = [];
452 2
            foreach ($columnFilters as $filterName => $filterValue) {
453 2
                $columnFilter[] = $filterName . '-' . $filterValue;
454
            }
455 2
            $params[$this->prefix . 'filter-' . $column] = implode('-', $columnFilter);
456
        }
457 13
        return $params;
458
    }
459
460
    /**
461
     * Get Url
462
     *
463
     * @param  array $params
464
     *
465
     * @return string
466
     */
467 13
    public function getUrl($params) : string
468
    {
469
        // prepare params
470 13
        $params = $this->getParams($params);
471
472
        // retrieve URL
473 13
        return Router::getUrl(
474 13
            $this->getModule(),
475 13
            $this->getController(),
476 13
            $params
477
        );
478
    }
479
480
    /**
481
     * Add column name for allow order
482
     *
483
     * @param string $column
484
     *
485
     * @return void
486
     */
487 34
    public function addAllowOrder($column) : void
488
    {
489 34
        $this->allowOrders[] = $column;
490 34
    }
491
492
    /**
493
     * Set allow orders
494
     *
495
     * @param  string[] $orders
496
     *
497
     * @return void
498
     */
499 34
    public function setAllowOrders(array $orders = []) : void
500
    {
501 34
        $this->allowOrders = [];
502 34
        foreach ($orders as $column) {
503 34
            $this->addAllowOrder($column);
504
        }
505 34
    }
506
507
    /**
508
     * Get allow orders
509
     *
510
     * @return array
511
     */
512 8
    public function getAllowOrders() : array
513
    {
514 8
        return $this->allowOrders;
515
    }
516
517
    /**
518
     * Check order column
519
     *
520
     * @param  string $column
521
     *
522
     * @return bool
523
     */
524 8
    protected function checkOrderColumn($column) : bool
525
    {
526 8
        return in_array($column, $this->getAllowOrders(), true);
527
    }
528
529
    /**
530
     * Check order name
531
     *
532
     * @param  string $order
533
     *
534
     * @return bool
535
     */
536 6
    protected function checkOrderName($order) : bool
537
    {
538 6
        return ($order === self::ORDER_ASC || $order === self::ORDER_DESC);
539
    }
540
541
    /**
542
     * Add order rule
543
     *
544
     * @param  string $column
545
     * @param  string $order
546
     *
547
     * @return void
548
     * @throws GridException
549
     */
550 8
    public function addOrder($column, $order = self::ORDER_ASC) : void
551
    {
552 8
        if (!$this->checkOrderColumn($column)) {
553 1
            throw new GridException("Order for column `$column` is not allowed");
554
        }
555
556 7
        if (!$this->checkOrderName($order)) {
557 1
            throw new GridException("Order name for column `$column` is incorrect");
558
        }
559
560 6
        $this->orders[$column] = $order;
561 6
    }
562
563
    /**
564
     * Add order rules
565
     *
566
     * @param  array $orders
567
     *
568
     * @return void
569
     * @throws GridException
570
     */
571 1
    public function addOrders(array $orders) : void
572
    {
573 1
        foreach ($orders as $column => $order) {
574 1
            $this->addOrder($column, $order);
575
        }
576 1
    }
577
578
    /**
579
     * Set order
580
     *
581
     * @param  string $column
582
     * @param  string $order ASC or DESC
583
     *
584
     * @return void
585
     * @throws GridException
586
     */
587 6
    public function setOrder($column, $order = self::ORDER_ASC) : void
588
    {
589 6
        $this->orders = [];
590 6
        $this->addOrder($column, $order);
591 4
    }
592
593
    /**
594
     * Set orders
595
     *
596
     * @param  array $orders
597
     *
598
     * @return void
599
     * @throws GridException
600
     */
601 1
    public function setOrders(array $orders) : void
602
    {
603 1
        $this->orders = [];
604 1
        $this->addOrders($orders);
605 1
    }
606
607
    /**
608
     * Get orders
609
     *
610
     * @return array
611
     */
612 25
    public function getOrders() : array
613
    {
614 25
        $default = $this->getDefaultOrder();
615
616
        // remove default order when another one is set
617 25
        if (is_array($default)
618 25
            && count($this->orders)
619 25
            && isset($this->orders[key($default)])
620 25
            && $this->orders[key($default)] === reset($default)
621
        ) {
622 3
            unset($this->orders[key($default)]);
623
        }
624
625 25
        return $this->orders;
626
    }
627
628
    /**
629
     * Add column name to allow filter it
630
     *
631
     * @param string $column
632
     *
633
     * @return void
634
     */
635 34
    public function addAllowFilter($column) : void
636
    {
637 34
        $this->allowFilters[] = $column;
638 34
    }
639
640
    /**
641
     * Set allowed filters
642
     *
643
     * @param  string[] $filters
644
     *
645
     * @return void
646
     */
647 34
    public function setAllowFilters(array $filters = []) : void
648
    {
649 34
        $this->allowFilters = [];
650 34
        foreach ($filters as $column) {
651 34
            $this->addAllowFilter($column);
652
        }
653 34
    }
654
655
    /**
656
     * Get allow filters
657
     *
658
     * @return array
659
     */
660 12
    public function getAllowFilters() : array
661
    {
662 12
        return $this->allowFilters;
663
    }
664
665
    /**
666
     * Check filter column
667
     *
668
     * @param  string $column
669
     *
670
     * @return bool
671
     */
672 12
    protected function checkFilterColumn($column) : bool
673
    {
674 12
        return array_key_exists($column, $this->getAllowFilters()) ||
675 12
            in_array($column, $this->getAllowFilters(), false);
676
    }
677
678
    /**
679
     * Check filter
680
     *
681
     * @param  string $filter
682
     *
683
     * @return bool
684
     */
685 12
    protected function checkFilterName($filter) : bool
686
    {
687 12
        return in_array($filter, $this->allowFilterNames, false);
688
    }
689
690
    /**
691
     * Add filter
692
     *
693
     * @param  string $column name
694
     * @param  string $filter
695
     * @param  string $value
696
     *
697
     * @return void
698
     * @throws GridException
699
     */
700 10
    public function addFilter($column, $filter, $value) : void
701
    {
702 10
        if (!$this->checkFilterColumn($column)) {
703 1
            throw new GridException("Filter for column `$column` is not allowed");
704
        }
705 9
        if (!$this->checkFilterName($filter)) {
706 1
            throw new GridException('Filter name is incorrect');
707
        }
708 8
        if (!isset($this->filters[$column])) {
709 8
            $this->filters[$column] = [];
710
        }
711 8
        $this->filters[$column][$filter] = $value;
712 8
    }
713
714
715
    /**
716
     * Get filter
717
     *
718
     * @param  string $column
719
     * @param  string $filter
720
     *
721
     * @return mixed
722
     */
723
    public function getFilter($column, $filter = null) : ?string
724
    {
725
        if (null === $filter) {
726
            return $this->filters[$column] ?? null;
727
        }
728
        return $this->filters[$column][$filter] ?? null;
729
    }
730
731
    /**
732
     * Get filters
733
     *
734
     * @return array
735
     */
736 21
    public function getFilters() : array
737
    {
738 21
        return $this->filters;
739
    }
740
741
    /**
742
     * Add alias for column name
743
     *
744
     * @param  string $column
745
     * @param  string $alias
746
     *
747
     * @return void
748
     */
749 32
    public function addAlias($column, $alias) : void
750
    {
751 32
        $this->aliases[$column] = $alias;
752 32
    }
753
754
    /**
755
     * Get column name by alias
756
     *
757
     * @param  string $alias
758
     *
759
     * @return string
760
     */
761
    protected function reverseAlias($alias) : string
762
    {
763
        return array_search($alias, $this->aliases, true) ?: $alias;
764
    }
765
766
    /**
767
     * Get alias by column name
768
     *
769
     * @param  string $column
770
     *
771
     * @return string
772
     */
773 34
    protected function applyAlias($column) : string
774
    {
775 34
        return $this->aliases[$column] ?? $column;
776
    }
777
778
    /**
779
     * Set page
780
     *
781
     * @param  integer $page
782
     *
783
     * @return void
784
     * @throws GridException
785
     */
786 34
    public function setPage(int $page = 1) : void
787
    {
788 34
        if ($page < 1) {
789 1
            throw new GridException('Wrong page number, should be greater than zero');
790
        }
791 34
        $this->page = $page;
792 34
    }
793
794
    /**
795
     * Get page
796
     *
797
     * @return integer
798
     */
799 17
    public function getPage() : int
800
    {
801 17
        return $this->page;
802
    }
803
804
    /**
805
     * Set limit per page
806
     *
807
     * @param  integer $limit
808
     *
809
     * @return void
810
     * @throws GridException
811
     */
812 34
    public function setLimit(int $limit) : void
813
    {
814 34
        if ($limit < 1) {
815 1
            throw new GridException('Wrong limit value, should be greater than zero');
816
        }
817 34
        $this->limit = $limit;
818 34
    }
819
820
    /**
821
     * Get limit per page
822
     *
823
     * @return integer
824
     */
825 24
    public function getLimit() : int
826
    {
827 24
        return $this->limit;
828
    }
829
830
    /**
831
     * Set default limit
832
     *
833
     * @param  integer $limit
834
     *
835
     * @return void
836
     * @throws GridException
837
     */
838 34
    public function setDefaultLimit(int $limit) : void
839
    {
840 34
        if ($limit < 1) {
841 1
            throw new GridException('Wrong default limit value, should be greater than zero');
842
        }
843 34
        $this->setLimit($limit);
844
845 34
        $this->defaultLimit = $limit;
846 34
    }
847
848
    /**
849
     * Get default limit
850
     *
851
     * @return integer
852
     */
853 1
    public function getDefaultLimit() : int
854
    {
855 1
        return $this->defaultLimit;
856
    }
857
858
    /**
859
     * Set default order
860
     *
861
     * @param  string $column
862
     * @param  string $order ASC or DESC
863
     *
864
     * @return void
865
     * @throws GridException
866
     */
867 5
    public function setDefaultOrder($column, $order = self::ORDER_ASC) : void
868
    {
869 5
        $this->setOrder($column, $order);
870
871 3
        $this->defaultOrder = [$column => $order];
872 3
    }
873
874
    /**
875
     * Get default order
876
     *
877
     * @return array
878
     */
879 24
    public function getDefaultOrder() : ?array
880
    {
881 24
        return $this->defaultOrder;
882
    }
883
}
884