phalcon/cphalcon · error · InvalidUserFunctionDefinition
Invalid definition for user function '{name}' in {file} on l
Error message
Invalid definition for user function '{name}' in {file} on line {line} What it means
The Volt template compiler throws this when a template calls a user-registered function whose definition is not usable. In Compiler.zep:1847 a definition registered via Compiler::addFunction() is only accepted if it is a string (macro) or a Closure (invoked with the resolved arguments); any other value (array callable, integer, plain object) reaches the throw. The message carries the function name and the exact template file/line that invoked it. It surfaces at compile time, i.e. the first time the template is rendered (or when the compiled cache is cold).
Source
Thrown at phalcon/Mvc/View/Engine/Volt/Compiler.zep:1847
*/
if typeof definition == "string" {
return definition . "(" . arguments . ")";
}
/**
* Execute the function closure returning the compiled
* definition
*/
if typeof definition == "object" {
if definition instanceof Closure {
return call_user_func_array(
definition,
[arguments, funcArguments]
);
}
}
throw new InvalidUserFunctionDefinition((string) name, (string) expr["file"], (int) expr["line"]);
}
}
/**
* This function includes the previous rendering stage
*/
if name == "get_content" || name == "content" {
return "$this->getContent()";
}
/**
* This function includes views of volt or others template engines
* dynamically
*/
if name == "partial" {
return "$this->partial(" . arguments . ")";
}
View on GitHub (pinned to b7419de9cd)
Solutions
- Change the definition to a Closure: $compiler->addFunction('myFunc', function ($resolvedArgs) { return 'my_php_func(' . $resolvedArgs . ')'; }) — the Closure must return a string of PHP code, not a value
- Or use the string macro form if it maps 1:1 to a PHP function: $compiler->addFunction('myFunc', 'my_php_func')
- If you wanted runtime logic, keep the Closure but return a valid PHP expression string that the compiled template will evaluate
- Register functions in a shared 'volt' service via the DI container so every view uses the same registrations
Example fix
// before
$voltCompiler->addFunction('highlight', ['Util', 'highlight']);
{{ highlight(title) }}
// after
$voltCompiler->addFunction(
'highlight',
function ($resolvedArgs) {
return 'Util::highlight(' . $resolvedArgs . ')';
}
); Defensive patterns
Strategy: validation
Validate before calling
$ok = is_string($definition) || $definition instanceof \Closure;
if (!$ok) {
throw new \InvalidArgumentException(
'Volt function definitions must be a string or Closure, got '
. gettype($definition)
);
}
$compiler->addFunction('myFunc', $definition); Type guard
function isVoltCallableDefinition($definition): bool
{
return is_string($definition) || $definition instanceof \Closure;
} Try / catch
try {
$view->render('page/index', $params);
} catch (\Phalcon\Mvc\View\Engine\Volt\Exception $e) {
// message includes the template file/line of the failing call
$logger->error('Volt compile failed: ' . $e->getMessage());
echo 'Template temporarily unavailable.';
} Prevention
- Keep all addFunction registrations in one dedicated volt service so definitions are reviewed in a single place
- Write a tiny registration helper that asserts is_string/\Closure and throws early with the function name
- Add a smoke test that renders a template exercising every registered function so bad definitions fail in CI, not production
When it happens
Trigger: Calling $compiler->addFunction('myFunc', ['MyClass','myMethod']) (PHP array-style callable), or addFunction('myFunc', 123), then using {{ myFunc(x) }} in a .volt template. Also passing a plain object that is not a Closure, or a definition that was coerced to the wrong type by config loading.
Common situations: Developers porting plain PHP helpers to Volt register them with array callables ['Str', 'method'] or 'Class::method' with 'new' syntax, both rejected. Definitions pulled from a config file or container service can arrive as non-Closure objects. The error only fires when the template is actually compiled, so a bad registration can pass tests that never render that template.
Related errors
- Before-Match callback is not callable in matched route '{pat
- Before-Match callback is not callable in matched route '{pat
- Invalid template engine registration for extension: {extensi
- Macro '{name}' does not exist
- 'mbstring' is required to perform the charset conversion
AI-assisted analysis of phalcon/cphalcon@b7419de9cd (2026-08-21).
Data as JSON: /api/errors/0f58073e3292a184.
Report an issue: GitHub.