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
- Use an arrow function so the callback is a Closure: {{ items|array_group_by(v => v.category) }} — works in and out of the sandbox
- Do the grouping in PHP (plugin or Twig extension) and pass the grouped array to the template
- 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
- Author array callbacks as Twig arrow functions (v => v.field) everywhere
- Lint sandboxed snippets for string callables passed to array_group_by
- Precompute grouped data in PHP when templates need the string shorthand
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
- Twig |filter("{arrow}") is not allowed.
- Twig |map("{arrow}") is not allowed.
- Twig |reduce("{arrow}") is not allowed.
- Twig |find("{arrow}") is not allowed.
- Twig |sort("{arrow}") is not allowed.
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/a3190f0e9f1f1472.
Report an issue: GitHub.