Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.66% covered (warning)
89.66%
52 / 58
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
GridRequest
91.23% covered (success)
91.23%
52 / 57
60.00% covered (warning)
60.00%
3 / 5
32.69
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromRequest
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
13
 parseFilters
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
6.60
 appendFilterArray
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
7.14
 appendPrefixedFilters
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Grid;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use Psr\Http\Message\ServerRequestInterface;
12
13/**
14 * DataGrid Request State DTO.
15 *
16 * Extracts and sanitizes HTTP query parameters for pagination, sorting, and column filtering.
17 *
18 * @package App\Core\Grid
19 */
20final readonly class GridRequest
21{
22    /**
23     * GridRequest constructor.
24     *
25     * @param int                   $page                 Current 1-indexed page number.
26     * @param int                   $limit                Number of items per page.
27     * @param string|null           $sortColumn           Active sort column key.
28     * @param string                $sortDirection        Sort direction ('ASC' or 'DESC').
29     * @param array<string, string> $filters              Column key -> search filter string mapping.
30     * @param bool                  $hasExplicitSort      Whether sort column was explicitly passed in request.
31     * @param bool                  $hasExplicitDirection Whether sort direction was explicitly passed in request.
32     * @param int|null              $recordStatus         Legacy status code (0..3, null=All).
33     * @param int|null              $specialAccess        Special access level (0..3, null=All).
34     * @param string|null           $level                System operational level (admin, client, etc.).
35     */
36    public function __construct(
37        public int $page = 1,
38        public int $limit = 15,
39        public ?string $sortColumn = null,
40        public string $sortDirection = 'ASC',
41        public array $filters = [],
42        public bool $hasExplicitSort = false,
43        public bool $hasExplicitDirection = false,
44        public ?int $recordStatus = null,
45        public ?int $specialAccess = null,
46        public ?string $level = null,
47    ) {
48    }
49
50    /**
51     * Factory method creating GridRequest from PSR-7 ServerRequestInterface instance.
52     *
53     * @param ServerRequestInterface $request     PSR-7 Server request instance.
54     * @param string                 $defaultSort Fallback sort column key.
55     * @return self Populated GridRequest value object.
56     */
57    public static function fromRequest(ServerRequestInterface $request, string $defaultSort = 'id'): self
58    {
59        /** @var array<string, mixed> $queryParams */
60        $queryParams = $request->getQueryParams();
61
62        $page = max(1, (int)($queryParams['page'] ?? 1));
63        $limitCandidate = (int)($queryParams['limit'] ?? 15);
64        $limit = in_array($limitCandidate, [10, 15, 25, 50, 100], true) ? $limitCandidate : 15;
65
66        $hasExplicitSort = isset($queryParams['sort']) && is_string($queryParams['sort'])
67            && trim($queryParams['sort']) !== '';
68        $sortColumn = $hasExplicitSort ? trim((string)$queryParams['sort']) : $defaultSort;
69
70        $rawDir = $queryParams['dir'] ?? ($queryParams['order'] ?? null);
71        $hasExplicitDirection = $rawDir !== null && is_string($rawDir) && trim($rawDir) !== '';
72        $dirCandidate = strtoupper(trim((string)($rawDir ?? 'ASC')));
73        $sortDirection = in_array($dirCandidate, ['ASC', 'DESC'], true) ? $dirCandidate : 'ASC';
74
75        $rawAccess = $queryParams['special_access'] ?? ($queryParams['record_status'] ?? null);
76        $specialAccess = null;
77        if ($rawAccess !== null && $rawAccess !== '') {
78            $candidate = (int)$rawAccess;
79            if (\App\Core\Engine\Domain\Model\SpecialAccess::isValid($candidate)) {
80                $specialAccess = $candidate;
81            }
82        }
83        $recordStatus = $specialAccess;
84
85        $rawLevel = $queryParams['level'] ?? null;
86        $level = is_string($rawLevel) && trim($rawLevel) !== '' ? trim($rawLevel) : null;
87
88        $filters = self::parseFilters($queryParams);
89
90        return new self(
91            $page,
92            $limit,
93            $sortColumn,
94            $sortDirection,
95            $filters,
96            $hasExplicitSort,
97            $hasExplicitDirection,
98            $recordStatus,
99            $specialAccess,
100            $level
101        );
102    }
103
104    /**
105     * Extracts and normalizes column filter parameters from query parameters array.
106     *
107     * @param array<string, mixed> $queryParams Query parameters map.
108     * @return array<string, string> Normalized filter map.
109     */
110    private static function parseFilters(array $queryParams): array
111    {
112        /** @var array<string, string> $filters */
113        $filters = [];
114
115        if (isset($queryParams['filter']) && is_array($queryParams['filter'])) {
116            self::appendFilterArray($filters, $queryParams['filter']);
117        }
118
119        if (isset($queryParams['filters'])) {
120            $rawFilters = is_string($queryParams['filters'])
121                ? json_decode($queryParams['filters'], true)
122                : $queryParams['filters'];
123            self::appendFilterArray($filters, $rawFilters);
124        }
125
126        self::appendPrefixedFilters($filters, $queryParams);
127
128        return $filters;
129    }
130
131    /**
132     * Appends key-value filter pairs from raw map.
133     *
134     * @param array<string, string> $filters Accumulated filters map.
135     * @param mixed                 $raw     Raw array or dictionary.
136     */
137    private static function appendFilterArray(array &$filters, mixed $raw): void
138    {
139        if (!is_array($raw)) {
140            return;
141        }
142        foreach ($raw as $key => $value) {
143            if (is_string($key) && (is_string($value) || is_numeric($value))) {
144                $trimmed = trim((string) $value);
145                if ($trimmed !== '') {
146                    $filters[$key] = $trimmed;
147                }
148            }
149        }
150    }
151
152    /**
153     * Appends filter_* prefixed query parameters (excluding reserved filter_id).
154     *
155     * @param array<string, string> $filters     Accumulated filters map.
156     * @param array<string, mixed>  $queryParams Query parameters map.
157     */
158    private static function appendPrefixedFilters(array &$filters, array $queryParams): void
159    {
160        foreach ($queryParams as $k => $v) {
161            if ($k === 'filter_id') {
162                continue;
163            }
164
165            if (str_starts_with($k, 'filter_') && is_string($v)) {
166                $trimmed = trim($v);
167                if ($trimmed !== '') {
168                    $filters[substr($k, 7)] = $trimmed;
169                }
170            }
171        }
172    }
173}