fallbackEvaluator = $fallbackEvaluator ?? function (Expr $expr) { throw new ConstExprEvaluationException( "Expression of type {$expr->getType()} cannot be evaluated" ); }; $this->functionsWhiteList = []; $this->staticCallsWhitelist = []; } public function setFunctionsWhitelist(array $functionsWhiteList): void { $this->functionsWhiteList = $functionsWhiteList; } public function setStaticCallsWhitelist(array $staticCallsWhitelist): void { $this->staticCallsWhitelist = $staticCallsWhitelist; } /** * Silently evaluates a constant expression into a PHP value. * * Thrown Errors, warnings or notices will be converted into a ConstExprEvaluationException. * The original source of the exception is available through getPrevious(). * * If some part of the expression cannot be evaluated, the fallback evaluator passed to the * constructor will be invoked. By default, if no fallback is provided, an exception of type * ConstExprEvaluationException is thrown. * * See class doc comment for caveats and limitations. * * @param Expr $expr Constant expression to evaluate * @return mixed Result of evaluation * * @throws ConstExprEvaluationException if the expression cannot be evaluated or an error occurred */ public function evaluateSilently(Expr $expr) { set_error_handler(function ($num, $str, $file, $line) { throw new \ErrorException($str, 0, $num, $file, $line); }); try { return $this->evaluate($expr); } catch (\Throwable $e) { if (!$e instanceof ConstExprEvaluationException) { $e = new ConstExprEvaluationException( "An error occurred during constant expression evaluation", 0, $e); } throw $e; } finally { restore_error_handler(); } } /** * Directly evaluates a constant expression into a PHP value. * * May generate Error exceptions, warnings or notices. Use evaluateSilently() to convert these * into a ConstExprEvaluationException. * * If some part of the expression cannot be evaluated, the fallback evaluator passed to the * constructor will be invoked. By default, if no fallback is provided, an exception of type * ConstExprEvaluationException is thrown. * * See class doc comment for caveats and limitations. * * @param Expr $expr Constant expression to evaluate * @return mixed Result of evaluation * * @throws ConstExprEvaluationException if the expression cannot be evaluated */ public function evaluateDirectly(Expr $expr) { return $this->evaluate($expr); } /** @return mixed */ private function evaluate(Expr $expr) { if ($expr instanceof Scalar\Int_ || $expr instanceof Scalar\Float_ || $expr instanceof Scalar\String_ ) { return $expr->value; } if ($expr instanceof Expr\Array_) { return $this->evaluateArray($expr); } if ($expr instanceof Expr\Variable) { return $this->evaluateVariable($expr); } // Unary operators if ($expr instanceof Expr\UnaryPlus) { return +$this->evaluate($expr->expr); } if ($expr instanceof Expr\UnaryMinus) { return -$this->evaluate($expr->expr); } if ($expr instanceof Expr\BooleanNot) { return !$this->evaluate($expr->expr); } if ($expr instanceof Expr\BitwiseNot) { return ~$this->evaluate($expr->expr); } if ($expr instanceof Expr\BinaryOp) { return $this->evaluateBinaryOp($expr); } if ($expr instanceof Expr\Ternary) { return $this->evaluateTernary($expr); } if ($expr instanceof Expr\ArrayDimFetch && null !== $expr->dim) { return $this->evaluate($expr->var)[$this->evaluate($expr->dim)]; } if ($expr instanceof Expr\ConstFetch) { return $this->evaluateConstFetch($expr); } if ($expr instanceof Expr\Isset_) { return $this->evaluateIsset($expr); } if ($expr instanceof Expr\ClassConstFetch) { return $this->evaluateClassConstFetch($expr); } if ($expr instanceof Expr\Cast) { return $this->evaluateCast($expr); } if ($expr instanceof Expr\StaticPropertyFetch) { return $this->evaluateStaticPropertyFetch($expr); } if ($expr instanceof Expr\FuncCall) { return $this->evaluateFuncCall($expr); } if ($expr instanceof Expr\StaticCall) { return $this->evaluateStaticCall($expr); } if ($expr instanceof Expr\NullsafePropertyFetch||$expr instanceof Expr\PropertyFetch) { return $this->evaluatePropertyFetch($expr); } if ($expr instanceof Expr\NullsafeMethodCall||$expr instanceof Expr\MethodCall) { return $this->evaluateMethodCall($expr); } return ($this->fallbackEvaluator)($expr); } private function evaluateArray(Expr\Array_ $expr): array { $array = []; foreach ($expr->items as $item) { if (null !== $item->key) { $array[$this->evaluate($item->key)] = $this->evaluate($item->value); } elseif ($item->unpack) { $array = array_merge($array, $this->evaluate($item->value)); } else { $array[] = $this->evaluate($item->value); } } return $array; } /** @return mixed */ private function evaluateTernary(Expr\Ternary $expr) { if (null === $expr->if) { return $this->evaluate($expr->cond) ?: $this->evaluate($expr->else); } return $this->evaluate($expr->cond) ? $this->evaluate($expr->if) : $this->evaluate($expr->else); } /** @return mixed */ private function evaluateBinaryOp(Expr\BinaryOp $expr) { if ($expr instanceof Expr\BinaryOp\Coalesce) { try { $var = $this->evaluate($expr->left); } catch(\Throwable $t) { //left expression cannot be evaluated (! isset for exeample) return $this->evaluate($expr->right); } return $var ?? $this->evaluate($expr->right); } // The evaluate() calls are repeated in each branch, because some of the operators are // short-circuiting and evaluating the RHS in advance may be illegal in that case $l = $expr->left; $r = $expr->right; switch ($expr->getOperatorSigil()) { case '&': return $this->evaluate($l) & $this->evaluate($r); case '|': return $this->evaluate($l) | $this->evaluate($r); case '^': return $this->evaluate($l) ^ $this->evaluate($r); case '&&': return $this->evaluate($l) && $this->evaluate($r); case '||': return $this->evaluate($l) || $this->evaluate($r); case '??': return $this->evaluate($l) ?? $this->evaluate($r); case '.': return $this->evaluate($l) . $this->evaluate($r); case '/': return $this->evaluate($l) / $this->evaluate($r); case '==': return $this->evaluate($l) == $this->evaluate($r); case '>': return $this->evaluate($l) > $this->evaluate($r); case '>=': return $this->evaluate($l) >= $this->evaluate($r); case '===': return $this->evaluate($l) === $this->evaluate($r); case 'and': return $this->evaluate($l) and $this->evaluate($r); case 'or': return $this->evaluate($l) or $this->evaluate($r); case 'xor': return $this->evaluate($l) xor $this->evaluate($r); case '-': return $this->evaluate($l) - $this->evaluate($r); case '%': return $this->evaluate($l) % $this->evaluate($r); case '*': return $this->evaluate($l) * $this->evaluate($r); case '!=': return $this->evaluate($l) != $this->evaluate($r); case '!==': return $this->evaluate($l) !== $this->evaluate($r); case '+': return $this->evaluate($l) + $this->evaluate($r); case '**': return $this->evaluate($l) ** $this->evaluate($r); case '<<': return $this->evaluate($l) << $this->evaluate($r); case '>>': return $this->evaluate($l) >> $this->evaluate($r); case '<': return $this->evaluate($l) < $this->evaluate($r); case '<=': return $this->evaluate($l) <= $this->evaluate($r); case '<=>': return $this->evaluate($l) <=> $this->evaluate($r); case '|>': $lval = $this->evaluate($l); return $this->evaluate($r)($lval); } throw new \Exception('Should not happen'); } /** @return mixed */ private function evaluateConstFetch(Expr\ConstFetch $expr) { try { $name = $expr->name; if(! is_string($name)) { //PHP_VERSION_ID usecase $name = $name->name; } if (defined($name)) { return constant($name); } } catch(\Throwable $t) {} return ($this->fallbackEvaluator)($expr); } /** @return bool */ private function evaluateIsset(Expr\Isset_ $expr) { try { foreach ($expr->vars as $var) { $var = $this->evaluate($var); if (! isset($var)) { return false; } } return true; } catch(\Throwable $t) { return false; } } /** @return mixed */ private function evaluateClassConstFetch(Expr\ClassConstFetch $expr) { try { $classname = $expr->class->name; $property = $expr->name->name; if ('class' === $property) { return $classname; } if (class_exists($classname)) { $class = new \ReflectionClass($classname); if (array_key_exists($property, $class->getConstants())) { $oReflectionConstant = $class->getReflectionConstant($property); if ($oReflectionConstant->isPublic()) { return $class->getConstant($property); } } } } catch(\Throwable $t) {} return ($this->fallbackEvaluator)($expr); } /** @return mixed */ private function evaluateCast(Expr\Cast $expr) { try { $subexpr = $this->evaluate($expr->expr); $type = get_class($expr); switch ($type) { case Expr\Cast\Array_::class: return (array) $subexpr; case Expr\Cast\Bool_::class: return (bool) $subexpr; case Expr\Cast\Double::class: switch ($expr->getAttribute("kind")) { case Expr\Cast\Double::KIND_DOUBLE: return (double) $subexpr; case Expr\Cast\Double::KIND_FLOAT: case Expr\Cast\Double::KIND_REAL: return (float) $subexpr; } break; case Expr\Cast\Int_::class: return (int) $subexpr; case Expr\Cast\Object_::class: return (object) $subexpr; case Expr\Cast\String_::class: return (string) $subexpr; } } catch(\Throwable $t) {} return ($this->fallbackEvaluator)($expr); } /** @return mixed */ private function evaluateStaticPropertyFetch(Expr\StaticPropertyFetch $expr) { try { $classname = $expr->class->name; if ($expr->name instanceof Identifier) { $property = $expr->name->name; } else { $property = $this->evaluate($expr->name); } if (class_exists($classname)) { $class = new \ReflectionClass($classname); if (array_key_exists($property, $class->getStaticProperties())) { $oReflectionProperty = $class->getProperty($property); if ($oReflectionProperty->isPublic()) { return $class->getStaticPropertyValue($property); } } } } catch (\Throwable $t) {} return ($this->fallbackEvaluator)($expr); } /** @return mixed */ private function evaluateFuncCall(Expr\FuncCall $expr) { try { $name = $expr->name; if ($name instanceof Name) { $function = $name->name; } else { $function = $this->evaluate($name); } if (! in_array($function, $this->functionsWhiteList)) { throw new Exception("FuncCall $function not supported"); } $args=[]; foreach ($expr->args as $arg) { /** @var \PhpParser\Node\Arg $arg */ $args[]=$arg->value->value; } $reflection_function = new \ReflectionFunction($function); return $reflection_function->invoke(...$args); } catch (\Throwable $t) {} return ($this->fallbackEvaluator)($expr); } /** @return mixed */ private function evaluateVariable(Expr\Variable $expr) { try { $name = $expr->name; if (array_key_exists($name, get_defined_vars())) { return $$name; } if (array_key_exists($name, $GLOBALS)) { global $$name; return $$name; } } catch (\Throwable $t) { } return ($this->fallbackEvaluator)($expr); } /** @return mixed */ private function evaluateStaticCall(Expr\StaticCall $expr) { try { $class = $expr->class->name; if ($expr->name instanceof Identifier) { $method = $expr->name->name; } else { $method = $this->evaluate($expr->name); } $static_call_description = "$class::$method"; if (! in_array($static_call_description, $this->staticCallsWhitelist)) { throw new Exception("StaticCall $static_call_description not supported"); } $args=[]; foreach ($expr->args as $arg) { /** @var \PhpParser\Node\Arg $arg */ $args[]=$arg->value->value; } $class = new \ReflectionClass($class); $method = $class->getMethod($method); if ($method->isPublic()) { return $method->invokeArgs(null, $args); } } catch (\Throwable $t) {} return ($this->fallbackEvaluator)($expr); } /** * @param \PhpParser\Node\Expr\NullsafePropertyFetch|\PhpParser\Node\Expr\PropertyFetch $expr * * @return mixed */ private function evaluatePropertyFetch($expr) { try { $var = $this->evaluate($expr->var); } catch (\Throwable $t) { $var = null; } if (! is_null($var)) { try { if ($expr->name instanceof Identifier) { $name = $expr->name->name; } else { $name = $this->evaluate($expr->name); } $reflectionClass = new \ReflectionClass(get_class($var)); $property = $reflectionClass->getProperty($name); if ($property->isPublic()) { return $property->getValue($var); } } catch (\Throwable $t) {} } else if ($expr instanceof Expr\NullsafePropertyFetch) { return null; } return ($this->fallbackEvaluator)($expr); } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall $expr * * @return mixed */ private function evaluateMethodCall($expr) { try { $var = $this->evaluate($expr->var); } catch (\Throwable $t) { $var = null; } if (! is_null($var)) { try { $args = []; foreach ($expr->args as $arg) { /** @var \PhpParser\Node\Arg $arg */ $args[] = $arg->value->value; } if ($expr->name instanceof Identifier) { $name = $expr->name->name; } else { $name = $this->evaluate($expr->name); } $reflectionClass = new \ReflectionClass(get_class($var)); $method = $reflectionClass->getMethod($name); if ($method->isPublic()) { return $method->invokeArgs($var, $args); } } catch (\Throwable $t) {} } else if ($expr instanceof Expr\NullsafeMethodCall) { return null; } return ($this->fallbackEvaluator)($expr); } }