Chris@13: fallbackEvaluator = $fallbackEvaluator ?? function(Expr $expr) { Chris@13: throw new ConstExprEvaluationException( Chris@13: "Expression of type {$expr->getType()} cannot be evaluated" Chris@13: ); Chris@13: }; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Silently evaluates a constant expression into a PHP value. Chris@13: * Chris@13: * Thrown Errors, warnings or notices will be converted into a ConstExprEvaluationException. Chris@13: * The original source of the exception is available through getPrevious(). Chris@13: * Chris@13: * If some part of the expression cannot be evaluated, the fallback evaluator passed to the Chris@13: * constructor will be invoked. By default, if no fallback is provided, an exception of type Chris@13: * ConstExprEvaluationException is thrown. Chris@13: * Chris@13: * See class doc comment for caveats and limitations. Chris@13: * Chris@13: * @param Expr $expr Constant expression to evaluate Chris@13: * @return mixed Result of evaluation Chris@13: * Chris@13: * @throws ConstExprEvaluationException if the expression cannot be evaluated or an error occurred Chris@13: */ Chris@13: public function evaluateSilently(Expr $expr) { Chris@13: set_error_handler(function($num, $str, $file, $line) { Chris@13: throw new \ErrorException($str, 0, $num, $file, $line); Chris@13: }); Chris@13: Chris@13: try { Chris@13: return $this->evaluate($expr); Chris@13: } catch (\Throwable $e) { Chris@13: if (!$e instanceof ConstExprEvaluationException) { Chris@13: $e = new ConstExprEvaluationException( Chris@13: "An error occurred during constant expression evaluation", 0, $e); Chris@13: } Chris@13: throw $e; Chris@13: } finally { Chris@13: restore_error_handler(); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Directly evaluates a constant expression into a PHP value. Chris@13: * Chris@13: * May generate Error exceptions, warnings or notices. Use evaluateSilently() to convert these Chris@13: * into a ConstExprEvaluationException. Chris@13: * Chris@13: * If some part of the expression cannot be evaluated, the fallback evaluator passed to the Chris@13: * constructor will be invoked. By default, if no fallback is provided, an exception of type Chris@13: * ConstExprEvaluationException is thrown. Chris@13: * Chris@13: * See class doc comment for caveats and limitations. Chris@13: * Chris@13: * @param Expr $expr Constant expression to evaluate Chris@13: * @return mixed Result of evaluation Chris@13: * Chris@13: * @throws ConstExprEvaluationException if the expression cannot be evaluated Chris@13: */ Chris@13: public function evaluateDirectly(Expr $expr) { Chris@13: return $this->evaluate($expr); Chris@13: } Chris@13: Chris@13: private function evaluate(Expr $expr) { Chris@13: if ($expr instanceof Scalar\LNumber Chris@13: || $expr instanceof Scalar\DNumber Chris@13: || $expr instanceof Scalar\String_ Chris@13: ) { Chris@13: return $expr->value; Chris@13: } Chris@13: Chris@13: if ($expr instanceof Expr\Array_) { Chris@13: return $this->evaluateArray($expr); Chris@13: } Chris@13: Chris@13: // Unary operators Chris@13: if ($expr instanceof Expr\UnaryPlus) { Chris@13: return +$this->evaluate($expr->expr); Chris@13: } Chris@13: if ($expr instanceof Expr\UnaryMinus) { Chris@13: return -$this->evaluate($expr->expr); Chris@13: } Chris@13: if ($expr instanceof Expr\BooleanNot) { Chris@13: return !$this->evaluate($expr->expr); Chris@13: } Chris@13: if ($expr instanceof Expr\BitwiseNot) { Chris@13: return ~$this->evaluate($expr->expr); Chris@13: } Chris@13: Chris@13: if ($expr instanceof Expr\BinaryOp) { Chris@13: return $this->evaluateBinaryOp($expr); Chris@13: } Chris@13: Chris@13: if ($expr instanceof Expr\Ternary) { Chris@13: return $this->evaluateTernary($expr); Chris@13: } Chris@13: Chris@13: if ($expr instanceof Expr\ArrayDimFetch && null !== $expr->dim) { Chris@13: return $this->evaluate($expr->var)[$this->evaluate($expr->dim)]; Chris@13: } Chris@13: Chris@13: if ($expr instanceof Expr\ConstFetch) { Chris@13: return $this->evaluateConstFetch($expr); Chris@13: } Chris@13: Chris@13: return ($this->fallbackEvaluator)($expr); Chris@13: } Chris@13: Chris@13: private function evaluateArray(Expr\Array_ $expr) { Chris@13: $array = []; Chris@13: foreach ($expr->items as $item) { Chris@13: if (null !== $item->key) { Chris@13: $array[$this->evaluate($item->key)] = $this->evaluate($item->value); Chris@13: } else { Chris@13: $array[] = $this->evaluate($item->value); Chris@13: } Chris@13: } Chris@13: return $array; Chris@13: } Chris@13: Chris@13: private function evaluateTernary(Expr\Ternary $expr) { Chris@13: if (null === $expr->if) { Chris@13: return $this->evaluate($expr->cond) ?: $this->evaluate($expr->else); Chris@13: } Chris@13: Chris@13: return $this->evaluate($expr->cond) Chris@13: ? $this->evaluate($expr->if) Chris@13: : $this->evaluate($expr->else); Chris@13: } Chris@13: Chris@13: private function evaluateBinaryOp(Expr\BinaryOp $expr) { Chris@13: if ($expr instanceof Expr\BinaryOp\Coalesce Chris@13: && $expr->left instanceof Expr\ArrayDimFetch Chris@13: ) { Chris@13: // This needs to be special cased to respect BP_VAR_IS fetch semantics Chris@13: return $this->evaluate($expr->left->var)[$this->evaluate($expr->left->dim)] Chris@13: ?? $this->evaluate($expr->right); Chris@13: } Chris@13: Chris@13: // The evaluate() calls are repeated in each branch, because some of the operators are Chris@13: // short-circuiting and evaluating the RHS in advance may be illegal in that case Chris@13: $l = $expr->left; Chris@13: $r = $expr->right; Chris@13: switch ($expr->getOperatorSigil()) { Chris@13: case '&': return $this->evaluate($l) & $this->evaluate($r); Chris@13: case '|': return $this->evaluate($l) | $this->evaluate($r); Chris@13: case '^': return $this->evaluate($l) ^ $this->evaluate($r); Chris@13: case '&&': return $this->evaluate($l) && $this->evaluate($r); Chris@13: case '||': return $this->evaluate($l) || $this->evaluate($r); Chris@13: case '??': return $this->evaluate($l) ?? $this->evaluate($r); Chris@13: case '.': return $this->evaluate($l) . $this->evaluate($r); Chris@13: case '/': return $this->evaluate($l) / $this->evaluate($r); Chris@13: case '==': return $this->evaluate($l) == $this->evaluate($r); Chris@13: case '>': return $this->evaluate($l) > $this->evaluate($r); Chris@13: case '>=': return $this->evaluate($l) >= $this->evaluate($r); Chris@13: case '===': return $this->evaluate($l) === $this->evaluate($r); Chris@13: case 'and': return $this->evaluate($l) and $this->evaluate($r); Chris@13: case 'or': return $this->evaluate($l) or $this->evaluate($r); Chris@13: case 'xor': return $this->evaluate($l) xor $this->evaluate($r); Chris@13: case '-': return $this->evaluate($l) - $this->evaluate($r); Chris@13: case '%': return $this->evaluate($l) % $this->evaluate($r); Chris@13: case '*': return $this->evaluate($l) * $this->evaluate($r); Chris@13: case '!=': return $this->evaluate($l) != $this->evaluate($r); Chris@13: case '!==': return $this->evaluate($l) !== $this->evaluate($r); Chris@13: case '+': return $this->evaluate($l) + $this->evaluate($r); Chris@13: case '**': return $this->evaluate($l) ** $this->evaluate($r); Chris@13: case '<<': return $this->evaluate($l) << $this->evaluate($r); Chris@13: case '>>': return $this->evaluate($l) >> $this->evaluate($r); Chris@13: case '<': return $this->evaluate($l) < $this->evaluate($r); Chris@13: case '<=': return $this->evaluate($l) <= $this->evaluate($r); Chris@13: case '<=>': return $this->evaluate($l) <=> $this->evaluate($r); Chris@13: } Chris@13: Chris@13: throw new \Exception('Should not happen'); Chris@13: } Chris@13: Chris@13: private function evaluateConstFetch(Expr\ConstFetch $expr) { Chris@13: $name = $expr->name->toLowerString(); Chris@13: switch ($name) { Chris@13: case 'null': return null; Chris@13: case 'false': return false; Chris@13: case 'true': return true; Chris@13: } Chris@13: Chris@13: return ($this->fallbackEvaluator)($expr); Chris@13: } Chris@13: }