Completed
Push — master ( 78beea...a25989 )
by Anton
22s queued 12s
created

Grid::processRequest()   B

Complexity

Conditions 8
Paths 25

Size

Total Lines 48
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 8.004

Importance

Changes 0
Metric Value
cc 8
eloc 25
nc 25
nop 0
dl 0
loc 48
ccs 24
cts 25
cp 0.96
crap 8.004
rs 8.4444
c 0
b 0
f 0
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 32
    public function __construct($options = null)
177
    {
178
        // initial default helper path
179 32
        $this->addHelperPath(__DIR__ . '/Helper/');
180
181 32
        if ($options) {
182
            $this->setOptions($options);
183
        }
184
185 32
        if ($this->getUid()) {
186 32
            $this->prefix = $this->getUid() . '-';
187
        }
188
189 32
        $this->init();
190
191 32
        $this->processRequest();
192 32
    }
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 32
    public function setAdapter(Source\AbstractSource $adapter): void
209
    {
210 32
        $this->adapter = $adapter;
211 32
    }
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 32
    public function getUid(): string
233
    {
234 32
        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 32
    public function processRequest(): void
311
    {
312 32
        $this->module = Request::getModule();
313 32
        $this->controller = Request::getController();
314
315 32
        $page = (int)Request::getParam($this->prefix . 'page', 1);
316 32
        $this->setPage($page);
317
318 32
        $limit = (int)Request::getParam($this->prefix . 'limit', $this->limit);
319 32
        $this->setLimit($limit);
320
321 32
        foreach ($this->allowOrders as $column) {
322 32
            $alias = $this->applyAlias($column);
323 32
            $order = Request::getParam($this->prefix . 'order-' . $alias);
324 32
            if (is_array($order)) {
325
                $order = current($order);
326
            }
327 32
            if (null !== $order) {
328 32
                $this->addOrder($column, $order);
0 ignored issues
show
Bug introduced by
It seems like $order can also be of type array; however, parameter $order of Bluz\Grid\Grid::addOrder() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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