twigphp/Twig · error · LogicException

Operators with more than 2 operands are not supported yet…

Error message

Operators with more than 2 operands are not supported yet, got %d.

What it means

During safe-analysis, when a node implements OperatorEscapeInterface the visitor intersects the safeness of its operands; the current implementation only supports one or two operands. An operator declaring more than 2 operands to escape hits this LogicException, signaling an unsupported operator definition in the optimizer pipeline.

Solutions

  1. Change getOperandNamesToEscape() to return at most two operand names; escape the extra operands by different means (e.g. mark them safe manually via setSafe or handle them in your own visitor).
  2. Split the multi-operand operation into nested binary nodes so each has at most 2 escapable operands.
  3. Implement a custom NodeVisitor that performs safe analysis for your operator and registers it, bypassing the core limitation.
  4. Keep the operands <= 2 when designing any node implementing OperatorEscapeInterface.

Example fix

// before
public function getOperandNamesToEscape(): array
{
    return ['left', 'middle', 'right'];
}
// after
public function getOperandNamesToEscape(): array
{
    return ['left', 'right']; // handle 'middle' elsewhere or nest nodes
}
Defensive patterns

Strategy: validation

Validate before calling

$operands = $node->getOperandNamesToEscape();
if (count($operands) > 2) {
    throw new \LogicException('Operator ' . get_class($node) . ' declares ' . count($operands) . ' escapable operands; max supported is 2.');
}

Try / catch

try {
    $env->compile($source);
} catch (\LogicException $e) {
    if (str_contains($e->getMessage(), 'more than 2 operands')) {
        // disable optimizer or fix the custom operator definition
    } else {
        throw $e;
    }
}

Prevention

When it happens

Trigger: A custom Twig operator/expression node implementing OperatorEscapeInterface::getOperandNamesToEscape() returning 3+ child names; adding an escape-analysis-aware operator extension with multiple operands processed by SafeAnalysisNodeVisitor with the optimizer enabled.

Common situations: Writing a custom ternary-style or variadic operator expression class in a Twig extension; a third-party extension that defines an OperatorEscapeInterface node designed for newer/older Twig internals; forked core modifications adding operands to an existing operator.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of twigphp/Twig@a414c3a491 (2026-09-13). Data as JSON: /api/errors/a8ba8ae810a2406e. Report an issue: GitHub.

Appendix: source

Thrown at src/NodeVisitor/SafeAnalysisNodeVisitor.php:108

        return $node;
    }

    public function leaveNode(Node $node, Environment $env): ?Node
    {
        if ($node instanceof ConstantExpression) {
            // constants are marked safe for all
            $this->setSafe($node, ['all']);
        } elseif ($node instanceof BlockReferenceExpression) {
            // blocks are safe by definition
            $this->setSafe($node, ['all']);
        } elseif ($node instanceof ParentExpression) {
            // parent block is safe by definition
            $this->setSafe($node, ['all']);
        } elseif ($node instanceof OperatorEscapeInterface) {
            // intersect safeness of operands
            $operands = $node->getOperandNamesToEscape();
            if (2 < \count($operands)) {
                throw new \LogicException(\sprintf('Operators with more than 2 operands are not supported yet, got %d.', \count($operands)));
            } elseif (2 === \count($operands)) {
                $safe = $this->intersectSafe($this->getSafe($node->getNode($operands[0])), $this->getSafe($node->getNode($operands[1])));
                $this->setSafe($node, $safe);
            }
        } elseif ($node instanceof FilterExpression) {
            // filter expression is safe when the filter is safe
            if ($node->hasAttribute('twig_callable')) {
                $filter = $node->getAttribute('twig_callable');
            } else {
                // legacy
                $filter = $env->getFilter($node->getAttribute('name'));
            }

            if ($filter) {
                $safe = $filter->getSafe($node->getNode('arguments'));
                if (null === $safe) {
                    trigger_deprecation('twig/twig', '3.16', 'The "%s::getSafe()" method should not return "null" anymore, return "[]" instead.', $filter::class);
                    $safe = [];

View on GitHub (pinned to a414c3a491)