twigphp/Twig · error · InvalidArgumentException

Invalid associativity

Error message

Invalid associativity "%s" for operator "%s".

What it means

Twig's ExtensionSet::initExtension converts legacy integer associativity codes from operator definitions into InfixAssociativity enums. Only 1 (left) and 2 (right) are valid; any other integer (typically 0, -1, or 3) means the extension registered a binary operator with a malformed associativity value, so the library refuses to build the parser.

Solutions

  1. Check the extension's getOperators() infix (second) array: every operator's 'associativity' must be the integer 1 (left) or 2 (right).
  2. Identify the offending operator from the message (%2$s in the sprintf) and fix or remove its associativity entry.
  3. Update or replace the third-party Twig extension so it returns InfixAssociativity enums or the new-style definition compatible with your Twig version.
  4. If you intended a non-associative operator, remove the associativity expectation and check your Twig version's parser API — use the current InfixAssociativity enum, not legacy integers.

Example fix

// before (extension getOperators infix array)
'**' => ['precedence' => 200, 'class' => PowExpression::class, 'associativity' => 0],
// after
'**' => ['precedence' => 200, 'class' => PowExpression::class, 'associativity' => 2],
Defensive patterns

Strategy: validation

Validate before calling

foreach ($operators[1] as $operator => $op) {
    if (!isset($op['associativity']) || !in_array($op['associativity'], [1, 2], true)) {
        throw new \InvalidArgumentException(sprintf('Operator "%s": associativity must be 1 (left) or 2 (right), got %s.', $operator, var_export($op['associativity'] ?? null, true)));
    }
}

Type guard

function isValidAssociativity(mixed $v): bool { return $v === 1 || $v === 2; }

Prevention

When it happens

Trigger: An extension (or custom AbstractExtension::getOperators()) returns an infix operator definition whose 'associativity' key is not 1 or 2 — e.g. the old convention 0 for 'none', a typo, or a third-party Twig extension written for an older Twig version whose codes changed.

Common situations: Upgrading Twig and using a third-party extension still returning legacy integer codes; hand-writing getOperators() and guessing that 0 means non-associative (it does not — there is no 'none' option here); copying an operator array and editing the precedence but corrupting associativity.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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

Appendix: source

Thrown at src/ExtensionSet.php:524

        $operators = $extension->getOperators();
        if (!\is_array($operators)) {
            throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', $extension::class, get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators)));
        }

        if (2 !== \count($operators)) {
            throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', $extension::class, \count($operators)));
        }

        $expressionParsers = [];
        foreach ($operators[0] as $operator => $op) {
            $expressionParsers[] = new UnaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['precedence_change'] ?? null, '', $op['aliases'] ?? []);
        }
        foreach ($operators[1] as $operator => $op) {
            $op['associativity'] = match ($op['associativity']) {
                1 => InfixAssociativity::Left,
                2 => InfixAssociativity::Right,
                default => throw new \InvalidArgumentException(\sprintf('Invalid associativity "%s" for operator "%s".', $op['associativity'], $operator)),
            };

            if (isset($op['callable'])) {
                $expressionParsers[] = $this->convertInfixExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, $op['aliases'] ?? [], $op['callable']);
            } else {
                $expressionParsers[] = new BinaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, '', $op['aliases'] ?? []);
            }
        }

        if (\count($expressionParsers)) {
            trigger_deprecation('twig/twig', '3.21', \sprintf('Extension "%s" uses the old signature for "getOperators()", please implement "getExpressionParsers()" instead.', $extension::class));

            $this->expressionParsers->add($expressionParsers);
        }
    }

    private function convertInfixExpressionParser(string $nodeClass, string $operator, int $precedence, InfixAssociativity $associativity, ?PrecedenceChange $precedenceChange, array $aliases, callable $callable): InfixExpressionParserInterface
    {

View on GitHub (pinned to a414c3a491)