PaginationFactory   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 219
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 34
lcom 1
cbo 2
dl 0
loc 219
ccs 62
cts 62
cp 1
rs 9.2
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
D create() 0 35 10
C getOrder() 0 22 7
C parseSort() 0 44 13
A parse() 0 7 4
1
<?php
2
declare(strict_types=1);
3
/**
4
 * Caridea
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
7
 * use this file except in compliance with the License. You may obtain a copy of
8
 * the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
 * License for the specific language governing permissions and limitations under
16
 * the License.
17
 *
18
 * @copyright 2015-2018 LibreWorks contributors
19
 * @license   Apache-2.0
20
 */
21
namespace Caridea\Http;
22
23
/**
24
 * Produces Pagination objects based on request parameters.
25
 *
26
 * @copyright 2015-2018 LibreWorks contributors
27
 * @license   Apache-2.0
28
 */
29
class PaginationFactory
30
{
31
    const DESC = "desc";
32
    const SORT = "sort";
33
    const ORDER = "order";
34
    const PAGE = "page";
35
    const START_PAGE = "startPage";
36
    const START_INDEX = "startIndex";
37
    const START = "start";
38
    const COUNT = "count";
39
    const MAX = "max";
40
    const LIMIT = "limit";
41
    const OFFSET = "offset";
42
    const RANGE = "Range";
43
    const REGEX_RANGE = '/^items=(\\d+)-(\\d+)$/';
44
    const REGEX_DOJO_SORT = '/^sort\\(.*\\)$/';
45
    
46
    private static $maxAlias = [self::MAX => null, self::LIMIT => null, self::COUNT => null];
47
    private static $offsetAlias = [self::START => null, self::OFFSET => null];
48
    private static $pageAlias = [self::PAGE => null, self::START_PAGE => null];
49
    
50
    /**
51
     * Checks the request query parameters and headers for pagination info.
52
     *
53
     * This class supports a good size sampling of different ways to provide
54
     * pagination info.
55
     *
56
     * #### Range
57
     * - `max` + `offset` (e.g. Grails): `&max=25&offset=0`
58
     * - `count` + `start` (e.g. `dojox.data.QueryReadStore`): `&count=25&start=0`
59
     * - `Range` header (e.g. `dojo.store.JsonRest`): `Range: items=0-24`
60
     * - `count` + `startIndex` (e.g. OpenSearch): `&count=25&startIndex=1`
61
     * - `count` + `startPage` (e.g. OpenSearch): `&count=25&startPage=1`
62
     * - `limit` + `page` (e.g. Spring Data REST): `&limit=25&page=1`
63
     * - `limit` + `start` (e.g. ExtJS): `&limit=25&start=0`
64
     *
65
     * Dojo, Grails, and ExtJS are all zero-based. OpenSearch and Spring Data
66
     * are one-based.
67
     *
68
     * #### Order
69
     * - OpenSearchServer: `&sort=foo&sort=-bar`
70
     * - OpenSearch extension: `&sort=foo:ascending&sort=bar:descending`
71
     * - Grails: `&sort=foo&order=asc`
72
     * - Spring Data REST: `&sort=foo,asc&sort=bar,desc`
73
     * - Dojo: `&sort(+foo,-bar)`
74
     * - Dojo w/field: `&[field]=+foo,-bar`
75
     * - ExtJS JSON: `&sort=[{"property":"foo","direction":"asc"},{"property":"bar","direction":"desc"}]`
76
     *
77
     * Because of the fact that many order syntaxes use multiple query string
78
     * parameters with the same name, it is absolutely *vital* that you do not
79
     * use a `ServerRequestInterface` that has been constructed with the `$_GET`
80
     * superglobal.
81
     *
82
     * The problem here is that PHP will overwrite entries in the `$_GET`
83
     * superglobal if they share the same name. With PHP, a request to
84
     * `file.php?foobar=foo&foobar=bar` will result in `$_GET` set to
85
     * `['foobar' => 'bar']`.
86
     *
87
     * Other platforms, like the Java Servlet specification, allow list-like
88
     * access to these parameters. Make sure the object you pass for `$request`
89
     * has a `queryParams` property that has been created to account for
90
     * multiple query parameters with the same name. The `QueryParams` class
91
     * will produce an array that accounts for this case.
92
     *
93
     * @param \Psr\Http\Message\ServerRequestInterface $request The server request. Please read important docs above.
94
     * @param string $sortParameter The name of the sort parameter
95
     * @param array<string,bool> $defaultSort The default sort if request lacks it
96
     * @return \Caridea\Http\Pagination The pagination details
97
     */
98 8
    public function create(\Psr\Http\Message\ServerRequestInterface $request, string $sortParameter = self::SORT, array $defaultSort = []): Pagination
99
    {
100 8
        $offset = 0;
101 8
        $max = PHP_INT_MAX;
0 ignored issues
show
Unused Code introduced by
$max is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
102
        
103 8
        $params = $request->getQueryParams();
104 8
        $range = $request->getHeaderLine(self::RANGE);
105
        
106 8
        if ($range !== null && preg_match(self::REGEX_RANGE, $range, $rm)) {
107
            // dojo.store.JsonRest style
108 1
            $offset = (int)$rm[1];
109 1
            $max = (int)$rm[2] - $offset + 1;
110
        } else {
111 8
            $max = $this->parse(self::$maxAlias, $params, PHP_INT_MAX);
112
            // Grails, ExtJS, dojox.data.QueryReadStore, all zero-based
113 8
            $offVal = $this->parse(self::$offsetAlias, $params, 0);
114 8
            if ($offVal > 0) {
115 4
                $offset = $offVal;
116 4
            } elseif (isset($params[self::START_INDEX])) {
117
            // OpenSearch style, 1-based
118 2
                $startIdx = isset($params[self::START_INDEX]) ? (int)$params[self::START_INDEX] : 0;
119 2
                if ($startIdx > 0) {
120 2
                    $offset = $startIdx - 1;
121
                }
122 4
            } elseif (isset($params[self::START_PAGE]) || isset($params[self::PAGE])) {
123
            // OpenSearch or Spring Data style, 1-based
124 3
                $startPage = $this->parse(self::$pageAlias, $params, 0);
125 3
                if ($startPage > 0) {
126 3
                    $offset = ($max * ($startPage - 1));
127
                }
128
            }
129
        }
130
        
131 8
        return new Pagination($max, $offset, $this->getOrder($request, $sortParameter, $defaultSort));
132
    }
133
    
134
    /**
135
     * Parses the order array.
136
     *
137
     * Because of the fact that many order syntaxes use multiple query string
138
     * parameters with the same name, it is absolutely *vital* that you do not
139
     * use a `ServerRequestInterface` that has been constructed with the `$_GET`
140
     * superglobal.
141
     *
142
     * The problem here is that PHP will overwrite entries in the `$_GET`
143
     * superglobal if they share the same name. With PHP, a request to
144
     * `file.php?foobar=foo&foobar=bar` will result in `$_GET` set to
145
     * `['foobar' => 'bar']`.
146
     *
147
     * Other platforms, like the Java Servlet specification, allow list-like
148
     * access to these parameters. Make sure the object you pass for `$request`
149
     * has a `queryParams` property that has been created to account for
150
     * multiple query parameters with the same name. The `QueryParams` class
151
     * will produce an array that accounts for this case.
152
     *
153
     * @param \Psr\Http\Message\ServerRequestInterface $request The request
154
     * @param string $sortParameter The sort parameter
155
     * @param array<string,bool> $default The default sort order
156
     * @return array<string,bool> String keys to boolean values
157
     */
158 8
    protected function getOrder(\Psr\Http\Message\ServerRequestInterface $request, string $sortParameter, array $default = []): array
159
    {
160 8
        $order = [];
161 8
        $params = $request->getQueryParams();
162 8
        if (isset($params[$sortParameter])) {
163 8
            if (isset($params[self::ORDER])) {
164
            // stupid Grails ordering
165 1
                $order[$params[$sortParameter]] = strcasecmp(self::DESC, $params[self::ORDER]) !== 0;
166
            } else {
167 7
                $param = $params[$sortParameter];
168 7
                foreach ((is_array($param) ? $param : [$param]) as $s) {
169 8
                    $this->parseSort($s, $order);
170
                }
171
            }
172
        } else {
173 1
            foreach (preg_grep(self::REGEX_DOJO_SORT, array_keys($params)) as $s) {
174
                // sort(+foo,-bar)
175 1
                $this->parseSort(substr($s, 5, -1), $order);
176
            }
177
        }
178 8
        return empty($order) ? $default : $order;
179
    }
180
    
181
    /**
182
     * Attempts to parse a single sort value.
183
     *
184
     * @param string $sort The sort value
185
     * @param array<string,bool> $sorts String keys to boolean values
186
     */
187 7
    protected function parseSort(string $sort, array &$sorts)
188
    {
189 7
        if (strlen(trim($sort)) === 0) {
190 1
            return;
191
        }
192 7
        if (substr($sort, 0, 1) == "[") {
193
            // it might be the ridiculous JSON ExtJS sort format
194 2
            $json = json_decode($sort);
195 2
            if (is_array($json)) {
196 1
                foreach ($json as $s) {
197 1
                    if (is_object($s)) {
198 1
                        $sorts[$s->property] = strcasecmp(self::DESC, $s->direction) !== 0;
199
                    }
200
                }
201 1
                return;
202
            }
203
        }
204 6
        if (substr($sort, -4) == ",asc") {
205
        // foo,asc
206 1
            $sorts[substr($sort, 0, strlen($sort) - 4)] = true;
207 6
        } elseif (substr($sort, -5) == ",desc") {
208
        // foo,desc
209 1
            $sorts[substr($sort, 0, strlen($sort) - 5)] = false;
210 5
        } elseif (substr($sort, -10) == ":ascending") {
211
        // foo:ascending
212 1
            $sorts[substr($sort, 0, strlen($sort) - 10)] = true;
213 5
        } elseif (substr($sort, -11) == ":descending") {
214
        // foo:descending
215 1
            $sorts[substr($sort, 0, strlen($sort) - 11)] = false;
216
        } else {
217 4
            foreach (explode(',', $sort) as $s) {
218 4
                if (substr($s, 0, 1) === '-') {
219
                // -foo
220 3
                    $sorts[substr($s, 1)] = false;
221 4
                } elseif (substr($s, 0, 1) === '+') {
222
                // +foo
223 3
                    $sorts[substr($s, 1)] = true;
224
                } else {
225
                // foo
226 4
                    $sorts[$s] = true;
227
                }
228
            }
229
        }
230 6
    }
231
232
    /**
233
     * Gets the first numeric value from `$params`, otherwise `$defaultValue`.
234
     *
235
     * @param array $names
236
     * @param array $params
237
     * @param int $defaultValue
238
     * @return int
239
     */
240
    protected function parse(array &$names, array &$params, int $defaultValue) : int
241
    {
242 8
        $value = array_reduce(array_intersect_key($params, $names), function ($carry, $item) {
243 7
            return $carry !== null ? $carry : (is_numeric($item) ? (int)$item : null);
244 8
        });
245 8
        return $value === null ? $defaultValue : $value;
246
    }
247
}
248