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

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

Error message

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

What it means

Router dispatcher caching (buildDispatcherDump / dumpDispatcher / useCache) serializes routes with var_export(), which cannot represent closures. Before dumping, every route's beforeMatch callback is inspected; if it is a \Closure, the dump is aborted with this exception naming the offending route id. Only string callables ('Class::method') or array callables (['Class', 'method']) survive serialization and can be cached.

Source

Thrown at phalcon/Mvc/Router.zep:729

    {
        var route, cb, converters, convName, converter, dumpedRoutes,
            routeToIdx, scalarIdx, scalarSubKey, scalarVal,
            methodRoutesScalar, candidatesScalar, staticScalar,
            innerKey, innerVal, mostInnerVal, mostInnerArr;

        if this->methodRoutesDirty {
            this->rebuildMethodIndex();
        }

        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"
                        );
                    }
                }
            }

View on GitHub (pinned to b7419de9cd)

Solutions

  1. Replace the closure with a static string callable: ->beforeMatch('App\Filters\MaintenanceFilter::check') or an array callable ['App\Filters\MaintenanceFilter', 'check']
  2. If the guard logic cannot live in a class, remove beforeMatch from that route and enforce the condition in the controller or middleware instead
  3. Skip router caching entirely (do not call dumpDispatcher/useCache) if closures must stay
  4. Run the dump in CI/deploy so closure routes fail the build with the route id, not production traffic

Example fix

// before
$router->add('/admin/:controller', ['controller' => 1])
       ->beforeMatch(function ($uri, $route) {
            return Auth::isAdmin(); // Closure -> cache dump throws
       });

// after
class AdminGate
{
    public static function check(string $uri, RouteInterface $route): bool
    {
        return Auth::isAdmin();
    }
}
$router->add('/admin/:controller', ['controller' => 1])
       ->beforeMatch([AdminGate::class, 'check']); // string/array callables are cacheable
Defensive patterns

Strategy: validation

Validate before calling

// Before enabling router caching, scan for closure beforeMatch callbacks
foreach ($router->getRoutes() as $route) {
    $cb = $route->getBeforeMatch();
    if ($cb instanceof \Closure) {
        throw new LogicException('Route ' . $route->getRouteId() . ' has a Closure beforeMatch; use a string/array callable to cache');
    }
}
$router->dumpDispatcher($path); // safe now

Type guard

function isCacheableBeforeMatch(mixed $cb): bool
{
    return $cb === null || (is_string($cb) && is_callable($cb)) || (is_array($cb) && is_callable($cb));
}

Try / catch

try {
    $router->dumpDispatcher($path);
} catch (\Phalcon\Mvc\Router\Exception $e) {
    // message names the offending route id - convert its callback and re-dump
    $router->useCache-off; // skip caching this build
    log($e->getMessage());
}

Prevention

When it happens

Trigger: Defining a route with ->beforeMatch(function ($uri, $route) {...}) (or fn() => ...) and then calling dumpDispatcher(), loadDispatcher-from-cache flows, or useCache($cacheAdapter); enabling router caching on an app whose routes were written with inline closure guards.

Common situations: Adding router caching to a previously-uncached production app and hitting the first closure guard (auth checks, maintenance-mode filters, A/B gate callbacks); generating the cache during a build step that fails only when certain dev routes are registered.

Related errors


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