# Overriding Controllers & Models (changing behavior)

> How to change what CBX does — a controller action, a model's query/validation/storage logic — from the customization layer. The key fact to internalise first…

Source: CBX documentation, version 3.x (released). Canonical page: https://docs.configbox.at/docs/customization/overriding_controllers_and_models. Last updated 2026-08-25.

---
How to change **what CBX does** — a controller action, a model's query/validation/storage logic —
from the customization layer. The key fact to internalise first: **the customization `controllers/` and
`models/` folders add *new* classes; they do not shadow existing core ones.** Expecting a same-named file
to override a core controller or model is the single biggest source of "my override does nothing"
confusion. To change the behavior of an *existing* core class you use `system_overrides/` (or, better,
an observer) — not the `controllers/`/`models/` folders.

Read `com_configbox_customization_overview.md` first. For changing a screen's *markup* see
`com_configbox_overriding_views_and_templates.md`; for adding *fields/data* to a model see
`com_configbox_extending_stock_models.md`. This guide is for **behavior**.

All paths are relative to the component root `docroot/components/com_configbox/`. Source references are
point-in-time (component **3.4.1**) — verify against the code.

---

## 1. The precedence rule — read this first

All three class loaders — controllers, models, and view classes — resolve the **same way: core first,
customization second**. The customization file is loaded **only when no core class of that name exists**,
so the customization `controllers/`/`models/`/`views/` folders are for **new** classes; none of them
shadows an existing core class.

| Kind | Loader | Checks **first** | Result |
|---|---|---|---|
| **Controller** | `KenedoController::getController()` (`external/kenedo/classes/KenedoController.php:101-108`) | **core** | Core **wins** → customization `controllers/` is for **new controllers only**; it **cannot shadow** a core controller. |
| **Model** | `KenedoModel::getModel()` (`external/kenedo/classes/KenedoModel.php:168-176`) | **core** | Core **wins** → customization `models/` is for **new models only**; it **cannot shadow** a core model. |
| **View class** | `KenedoView::getView()` (`KenedoView.php:202-207`) | **core** | Core **wins** → **new views only**. |

All three loaders share the same shape — core path first, customization path as the fallback:

```php
// KenedoController::getController()  (:101-108) — KenedoModel/KenedoView are identical in shape
if (is_file($regularPath)) { $path = $regularPath; }    // ← core FIRST → core wins
elseif (is_file($customPath)) { $path = $customPath; }  // only reached when no core class of that name
```

> **The one-line takeaway:** you **cannot** override an existing core controller, model, or view by
> dropping a same-named file in `data/customization/`. The customization `controllers/`/`models/`/`views/`
> folders are for **new** classes. To change the behavior of an *existing* core class, replace it via
> `system_overrides/` (§5) — or, far better where it fits, react to it with an **observer**. Verify the
> precedence against your component version before relying on it.

A corroborating fact from the shipped customization layer: every customization controller and model in
Rovexo's own `data/customization/` is a **new** one (zero share a file name with a core controller/model).
That matches the loader behavior — the folders only ever load *new* classes — and reflects how real
customization works in practice: add *new* controllers/models, and change *existing* behavior through
observers, property injection, templates, or system overrides.

---

## 2. The naming contract (both kinds)

Derived in the loaders from the class name:

| Kind | Class name | File |
|---|---|---|
| Controller | `ConfigboxController<Name>` | `controllers/<name>.php` (`<name>` = lowercase of the part after `Controller`) |
| Model | `ConfigboxModel<Name>` | `models/<name>.php` (`<name>` = lowercase of the part after `Model`) |

Examples: `ConfigboxControllerCart` → `controllers/cart.php`; `ConfigboxModelProducts` →
`models/products.php` (model classes carry **no** Admin prefix — only controllers do, so the admin
products *controller* is `adminproducts` while its model is `products`). The component is derived as
`com_<lowercase-before-Controller/Model>`, and a
legacy `com_cbcheckout` is rewritten to `com_configbox` (`KenedoController.php:92-94`,
`KenedoModel.php:159-161`).

> **Legacy `Cbcheckout*` fallback.** After loading the file, if the expected `Configbox…` class isn't
> defined, the loader looks for the old `Cbcheckout…` name and uses it with a deprecation log
> (`KenedoController.php:122-126`, `KenedoModel.php:191-196`). This exists for old customizations — **write
> new code with the `Configbox…` class name**; don't rely on the fallback.

---

## 3. Controllers — you can add, but not shadow

Because controllers are **core-first**, the customization `controllers/` folder is consulted **only when no
core controller of that name exists** (`getController()`, §1). So:

- **Adding a new controller** — supported and common. Drop `data/customization/controllers/<name>.php`
  with class `ConfigboxController<Name>`; it loads because there is no core file of that name.
- **Changing an existing core controller's behavior** — the customization `controllers/` folder will
  **not** do it (a same-named file is never reached). Replace the class via `system_overrides/` (§5), or,
  where you only need to *react* to an action rather than alter it, use an observer.

### 3.1 Adding a new controller

Declare the class extending the base `KenedoController` — exactly as the shipped customization controllers
do:

```php
// data/customization/controllers/myexport.php   (a NEW controller — no core 'myexport')
<?php
defined('CB_VALID_ENTRY') or die();

class ConfigboxControllerMyexport extends KenedoController {
    function run() {
        // … your action; reach the URL via index.php?option=com_configbox&controller=myexport&task=run
    }
}
```

The file name is the lowercased part after `Controller` (§2): `ConfigboxControllerMyexport` →
`controllers/myexport.php`. A new controller usually needs a route or menu entry to reach it — see
`../platform/joomla/com_configbox_sef_urls.md`.

### 3.2 Changing an existing core controller's behavior

You cannot do this from the `controllers/` folder. Use `system_overrides/`, which loads your class
**before** core would (so core never defines its version) — this both avoids the redeclaration conflict
*and* lets you subclass core cleanly. The pattern: load the core file, alias it, then declare the
dispatcher's class name extending the alias:

```php
// data/customization/system_overrides/ConfigboxControllerCart.php
<?php
defined('CB_VALID_ENTRY') or die();

// Loaded eagerly at boot, BEFORE the lazy loader would pull core. Require the core file, keep a
// handle on its class under a different name, then declare the dispatcher's name extending it.
require_once KenedoPlatform::p()->getComponentDir('com_configbox').'/controllers/cart.php';
class_alias('ConfigboxControllerCart', 'ConfigboxControllerCartCore');

class ConfigboxControllerCart extends ConfigboxControllerCartCore {
    function add() {
        // pre-step …
        parent::add();          // keep core behavior
        // post-step (e.g. fire your own logic) …
    }
}
```

Because your `ConfigboxControllerCart` is already defined when the dispatcher asks for it, the core
`controllers/cart.php` is never loaded as the dispatcher's class — you get a clean subclass with the name
the dispatcher expects. See §5 and the system-overrides guide for the boot mechanics and load order.

---

## 4. Models — you can add, but not shadow

The customization `models/` folder is **only** consulted when no core model of that name exists
(`getModel()` is core-first, §1). So:

- **Adding a new model** — supported. Put `data/customization/models/<newname>.php` with class
  `ConfigboxModel<Newname>`; `getModel('ConfigboxModel<Newname>')` finds it because there's no core file.
  Use this for models backing your own custom screens/controllers.

  > **If that model also returns a name from `getEntityName()`** — opting it into the REST API
  > (`com_configbox_entity_api.md`) and the MCP tools (`com_configbox_mcp_server.md`) — it needs a
  > scope **area**, so a token can be checked against the right `<area>:read`/`<area>:write` before it
  > touches your entity. The central map
  > (`ConfigboxApiTokenHelper::getEntityAreas()`) has no entry for a customization's entity, so it
  > falls back to `catalog` — right for content that belongs next to the products (a summary-panel
  > image, say), wrong for anything else. **Override `getApiArea()`** whenever the fallback is wrong:
  >
  > ```php
  > function getApiArea() {
  >     return ConfigboxApiTokenHelper::AREA_CUSTOMERS;
  > }
  > ```
  >
  > Return one of `AREA_CATALOG`, `AREA_STORE`, `AREA_CUSTOMERS`, `AREA_ORDERS`,
  > `AREA_CONVERSATIONS`, `AREA_CODE` or `AREA_SYSTEM`. Skipping this is a real exposure, not just a
  > mismatch: a custom model holding customer or order data that falls back to `catalog` is readable
  > by any token minted only to browse the catalog.
- **Changing an existing core model** — the customization `models/` folder will **not** do it. Pick by what
  you're changing:
  - **Data shape** (add/remove/retune fields, list columns, storage of a field) → `model_property_customization/`
    (the **merge** mechanism). See `com_configbox_extending_stock_models.md`. This is the right tool for
    most "I need the model to carry/expose X" needs and never touches behavior.
  - **Behavior** (a method body — a custom query, a validation rule that a property can't express, a
    storage side-effect) → **replace the class via `system_overrides/`** (§5).
  - **React to** a store/copy/delete rather than change it → an **observer**
    (`onAfterStoreRecord`, `onAfterCopyRecord`, `onAfterDeleteRecord`). Additive and far safer than
    replacing the class. See `com_configbox_events_and_observers.md`.
  - **Check for a purpose-built hook first.** Some core models deliberately ask another object rather
    than decide for themselves, and overriding *that* object is both supported and a fraction of the
    work. The clearest case: `ConfigboxModelOrderRecord` freezes an order line's SKU by calling
    `ConfigboxQuestion::getSku($selection)`, so a question type that derives a part number from a
    free-entry selection needs a ~10-line sub-class and no order-record override at all
    (`com_configbox_custom_question_types.md` §3).

> Prefer observers and property injection over class replacement for models. Replacing a model class means
> copying its logic and maintaining it against every future core update — real drift risk on a ~218K-LOC
> component.

---

## 5. Behavior override via `system_overrides/` (the reliable path for existing core classes)

`system_overrides/*.php` files are `require_once`'d **eagerly at boot**, *before* the lazy loaders would
pull the core class (`helpers/overrides.php:24-39`, fired from the `onConfigboxInitialized` observer). So a
class you define there is already in memory when `getController()`/`getModel()`/`getView()` run — and because the class
name is already defined, the core file's `class …` is never reached. That makes `system_overrides/` the
**way to change an existing core class' behavior** (controller, model, *or* view) while keeping its
name — the one thing the per-type `controllers/`/`models/`/`views/` folders cannot do.

You have two patterns:

- **Subclass core** (recommended — minimal override): require the core file, alias its class, then declare
  the same name extending the alias, overriding only the methods you need. This keeps all core behavior.
  See the worked controller example in §3.2 — the same pattern works for a model (require
  `models/<name>.php`) or a view.
- **Wholesale rewrite**: declare the class from scratch. Only when you genuinely replace everything; you
  then copy and maintain every core method you didn't change.

Either way, override the minimum and revisit after each component upgrade — a subclass tracks core method
signatures, a rewrite tracks whole methods. Full details, load order, and the boot/settings hooks live in
`com_configbox_system_overrides_and_boot_hooks.md`; this section points you there as the correct
destination for behavior overrides of any core controller, model, or view.

---

## 6. Deployment checklist

```
data/customization/
  controllers/<name>.php       ← a NEW controller (ConfigboxController<Name>); does NOT shadow a core controller
  models/<name>.php            ← a NEW model (ConfigboxModel<Name>); does NOT shadow a core model
  system_overrides/<Class>.php ← change an existing core controller/model class' behavior (keeps the name)
```

1. **Classify the change**: markup → templates guide; data/fields → stock-models guide; **behavior** → here.
2. **Controller, new** → customization `controllers/` (§3.1).
3. **Controller, behavior change of an existing one** → `system_overrides/` (§3.2 / §5).
4. **Model, new** → customization `models/` (§4).
5. **Model, behavior change** → `system_overrides/` (§5). **Model, data/field change** →
   `model_property_customization/`. **Model, react-only** → an observer.
6. **Use the `Configbox…` class name** (not legacy `Cbcheckout…`) and the lowercase file name (§2).
7. **Verify manually** — exercise the real action/flow, including XHR-injected admin actions. CBX has
   **zero automated tests**.
8. **After each update**, re-check any `system_overrides/` class against the (possibly changed) core class.

---

## 7. Conventions & gotchas

- **Controllers, models, and view classes all load core-first (new-only).** The customization
  `controllers/`/`models/`/`views/` folders add *new* classes; none shadows an existing core class. The
  single most important fact in this guide (§1).
- **To change an existing core class, use `system_overrides/`.** It loads your class *before* core would,
  so you can subclass core cleanly and keep the dispatcher's class name (§3.2, §5). The per-type folders
  can't do this — a same-named file there is never reached.
- **A customization `controllers/<corename>.php` is silently ignored.** Because core wins, a same-named
  file never loads. Name new controllers distinctly; reach for `system_overrides/` to override an existing one.
- **Prefer additive over replace.** Observers (react to events) and `model_property_customization/`
  (add/retune fields) don't drift across updates; class replacement does.
- **Legacy fallback is a bridge, not an API.** Write `Configbox…` class names; the `Cbcheckout…` fallback
  exists only for old code and logs a deprecation.
- **Escape on output, sanitize on input.** SQL is traditionally hand-built mysqli: `getQuoted()`
  backtick-quotes **identifiers**, `getEscaped()` escapes **values** (add the surrounding quotes
  yourself) — or bind values with `setPreparedQuery($sql, $params)` and skip manual escaping. Either
  way validate request data before it reaches a query.
- **Match the idioms.** No namespaces/PSR-4, static singletons, `KText::_()` for strings. Follow neighboring
  controllers/models.

---

## See also

- `com_configbox_customization_overview.md` — the per-kind precedence table and the resolution rule.
- `com_configbox_extending_stock_models.md` — change a model's **data/fields** (no behavior change needed).
- `com_configbox_events_and_observers.md` — **react to** store/copy/delete and domain events (the safest
  way to inject model-adjacent behavior).
- `com_configbox_system_overrides_and_boot_hooks.md` — change an existing core class' behavior (the
  reliable path for core controllers, models, and views).
- `com_configbox_overriding_views_and_templates.md` — change a screen's markup / add a new view.
- `../technical/com_configbox_kenedo_controller.md` — **`KenedoController` in depth**: the layer under
  this guide — how a controller dispatches tasks, the `store()`/`delete()`/`copy()` flows, and
  authorization. Read it before adding a custom task or overriding a core controller.
- `external/kenedo/classes/KenedoController.php` (`getController`, `:101`), `…/KenedoModel.php`
  (`getModel`, `:168`), `…/KenedoView.php` (`getView`, `:181`) — the three loaders, all core-first.
