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

  1. Define the model on the builder: $builder->from(Invoices::class) (or the 'models' constructor option) so getModels() returns a class name.
  2. If the table has no model, create a minimal model class mapped to that table and use it in the builder.
  3. 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

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


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