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
- Check the extension's getOperators() infix (second) array: every operator's 'associativity' must be the integer 1 (left) or 2 (right).
- Identify the offending operator from the message (%2$s in the sprintf) and fix or remove its associativity entry.
- Update or replace the third-party Twig extension so it returns InfixAssociativity enums or the new-style definition compatible with your Twig version.
- 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
- Use the InfixAssociativity enum directly instead of legacy integers when your Twig version supports it.
- Run your extension's registration in a test that constructs the Environment before shipping.
- Never assume 0 means non-associative; the accepted codes are only 1 and 2.
- Pin third-party Twig extensions to Twig versions their operator definitions support.
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
- The "deprecation_info" option must be an instance of
- When setting the "deprecation_info" option, you need to…
- You cannot enable the "use_yield" option of Twig as node
- The sandbox requires a strict security policy, call…
- The environment passed to
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)