phalcon/cphalcon · error · Phalcon\Mvc\Router\Exception

Cannot cache router: route id '{routeId}' has a Closure conv

Error message

Cannot cache router: route id '{routeId}' has a Closure converter for '{convName}' - only string/array callables are cacheable

What it means

Like beforeMatch callbacks, route converters (parameter transformers registered with ->convert()) are inspected during router dispatcher cache dumps. A converter stored as a \Closure cannot be exported by var_export(), so buildDispatcherDump()/dumpDispatcher()/useCache() aborts with this exception, naming the route id and the converter name (e.g. the 'id' converter).

Source

Thrown at phalcon/Mvc/Router.zep:739

        let dumpedRoutes = [];
        let routeToIdx   = [];

        for scalarIdx, route in this->routes {
            let routeToIdx[spl_object_id(route)] = scalarIdx;

            let cb = route->getBeforeMatch();
            if cb !== null && cb instanceof \Closure {
                throw new Exception(
                    "Cannot cache router: route id '" . route->getRouteId() .
                    "' has a Closure beforeMatch - only string/array callables are cacheable"
                );
            }

            let converters = route->getConverters();
            if typeof converters === "array" {
                for convName, converter in converters {
                    if converter instanceof \Closure {
                        throw new Exception(
                            "Cannot cache router: route id '" . route->getRouteId() .
                            "' has a Closure converter for '" . convName .
                            "' - only string/array callables are cacheable"
                        );
                    }
                }
            }

            let dumpedRoutes[] = [
                "class":       get_class(route),
                "pattern":     route->getPattern(),
                "paths":       route->getPaths(),
                "methods":     route->getHttpMethods(),
                "hostname":    route->getHostname(),
                "name":        route->getName(),
                "id":          route->getRouteId(),
                "beforeMatch": cb,
                "converters":  converters

View on GitHub (pinned to b7419de9cd)

Solutions

  1. Move the conversion into a static method and reference it as a string callable 'App\Converters\IntConverter::cast' or array callable
  2. For pure casts, let the dispatcher/controller handle the value instead of a converter, or drop the converter if the parameter needs no transformation
  3. Keep closures only on routes excluded from the cached set - but note the dumper has no exclude flag, so realistically all converters must be non-closure
  4. Validate routes for closure converters during CI so failures surface before deploy

Example fix

// before
$route->convert('id', function ($value) {
    return (int) $value; // Closure converter -> cache dump throws
});

// after
class Cast
{
    public static function toInt($value): int
    {
        return (int) $value;
    }
}
$route->convert('id', [Cast::class, 'toInt']); // cacheable
Defensive patterns

Strategy: validation

Validate before calling

// Before dumping, verify no converter is a Closure
foreach ($router->getRoutes() as $route) {
    foreach ($route->getConverters() ?? [] as $name => $converter) {
        if ($converter instanceof \Closure) {
            throw new LogicException('Converter "' . $name . '" on route ' . $route->getRouteId() . ' is a Closure; use Class::method');
        }
    }
}
$router->dumpDispatcher($path);

Type guard

function isCacheableConverter(mixed $converter): bool
{
    return (is_string($converter) || is_array($converter)) && is_callable($converter);
}

Try / catch

try {
    $router->dumpDispatcher($path);
} catch (\Phalcon\Mvc\Router\Exception $e) {
    // e.g. "Closure converter for 'id'" - refactor that converter to a static method, re-run
    $logger->error($e->getMessage());
}

Prevention

When it happens

Trigger: Defining ->convert('id', function ($value) { return (int) $value; }) (or any closure converter) on a route and then enabling router caching; casting/normalizing parameters inline via closures, which is the common idiom in docs; dumping the dispatcher in a deploy script.

Common situations: Idiomatic closure converters copied from documentation or older projects; adding useCache() to speed up route matching and discovering the first route that normalizes parameters with a closure; the exception fires at cache-build time, so it blocks deploys rather than requests.

Related errors


AI-assisted analysis of phalcon/cphalcon@b7419de9cd (2026-08-21). Data as JSON: /api/errors/bd0b7187eefb8ecc. Report an issue: GitHub.