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;
 | 
						|
    }
 | 
						|
} |