PHP validate a list of constraints

2.5k Views Asked by At

I have an array of < and > constraints on variable names that I get from the user:

$constraints = array('1<x','x<5','y>4');

Where $x and $y are defined in the relevant scope.

I want to verify that all the constraints hold (return true or false)

How can I do this without using eval ?

4

There are 4 best solutions below

3
On BEST ANSWER

I concocted a partial answer here. It doesn't loop, but it does support the five different comparison operators.

function lt($p1, $p2) {
    return ($p1 < $p2);
}
function le($p1, $p2) {
    return ($p1 <= $p2);
}

function gt($p1, $p2) {
    return ($p1 > $p2);
}
function ge($p1, $p2) {
    return ($p1 >= $p2);
}
function eq($p1, $pw) {
    return ($p1 == $p2);
}

function apply_rule($rule, $x, $y) {
    $matches = NULL;
    if (!preg_match('/^([a-zA-Z0-9]+)(<|>|=|<=|>=)([a-zA-Z0-9]+)$/', $rule, $matches)) {
        throw new Exception("Invalid rule: " . $rule);
    }
    //var_dump($matches);
    $p1 = $matches[1];
    $operator = $matches[2];
    $p2 = $matches[3];

    // check if first param is a variable
    if (preg_match('/([a-zA-Z]+)/', $p1)) {
        $p1 = $$p1;
    }
    // check if second param is a variable
    if (preg_match('/([a-zA-Z]+)/', $p2)) {
        $p2 = $$p2;
    }

    switch($operator) {
        case "<":
            return lt($p1, $p2);
        case "<=":
            return le($p1, $p2);
        case ">":
            return gt($p1, $p2);
        case ">=":
            return ge($p1, $p2);
        case "=":
            return eq($p1, $p2);
    }
}

var_dump(apply_rule("x>=10", 10, 20));
0
On

A lot of people do know that the $ symbol in php is actually an operator that evaluates the variable.

$total_expressions = true;
foreach($constraints as $c) {
  #parse the expression in to the number, and the variable
  $parts = explode(">",str_replace("<",">",$c));
  $i = is_numeric($parts[0]) ? 0 : 1 ;
  $n = $parts[$i];
  $v = $parts[1-$i];
  # At this stage, $v is the variable name, and $n is the number
  # This line is kinda hard coded to only ">" or "<", but you get the idea
  $expression = strpos(">",$c) && $i ? $$v > $n : $$v < $n;
  $total_expressions = $total_expressions && $expression;
  if (!$total_expressions)
      break;
}

$total_expressions would be true only if all the constraints hold.

5
On

If you just want to know that all constraints are valid, you can pass them to a function that does the checking. It can check each constraint one by one using a foreach loop. If the current constraint is not valid then it will return false and stop checking. Otherwise, if it reaches the end of the loop it will return true. The values for the variables are passed into the function as two arrays as used in str_replace().

function validate($constraints, $search, $replace) {

    foreach ($constraints as $constraint) {

        // replace variables in string with submitted values
        $constraint = str_replace($search, $replace, $constraint);

        if (strpos($constraint, '<') !== false) {
            // extract parts from less than constraint
            list($a, $b) = explode('<', $constraint, 2);
            if ($a >= $b) {
                // $a is greater than or equal to $b i.e. not less than
                return false;
            }
        } else if (strpos($constraint, '>') !== false) {
            // extract parts from greater than constraint
            list($a, $b) = explode('>', $constraint, 2);
            if ($a <= $b) {
                // $a is less than or equal to $b i.e. not greater than
                return false;
            }
        }
    }
    // no invalid constraints were found...
    return true;
}

You can then use it to check your $constraints array,

// variables to search for
$search = ['x', 'y'];

// variable replacements
$replace = [5, 2];

// constraints array
$constraints = array('4<x','x<6','y>1');

// run the function
var_dump(validate($constraints, $search, $replace));

The function does assume that the data is passed to it exactly as you've described. You may want to add some checks if the data format could vary.

1
On

If you have to evaluate only simple expressions and you know in advance the number and the names of the variables then you can write a simple parser:

/**
 * Parse and evaluate a simple comparison.
 *
 * @param string  $condition e.g. 'x<4'
 * @param integer $x         the value of 'x'
 * @param integer $y         the value of 'y'
 */
function compare($condition, $x, $y)
{
    // Verify that the condition uses the format accepted by this function
    // Also extract the pieces in $m
    $m = array();
    if (! preg_match('/^(x|y|\d+)([<>])(x|y|\d+)$/', $condition, $m)) {
        throw new RuntimeException("Cannot parse the condition");
    }

    // $m[0] is the entire string that matched the expression
    // $m[1] and $m[3] are the operands (the first and the third groups)
    // $m[2] is the operator (the second group in the regex)

    // Replace the variables with their values in $m[1] and $m[3]
    foreach (array(1, 3) as $i) {
        switch ($m[$i]) {
        case 'x':
            $m[$i] = $x;
            break;
        case 'y':
            $m[$i] = $y;
            break;
        default:
            $m[$i] = (int)$m[$i];
            break;
        }
    }

    // Compare the values, return a boolean
    return ($m[2] == '<') ? ($m[1] < $m[3]) : ($m[1] > $m[3]);
}

// A simple test
$x = 7;
$y = 3;
echo('$x='.$x."\n");
echo('$y='.$y."\n");

echo('1<x: '.(compare('1<x', $x, $y) ? 'TRUE' : 'FALSE')."\n");
echo('x<5: '.(compare('x<5', $x, $y) ? 'TRUE' : 'FALSE')."\n");
echo('y>4: '.(compare('y>4', $x, $y) ? 'TRUE' : 'FALSE')."\n");

The code works with integer values. To make it work with floating point values just replace (int) with (double) on the default branch of the switch statement.

The regular expression:

^               # match the beginning of the string
(               # start capturing group #1
  x               # match the 'x' character 
  |y              # ... OR (|) the 'y' character
  |\d+            # ... OR (|) a sequence of 1 or more (+) digits (\d)
)               # end capturing group #1     <-- find the captured value in $m[1]
(               # start capturing group #2
  [               # match any character from the range
    <>              # match '<' or '>'
  ]               # end range
)               # end capturing group #2     <-- find the captured value in $m[2]
(x|y|\d+)       # the capturing group #3, identical to group #1
$               # match the end of the string

With simple changes the code above can be adjusted to also allow <=, >=, = (change the regex) or a list of variables that is not known in advance (pass the variables in an array indexed by their names, use $m[$i] to find the value in the array).