twigphp/Twig · error · RuntimeError

Positional arguments cannot be used after named arguments…

Error message

Positional arguments cannot be used after named arguments for macro "%s".

What it means

Twig throws this RuntimeError when a macro call passes positional arguments after named ones, e.g. {{ m(a=1, 2) }}. PHP itself forbids this ordering, so callLegacy() in TwigMacro detects the mixed, misordered call upfront and reports it with the template call-site line and source instead of a cryptic PHP error. It is one of the lenient-to-strict deprecation checks that become hard errors in Twig 4.0.

Solutions

  1. Reorder the macro call so all positional arguments come before any named arguments.
  2. Convert the positional arguments to named arguments so the whole call is uniformly named.
  3. If arguments are built dynamically in PHP, append positional values before named ones or key everything by name before invoking the macro.

Example fix

{# before #}
{{ macro.box(padding=2, 'title') }}
{# after #}
{{ macro.box('title', padding=2) }}
Defensive patterns

Strategy: validation

Validate before calling

// PHP, before invoking a macro programmatically
$posSeen = false; $namedSeen = false;
foreach (array_keys($args) as $k) {
    if (is_int($k)) { $posSeen = $posSeen || $namedSeen; }
    else { $namedSeen = true; }
}
if ($posSeen) { throw new InvalidArgumentException('Reorder: positional args must precede named args'); }

Type guard

function argsAreWellOrdered(array $args): bool {
    $namedSeen = false;
    foreach (array_keys($args) as $k) {
        if (is_int($k)) { if ($namedSeen) return false; }
        else { $namedSeen = true; }
    }
    return true;
}

Try / catch

try {
    $out = $macro->callLegacy($args, $source, $lineno);
} catch (Twig\Error\RuntimeError $e) {
    if (str_contains($e->getMessage(), 'Positional arguments cannot be used after named arguments')) {
        ksort($args, SORT_STRING); // or reorder so integers come first
        $out = $macro->callLegacy($args, $source, $lineno);
    } else { throw $e; }
}

Prevention

When it happens

Trigger: Calling TwigMacro::callLegacy() with an arguments array that is not a list and contains an integer key occurring after a string (named) key — i.e. a template calling a macro with positional arguments following named arguments.

Common situations: Template authors start adding a new named argument to a macro call and leave existing positional arguments after it; dynamically built call arrays where names are added before leftover positional values; templates upgraded from Twig 2/3 that previously relied on lenient handling.

Related errors


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

Appendix: source

Thrown at src/TwigMacro.php:129

                $misordered = $misordered || $sawNamed;
                ++$positionalCount;
            } else {
                $sawNamed = true;
                if (null === $i = $this->argumentIndexes[$key] ?? null) {
                    $hasUnknownNamed = true;
                } else {
                    if (null === $duplicate && $i < $positionalCount) {
                        $duplicate = $key;
                    }
                    if (isset($this->requiredNames[$key])) {
                        ++$namedRequired;
                    }
                }
            }
        }

        if ($misordered) {
            throw new RuntimeError(\sprintf('Positional arguments cannot be used after named arguments for macro "%s".', $this->name), $lineno, $source);
        }
        if (null !== $duplicate) {
            throw new RuntimeError(\sprintf('Argument "%s" is defined twice for macro "%s".', $duplicate, $this->name), $lineno, $source);
        }

        // For a fully named call, the coverage of the required arguments is exact; a
        // mixed call falls back to the precise (and slower) per-argument check.
        $mayMissRequired = 0 === $positionalCount
            ? $namedRequired < \count($this->requiredNames)
            : $positionalCount < $this->requiredCount;

        if ($mayMissRequired || (!$this->variadic && ($hasUnknownNamed || $positionalCount > \count($this->arguments)))) {
            $this->triggerLegacyDeprecations($arguments, $positionalCount, $source, $lineno);
        }

        foreach ($this->renamedArguments as $name => $parameterName) {
            if (\array_key_exists($name, $arguments)) {
                $arguments[$parameterName] = $arguments[$name];

View on GitHub (pinned to a414c3a491)