phalcon/cphalcon · error · Phalcon\ADR\Exceptions\NotAnAction
Class '{className}' is not an ADR Action.
Error message
Class '{className}' is not an ADR Action. What it means
Thrown by QueryBuilder::paginate() (BuilderModelNotDefined) when the total count must run as a native wrapped query — the builder has a HAVING clause or a multiple-column GROUP BY/DISTINCT — and the builder has no models defined (getModels() === null). The adapter builds 'SELECT COUNT(*) FROM (...sql...) as T1' and needs to instantiate the model to obtain its read connection service; with no model it cannot resolve the connection and throws.
Source
Thrown at phalcon/ADR/Dispatcher.zep:77
let this->container = container,
this->events = events,
this->globalMiddleware = globalMiddleware;
}
/**
* @phpstan-param class-string $actionClass
* @phpstan-param adr_middleware_names $routeMiddleware
*/
public function dispatch(
string actionClass,
<AttributeRequest> request,
array routeMiddleware = []
) -> <ResponseInterface> {
var action, middleware, terminal, pipeline, response;
let action = this->container->getService(actionClass);
if !(action instanceof Action) {
throw new NotAnAction(actionClass);
}
let middleware = array_merge(this->resolveGlobal(), this->resolveAll(routeMiddleware)),
terminal = new EventfulHandler(action, this->events),
pipeline = new Pipeline(middleware, terminal);
this->events->fire(Event::PIPELINE_BEFORE_DISPATCH, this, request);
let response = pipeline->__invoke(request);
this->events->fire(Event::PIPELINE_AFTER_DISPATCH, this, response);
return response;
}
protected function resolveAll(array classes) -> array
{
var className;View on GitHub (pinned to b7419de9cd)
Solutions
- Define the model on the builder: $builder->from(Invoices::class) (or the 'models' constructor option) so getModels() returns a class name.
- If the table has no model, create a minimal model class mapped to that table and use it in the builder.
- Avoid the native-count path when models are impossible: restructure to a single GROUP BY column (COUNT(DISTINCT col) path) or paginate via a different adapter.
Example fix
// before
$builder = new Builder(
[
'tables' => 'invoices',
'having' => 'COUNT(customerId) > 3',
]
);
$paginator = new QueryBuilder(['builder' => $builder, 'limit' => 10]);
$paginator->paginate(); // throws BuilderModelNotDefined
// after
$builder = new Builder(
[
'models' => Invoices::class, // model lets the adapter resolve the read connection
'having' => 'COUNT(customerId) > 3',
]
); Defensive patterns
Strategy: validation
Validate before calling
// Before paginate(): the native-count path needs a model on the builder
$needsWrappedCount = !empty($builder->getHaving())
|| count((array) ($builder->getGroupBy() ?: [])) > 1;
if ($needsWrappedCount && $builder->getModels() === null) {
throw new \InvalidArgumentException(
'Builder needs models (from()/models) because HAVING or multi-column '
. 'GROUP BY forces the wrapped COUNT(*) query'
);
} Prevention
- Always build paginated queries with a model class (models/from), never bare table names.
- Guard dynamically assembled builders: assert getModels() !== null before handing them to the paginator.
When it happens
Trigger: paginate() on a builder created from raw tables, e.g. new Builder(['tables' => 'invoices']) or any builder where models()/from() was never set, combined with having() or a multi-column groupBy() / 'DISTINCT a, b' columns.
Common situations: Reporting queries built directly on table names instead of model classes; dynamically assembled builders where the from() was conditionally skipped; DISTINCT multi-column projections that flip the paginator into the subquery path.
Related errors
- No route matched the request.
- No action directory set; call setActionDirectory().
- Identity column '{identityField}' isn't part of the column m
- Identity column '{identityField}' isn't part of the table co
- Column '{column}' have not defined a data type in '{classNam
AI-assisted analysis of phalcon/cphalcon@b7419de9cd (2026-08-21).
Data as JSON: /api/errors/b430d2605a24f705.
Report an issue: GitHub.