Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
191 / 191 |
|
100.00% |
9 / 9 |
CRAP | |
100.00% |
1 / 1 |
Expression | |
100.00% |
191 / 191 |
|
100.00% |
9 / 9 |
98 | |
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 | |||
isShorthand | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
7 | |||
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 (https://www.popphp.org/) |
4 | * |
5 | * @link https://github.com/popphp/popphp-framework |
6 | * @author Nick Sagona, III <dev@noladev.com> |
7 | * @copyright Copyright (c) 2009-2025 NOLA Interactive, LLC. |
8 | * @license https://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@noladev.com> |
22 | * @copyright Copyright (c) 2009-2025 NOLA Interactive, LLC. |
23 | * @license https://www.popphp.org/license New BSD License |
24 | * @version 6.6.5 |
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 check if the column is shorthand |
181 | * |
182 | * @param string $column |
183 | * @return bool |
184 | */ |
185 | public static function isShorthand(string $column): bool |
186 | { |
187 | return str_contains($column, '%') || str_ends_with($column, '-') || str_ends_with($column, '>=') || |
188 | str_ends_with($column, '<=') || str_ends_with($column, '!=') || str_ends_with($column, '>') || |
189 | str_ends_with($column, '<'); |
190 | } |
191 | |
192 | /** |
193 | * Method to parse the shorthand columns to create expressions and their parameters |
194 | * |
195 | * @param array $columns |
196 | * @param ?string $placeholder |
197 | * @param bool $flatten |
198 | * @return array |
199 | */ |
200 | public static function parseShorthand(array $columns, ?string $placeholder = null, bool $flatten = true): array |
201 | { |
202 | $expressions = []; |
203 | $params = []; |
204 | $i = 1; |
205 | $j = 0; |
206 | |
207 | foreach ($columns as $column => $value) { |
208 | ['column' => $parsedColumn, 'operator' => $operator] = Operator::parse($column); |
209 | |
210 | $pHolder = $placeholder; |
211 | if ($placeholder == ':') { |
212 | $pHolder .= $parsedColumn; |
213 | } else if ($placeholder == '$') { |
214 | $pHolder .= $i; |
215 | } |
216 | |
217 | // IS NULL/IS NOT NULL |
218 | if ($value === null) { |
219 | $newExpression = $parsedColumn . ' IS ' . (($operator == 'NOT') ? 'NOT ' : '') . 'NULL'; |
220 | if ($placeholder == ':') { |
221 | $expressions[$parsedColumn] = $newExpression; |
222 | } else { |
223 | $expressions[] = $newExpression; |
224 | } |
225 | // IN/NOT IN |
226 | } else if (is_array($value)) { |
227 | $p = []; |
228 | if ($placeholder == ':') { |
229 | $pHolders = []; |
230 | foreach ($value as $j => $val) { |
231 | $ph = $pHolder . ($j + 1); |
232 | $pHolders[] = $ph; |
233 | $p[] = $val; |
234 | } |
235 | } else if ($placeholder == '$') { |
236 | $pHolders = []; |
237 | foreach ($value as $val) { |
238 | $pHolders[] = $placeholder . $i++; |
239 | $p[] = $val; |
240 | } |
241 | } else { |
242 | $pHolders = array_fill(0, count($value), $pHolder); |
243 | $p = $value; |
244 | $i++; |
245 | } |
246 | if ($placeholder !== null) { |
247 | $newExpression = $parsedColumn . (($operator == 'NOT') ? ' NOT ' : ' ') . 'IN (' . |
248 | implode(', ', $pHolders) . ')'; |
249 | if ($placeholder == ':') { |
250 | $expressions[$parsedColumn] = $newExpression; |
251 | } else { |
252 | $expressions[] = $newExpression; |
253 | } |
254 | } else { |
255 | $expressions[] = $parsedColumn . (($operator == 'NOT') ? ' NOT ' : ' ') . 'IN (' . |
256 | implode(', ', array_map('Pop\Db\Sql\Parser\Expression::quote', $value)) . ')'; |
257 | } |
258 | if ($placeholder == ':') { |
259 | $params[$parsedColumn] = $p; |
260 | } else { |
261 | $params[$j] = $p; |
262 | } |
263 | // BETWEEN/NOT BETWEEN |
264 | } else if (is_string($value) && (str_starts_with($value, '(')) && (str_ends_with($value, ')')) && |
265 | (str_contains($value, ','))) { |
266 | $values = substr($value, (strpos($value, '(') + 1)); |
267 | $values = substr($values, 0, strpos($values, ')')); |
268 | [$value1, $value2] = array_map('trim', explode(',', $values)); |
269 | $p = [$value1, $value2]; |
270 | |
271 | if ($placeholder == ':') { |
272 | $pHolder2 = $pHolder . 2; |
273 | $pHolder .= 1; |
274 | } else if ($placeholder == '$') { |
275 | $pHolder2 = $placeholder . ++$i; |
276 | } else { |
277 | $pHolder2 = $pHolder; |
278 | } |
279 | |
280 | if ($placeholder !== null) { |
281 | $newExpression = $parsedColumn . (($operator == 'NOT') ? ' NOT ' : ' ') . |
282 | 'BETWEEN ' . $pHolder . ' AND ' . $pHolder2; |
283 | if ($placeholder == ':') { |
284 | $expressions[$parsedColumn] = $newExpression; |
285 | } else { |
286 | $expressions[] = $newExpression; |
287 | } |
288 | } else { |
289 | $expressions[] = $parsedColumn . (($operator == 'NOT') ? ' NOT ' : ' ') . |
290 | 'BETWEEN ' . self::quote($value1) . ' AND ' . self::quote($value2); |
291 | } |
292 | if ($placeholder == ':') { |
293 | $params[$parsedColumn] = $p; |
294 | } else { |
295 | $params[$j] = $p; |
296 | } |
297 | $i++; |
298 | // LIKE/NOT LIKE or Standard Operators |
299 | } else { |
300 | if ((str_starts_with($column, '%')) || (str_starts_with($column, '-%'))) { |
301 | $value = '%' . $value; |
302 | } |
303 | if ((str_ends_with($column, '%')) || (str_ends_with($column, '%-'))) { |
304 | $value .= '%'; |
305 | } |
306 | if ($placeholder !== null) { |
307 | $newExpression = $parsedColumn . ' ' . $operator . ' ' . $pHolder; |
308 | if ($placeholder == ':') { |
309 | $expressions[$parsedColumn] = $newExpression; |
310 | } else { |
311 | $expressions[] = $newExpression; |
312 | } |
313 | } else { |
314 | $expressions[] = $parsedColumn . ' ' . $operator . ' ' . self::quote($value); |
315 | } |
316 | if ($placeholder == ':') { |
317 | $params[$parsedColumn] = $value; |
318 | } else { |
319 | $params[$j] = $value; |
320 | } |
321 | $i++; |
322 | } |
323 | $j++; |
324 | } |
325 | |
326 | if ($flatten) { |
327 | $flattenParams = []; |
328 | |
329 | foreach ($params as $key => $value) { |
330 | if (is_array($value)) { |
331 | foreach ($value as $k => $v) { |
332 | if ($placeholder == ':') { |
333 | $flattenParams[$key . ($k + 1)] = $v; |
334 | } else { |
335 | $flattenParams[] = $v; |
336 | } |
337 | } |
338 | } else { |
339 | if ($placeholder == ':') { |
340 | $flattenParams[$key] = $value; |
341 | } else { |
342 | $flattenParams[] = $value; |
343 | } |
344 | } |
345 | } |
346 | |
347 | return ['expressions' => $expressions, 'params' => $flattenParams]; |
348 | } else { |
349 | return ['expressions' => $expressions, 'params' => $params]; |
350 | } |
351 | |
352 | } |
353 | |
354 | /** |
355 | * Strip ID quotes |
356 | * |
357 | * @param string $identifier |
358 | * @return string |
359 | */ |
360 | public static function stripIdQuotes(string $identifier): string |
361 | { |
362 | if (((str_starts_with($identifier, '"')) && (str_ends_with($identifier, '"'))) || |
363 | ((str_starts_with($identifier, '`')) && (str_ends_with($identifier, '`'))) || |
364 | ((str_starts_with($identifier, '[')) && (str_ends_with($identifier, ']')))) { |
365 | $identifier = substr($identifier, 1); |
366 | $identifier = substr($identifier, 0, -1); |
367 | } |
368 | |
369 | return $identifier; |
370 | } |
371 | |
372 | /** |
373 | * Strip quotes |
374 | * |
375 | * @param string $value |
376 | * @return string |
377 | */ |
378 | public static function stripQuotes(string $value): string |
379 | { |
380 | if (((str_starts_with($value, '"')) && (str_ends_with($value, '"'))) || |
381 | ((str_starts_with($value, "'")) && (str_ends_with($value, "'")))) { |
382 | $value = substr($value, 1); |
383 | $value = substr($value, 0, -1); |
384 | } |
385 | |
386 | return $value; |
387 | } |
388 | |
389 | /** |
390 | * Quote the value (if it is not a numeric value) |
391 | * |
392 | * @param string $value |
393 | * @return string |
394 | */ |
395 | public static function quote(string $value): string |
396 | { |
397 | if (($value == '') || |
398 | (($value != '?') && (!str_starts_with($value, ':')) && (preg_match('/^\$\d*\d$/', $value) == 0) && |
399 | !is_int($value) && !is_float($value) && (preg_match('/^\d*$/', $value) == 0))) { |
400 | $value = "'" . $value . "'"; |
401 | } |
402 | return $value; |
403 | } |
404 | |
405 | } |