getgrav/grav · error · RuntimeError

The callable passed to the "array_group_by" filter must be a

Error message

The callable passed to the "array_group_by" filter must be a Closure in sandbox mode.

What it means

Grav's array_group_by filter accepts either a property-name string or a callable to compute the group key. When the render is sandboxed (Twig injects $isSandboxed), only a \Closure is accepted: a string could name an arbitrary PHP function that would then be invoked once per item. Twig arrow functions (v => v.category) compile to Closures, so idiomatic templates are unaffected; the string shorthand keeps working only in unsandboxed templates.

Source

Thrown at system/src/Grav/Common/Twig/Extension/GravExtension.php:1395

     *
     * A string $callback would let sandboxed content call an arbitrary
     * method on each item by name ($item->$callback()), and any other PHP
     * callable would let it invoke an arbitrary global function via
     * call_user_func() — neither goes through the sandbox, so both are
     * refused while sandboxed. Outside the sandbox both keep working exactly
     * as before.
     *
     * @param bool $isSandboxed Whether the current render is sandboxed (injected by Twig)
     * @param array|\Traversable $array The array or collection to group
     * @param string|callable $callback Property name or callable to determine group key.
     *                                  Must be a \Closure when $isSandboxed is true.
     * @return array Grouped array with keys as group identifiers and values as arrays of items
     * @throws RuntimeError if $callback is not a Closure while sandboxed
     */
    public function arrayGroupByFilter(bool $isSandboxed, $array, $callback): array
    {
        if ($isSandboxed && !$callback instanceof \Closure) {
            throw new RuntimeError('The callable passed to the "array_group_by" filter must be a Closure in sandbox mode.');
        }

        $groups = [];

        // Convert to array if it's a Traversable object (like Grav Collections)
        if ($array instanceof \Traversable) {
            $array = iterator_to_array($array);
        }

        if (!is_array($array)) {
            return [];
        }

        foreach ($array as $key => $item) {
            if ($callback instanceof \Closure) {
                // Sandboxed arrow function: attribute access inside it is
                // already checked by Twig's own attribute compilation.
                $groupKey = $callback($item, $key);

View on GitHub (pinned to 6040efed04)

Solutions

  1. Use an arrow function so the callback is a Closure: {{ items|array_group_by(v => v.category) }} — works in and out of the sandbox
  2. Do the grouping in PHP (plugin or Twig extension) and pass the grouped array to the template
  3. Avoid array_group_by in sandboxed content if the callback shape cannot be changed

Example fix

{# before (sandboxed): string callable refused #}
{{ items|array_group_by('category') }}

{# after: arrow function compiles to a Closure #}
{{ items|array_group_by(v => v.category) }}
Defensive patterns

Strategy: validation

Validate before calling

// normalize the callback before rendering sandboxed content that uses array_group_by
if ($isSandboxed && is_string($callback) && !$callback instanceof \Closure) {
    $field = $callback;
    $callback = fn($item) => is_array($item) ? ($item[$field] ?? null) : null; // now a Closure
}

Type guard

function isSandboxSafeCallback(mixed $callback, bool $isSandboxed): bool
{
    return !$isSandboxed || $callback instanceof \Closure;
}

Try / catch

use Twig\Error\RuntimeError;
try { echo $twig->render($sandboxedTemplate, $data); }
catch (RuntimeError $e) { log_template_error($e); echo '<!-- grouping refused in sandbox -->'; }

Prevention

When it happens

Trigger: In sandboxed content: {{ items|array_group_by('category') }} (string shorthand) or any non-Closure callable; passing a string that happens to name a PHP function; snippets written for unsandboxed theme templates reused inside sandboxed page content.

Common situations: Page markdown that groups collections via the string shorthand; templates written before the sandbox hardening; mixed theme/content code paths where one context works and the other throws.

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/a3190f0e9f1f1472. Report an issue: GitHub.