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
- Replace the closure with a static string callable: ->beforeMatch('App\Filters\MaintenanceFilter::check') or an array callable ['App\Filters\MaintenanceFilter', 'check']
- If the guard logic cannot live in a class, remove beforeMatch from that route and enforce the condition in the controller or middleware instead
- Skip router caching entirely (do not call dumpDispatcher/useCache) if closures must stay
- 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
- Ban closures in beforeMatch via a coding standard once router caching is enabled
- Run cache generation in CI so violations block merge with the route id in the log
- Keep an inventory of route callbacks as class::method references
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
- Cannot cache router: route id '{routeId}' has a Closure conv
- Router cache is missing 'version' field
- Router cache version {dumpVersion} is not supported (this bu
- Router cache is missing 'routes' field
- Failed to write router cache temp file: {tmpPath}
AI-assisted analysis of phalcon/cphalcon@b7419de9cd (2026-08-21).
Data as JSON: /api/errors/ce8f3fa011f26795.
Report an issue: GitHub.