5 use PhpParser\Node\Expr;
6 use PhpParser\Node\Scalar;
9 * Evaluates constant expressions.
11 * This evaluator is able to evaluate all constant expressions (as defined by PHP), which can be
12 * evaluated without further context. If a subexpression is not of this type, a user-provided
13 * fallback evaluator is invoked. To support all constant expressions that are also supported by
14 * PHP (and not already handled by this class), the fallback evaluator must be able to handle the
15 * following node types:
17 * * All Scalar\MagicConst\* nodes.
18 * * Expr\ConstFetch nodes. Only null/false/true are already handled by this class.
19 * * Expr\ClassConstFetch nodes.
21 * The fallback evaluator should throw ConstExprEvaluationException for nodes it cannot evaluate.
23 * The evaluation is dependent on runtime configuration in two respects: Firstly, floating
24 * point to string conversions are affected by the precision ini setting. Secondly, they are also
25 * affected by the LC_NUMERIC locale.
27 class ConstExprEvaluator
29 private $fallbackEvaluator;
32 * Create a constant expression evaluator.
34 * The provided fallback evaluator is invoked whenever a subexpression cannot be evaluated. See
35 * class doc comment for more information.
37 * @param callable|null $fallbackEvaluator To call if subexpression cannot be evaluated
39 public function __construct(callable $fallbackEvaluator = null) {
40 $this->fallbackEvaluator = $fallbackEvaluator ?? function(Expr $expr) {
41 throw new ConstExprEvaluationException(
42 "Expression of type {$expr->getType()} cannot be evaluated"
48 * Silently evaluates a constant expression into a PHP value.
50 * Thrown Errors, warnings or notices will be converted into a ConstExprEvaluationException.
51 * The original source of the exception is available through getPrevious().
53 * If some part of the expression cannot be evaluated, the fallback evaluator passed to the
54 * constructor will be invoked. By default, if no fallback is provided, an exception of type
55 * ConstExprEvaluationException is thrown.
57 * See class doc comment for caveats and limitations.
59 * @param Expr $expr Constant expression to evaluate
60 * @return mixed Result of evaluation
62 * @throws ConstExprEvaluationException if the expression cannot be evaluated or an error occurred
64 public function evaluateSilently(Expr $expr) {
65 set_error_handler(function($num, $str, $file, $line) {
66 throw new \ErrorException($str, 0, $num, $file, $line);
70 return $this->evaluate($expr);
71 } catch (\Throwable $e) {
72 if (!$e instanceof ConstExprEvaluationException) {
73 $e = new ConstExprEvaluationException(
74 "An error occurred during constant expression evaluation", 0, $e);
78 restore_error_handler();
83 * Directly evaluates a constant expression into a PHP value.
85 * May generate Error exceptions, warnings or notices. Use evaluateSilently() to convert these
86 * into a ConstExprEvaluationException.
88 * If some part of the expression cannot be evaluated, the fallback evaluator passed to the
89 * constructor will be invoked. By default, if no fallback is provided, an exception of type
90 * ConstExprEvaluationException is thrown.
92 * See class doc comment for caveats and limitations.
94 * @param Expr $expr Constant expression to evaluate
95 * @return mixed Result of evaluation
97 * @throws ConstExprEvaluationException if the expression cannot be evaluated
99 public function evaluateDirectly(Expr $expr) {
100 return $this->evaluate($expr);
103 private function evaluate(Expr $expr) {
104 if ($expr instanceof Scalar\LNumber
105 || $expr instanceof Scalar\DNumber
106 || $expr instanceof Scalar\String_
111 if ($expr instanceof Expr\Array_) {
112 return $this->evaluateArray($expr);
116 if ($expr instanceof Expr\UnaryPlus) {
117 return +$this->evaluate($expr->expr);
119 if ($expr instanceof Expr\UnaryMinus) {
120 return -$this->evaluate($expr->expr);
122 if ($expr instanceof Expr\BooleanNot) {
123 return !$this->evaluate($expr->expr);
125 if ($expr instanceof Expr\BitwiseNot) {
126 return ~$this->evaluate($expr->expr);
129 if ($expr instanceof Expr\BinaryOp) {
130 return $this->evaluateBinaryOp($expr);
133 if ($expr instanceof Expr\Ternary) {
134 return $this->evaluateTernary($expr);
137 if ($expr instanceof Expr\ArrayDimFetch && null !== $expr->dim) {
138 return $this->evaluate($expr->var)[$this->evaluate($expr->dim)];
141 if ($expr instanceof Expr\ConstFetch) {
142 return $this->evaluateConstFetch($expr);
145 return ($this->fallbackEvaluator)($expr);
148 private function evaluateArray(Expr\Array_ $expr) {
150 foreach ($expr->items as $item) {
151 if (null !== $item->key) {
152 $array[$this->evaluate($item->key)] = $this->evaluate($item->value);
154 $array[] = $this->evaluate($item->value);
160 private function evaluateTernary(Expr\Ternary $expr) {
161 if (null === $expr->if) {
162 return $this->evaluate($expr->cond) ?: $this->evaluate($expr->else);
165 return $this->evaluate($expr->cond)
166 ? $this->evaluate($expr->if)
167 : $this->evaluate($expr->else);
170 private function evaluateBinaryOp(Expr\BinaryOp $expr) {
171 if ($expr instanceof Expr\BinaryOp\Coalesce
172 && $expr->left instanceof Expr\ArrayDimFetch
174 // This needs to be special cased to respect BP_VAR_IS fetch semantics
175 return $this->evaluate($expr->left->var)[$this->evaluate($expr->left->dim)]
176 ?? $this->evaluate($expr->right);
179 // The evaluate() calls are repeated in each branch, because some of the operators are
180 // short-circuiting and evaluating the RHS in advance may be illegal in that case
183 switch ($expr->getOperatorSigil()) {
184 case '&': return $this->evaluate($l) & $this->evaluate($r);
185 case '|': return $this->evaluate($l) | $this->evaluate($r);
186 case '^': return $this->evaluate($l) ^ $this->evaluate($r);
187 case '&&': return $this->evaluate($l) && $this->evaluate($r);
188 case '||': return $this->evaluate($l) || $this->evaluate($r);
189 case '??': return $this->evaluate($l) ?? $this->evaluate($r);
190 case '.': return $this->evaluate($l) . $this->evaluate($r);
191 case '/': return $this->evaluate($l) / $this->evaluate($r);
192 case '==': return $this->evaluate($l) == $this->evaluate($r);
193 case '>': return $this->evaluate($l) > $this->evaluate($r);
194 case '>=': return $this->evaluate($l) >= $this->evaluate($r);
195 case '===': return $this->evaluate($l) === $this->evaluate($r);
196 case 'and': return $this->evaluate($l) and $this->evaluate($r);
197 case 'or': return $this->evaluate($l) or $this->evaluate($r);
198 case 'xor': return $this->evaluate($l) xor $this->evaluate($r);
199 case '-': return $this->evaluate($l) - $this->evaluate($r);
200 case '%': return $this->evaluate($l) % $this->evaluate($r);
201 case '*': return $this->evaluate($l) * $this->evaluate($r);
202 case '!=': return $this->evaluate($l) != $this->evaluate($r);
203 case '!==': return $this->evaluate($l) !== $this->evaluate($r);
204 case '+': return $this->evaluate($l) + $this->evaluate($r);
205 case '**': return $this->evaluate($l) ** $this->evaluate($r);
206 case '<<': return $this->evaluate($l) << $this->evaluate($r);
207 case '>>': return $this->evaluate($l) >> $this->evaluate($r);
208 case '<': return $this->evaluate($l) < $this->evaluate($r);
209 case '<=': return $this->evaluate($l) <= $this->evaluate($r);
210 case '<=>': return $this->evaluate($l) <=> $this->evaluate($r);
213 throw new \Exception('Should not happen');
216 private function evaluateConstFetch(Expr\ConstFetch $expr) {
217 $name = $expr->name->toLowerString();
219 case 'null': return null;
220 case 'false': return false;
221 case 'true': return true;
224 return ($this->fallbackEvaluator)($expr);