Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
188 / 188 |
|
100.00% |
8 / 8 |
CRAP | |
100.00% |
1 / 1 |
Expression | |
100.00% |
188 / 188 |
|
100.00% |
8 / 8 |
91 | |
100.00% |
1 / 1 |
parse | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
10 | |||
parseExpressions | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
convertExpressionToShorthand | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
16 | |||
convertExpressionsToShorthand | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
parseShorthand | |
100.00% |
95 / 95 |
|
100.00% |
1 / 1 |
41 | |||
stripIdQuotes | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
7 | |||
stripQuotes | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
5 | |||
quote | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
8 |
1 | <?php |
2 | /** |
3 | * Pop PHP Framework (http://www.popphp.org/) |
4 | * |
5 | * @link https://github.com/popphp/popphp-framework |
6 | * @author Nick Sagona, III <dev@nolainteractive.com> |
7 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
8 | * @license http://www.popphp.org/license New BSD License |
9 | */ |
10 | |
11 | /** |
12 | * @namespace |
13 | */ |
14 | namespace Pop\Db\Sql\Parser; |
15 | |
16 | /** |
17 | * Predicate expression parser class |
18 | * |
19 | * @category Pop |
20 | * @package Pop\Db |
21 | * @author Nick Sagona, III <dev@nolainteractive.com> |
22 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
23 | * @license http://www.popphp.org/license New BSD License |
24 | * @version 6.5.0 |
25 | */ |
26 | class Expression |
27 | { |
28 | |
29 | /** |
30 | * Allowed operators |
31 | * @var array |
32 | */ |
33 | protected static array $operators = [ |
34 | '>=', '<=', '!=', '=', '>', '<', |
35 | 'NOT LIKE', 'LIKE', 'NOT BETWEEN', 'BETWEEN', |
36 | 'NOT IN', 'IN', 'IS NOT NULL', 'IS NULL' |
37 | ]; |
38 | |
39 | /** |
40 | * Method to parse a predicate string expression into its components |
41 | * |
42 | * @param string $expression |
43 | * @throws Exception |
44 | * @return array |
45 | */ |
46 | public static function parse(string $expression): array |
47 | { |
48 | $column = null; |
49 | $operator = null; |
50 | $value = null; |
51 | |
52 | if (stripos($expression, ' NULL') !== false) { |
53 | $column = self::stripIdQuotes(trim(substr($expression, 0, strpos($expression, ' ')))); |
54 | $operator = (stripos($expression, ' IS NOT NULL') !== false) ? 'IS NOT NULL' : 'IS NULL'; |
55 | } else if (stripos($expression, ' IN ') !== false) { |
56 | $column = self::stripIdQuotes(trim(substr($expression, 0, strpos($expression, ' ')))); |
57 | $operator = (stripos($expression, ' NOT IN ') !== false) ? 'NOT IN' : 'IN'; |
58 | $values = substr($expression, (strpos($expression, '(') + 1)); |
59 | $values = substr($values, 0, strpos($values, ')')); |
60 | $values = array_map(function($value) { |
61 | return \Pop\Db\Sql\Parser\Expression::stripQuotes(trim($value)); |
62 | }, explode(',', $values)); |
63 | $value = $values; |
64 | } else if (stripos($expression, ' BETWEEN ') !== false) { |
65 | $column = self::stripIdQuotes(trim(substr($expression, 0, strpos($expression, ' ')))); |
66 | $operator = (stripos($expression, ' NOT BETWEEN ') !== false) ? 'NOT BETWEEN' : 'BETWEEN'; |
67 | $value1 = substr($expression, (strpos($expression, 'BETWEEN ') + 8)); |
68 | $value1 = trim(substr($value1, 0, strpos($value1, ' '))); |
69 | $value2 = trim(substr($expression, (stripos($expression, ' AND ') + 5))); |
70 | $value = [self::stripQuotes($value1), self::stripQuotes($value2)]; |
71 | } else if (stripos($expression, ' LIKE ') !== false) { |
72 | $column = self::stripIdQuotes(trim(substr($expression, 0, strpos($expression, ' ')))); |
73 | $operator = (stripos($expression, ' NOT LIKE ') !== false) ? 'NOT LIKE' : 'LIKE'; |
74 | $value = self::stripQuotes(trim(substr($expression, (stripos($expression, ' LIKE ') + 6)))); |
75 | } else { |
76 | $column = substr($expression, 0, strpos($expression, ' ')); |
77 | $operator = substr($expression, (strlen($column) + 1)); |
78 | $operator = substr($operator, 0, strpos($operator, ' ')); |
79 | $value = self::stripQuotes(trim(substr($expression, (strpos($expression, $operator) + strlen($operator))))); |
80 | } |
81 | |
82 | if (!in_array($operator, self::$operators)) { |
83 | throw new Exception("Error: The operator '" . $operator . "' is not allowed."); |
84 | } |
85 | |
86 | return [ |
87 | 'column' => $column, |
88 | 'operator' => $operator, |
89 | 'value' => $value |
90 | ]; |
91 | } |
92 | |
93 | /** |
94 | * Method to parse predicate string expressions into its components |
95 | * |
96 | * @param array $expressions |
97 | * @return array |
98 | */ |
99 | public static function parseExpressions(array $expressions): array |
100 | { |
101 | $components = []; |
102 | |
103 | foreach ($expressions as $expression) { |
104 | $components[] = self::parse($expression); |
105 | } |
106 | |
107 | return $components; |
108 | } |
109 | |
110 | /** |
111 | * Convert to expression to shorthand value |
112 | * |
113 | * @param string $expression |
114 | * @return array |
115 | */ |
116 | public static function convertExpressionToShorthand(string $expression): array |
117 | { |
118 | ['column' => $column, 'operator' => $operator, 'value' => $value] = self::parse($expression); |
119 | |
120 | switch ($operator) { |
121 | case '>=': |
122 | case '<=': |
123 | case '!=': |
124 | case '>': |
125 | case '<': |
126 | $column .= $operator; |
127 | break; |
128 | case 'LIKE': |
129 | if (str_starts_with($value, '%')) { |
130 | $column = '%' . $column; |
131 | $value = substr($value, 1); |
132 | } |
133 | if (str_ends_with($value, '%')) { |
134 | $column .= '%'; |
135 | $value = substr($value, 0, -1); |
136 | } |
137 | break; |
138 | case 'NOT LIKE': |
139 | if (str_starts_with($value, '%')) { |
140 | $column = '-%' . $column; |
141 | $value = substr($value, 1); |
142 | } |
143 | if (str_ends_with($value, '%')) { |
144 | $column .= '%-'; |
145 | $value = substr($value, 0, -1); |
146 | } |
147 | break; |
148 | case 'NOT IN': |
149 | case 'NOT BETWEEN': |
150 | case 'IS NOT NULL': |
151 | $column .= '-'; |
152 | break; |
153 | } |
154 | |
155 | if (str_contains($expression, ' BETWEEN ')) { |
156 | $value = '(' . implode(', ', $value) . ')'; |
157 | } |
158 | |
159 | return [$column => $value]; |
160 | } |
161 | |
162 | /** |
163 | * Convert to expression to shorthand value |
164 | * |
165 | * @param array $expressions |
166 | * @return array |
167 | */ |
168 | public static function convertExpressionsToShorthand(array $expressions): array |
169 | { |
170 | $conditions = []; |
171 | |
172 | foreach ($expressions as $expression) { |
173 | $conditions = array_merge($conditions, self::convertExpressionToShorthand($expression)); |
174 | } |
175 | |
176 | return $conditions; |
177 | } |
178 | |
179 | /** |
180 | * Method to parse the shorthand columns to create expressions and their parameters |
181 | * |
182 | * @param array $columns |
183 | * @param ?string $placeholder |
184 | * @param bool $flatten |
185 | * @return array |
186 | */ |
187 | public static function parseShorthand(array $columns, ?string $placeholder = null, bool $flatten = true): array |
188 | { |
189 | $expressions = []; |
190 | $params = []; |
191 | $i = 1; |
192 | $j = 0; |
193 | |
194 | foreach ($columns as $column => $value) { |
195 | ['column' => $parsedColumn, 'operator' => $operator] = Operator::parse($column); |
196 | |
197 | $pHolder = $placeholder; |
198 | if ($placeholder == ':') { |
199 | $pHolder .= $parsedColumn; |
200 | } else if ($placeholder == '$') { |
201 | $pHolder .= $i; |
202 | } |
203 | |
204 | // IS NULL/IS NOT NULL |
205 | if ($value === null) { |
206 | $newExpression = $parsedColumn . ' IS ' . (($operator == 'NOT') ? 'NOT ' : '') . 'NULL'; |
207 | if ($placeholder == ':') { |
208 | $expressions[$parsedColumn] = $newExpression; |
209 | } else { |
210 | $expressions[] = $newExpression; |
211 | } |
212 | // IN/NOT IN |
213 | } else if (is_array($value)) { |
214 | $p = []; |
215 | if ($placeholder == ':') { |
216 | $pHolders = []; |
217 | foreach ($value as $j => $val) { |
218 | $ph = $pHolder . ($j + 1); |
219 | $pHolders[] = $ph; |
220 | $p[] = $val; |
221 | } |
222 | } else if ($placeholder == '$') { |
223 | $pHolders = []; |
224 | foreach ($value as $val) { |
225 | $pHolders[] = $placeholder . $i++; |
226 | $p[] = $val; |
227 | } |
228 | } else { |
229 | $pHolders = array_fill(0, count($value), $pHolder); |
230 | $p = $value; |
231 | $i++; |
232 | } |
233 | if ($placeholder !== null) { |
234 | $newExpression = $parsedColumn . (($operator == 'NOT') ? ' NOT ' : ' ') . 'IN (' . |
235 | implode(', ', $pHolders) . ')'; |
236 | if ($placeholder == ':') { |
237 | $expressions[$parsedColumn] = $newExpression; |
238 | } else { |
239 | $expressions[] = $newExpression; |
240 | } |
241 | } else { |
242 | $expressions[] = $parsedColumn . (($operator == 'NOT') ? ' NOT ' : ' ') . 'IN (' . |
243 | implode(', ', array_map('Pop\Db\Sql\Parser\Expression::quote', $value)) . ')'; |
244 | } |
245 | if ($placeholder == ':') { |
246 | $params[$parsedColumn] = $p; |
247 | } else { |
248 | $params[$j] = $p; |
249 | } |
250 | // BETWEEN/NOT BETWEEN |
251 | } else if (is_string($value) && (str_starts_with($value, '(')) && (str_ends_with($value, ')')) && |
252 | (str_contains($value, ','))) { |
253 | $values = substr($value, (strpos($value, '(') + 1)); |
254 | $values = substr($values, 0, strpos($values, ')')); |
255 | [$value1, $value2] = array_map('trim', explode(',', $values)); |
256 | $p = [$value1, $value2]; |
257 | |
258 | if ($placeholder == ':') { |
259 | $pHolder2 = $pHolder . 2; |
260 | $pHolder .= 1; |
261 | } else if ($placeholder == '$') { |
262 | $pHolder2 = $placeholder . ++$i; |
263 | } else { |
264 | $pHolder2 = $pHolder; |
265 | } |
266 | |
267 | if ($placeholder !== null) { |
268 | $newExpression = $parsedColumn . (($operator == 'NOT') ? ' NOT ' : ' ') . |
269 | 'BETWEEN ' . $pHolder . ' AND ' . $pHolder2; |
270 | if ($placeholder == ':') { |
271 | $expressions[$parsedColumn] = $newExpression; |
272 | } else { |
273 | $expressions[] = $newExpression; |
274 | } |
275 | } else { |
276 | $expressions[] = $parsedColumn . (($operator == 'NOT') ? ' NOT ' : ' ') . |
277 | 'BETWEEN ' . self::quote($value1) . ' AND ' . self::quote($value2); |
278 | } |
279 | if ($placeholder == ':') { |
280 | $params[$parsedColumn] = $p; |
281 | } else { |
282 | $params[$j] = $p; |
283 | } |
284 | $i++; |
285 | // LIKE/NOT LIKE or Standard Operators |
286 | } else { |
287 | if ((str_starts_with($column, '%')) || (str_starts_with($column, '-%'))) { |
288 | $value = '%' . $value; |
289 | } |
290 | if ((str_ends_with($column, '%')) || (str_ends_with($column, '%-'))) { |
291 | $value .= '%'; |
292 | } |
293 | if ($placeholder !== null) { |
294 | $newExpression = $parsedColumn . ' ' . $operator . ' ' . $pHolder; |
295 | if ($placeholder == ':') { |
296 | $expressions[$parsedColumn] = $newExpression; |
297 | } else { |
298 | $expressions[] = $newExpression; |
299 | } |
300 | } else { |
301 | $expressions[] = $parsedColumn . ' ' . $operator . ' ' . self::quote($value); |
302 | } |
303 | if ($placeholder == ':') { |
304 | $params[$parsedColumn] = $value; |
305 | } else { |
306 | $params[$j] = $value; |
307 | } |
308 | $i++; |
309 | } |
310 | $j++; |
311 | } |
312 | |
313 | if ($flatten) { |
314 | $flattenParams = []; |
315 | |
316 | foreach ($params as $key => $value) { |
317 | if (is_array($value)) { |
318 | foreach ($value as $k => $v) { |
319 | if ($placeholder == ':') { |
320 | $flattenParams[$key . ($k + 1)] = $v; |
321 | } else { |
322 | $flattenParams[] = $v; |
323 | } |
324 | } |
325 | } else { |
326 | if ($placeholder == ':') { |
327 | $flattenParams[$key] = $value; |
328 | } else { |
329 | $flattenParams[] = $value; |
330 | } |
331 | } |
332 | } |
333 | |
334 | return ['expressions' => $expressions, 'params' => $flattenParams]; |
335 | } else { |
336 | return ['expressions' => $expressions, 'params' => $params]; |
337 | } |
338 | |
339 | } |
340 | |
341 | /** |
342 | * Strip ID quotes |
343 | * |
344 | * @param string $identifier |
345 | * @return string |
346 | */ |
347 | public static function stripIdQuotes(string $identifier): string |
348 | { |
349 | if (((str_starts_with($identifier, '"')) && (str_ends_with($identifier, '"'))) || |
350 | ((str_starts_with($identifier, '`')) && (str_ends_with($identifier, '`'))) || |
351 | ((str_starts_with($identifier, '[')) && (str_ends_with($identifier, ']')))) { |
352 | $identifier = substr($identifier, 1); |
353 | $identifier = substr($identifier, 0, -1); |
354 | } |
355 | |
356 | return $identifier; |
357 | } |
358 | |
359 | /** |
360 | * Strip quotes |
361 | * |
362 | * @param string $value |
363 | * @return string |
364 | */ |
365 | public static function stripQuotes(string $value): string |
366 | { |
367 | if (((str_starts_with($value, '"')) && (str_ends_with($value, '"'))) || |
368 | ((str_starts_with($value, "'")) && (str_ends_with($value, "'")))) { |
369 | $value = substr($value, 1); |
370 | $value = substr($value, 0, -1); |
371 | } |
372 | |
373 | return $value; |
374 | } |
375 | |
376 | /** |
377 | * Quote the value (if it is not a numeric value) |
378 | * |
379 | * @param string $value |
380 | * @return string |
381 | */ |
382 | public static function quote(string $value): string |
383 | { |
384 | if (($value == '') || |
385 | (($value != '?') && (!str_starts_with($value, ':')) && (preg_match('/^\$\d*\d$/', $value) == 0) && |
386 | !is_int($value) && !is_float($value) && (preg_match('/^\d*$/', $value) == 0))) { |
387 | $value = "'" . $value . "'"; |
388 | } |
389 | return $value; |
390 | } |
391 | |
392 | } |