{"record":{"id":"b430d2605a24f705","repo":"phalcon/cphalcon","slug":"class-classname-is-not-an-adr-action","errorCode":null,"errorMessage":"Class '{className}' is not an ADR Action.","messagePattern":"Class '(.+?)' is not an ADR Action\\.","errorType":"exception","errorClass":"Phalcon\\ADR\\Exceptions\\NotAnAction","httpStatus":null,"severity":"error","filePath":"phalcon/ADR/Dispatcher.zep","lineNumber":77,"sourceCode":"        let this->container        = container,\n            this->events           = events,\n            this->globalMiddleware = globalMiddleware;\n    }\n\n    /**\n     * @phpstan-param class-string          $actionClass\n     * @phpstan-param adr_middleware_names  $routeMiddleware\n     */\n    public function dispatch(\n        string actionClass,\n        <AttributeRequest> request,\n        array routeMiddleware = []\n    ) -> <ResponseInterface> {\n        var action, middleware, terminal, pipeline, response;\n\n        let action = this->container->getService(actionClass);\n        if !(action instanceof Action) {\n            throw new NotAnAction(actionClass);\n        }\n\n        let middleware = array_merge(this->resolveGlobal(), this->resolveAll(routeMiddleware)),\n            terminal   = new EventfulHandler(action, this->events),\n            pipeline   = new Pipeline(middleware, terminal);\n\n        this->events->fire(Event::PIPELINE_BEFORE_DISPATCH, this, request);\n\n        let response = pipeline->__invoke(request);\n\n        this->events->fire(Event::PIPELINE_AFTER_DISPATCH, this, response);\n\n        return response;\n    }\n\n    protected function resolveAll(array classes) -> array\n    {\n        var className;","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/phalcon/cphalcon/blob/b7419de9cd0a8a3f48441ead84c9f8415d463e25/phalcon/ADR/Dispatcher.zep#L59-L95","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n$builder = new Builder(\n    [\n        'tables' => 'invoices',\n        'having' => 'COUNT(customerId) > 3',\n    ]\n);\n$paginator = new QueryBuilder(['builder' => $builder, 'limit' => 10]);\n$paginator->paginate(); // throws BuilderModelNotDefined\n\n// after\n$builder = new Builder(\n    [\n        'models' => Invoices::class, // model lets the adapter resolve the read connection\n        'having' => 'COUNT(customerId) > 3',\n    ]\n);","handlingStrategy":"validation","validationCode":"// Before paginate(): the native-count path needs a model on the builder\n$needsWrappedCount = !empty($builder->getHaving())\n    || count((array) ($builder->getGroupBy() ?: [])) > 1;\nif ($needsWrappedCount && $builder->getModels() === null) {\n    throw new \\InvalidArgumentException(\n        'Builder needs models (from()/models) because HAVING or multi-column '\n        . 'GROUP BY forces the wrapped COUNT(*) query'\n    );\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["paginator","querybuilder","orm","model","phalcon"],"backgroundTag":"orm-query-missing-model","analyzedSha":"b7419de9cd0a8a3f48441ead84c9f8415d463e25","analyzedAt":"2026-08-21T06:21:18.811Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}