183 lines
5.9 KiB
PHP
183 lines
5.9 KiB
PHP
<?php
|
|
|
|
class FieldName {
|
|
public string $fieldName;
|
|
|
|
public function __construct(string $fieldName)
|
|
{
|
|
$this->fieldName = $fieldName;
|
|
}
|
|
}
|
|
|
|
class ExpressionBuilder
|
|
{
|
|
// Automatic typecast.
|
|
static function automaticTypecast(string $rval) : mixed
|
|
{
|
|
if (is_numeric($rval)) { // is it a numeric value?
|
|
if (((int)$rval) == ((double)$rval)) { // is it an integer?
|
|
return (int)$rval;
|
|
} else { // is it a float?
|
|
return (double)$rval;
|
|
}
|
|
} else if (in_array(strtolower($rval), ["true", "false"]) ) { // it's a boolean
|
|
return $rval === "true";
|
|
} else if (str_starts_with($rval, '"') && str_ends_with($rval, '"')){ // it's a string
|
|
return substr($rval, 1, strlen($rval) - 2); // strip leading and trailing quotes
|
|
} else { // it must be a column name
|
|
return new FieldName($rval);
|
|
}
|
|
}
|
|
|
|
// Divide expression into operands and operators.
|
|
static function splitCriterion(string $crstr): array|Closure
|
|
{
|
|
preg_match("/([<>=!]+|LIKE|NOT LIKE|IN|NOT IN|CONTAINS|NOT CONTAINS|BETWEEN|NOT BETWEEN|EXISTS)/", $crstr, $matches, PREG_OFFSET_CAPTURE);
|
|
|
|
// extract operator
|
|
$op = $matches[0][0];
|
|
$op_pos = $matches[0][1];
|
|
|
|
// extract operands
|
|
$left = trim(substr($crstr, 0, $op_pos));
|
|
$right = trim(substr($crstr, $op_pos + strlen($op), strlen($crstr)));
|
|
|
|
// automatic type conversion
|
|
if (str_starts_with($right, "[") && str_ends_with($right, "]")) { // is it an array?
|
|
$right = substr($right, 1, -1); // strip leading and trailing brackets
|
|
$elements = explode(",", $right); // extract array elements
|
|
$right = []; // re-init right value, since it's an array
|
|
foreach ($elements as $element) { // insert array elements
|
|
$element = trim($element);
|
|
if ($element !== "") {
|
|
$right[] = self::automaticTypecast($element);
|
|
}
|
|
}
|
|
} else { // it must be a single value
|
|
$right = self::automaticTypecast($right);
|
|
}
|
|
|
|
// handle "default" criteria and closures
|
|
if (is_a($right, FieldName::class)) { // field name
|
|
return function($a) use ($left, $right, $op): bool {
|
|
$X = &$a[$left];
|
|
$Y = &$a[$right->fieldName];
|
|
switch ($op) {
|
|
case "=": {
|
|
return $X == $Y;
|
|
}
|
|
case "!=": {
|
|
return $X != $Y;
|
|
}
|
|
case "<": {
|
|
return $X < $Y;
|
|
}
|
|
case ">": {
|
|
return $X > $Y;
|
|
}
|
|
case "<=": {
|
|
return $X <= $Y;
|
|
}
|
|
case ">=": {
|
|
return $X >= $Y;
|
|
}
|
|
default: {
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
} else { // default critera
|
|
return [$left, $op, $right];
|
|
}
|
|
}
|
|
|
|
// Build SleekDB query expression. Processes encapsulated expressions recursively as well.
|
|
static function buildQuery(string $filter): array
|
|
{
|
|
// skip empty filter processing
|
|
if (trim($filter) === "") {
|
|
return [];
|
|
}
|
|
|
|
// subfilters and operations
|
|
$subfilts = [];
|
|
$operations = [];
|
|
|
|
// buffer and scoring
|
|
$k = 0;
|
|
$k_prev = 0;
|
|
$buffer = "";
|
|
|
|
for ($i = 0; $i < strlen($filter); $i++) {
|
|
$c = $filter[$i];
|
|
|
|
// extract groups surrounded by parantheses
|
|
if ($c === "(") {
|
|
$k++;
|
|
} elseif ($c === ")") {
|
|
$k--;
|
|
}
|
|
|
|
// only omit parentheses at the top-level expression
|
|
if (!((($c === "(") && ($k === 1)) || (($c === ")") && ($k === 0)))) {
|
|
$buffer .= $c;
|
|
}
|
|
|
|
// if k = 0, then we found a subfilter
|
|
if (($k === 0) && ($k_prev === 1)) {
|
|
$subfilts[] = trim($buffer);
|
|
$buffer = "";
|
|
} elseif (($k === 1) && ($k_prev === 0)) {
|
|
$op = trim($buffer);
|
|
if ($op !== "") {
|
|
$operations[] = $op;
|
|
}
|
|
$buffer = "";
|
|
}
|
|
|
|
// save k to be used next iteration
|
|
$k_prev = $k;
|
|
}
|
|
|
|
// decide, whether further expansion of condition is needed
|
|
$criteria = [];
|
|
for ($i = 0; $i < count($subfilts); $i++) {
|
|
$subfilt = $subfilts[$i];
|
|
|
|
// add subcriterion
|
|
if ($subfilt[0] === "(") {
|
|
$criteria[] = self::buildQuery($subfilt);
|
|
} else {
|
|
$criteria[] = self::splitCriterion($subfilt);
|
|
}
|
|
|
|
// add operator
|
|
if (($i + 1) < count($subfilts)) {
|
|
$criteria[] = $operations[$i];
|
|
}
|
|
}
|
|
|
|
return $criteria;
|
|
}
|
|
|
|
// Build SleekDB ordering.
|
|
static function buildOrdering(string $orderby): array
|
|
{
|
|
// don't process empty order instructions
|
|
if ($orderby === "") {
|
|
return [];
|
|
}
|
|
|
|
// explode string at tokens delimiting separate order criteria
|
|
$ordering = [];
|
|
$subcriteria = explode(";", $orderby);
|
|
foreach ($subcriteria as $subcriterion) {
|
|
$parts = explode(":", $subcriterion); // fetch parts
|
|
$field_name = trim($parts[0], "\ \n\r\t\v\0\"'"); // strip leading and trailing quotes if exists
|
|
$direction = strtolower(trim($parts[1])); // fetch ordering direction
|
|
$ordering[$field_name] = $direction; // build ordering instruction
|
|
}
|
|
|
|
return $ordering;
|
|
}
|
|
} |