-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathQueryParser.php
395 lines (347 loc) · 11.7 KB
/
QueryParser.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
<?php namespace RestExtension;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\Request;
use RestExtension\Fields\QueryField;
use RestExtension\Filter\Operators;
use RestExtension\Filter\QueryFilter;
use RestExtension\Includes\QueryInclude;
use RestExtension\Ordering\QueryOrder;
/**
* Created by PhpStorm.
* User: martin
* Date: 2018-12-04
* Time: 12:07
*
* @property Request $request
* @property QueryFilter[][] $filters
* @property QueryFilter[][] $searchFilters
* @property QueryInclude[] $includes
* @property QueryOrder[] $ordering
* @property QueryField[] $fields
* @property int $limit
* @property int $offset
* @property int $count
*/
class QueryParser {
/*
* This uses the RFS filter method
*
* Example
* GET /items?filter=price:>=10,price:<=100.
* This will filter items with price greater than or equal to 10 AND lower than or equal to 100. (>=10 & <=100)
*
* Filters are made up of rules, with a property, a value and an operator. Where property is the field you want to
* filter against, e.g. featured, value is what you want to match e.g. true and operator is most commonly 'equals' e.g. :.
*
* When specifying a filter, the property and value are always separated by a colon :.
* If no other operator is provided, then this is a simple 'equals' comparison.
*
* featured:true - return all posts where the featured property is equal to true.
*
* You can also do a 'not equals' query, by adding the 'not' operator - after the colon.
* For example, if you wanted to find all posts which have an image,
* you could look for all posts where image is not null: feature_image:-null.
*
* Filters with multiple rules
* You can combine rules using either 'and' or 'or'. If you'd like to find all posts that are either featured,
* or they have an image, then you can combine these two rules with a comma , which represents 'or':
* filter=featured:true,feature_image:-null.
*
* If you're looking for all published posts which are not static pages,
* then you can combine the two rules with a plus + which represents 'and': filter=status:published+page:false.
* This is the default query performed by the posts endpoint.
*
* Syntax Reference
* A filter expression is a string which provides the property, operator and value in the form property:operatorvalue:
*
* - property - a path representing the key to filter on
* - : - separator between property and an operator-value expression
* - operator is optional, so : on its own is roughly =
*
* Property
* Matches: [a-zA-Z_][a-zA-Z0-9_.]
*
* - can contain only alpha-numeric characters and _
* - cannot contain whitespace
* - must start with a letter
* - supports . separated paths, E.g. authors.slug or posts.count
* - is always lowercase, but accepts and converts uppercase
*
* Value
* Can be one of the following
*
* - null
* - true
* - false
* - a _number _(integer)
* - a literal
* - Any character string which follows these rules:
* - Cannot start with - but may contain it
* - Cannot contain any of these symbols: '"+,()><=[] unless they are escaped
* - Cannot contain whitespace
* - a string
* - ' string here ' Any character except a single or double quote surrounded by single quotes
* - Single or Double quote _MUST _be escaped*
* - Can contain whitespace
* - A string can contain a date any format that can be understood by new Date()
*
* Operators
* - not operator
* > greater than operator
* >= greater than or equals operator
* < less than operator
* <= less than or equals operator
*
* Combinations
* + - represents and OBS! Not supported
* , - represents or
* ( filter expression ) - overrides operator precedence OBS! Not supported
* [] - grouping fpr IN style, ex. tags:[first-tag,second-tag]
*
*
* More info https://api.ghost.org/docs/filter
*
*/
private $includes = [];
private $filters = [];
private $searchFilters = [];
private $ordering = [];
private $fields = [];
private $limit = null;
private $offset = null;
private $count = null;
public $request;
public function parseRequest(Request $request) {
$this->request = $request;
if ($request instanceof IncomingRequest) {
$includes = $request->getGet('include');
if ($includes) {
$this->parseInclude($request->getGet('include'));
}
$filter = $request->getGet('filter');
if ($filter) {
$this->parseFilter($request->getGet('filter'));
}
$ordering = $request->getGet('ordering');
if ($ordering) {
$this->parseOrdering($request->getGet('ordering'));
}
$fields = $request->getGet('fields');
if ($fields) {
$this->parseFields($request->getGet('fields'));
}
$this->limit = $request->getGet('limit');
$this->offset = $request->getGet('offset');
$this->count = $request->getGet('count');
}
}
public static function parse($line) {
$item = new QueryParser();
parse_str($line, $params);
if (isset($params['include'])) {
$item->parseInclude($params['include']);
}
if (isset($params['filter'])) {
$item->parseFilter($params['filter']);
}
if (isset($params['ordering'])) {
$item->parseOrdering($params['ordering']);
}
if (isset($params['fields'])) {
$item->parseFields($params['fields']);
}
if (isset($params['limit'])) {
$item->limit = $params['limit'];
}
if (isset($params['offset'])) {
$item->offset = $params['offset'];
}
if (isset($params['count'])) {
$item->count = $params['count'];
}
return $item;
}
public function parseInclude(string $value) {
foreach (explode(',', $value) as $line) {
$this->includes[] = QueryInclude::parse($line);
}
}
public function parseOrdering(string $value) {
foreach (explode(',', $value) as $line) {
$this->ordering[] = QueryOrder::parse($line);
}
}
public function parseFields(string $value) {
foreach (explode(',', $value) as $line) {
$this->fields[] = QueryField::parse($line);
}
}
public function parseFilter(string $line) {
$filters = [];
$buffer = '';
$inSquareBracket = false;
$inString = false;
for ($i = 0; $i < strlen($line); $i++) {
$char = substr($line, $i, 1);
if (in_array($char, ['[', ']'])) $inSquareBracket = !$inSquareBracket;
if (in_array($char, ['"', "'"])) $inString = !$inString;
if ($char == ',' & !$inSquareBracket & !$inString) {
$filters[] = $buffer;
$buffer = '';
} else
$buffer .= $char;
}
if (strlen($buffer))
$filters[] = $buffer;
foreach ($filters as $filter) {
$item = QueryFilter::parse($filter);
$this->pushFilter($item->property, $item);
}
}
/**
* @param string $name
* @param Filter\QueryFilter $filter
*/
private function pushFilter($name, $filter) {
switch ($filter->operator) {
case Operators::Search:
if (!isset($this->searchFilters[$name])) $this->searchFilters[$name] = [];
$this->searchFilters[$name][] = $filter;
break;
default:
if (!isset($this->filters[$name])) $this->filters[$name] = [];
$this->filters[$name][] = $filter;
}
}
public function delFilter($name) {
unset($this->filters[$name]);
}
public function hasFilter($name): bool {
return isset($this->filters[$name]);
}
/**
* @param $name
* @return QueryFilter[]
*/
public function getFilter($name) {
return $this->filters[$name];
}
/**
* @return QueryFilter[]
*/
public function getFilters() {
$all = [];
foreach ($this->filters as $name => $filters)
foreach ($filters as $filter)
$all[] = $filter;
return $all;
}
/**
* @return QueryFilter[]
*/
public function getSearchFilters() {
$all = [];
foreach ($this->searchFilters as $name => $filters)
foreach ($filters as $filter)
$all[] = $filter;
return $all;
}
public function delSearchFilter($name) {
unset($this->searchFilters[$name]);
}
public function hasSearchFilter($name): bool {
return isset($this->searchFilters[$name]);
}
/**
* @param $name
* @return QueryFilter[]
*/
public function getSearchFilter($name) {
return $this->searchFilters[$name];
}
public function getOffset(): int {
return $this->offset;
}
public function hasOffset(): bool {
return !is_null($this->offset);
}
public function getLimit(): int {
return $this->limit;
}
public function hasLimit(): bool {
return !is_null($this->limit);
}
public function isCount(): bool {
return !is_null($this->count);
}
public function getCount(): bool {
return $this->count;
}
public function getIncludes() {
return $this->includes;
}
public function getOrdering() {
return $this->ordering;
}
public function getFields() {
return $this->fields;
}
public function getFieldsArray() {
$fields_arr = [];
foreach ($this->getFields() as $field) {
$fields_arr[] = $field->fieldName;
}
return $fields_arr;
}
public function getFieldsImplode() {
$fields_arr = array();
foreach ($this->getFields() as $field) {
$fields_arr[] = $field->fieldName;
}
return implode(',', $fields_arr);
}
public function hasInclude(string $name): bool {
foreach ($this->includes as $include) {
if ($include->property == $name)
return true;
}
return false;
}
public function getInclude(string $name): ?QueryInclude {
foreach ($this->includes as $include) {
if ($include->property == $name)
return $include;
}
return null;
}
public function delInclude(string $name) {
for ($i = 0; $i < count($this->includes); $i++) {
if ($this->includes[$i]->property == $name) {
array_splice($this->includes, $i, 1);
return;
}
}
}
public function hasOrder(string $name): bool {
foreach ($this->ordering as $ordering) {
if ($ordering->property == $name)
return true;
}
return false;
}
public function getOrder(string $name): ?QueryOrder {
foreach ($this->ordering as $ordering) {
if ($ordering->property == $name)
return $ordering;
}
return null;
}
public function delOrder(string $name) {
for ($i = 0; $i < count($this->ordering); $i++) {
if ($this->ordering[$i]->property == $name) {
array_splice($this->ordering, $i, 1);
return;
}
}
}
}