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": convertersView on GitHub (pinned to b7419de9cd)
Solutions
- Move the conversion into a static method and reference it as a string callable 'App\Converters\IntConverter::cast' or array callable
- For pure casts, let the dispatcher/controller handle the value instead of a converter, or drop the converter if the parameter needs no transformation
- 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
- 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
- Convert parameter normalization into small static converter classes from day one
- Include route-metadata linting (no Closures anywhere) in the build that generates the cache
- After refactoring any callable, regenerate the cache in the same commit
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
- Cannot cache router: route id '{routeId}' has a Closure befo
- 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/bd0b7187eefb8ecc.
Report an issue: GitHub.