twigphp/Twig · error · SyntaxError

Argument " " is defined twice for " ".

Error message

Argument "%s" is defined twice for %s "%s".

What it means

While mapping extracted template arguments onto the callable's parameters, extractArguments() detects when a parameter has already received a value — both positionally and by name. It throws a SyntaxError naming the doubly-defined argument, the callable type, and its name, preventing silent overwrites during compilation.

Solutions

  1. Remove the duplicate — keep either the positional or the named value, not both.
  2. Use named arguments exclusively for the affected call.
  3. In extension code, guard against programmatic argument arrays that can collide with positional entries.

Example fix

{# before #}
{{ filter.colorize('text', color='red', 'red') }}
{# after #}
{{ filter.colorize('text', color='red') }}
Defensive patterns

Strategy: validation

Validate before calling

// Normalize names the way Twig does and reject collisions before rendering
function hasDuplicateCallableArgs(array $supplied, array $paramNames): bool {
    $norm = fn (string $s) => strtolower(preg_replace('/[_A-Z]/', '_', lcfirst($s)));
    $used = [];
    foreach ($supplied as $k => $_) {
        $key = is_int($k) ? (isset($paramNames[$k]) ? $norm($paramNames[$k]) : '#'.$k) : $norm($k);
        if (isset($used[$key])) { return true; }
        $used[$key] = true;
    }
    return false;
}

Try / catch

try {
    $out = $twig->render($template, $ctx);
} catch (Twig\Error\SyntaxError $e) {
    if (str_contains($e->getMessage(), 'is defined twice for')) {
        error_log($e->getMessage().' at '.$e->getTemplateLine());
    }
    throw $e;
}

Prevention

When it happens

Trigger: A template call supplies argument N positionally and the same parameter also by name (normalized names collide), e.g. {{ replace('a', 'b', from='a') }} where the first parameter is $from.

Common situations: Migrating calls from positional to named style and leaving one value in both forms; dynamic argument building in extensions producing collisions; case/underscore naming differences that normalize to the same parameter.

Related errors


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

Appendix: source

Thrown at src/Util/CallableArgumentsExtractor.php:96

        $missingArguments = [];
        $optionalArguments = [];
        $pos = 0;
        foreach ($callableParameters as $callableParameter) {
            $callableParameterName = $callableParameter->name;
            if (\PHP_VERSION_ID >= 80000 && 'range' === $callable) {
                if ('start' === $callableParameterName) {
                    $callableParameterName = 'low';
                } elseif ('end' === $callableParameterName) {
                    $callableParameterName = 'high';
                }
            }

            $callableParameterNames[] = $callableParameterName;
            $normalizedCallableParameterName = $this->normalizeName($callableParameterName);

            if (\array_key_exists($normalizedCallableParameterName, $extractedArguments)) {
                if (\array_key_exists($pos, $extractedArguments)) {
                    throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext());
                }

                if (\count($missingArguments)) {
                    throw new SyntaxError(\sprintf(
                        'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".',
                        $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments)
                    ), $this->node->getTemplateLine(), $this->node->getSourceContext());
                }

                $arguments = array_merge($arguments, $optionalArguments);
                $arguments[] = $extractedArguments[$normalizedCallableParameterName];
                unset($extractedArguments[$normalizedCallableParameterName]);
                $optionalArguments = [];
            } elseif (\array_key_exists($pos, $extractedArguments)) {
                $arguments = array_merge($arguments, $optionalArguments);
                $arguments[] = $extractedArguments[$pos];
                unset($extractedArguments[$pos]);
                $optionalArguments = [];

View on GitHub (pinned to a414c3a491)