Passed
Push — master ( 8e43c9...4981d1 )
by Tarmo
116:46 queued 51:34
created

RequestHandler   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 228
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 22
eloc 48
c 3
b 0
f 0
dl 0
loc 228
ccs 56
cts 56
cp 1
rs 10
1
<?php
2
declare(strict_types = 1);
3
/**
4
 * /src/Rest/RequestHandler.php
5
 *
6
 * @author TLe, Tarmo Leppänen <[email protected]>
7
 */
8
9
namespace App\Rest;
10
11
use App\Utils\JSON;
12
use Closure;
13
use JsonException;
14
use LogicException;
15
use Symfony\Component\HttpFoundation\Request as HttpFoundationRequest;
16
use Symfony\Component\HttpFoundation\Response as HttpFoundationResponse;
17
use Symfony\Component\HttpKernel\Exception\HttpException;
18
use function abs;
19
use function array_filter;
20
use function array_key_exists;
21
use function array_unique;
22
use function array_values;
23
use function array_walk;
24
use function explode;
25
use function in_array;
26
use function is_array;
27
use function is_string;
28
use function mb_strtoupper;
29
use function mb_substr;
30
use function strncmp;
31
32
/**
33
 * Class RequestHandler
34
 *
35
 * @package App\Rest
36
 * @author TLe, Tarmo Leppänen <[email protected]>
37
 */
38
final class RequestHandler
39
{
40
    /**
41
     * Method to get used criteria array for 'find' and 'count' methods. Some
42
     * examples below.
43
     *
44
     * Basic usage:
45
     *  ?where={"foo": "bar"} => WHERE entity.foo = 'bar'
46
     *  ?where={"bar.foo": "foobar"} => WHERE bar.foo = 'foobar'
47
     *  ?where={"id": [1,2,3]} => WHERE entity.id IN (1,2,3)
48
     *  ?where={"bar.foo": [1,2,3]} => WHERE bar.foo IN (1,2,3)
49
     *
50
     * Advanced usage:
51
     *  By default you cannot make anything else that described above,
52
     *  but you can easily manage special cases within your controller
53
     *  'processCriteria' method, where you can modify this generated
54
     *  'criteria' array as you like.
55
     *
56
     *  Note that with advanced usage you can easily use everything that
57
     *  App\Repository\Base::getExpression method supports - and that is
58
     *  basically 99% that you need on advanced search criteria.
59
     *
60
     * @return array<string, mixed>
61
     *
62
     * @throws HttpException
63
     */
64
    public static function getCriteria(HttpFoundationRequest $request): array
65 35
    {
66
        try {
67
            $where = array_filter(
68 35
                (array)JSON::decode((string)$request->get('where', '{}'), true),
69 35
                static fn ($value): bool => $value !== null,
70 34
            );
71 34
        } catch (JsonException $error) {
72 1
            throw new HttpException(
73 1
                HttpFoundationResponse::HTTP_BAD_REQUEST,
74 1
                'Current \'where\' parameter is not valid JSON.',
75 1
                $error,
76
            );
77
        }
78
79
        return $where;
80 34
    }
81
82
    /**
83
     * Getter method for used order by option within 'find' method. Some
84
     * examples below.
85
     *
86
     * Basic usage:
87
     *  ?order=column1 => ORDER BY entity.column1 ASC
88
     *  ?order=-column1 => ORDER BY entity.column2 DESC
89
     *  ?order=foo.column1 => ORDER BY foo.column1 ASC
90
     *  ?order=-foo.column1 => ORDER BY foo.column2 DESC
91
     *
92
     * Array parameter usage:
93
     *  ?order[column1]=ASC => ORDER BY entity.column1 ASC
94
     *  ?order[column1]=DESC => ORDER BY entity.column1 DESC
95
     *  ?order[column1]=foobar => ORDER BY entity.column1 ASC
96
     *  ?order[column1]=DESC&order[column2]=DESC => ORDER BY entity.column1 DESC, entity.column2 DESC
97
     *  ?order[foo.column1]=ASC => ORDER BY foo.column1 ASC
98
     *  ?order[foo.column1]=DESC => ORDER BY foo.column1 DESC
99
     *  ?order[foo.column1]=foobar => ORDER BY foo.column1 ASC
100
     *  ?order[foo.column1]=DESC&order[column2]=DESC => ORDER BY foo.column1 DESC, entity.column2 DESC
101
     *
102
     * @return array<string, string>
103
     */
104
    public static function getOrderBy(HttpFoundationRequest $request): array
105 25
    {
106
        // Normalize parameter value
107
        $input = array_filter((array)$request->get('order', []));
108 25
109
        // Initialize output
110
        $output = [];
111 25
112
        // Process user input
113
        array_walk($input, self::getIterator($output));
114 25
115
        return $output;
116 25
    }
117
118
    /**
119
     * Getter method for used limit option within 'find' method.
120
     *
121
     * Usage:
122
     *  ?limit=10
123
     */
124
    public static function getLimit(HttpFoundationRequest $request): ?int
125 16
    {
126
        $limit = $request->get('limit');
127 16
128
        return $limit !== null ? (int)abs((float)$limit) : null;
129 16
    }
130
131
    /**
132
     * Getter method for used offset option within 'find' method.
133
     *
134
     * Usage:
135
     *  ?offset=10
136
     */
137
    public static function getOffset(HttpFoundationRequest $request): ?int
138 16
    {
139
        $offset = $request->get('offset');
140 16
141
        return $offset !== null ? (int)abs((float)$offset) : null;
142 16
    }
143
144
    /**
145
     * Getter method for used search terms within 'find' and 'count' methods.
146
     * Note that these will affect to columns / properties that you have
147
     * specified to your resource service repository class.
148
     *
149
     * Usage examples:
150
     *  ?search=term
151
     *  ?search=term1+term2
152
     *  ?search={"and": ["term1", "term2"]}
153
     *  ?search={"or": ["term1", "term2"]}
154
     *  ?search={"and": ["term1", "term2"], "or": ["term3", "term4"]}
155
     *
156
     * @return array<mixed>
157
     *
158
     * @throws HttpException
159
     */
160
    public static function getSearchTerms(HttpFoundationRequest $request): array
161 38
    {
162
        $search = $request->get('search');
163 38
164
        return $search !== null ? self::getSearchTermCriteria($search) : [];
165 38
    }
166
167
    /**
168
     * Method to return search term criteria as an array that repositories can easily use.
169
     *
170
     * @return array<int|string, array<int, string>>
171
     *
172
     * @throws HttpException
173
     */
174
    private static function getSearchTermCriteria(string $search): array
175 11
    {
176
        $searchTerms = self::determineSearchTerms($search);
177 11
178
        // By default we want to use 'OR' operand with given search words.
179
        $output = [
180
            'or' => array_unique(array_values(array_filter(explode(' ', $search)))),
181 10
        ];
182
183
        if ($searchTerms !== null) {
184 10
            $output = self::normalizeSearchTerms($searchTerms);
185 5
        }
186
187
        return $output;
188 10
    }
189
190
    /**
191
     * Method to determine used search terms. Note that this will first try to JSON decode given search term. This is
192
     * for cases that 'search' request parameter contains 'and' or 'or' terms.
193
     *
194
     * @return array<int|string, array<int, string>>|null
195
     *
196
     * @throws HttpException
197
     */
198
    private static function determineSearchTerms(string $search): ?array
199 11
    {
200
        try {
201
            $searchTerms = JSON::decode($search, true);
202 11
203
            self::checkSearchTerms($searchTerms);
204 7
        } catch (JsonException | LogicException) {
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected ')', expecting '|' or T_VARIABLE on line 204 at column 47
Loading history...
205 6
            $searchTerms = null;
206 5
        }
207
208 5
        return $searchTerms;
209
    }
210
211 10
    /**
212
     * @throws LogicException
213
     * @throws HttpException
214
     */
215
    private static function checkSearchTerms(mixed $searchTerms): void
216
    {
217
        if (!is_array($searchTerms)) {
218
            throw new LogicException('Search term is not an array, fallback to string handling');
219
        }
220 7
221
        if (!array_key_exists('and', $searchTerms) && !array_key_exists('or', $searchTerms)) {
222 7
            throw new HttpException(
223 1
                HttpFoundationResponse::HTTP_BAD_REQUEST,
224
                'Given search parameter is not valid, within JSON provide \'and\' and/or \'or\' property.'
225
            );
226 6
        }
227 1
    }
228 1
229 1
    /**
230
     * Method to normalize specified search terms. Within this we will just filter out any "empty" values and return
231
     * unique terms after that.
232 5
     *
233
     * @param array<int|string, array<int, string>> $searchTerms
234
     *
235
     * @return array<int|string, array<int, string>>
236
     */
237
    private static function normalizeSearchTerms(array $searchTerms): array
238
    {
239
        // Normalize user input, note that this support array and string formats on value
240
        array_walk($searchTerms, static fn (array $terms): array => array_unique(array_values(array_filter($terms))));
241
242 5
        return $searchTerms;
243
    }
244
245 5
    /**
246
     * @param array<string, string> $output
247 5
     */
248
    private static function getIterator(array &$output): Closure
249
    {
250
        return static function (string $value, string | int $key) use (&$output): void {
251
            $order = in_array(mb_strtoupper($value), ['ASC', 'DESC'], true) ? mb_strtoupper($value) : 'ASC';
252
            $column = is_string($key) ? $key : $value;
253
254
            if (strncmp($column, '-', 1) === 0) {
255 25
                $column = mb_substr($column, 1);
256
                $order = 'DESC';
257 25
            }
258 14
259 14
            $output[$column] = $order;
260
        };
261 14
    }
262
}
263